js-maker:   a Syntax-Driven Racket-to-Java  Script Generator
1 Public API
js
js/  expression
2 Mental model
2.1 Racket value interpolation with eval
3 Supported expression and statement forms
4 Truthiness and booleans
5 Bindings and return behavior
6 Functions, calls, and Java  Script interop
7 Numbers and arithmetic
8 Comparisons and equality
9 Strings and regular expressions
10 Lists, vectors, and array-backed sequences
11 Hashes
12 Exceptions
13 Gregor-style date and time helpers
13.1 Representation and the Java  Script/  Racket boundary
13.2 Basic construction and access
13.3 Parsing browser input values
13.4 Formatting
13.5 Native Java  Script Date interoperability
13.6 Limitations
14 DOM and browser-oriented code
15 Generated code style
16 Testing infrastructure
17 Package layout
18 Limitations and non-goals
19 Extending js-maker
20 Further examples
9.2.0.5

js-maker: a Syntax-Driven Racket-to-JavaScript Generator🔗ℹ

Hans Dijkema <>

 (require js-maker) package: js-maker

js-maker is a small, syntax-driven JavaScript generator for writing a practical JavaScript subset in Racket notation. It provides two macros, js and js/expression. Both macros run at expansion time and return JavaScript source code as a string. The generated JavaScript can then be embedded in a page, written to a file, tested with Node or another JavaScript engine, or used as part of a larger code-generation workflow.

The package is deliberately not a full Racket compiler. It recognizes a well-defined set of Racket-like forms and maps them to JavaScript while trying to preserve important Racket conventions such as #f-only falsiness, sequential let* bindings, and single evaluation of chained comparison operands. Unsupported forms should fail during macro expansion rather than silently emit JavaScript with different semantics.

1 Public API🔗ℹ

syntax

(js form ...)

Translates one or more Racket-like forms to JavaScript statement code. The result is a string. This is the usual entry point for functions, classes, assignments, DOM scripts, and top-level JavaScript snippets.

(displayln
 (js
  (define (square x)
    (return (* x x)))))

emits JavaScript similar to:

function square(x) {
  return (x * x);
}

Inside function bodies, the last expression is returned automatically unless it is already a statement form such as return, define, set!, while, or for. At the top level of a js form, js-maker also returns the value of a final value-producing form, such as let, begin, if, or with-handlers. This makes js output suitable as the body of a WebView runJavaScript wrapper function.

For example:

