#lang scribble/manual @(require (for-label racket/base racket/list racket/string "../main.rkt")) @title{jsmaker} @author+email["Hans Dijkema" ""] @defmodule[jsmaker] The @racketmodname[jsmaker] collection provides two syntax forms that translate a practical subset of Racket expressions to JavaScript source code. The translation is syntax-driven: the Racket expression is not evaluated, but is inspected by the macro and emitted as a JavaScript string. The goal is not to implement a complete Racket compiler. The goal is a useful and predictable source generator for small pieces of JavaScript, including callbacks, browser-facing functions, simple data processing, regular expressions, and some date/time helper forms. @section{Public API} @defform[(js form ...)]{ Translates one or more Racket forms to JavaScript statement code. The result is a string. @racketblock[ (js (define (f x) (if (and (> x 10) (< x 15)) (begin (console.log x) (return x)) (return (* x x))))) ] The generated JavaScript is statement-oriented: @codeblock{ function f(x) { if ((x > 10) && (x < 15)) { console.log(x); return x; } else { return (x * x); } } } Inside function bodies, the final expression is returned automatically unless an explicit @racket[return] form is used. } @defform[(js/expression form)]{ Translates a single Racket expression to a JavaScript expression string. @racketblock[ (js/expression (let loop ([i 0] [acc 0]) (if (< i 5) (loop (+ i 1) (+ acc i)) acc))) ] Tail-recursive named @racket[let] loops are lowered to JavaScript @tt{while (true)} loops when the recursive call is in tail position. } @section{Supported expression subset} The supported subset includes literals, identifiers, function calls, @racket[lambda], @racket[λ], @racket[define], @racket[set!], @racket[if], @racket[begin], @racket[begin0], @racket[cond], @racket[case], @racket[let], @racket[let*], @racket[letrec], @racket[let-values], @racket[let*-values], named @racket[let], @racket[when], @racket[unless], @racket[while], @racket[for], @racket[for/list], @racket[for/vector], and @racket[for/fold]. The common arithmetic, comparison, list/vector/string, hash, and higher-order forms used in the regression suite are also supported. Examples include @racket[+], @racket[-], @racket[*], @racket[/], @racket[quotient], @racket[remainder], @racket[<], @racket[<=], @racket[>], @racket[>=], @racket[=], @racket[equal?], @racket[and], @racket[or], @racket[not], @racket[list], @racket[vector], @racket[cons], @racket[append], @racket[map], @racket[filter], @racket[foldl], @racket[foldr], @racket[substring], @racket[string-append], @racket[hash], and @racket[hash-ref]. @section{Truthiness and boolean simplification} Racket treats only @racket[#f] as false. JavaScript treats many values as false. The generator therefore preserves Racket truthiness where needed. When a form is known to produce a JavaScript boolean, the generator emits simpler JavaScript. For example: @racketblock[ (js/expression (and (> x 10) (< x 15))) ] emits: @codeblock{ ((x > 10) && (x < 15)) } A chained comparison such as @racket[(< x y 10)] is emitted directly when all reused operands are simple: @codeblock{ ((x < y) && (y < 10)) } When an intermediate expression might have side effects, the generator uses temporaries so the expression is evaluated once and in order. @section{Regular expressions} Racket @tt{#rx} and common @tt{#px} patterns are translated to JavaScript @tt{RegExp} values where the syntax is compatible. The generator supports @racket[regexp?], @racket[pregexp?], @racket[regexp-match], @racket[regexp-match?], @racket[regexp-match*], @racket[regexp-match-positions], @racket[regexp-split], @racket[regexp-replace], @racket[regexp-replace*], and @racket[regexp-quote]. Match results are normalized to Racket-like values: a failed match becomes @tt{false}, successful matches become JavaScript arrays, and an unmatched optional capture becomes @tt{false}. The regexp support is deliberately conservative. Known incompatible constructs such as inline option groups and atomic groups are rejected instead of being silently miscompiled. @section{Exceptions} A small @racket[with-handlers] subset is supported: @racketblock[ (js/expression (with-handlers ([exn? (lambda (e) (string-append "caught:" (exn-message e)))]) (error "boom"))) ] The generated JavaScript uses @tt{try}/@tt{catch}. Only a generic @racket[exn?] predicate is supported. Handler procedures are emitted as function expressions in callee position, so rest-argument handlers such as @racket[(lambda args ...)] are valid JavaScript. Division by zero in @racket[/] is checked at run time and throws a JavaScript @tt{Error}, which this subset can catch with @racket[with-handlers]. The generator does not model Racket's exception hierarchy, continuable exceptions, exception marks, or the full exact/inexact numeric distinction. @section{Gregor-style date and time helpers} The generator includes a small JavaScript-side representation for a subset of Gregor-style date/time operations. Prefixes are not hardcoded. A call such as @racket[(g:date 2026 5 25)], @racket[(gregor:date 2026 5 25)], or @racket[(date 2026 5 25)] is matched by the local name @racket[date]. Supported local names include @racket[date], @racket[time], @racket[datetime], @racket[moment], @racket[parse-date], @racket[parse-time], @racket[parse-datetime], @racket[parse-moment], @racket[string->date], @racket[string->time], @racket[string->datetime], @racket[date->string], @racket[time->string], @racket[datetime->string], @racket[moment->string], @racket[date?], @racket[time?], @racket[datetime?], @racket[moment?], @racket[->year], @racket[->month], @racket[->day], @racket[->hours], @racket[->minutes], @racket[->seconds], @racket[->js-date], and @racket[js-date->datetime]. Plain dates and times are represented as tagged JavaScript objects rather than native @tt{Date} objects, avoiding accidental timezone shifts. Use @racket[->js-date] when a native JavaScript @tt{Date} is desired. @section{JavaScript interop forms} Several forms are emitted as direct JavaScript interop: @itemlist[#:style 'compact @item{@racket[(send obj method arg ...)] emits @tt{obj.method(arg, ...)}.} @item{@racket[(new cls arg ...)] emits @tt{new cls(arg, ...)}.} @item{@racket[(js-ref obj key)] emits @tt{obj[key]}.} @item{@racket[(js-dot obj field)] emits @tt{obj.field}.} @item{@racket[(object key value ...)] emits a JavaScript object literal.} @item{@racket[(array value ...)] emits a JavaScript array literal.}] @section{JavaScript use case demos} The @filepath{demo/js-usecases.rkt} file contains a set of practical JavaScript examples written in the Racket surface syntax accepted by @racket[js]. The module also writes @filepath{demo/js-usecases.generated.js}, which contains the generated JavaScript snippets wrapped as callable demo functions. The corresponding tests are in @filepath{testing/jsmaker-usecases.rkt} and are included by @filepath{testing/jsmaker-regressions.rkt}. These tests execute the generated JavaScript with the configured JavaScript executor. Promise-valued results are awaited by the test framework, which allows examples such as Fetch API success/error handling to be checked directly. The use case set covers random numbers, @tt{Set}, JavaScript falsey values, currying, object destructuring, intervals, object property get/set/delete, string concatenation order, @tt{Object.freeze} and @tt{Object.seal}, switch/case, classes with constructor defaults, sorting objects, array deletion techniques, Bubble Sort, recursive Binary Search, @tt{Map} counting, DOM HTML access, anagram checks, pair-sum checks, and Fetch API result/error handling. @section{Testing} The regression suite lives in @filepath{testing/}. The main entry point is @filepath{testing/jsmaker-regressions.rkt}. The test framework searches for a JavaScript engine such as Node, Deno, Bun, QuickJS, or another supported executor. If no engine is available, the JavaScript test files are generated and execution is skipped with warnings. This skip is intentional so package tests do not fail on systems without a JavaScript runtime. The usual command is: @codeblock{ raco test jsmaker/testing/jsmaker-regressions.rkt } @section{Private compatibility files} The @filepath{private/} directory contains legacy helper material from the source project. The current public @racketmodname[jsmaker] module, demos, and regression tests do not require these helper files. They are kept in the package layout for compatibility and are omitted from compilation and the test entry point in @filepath{info.rkt}. @section{Limitations} This package is not a full Racket compiler. It does not expand arbitrary Racket macros, implement modules, contracts, classes, continuations, parameters, or the full numeric tower. Unsupported forms should fail explicitly rather than silently generate JavaScript with different semantics. @section{Use-case documentation} The practical JavaScript examples are documented separately in @other-doc['(lib "jsmaker/scrbl/usecases.scrbl")]. That document shows each use case as Racket/js-maker source next to representative generated JavaScript, and lists the behavior covered by the regression test.