(let ([html "<h1>Hi</h1>"])
  (displayln
   (js
    (let ([el (send document getElementById 'test)])
      (set! (js-dot el innerHTML) (eval html))
      #t))))

emits JavaScript similar to:

{
  let el = document.getElementById("test");
  el.innerHTML = "<h1>Hi</h1>";
  return true;
}

syntax

(js/expression expr)

Translates a single Racket-like expression to a JavaScript expression string. Complex control forms are wrapped in immediately-invoked function expressions when JavaScript has no direct expression equivalent.

(displayln
 (js/expression
  (let loop ([i 0] [acc 0])
    (if (< i 5)
        (loop (+ i 1) (+ acc i))
        acc))))

The named let is emitted as a loop when it is used as a tail-recursive self-call.

2 Mental model🔗ℹ

The generator is best understood as a source-to-source translator over syntax. The input is converted to datum form and matched against the supported surface language. The Racket code is normally not evaluated. The deliberate exception is (eval racket-expr), which interpolates a Racket value into the JavaScript source text. The expression is evaluated in the lexical context of the js or js/expression use and its value is emitted as a JavaScript literal. This has a few important consequences:

  • Only forms known to js-maker are translated specially. Unknown calls are emitted as JavaScript calls with translated arguments.

  • Identifiers are mapped to JavaScript identifiers. Reserved words are avoided in variable position, while method/property names may use modern JavaScript reserved property names such as catch.

  • Macros from other Racket modules are not expanded by js-maker. If a macro should be supported, add an explicit js-maker form or compile the expanded shape.

  • The output is source text, not a JavaScript AST. Statement output is pretty-printed and indented, but some expression output uses inline helper functions/IIFEs to preserve semantics.

2.1 Racket value interpolation with eval🔗ℹ

The form (eval racket-expr) is an interpolation escape hatch inherited from the original transformer. It evaluates racket-expr as Racket in the use-site lexical context and then splices the resulting value into the JavaScript source as a literal. This makes surrounding Racket bindings visible to the interpolation expression.

Racket source

  

Generated JavaScript

(let ([x 10]

      [y 20])

  (js (let ([a (eval (* x y))])

        (return (* a a)))))

  

{

  let a = 200;

  return (a * a);

}

Racket source

  

Generated JavaScript

(js/expression (array (eval (+ 1 2))

                      (eval (string-append "a" "b"))))

  

[3, "ab"]

This is not JavaScript eval. To call JavaScript eval, call the JavaScript function explicitly, for example (send window eval "1 + 2"). Racket-side eval is best used for constants, generated literal data, and small configuration values that are known while the JavaScript source is being constructed. It should not be used for run-time browser state, DOM access, or user input.

3 Supported expression and statement forms🔗ℹ

The following Racket forms are supported as either expressions, statements, or both, depending on context:

The sequence forms recognized by for and friends are in-list, in-vector, in-string, and in-range.

4 Truthiness and booleans🔗ℹ

Racket treats only #f as false. JavaScript treats false, 0, "", null, undefined, and NaN as falsey. The generator therefore uses Racket-style truth tests where necessary. For example, if, when, unless, cond, and, or, and filter test against false when the input expression could be any value.

When the operands are known boolean-producing expressions, js-maker emits ordinary JavaScript boolean operators. For example:

Racket source

  

Generated JavaScript

(js/expression (and (> x 10) (< x 15)))

  

((x > 10) && (x < 15))

A general and still returns the first #f or the final value, and a general or still returns the first non-#f value.

5 Bindings and return behavior🔗ℹ

let evaluates all right-hand sides before introducing the JavaScript bindings. This avoids temporal-dead-zone bugs for Racket code such as (let ([x x]) ...), where the right-hand side must see an outer binding.

let* is emitted directly in the common sequential case:

Racket source

  

Generated JavaScript

(js

 (let* ([x 10]

        [y (+ x x)])

   (return y)))

  

{

  let x = 10;

  let y = (x + x);

  return y;

}

If a let* right-hand side mentions the identifier being introduced, js-maker uses a temporary variable so that Racket scoping is preserved and JavaScript’s temporal dead zone is avoided.

A return form emits a JavaScript return statement in statement context. (return) emits return undefined;. In expression context, (return e) is treated as e, which is useful for lambda bodies written in an explicit-return style.

6 Functions, calls, and JavaScript interop🔗ℹ

Normal calls are emitted as JavaScript calls. A lambda in callee position is parenthesized so the result is a valid JavaScript function expression:

(function(...args) {
  return console.log(args);
})(exn);

Interop forms provide direct access to JavaScript object and method syntax:

  • (send obj method arg ...) emits obj.method(arg, ...).

  • (new cls arg ...) emits new cls(arg, ...).

  • (js-ref obj key) emits obj[key].

  • (js-dot obj field) emits obj.field. It is the preferred explicit form for property access in generated code.

  • (set! (js-dot obj field) value) emits obj.field = value.

  • (set! expr.field value) is also accepted by the reader as (set! expr .field value) and is emitted as a direct property assignment. This is mainly a convenience for DOM code such as (set! (send document getElementById 'test) .innerHTML html).

  • (set-prop! obj key value) emits obj[key] = value.

  • (delete-prop! obj key) and (js-delete obj key) emit JavaScript delete.

  • (array v ...) emits a JavaScript array literal.

  • (object key value ...) emits a JavaScript object literal.

Object destructuring is available through let-object:

(js
 (define (describe person)
   (let-object ([name 'name]
                [age  'age 0])
               person
     (return (string-append name ":" (number->string age))))))

Classes can be generated with define-class. Constructors may contain simple default values:

(js
 (define-class Greeter
   (constructor ([name "world"])
     (set! (js-dot this name) name))
   (method greet ()
     (return (string-append "Hello " (js-dot this name))))))

7 Numbers and arithmetic🔗ℹ

The arithmetic operators +, -, *, /, quotient, remainder, modulo, add1, sub1, abs, floor, ceiling, round, max, min, sqrt, sqr, expt, sin, cos, tan, asin, acos, atan, log, and exp are supported.

The predicates zero?, positive?, negative?, even?, and odd? are also supported.

Division is intentionally special. JavaScript evaluates 10 / 0 to Infinity; exact Racket division by zero raises an exception. js-maker therefore emits a run-time zero check for /. This allows the supported with-handlers subset to catch the common division-by-zero case.

8 Comparisons and equality🔗ℹ

The comparison operators =, ==, <, >, <=, and >= support n-ary comparisons. Simple chained comparisons are emitted directly:

Racket source

  

Generated JavaScript

(js/expression (< x y 10))

  

((x < y) && (y < 10))

If a chained comparison would otherwise evaluate an intermediate expression more than once, js-maker uses temporaries instead.

equal? uses a pragmatic deep-equality helper based on JavaScript values and JSON-style comparison for arrays and objects. eq? and eqv? use Object.is. This is useful for tests and simple data, but it is not a complete implementation of Racket’s equality predicates.

9 Strings and regular expressions🔗ℹ

The string operations string-append, substring, string-upcase, string-downcase, string-trim, string-contains?, string=?, string-ci=?, string<?, string>?, string<=?, string>=?, number->string, symbol->string, string->symbol, and string->number are supported.

Racket #rx and common #px patterns are translated to JavaScript RegExp values when the syntax is compatible. The supported operations are regexp?, pregexp?, regexp-match, regexp-match?, regexp-match*, regexp-match-positions, regexp-split, regexp-replace, regexp-replace*, and regexp-quote.

Match results are normalized to Racket-like values: a failed match becomes false, successful matches become JavaScript arrays, and an unmatched optional capture becomes false. Known incompatible constructs such as inline option groups and atomic groups are rejected instead of being silently miscompiled. Byte regexps are not supported.

10 Lists, vectors, and array-backed sequences🔗ℹ

Lists and vectors are represented as JavaScript arrays. This is the most useful mapping for JavaScript interoperability, but it is not the same as Racket’s linked-pair representation.

Supported list/vector operations include:

member returns a tail array or false. memq and memv use Object.is. filter keeps every value that is not false, preserving Racket truthiness rather than JavaScript truthiness.

11 Hashes🔗ℹ

Hashes are represented as plain JavaScript objects. This supports common symbol/string-keyed data well, but does not implement Racket’s arbitrary key semantics, custom equality, mutation contracts, weak hashes, or the differences between hash, hasheq, and hasheqv.

Supported hash operations include:

hash-ref supports a default value and a default thunk. Mutating operations mutate the JavaScript object representation directly. Immutable operations create shallow copies.

12 Exceptions🔗ℹ

A narrow with-handlers subset is supported:

(js
 (with-handlers ([exn? (lambda (e)
                         (displayln (exn-message e)))])
   (/ 10 0)))

This emits a JavaScript try/catch. Only the generic exn? predicate is supported. More specific Racket exception predicates are rejected because JavaScript has a single catch channel and no Racket exception hierarchy. The handler is called with the JavaScript thrown value. The helper exn-message extracts .message when present and otherwise converts the thrown value to a string.

This feature is useful for generated JavaScript that throws JavaScript Error objects, including js-maker’s division-by-zero check. It does not model exception marks, continuable exceptions, parameterization, or Racket’s full exception hierarchy.

13 Gregor-style date and time helpers🔗ℹ

The generator includes a small JavaScript-side value model for a subset of Gregor-style date and time operations. This layer is intended for browser input/output code and for JSON-safe communication between generated JavaScript and Racket. It is not a complete implementation of Gregor.

Prefixes are intentionally not hardcoded. A call such as (g:date 2026 5 27), (gregor:date 2026 5 27), or (date 2026 5 27) is matched by the local name date. This keeps the generator independent of the import prefix used by the Racket module.

Supported local names include date, time, datetime, moment, make-date, make-time, make-datetime, make-moment, parse-date, parse-time, parse-datetime, parse-moment, string->date, string->time, string->datetime, date->string, time->string, datetime->string, moment->string, date?, time?, datetime?, moment?, ->year, ->month, ->day, ->hours, ->minutes, ->seconds, ->js-date, and js-date->datetime.

13.1 Representation and the JavaScript/Racket boundary🔗ℹ

Plain dates, times, datetimes, and moments are represented as tagged JavaScript objects. They are deliberately not represented as native JavaScript Date objects by default, because Date always carries timezone and instant semantics. A plain HTML input type="date" value such as "2026-05-27" should not accidentally shift to another day when serialized or interpreted in a different timezone.

A date value is therefore JSON-compatible data:

{ "$type": "gregor-date", "year": 2026, "month": 5, "day": 27 }

A time value is also JSON-compatible:

{ "$type": "gregor-time", "hour": 13, "minute": 45,
  "second": 0, "millisecond": 0 }

A datetime or moment is represented similarly:

{ "$type": "gregor-datetime", "year": 2026, "month": 5, "day": 27,
  "hour": 13, "minute": 45, "second": 30, "millisecond": 0 }

This convention is important for WebView integration. Generated JavaScript may freely use native JavaScript values internally, but values that cross back to Racket should be JSON-compatible. Non-JSON JavaScript values such as undefined, NaN, Infinity, Date, Map, Set, Error, DOM nodes, and functions should be converted explicitly before returning them to Racket.

In a WebView bridge this usually means that the JavaScript wrapper around the user code should return a JSON string, or at least a plain JSON object, whose result field has already been encoded. Native Date values should be encoded as tagged values, for example:

{ "$type": "js-date", "iso": "2026-05-27T11:45:30.000Z" }

Use ->js-date only when native JavaScript Date behavior is required inside JavaScript code. Before such a value is returned across the Racket boundary, convert it back with js-date->datetime or let the WebView boundary encoder tag it as "js-date".

13.2 Basic construction and access🔗ℹ

Racket source

  

Generated JavaScript

(js/expression (date 2026 5 27))

  

({"$type":"gregor-date","year":2026,"month":5,"day":27})

Racket source

  

Generated JavaScript

(js/expression (time 13 45 30))

  

({"$type":"gregor-time","hour":13,"minute":45,

  "second":30,"millisecond":0})

Racket source

  

Generated JavaScript

(js/expression (datetime 2026 5 27 13 45 30))

  

({"$type":"gregor-datetime","year":2026,"month":5,"day":27,

  "hour":13,"minute":45,"second":30,"millisecond":0})

Field helpers read from these tagged objects:

(js/expression
 (let ([d (date 2026 5 27)])
   (array (->year d) (->month d) (->day d))))

which evaluates to a JavaScript array equivalent to:

[2026, 5, 27]

Predicate helpers test the tag:

(js/expression
 (array (date? (date 2026 5 27))
        (time? (date 2026 5 27))))

which evaluates to:

[true, false]

13.3 Parsing browser input values🔗ℹ

The intended input formats are deliberately simple and ISO-like. They match the strings normally returned by HTML date/time controls:

  • input type="date": "2026-05-27".

  • input type="time": "13:45" or "13:45:30".

  • input type="datetime-local": "2026-05-27T13:45" or "2026-05-27T13:45:30".

Examples:

(js/expression (string->date "2026-05-27"))
(js/expression (string->time "13:45"))
(js/expression (string->time "13:45:30"))
(js/expression (string->datetime "2026-05-27T13:45"))
(js/expression (string->datetime "2026-05-27T13:45:30"))

For project code that accepts both minute-precision and second-precision datetime-local strings, a Racket-side helper might look like this:

(define (string->datetime s)
  (with-handlers ([exn:fail?
                   (lambda (e)
                     (g:parse-moment s "yyyy-MM-dd'T'HH:mm:ss"))])
    (g:parse-moment s "yyyy-MM-dd'T'HH:mm")))

The current JavaScript backend documents with-handlers as a generic JavaScript catch facility. If this helper is compiled with js-maker, the exception predicate should either be the supported generic predicate, or the backend should explicitly treat exn:fail? as an alias for the same JavaScript catch behavior. The important point is that parsing failures are a boundary concern: browser strings are converted to tagged, JSON-safe date/time values before they are returned to Racket or stored in application data.

13.4 Formatting🔗ℹ

Formatting helpers produce simple ISO-like strings:

(js/expression (date->string (date 2026 5 27)))
(js/expression (time->string (time 13 45 30)))
(js/expression (datetime->string (datetime 2026 5 27 13 45 30)))

They are intended for stable machine-readable values, not for full Gregor or locale-sensitive formatting. Arbitrary format strings, localized month names, calendar systems, week numbers, and timezone formatting are intentionally out of scope.

13.5 Native JavaScript Date interoperability🔗ℹ

Use ->js-date when a native JavaScript API requires a Date object:

(js/expression
 (let* ([dt (datetime 2026 5 27 13 45 30)]
        [jsd (->js-date dt)])
   (send jsd toISOString)))

Conversely, if JavaScript returns a native Date, convert it before it crosses the Racket boundary:

(js/expression
 (js-date->datetime (new Date "2026-05-27T11:45:30.000Z")))

The result should be a tagged JSON-compatible datetime value, not a raw Date. This keeps WebView return values predictable when they are passed through JSON.stringify, QVariant, QJsonObject, or Racket’s JSON reader.

13.6 Limitations🔗ℹ

The Gregor compatibility layer is intentionally small:

  • It does not implement the full Gregor API.

  • Prefixes are ignored; only the local identifier name is used.

  • Date/time values are tagged JSON-compatible objects, not Racket structs.

  • Native JavaScript Date is an explicit interop representation, not the default representation.

  • Parsing and formatting are ISO-like and intentionally conservative.

  • Timezone, locale, calendar, duration, period, and arbitrary format-string semantics are not modeled.

14 DOM and browser-oriented code🔗ℹ

js-maker can generate browser code through the JavaScript interop forms. For example:

(js
 (let* ([p (send document querySelector "p")])
   (set! p.innerHTML
         (regexp-replace* #px"\\b\\w{9,}\\b"
                          p.innerHTML
                          (lambda (word)
                            (string-append "<span style=\"background: yellow\">"
                                           word
                                           "</span>"))))))

The DOM regression tests use fake DOM objects in Node. This tests the generated JavaScript syntax and behavior without requiring a real browser. Browser-only APIs such as real layout, events, CSSOM, and asynchronous page loading are out of scope for the core test harness.

15 Generated code style🔗ℹ

Statement output is indented and block-oriented. Common cases such as simple let*, boolean and/or, and simple chained comparisons are emitted directly. More complex cases use generated temporary variables and immediately-invoked function expressions. That output is more verbose, but it preserves evaluation order, Racket truthiness, or expression/statement context.

The current implementation is a string emitter rather than a structured JavaScript AST pretty-printer. Improving the formatter is a separate concern from extending the supported language.

16 Testing infrastructure🔗ℹ

The regression suite lives in "testing/". The main entry point is "testing/jsmaker-regressions.rkt". It includes core expression tests, regexp tests, program tests, DOM exercises, practical JavaScript use cases, list tests, and hash tests.

The test framework writes generated JavaScript to temporary files and runs it with a JavaScript executor. The executor module searches for engines such as Node, Deno, Bun, QuickJS, V8 d8, JavaScriptCore jsc, SpiderMonkey js, and an optional Chromium fallback. Node is the preferred default.

If no JavaScript engine is available, tests are generated but execution is skipped with clear warnings and a successful exit status. This is intentional so package tests do not fail merely because a JavaScript runtime is missing. Set JSMAKER_REQUIRE_ENGINE=1 or JSMAKER_REQUIRE_NODE=1 to make a missing engine a hard failure.

Useful commands are:

raco make main.rkt testing/jsmaker-regressions.rkt scrbl/jsmaker.scrbl
racket testing/jsmaker-regressions.rkt
raco test testing/jsmaker-regressions.rkt

17 Package layout🔗ℹ

The package uses this layout:

js-maker/
  main.rkt
  info.rkt
  private/
  testing/
  demo/
  scrbl/

"main.rkt" is the public module. "testing/" contains the executor and regression framework. "demo/" contains generated-code examples. "scrbl/" contains this reference and the use-case document. The "private/" directory contains compatibility/helper material from the source project. The current public module and tests do not depend on those private files; they are kept for downstream compatibility and are omitted from compilation and the package test entry point in "info.rkt".

18 Limitations and non-goals🔗ℹ

js-maker intentionally implements a pragmatic subset. The following are important limitations:

  • It does not compile arbitrary Racket programs. There is no module compiler, macro expander, contract compiler, class compiler, continuation implementation, parameter model, or place/thread model.

  • Racket’s numeric tower is not implemented. JavaScript numbers are used; exactness, rationals, complex numbers, flonum/fixnum distinctions, and overflow behavior are not modeled.

  • Lists are JavaScript arrays, not chains of pairs. Dotted pair semantics are only approximated where useful.

  • Hashes are JavaScript objects, not true Racket hash tables. Arbitrary object keys and equality-mode distinctions are not preserved.

  • Regular expression support targets the common intersection of Racket regexps and JavaScript RegExp. Incompatible constructs are rejected where known.

  • Exception support is limited to generic exn? handlers over JavaScript try/catch.

  • Gregor support is a small compatibility layer, not the full Gregor API.

  • The emitter generates JavaScript source strings. It is not yet an AST optimizer or full pretty-printer.

19 Extending js-maker🔗ℹ

New forms are normally added in one of three places in "main.rkt":

  • Statement forms in the statement compiler.

  • Expression forms in the expression compiler.

  • Function/operator mappings in the operator table.

Every extension should include a regression test that compiles the Racket form, runs the generated JavaScript with the executor, and checks the result. When a new feature needs JavaScript environment support, keep that support in the test harness rather than hiding raw JavaScript inside the feature implementation.

20 Further examples🔗ℹ

The companion use-case manual contains practical examples such as DOM manipulation, Set, currying, object destructuring, timers, fetch, sorting, binary search, and map-based counting. Each use case shows the Racket/js-maker source, representative JavaScript output, and the behavior tested by the regression suite.

The source file for that companion manual is "scrbl/usecases.scrbl". The relative link above is used instead of other-doc because the latter can render as an unresolved (part ... "top") tag when the two documents are built outside Racket’s installed documentation index.