js-maker simpeler

This commit is contained in:
2026-06-08 12:15:04 +02:00
parent cffb5ec91a
commit a9610e6e0c
5 changed files with 92 additions and 1003 deletions
+7 -148
View File
@@ -1,153 +1,12 @@
# js-maker # js-maker
A syntax-driven Racket-to-JavaScript macro module. A compact Racket-to-JavaScript string maker macro.
This js-maker module was started as part of racket-webview, and has since This js-maker 3 package is a clean restart from `js-transform.rkt`. It exports
evolved under supervision of the author using ChatGPT as AI agent. only:
## Layout - `js`
- `js1`
```text There is deliberately no `js/expression` compatibility macro in this branch.
js-maker/ Use `js1` when the expression-level generator is needed directly.
main.rkt public macro module
info.rkt package metadata and package test entry
scrbl/
js-maker.scrbl Scribble documentation
testing/
jsmaker-executors.rkt JavaScript engine discovery/execution
jsmaker-test-framework.rkt JS regression framework
jsmaker-test-runner.rkt old-name compatibility wrapper
jsmaker-regression.rkt core expression tests
jsmaker-regexp-regression.rkt regexp tests
jsmaker-program-regression.rkt larger program tests
jsmaker-regressions.rkt aggregate test entry
demo/
show-jsmaker-output.rkt
show-optimized.rkt
```
## Added language support
This package includes conservative support for:
- `(with-handlers ([exn? handler]) body ...)`, translated to JavaScript
`try`/`catch`. Only generic `exn?` predicates are accepted.
- Gregor-style local names such as `date`, `time`, `moment`, `parse-date`,
`parse-time`, `parse-moment`, `date->string`, `time->string`, `->year`,
`->month`, `->day`, `->hours`, `->minutes`, `->seconds`, `->js-date`, and
`js-date->datetime`. Import prefixes are deliberately not hardcoded; the
compiler matches on the local identifier name after any `prefix:` part.
## Run tests
From the directory above `jsmaker`:
```bash
raco make jsmaker/main.rkt jsmaker/testing/jsmaker-regressions.rkt \
jsmaker/scrbl/jsmaker.scrbl
racket jsmaker/testing/jsmaker-regressions.rkt
raco test -p jsmaker
```
The test framework looks for JavaScript engines such as `node`, `deno`,
`bun`, `qjs`, `d8`, `jsc`, `js`. Chromium is only used when explicitly selected
or when `JSMAKER_BROWSER_FALLBACK=1` is set.
When no JavaScript engine is available, the tests generate the JavaScript test
files and use an explicit `non-failing-javascript-stub`. The stub prints notes
to stdout, does not execute the generated JavaScript, and succeeds unless
`JSMAKER_REQUIRE_ENGINE` or `JSMAKER_REQUIRE_NODE` is set. This avoids package
server failures caused solely by a missing JavaScript runtime.
Useful environment variables:
```bash
JSMAKER_ENGINE=auto|node|deno|bun|qjs|d8|jsc|js|chromium
JSMAKER_ENGINE_PATH=/path/to/executable
JSMAKER_NODE=/path/to/node
JSMAKER_REQUIRE_ENGINE=1
JSMAKER_ENGINE_TIMEOUT_SECONDS=15
JSMAKER_BROWSER_FALLBACK=1
```
## Suggested start prompt for future work
Use this prompt when asking ChatGPT to make a new Racket module or extend this
one:
```text
Work on a Racket module/package in a versioned build directory.
Important:
- Always create a new subdirectory for the deliverable, for example
/mnt/data/<project>-build-NNN/<collection-name>.
- Do not work directly in /mnt/data with loose files that have the same names;
avoid version confusion by copying and patching everything inside that build
directory.
- Keep the package structure stable:
- main.rkt for the public module;
- testing/ for test infrastructure and regression tests;
- demo/ for demonstration files;
- info.rkt for package metadata and test entry points.
- Adjust require paths to that structure before testing.
- Test with Racket itself, for example:
/tmp/racket/bin/raco make <collection>/main.rkt <collection>/testing/<tests>.rkt
/tmp/racket/bin/racket <collection>/testing/<tests>.rkt
- If JavaScript execution is needed, use a separate executor/test-framework
module. Tests must not fail solely because node/deno/bun/qjs is missing; in
that case they should use an explicit non-failing JavaScript stub that reports
this to stdout, unless a REQUIRE environment variable is set.
- Do not use shell-based internet access for dependencies. If packages are
needed, fetch them through the rktsndbx bootstrap/package-index flow.
- Deliver a zip file containing exactly the tested build directory.
- Briefly report which commands were run, what the test results were, and which
zip contains the tested result.
```
## Latest tested fix
This build includes the `with-handlers` callee-position fix for inline lambda
handlers, including rest-argument handlers such as `(lambda args ...)`. It also
fixes top-level `js` statement-context handling in general: `js` now emits
program/statement text and does not invent an implicit top-level `return`. This
keeps snippets valid when they are passed directly to WebView
`runJavaScript`/`evaluateJavaScript`, where a top-level JavaScript `return` is
a syntax error. Use `js/expression` when a generated JavaScript value is needed.
The build also adds a Racket-like division-by-zero runtime check for `/`, so the
generic `exn?` handler subset can catch `(/ 10 0)`.
## JavaScript use case demos
The package includes a larger set of JavaScript use case snippets in
`demo/js-usecases.rkt`. They are written in the Racket surface syntax accepted
by `js` and compiled to JavaScript by the macro. The generated JavaScript is
also written to `demo/js-usecases.generated.js`.
The corresponding regression tests live in `testing/jsmaker-usecases.rkt` and
are included by `testing/jsmaker-regressions.rkt`. The test framework now awaits
Promise-valued tests, so asynchronous examples such as the Fetch API can be
checked with Node as well.
Covered use cases include random numbers, `Set`, JavaScript falsey values,
currying, object destructuring, `setInterval`/`clearInterval`, object property
get/set/delete, string concatenation order, `Object.freeze`/`Object.seal`,
switch/case, classes with constructor defaults, sorting objects, array deletion
techniques, Bubble Sort, recursive Binary Search, `Map` counting, DOM HTML
access, anagram checks, pair-sum checks, and Fetch API result/error handling.
## Use-case documentation
The file `scrbl/usecases.scrbl` documents the JavaScript use cases from
`demo/js-usecases.rkt`. Each use case is shown as Racket/js-maker source next
to representative generated JavaScript, followed by the behavior covered by the
regression test.
The use-case tests in `testing/jsmaker-usecases.rkt` intentionally use
`js/expression` for the test calls wherever possible. Raw JavaScript is kept
only for small test-harness preambles such as fake timers, fake DOM objects, and
fake fetch.
## Hash regression tests
This build adds `testing/jsmaker-hash-regression.rkt`, covering common hash operations such as `hash`, `make-hash`, `hash-ref`, `hash-set`, `hash-set!`, `hash-remove`, `hash-remove!`, `hash-update`, `hash-update!`, `hash-clear`, `hash-clear!`, `hash-copy`, `hash-keys`, `hash-values`, `hash->list`, `hash-map`, and `hash-for-each`. The current JavaScript backend represents hashes as plain JavaScript objects, so this is a practical string/symbol-key subset rather than a full Racket hash-table implementation for arbitrary keys.
+7 -27
View File
@@ -3,35 +3,15 @@
(define collection "js-maker") (define collection "js-maker")
(define version "0.3") (define version "0.3")
(define license 'MIT) (define license 'MIT)
(define pkg-desc "Syntax-driven Racket-to-JavaScript maker macro.") (define pkg-desc "Small syntax-driven Racket-to-JavaScript maker macro.")
(define pkg-authors '(hnmdijkema)) (define pkg-authors '(hnmdijkema))
(define deps '("base")) (define deps '("base"))
(define build-deps '("scribble-lib" "racket-doc")) (define build-deps '("scribble-lib" "racket-doc" "rackunit-lib"))
(define scribblings (define scribblings
'(("scrbl/js-maker.scrbl" () (library)) '(("scrbl/js-maker.scrbl" () (library))))
("scrbl/usecases.scrbl" () (library))
)
)
;; Running the package test suite should invoke exactly the maintained ;; js-maker 3 is a clean restart. The old demo/testing tree from the larger
;; regression entry point. The regression framework itself skips JavaScript ;; branch is intentionally not shipped. The maintained package test suite is
;; execution with warnings when no JavaScript engine is available, unless ;; this compact smoke test.
;; JSMAKER_REQUIRE_ENGINE or JSMAKER_REQUIRE_NODE is set. (define test-include-paths '("smoke-test.rkt"))
(define test-include-paths '("testing/jsmaker-regressions.rkt"))
;; These files are supporting/reference files in this package layout and are
;; not part of the package test entry point.
(define test-omit-paths
'("demo/show-jsmaker-output.rkt"
"demo/show-optimized.rkt"
"testing/jsmaker-executors.rkt"
"testing/jsmaker-test-framework.rkt"
"testing/jsmaker-test-runner.rkt"
"testing/jsmaker-regression.rkt"
"testing/jsmaker-regexp-regression.rkt"
"testing/jsmaker-program-regression.rkt"
"testing/jsmaker-dom-exercises.rkt"
"testing/jsmaker-usecases.rkt"
"testing/jsmaker-list-regression.rkt"
"testing/jsmaker-hash-regression.rkt"))
+11 -1
View File
@@ -173,9 +173,13 @@
;; First evaluate all RHS expressions into temporary names. ;; First evaluate all RHS expressions into temporary names.
;; This preserves ordinary Racket/Scheme `let` scoping: ;; This preserves ordinary Racket/Scheme `let` scoping:
;; newly bound names are not visible to other RHS expressions. ;; newly bound names are not visible to other RHS expressions.
;; The real bindings are introduced in an inner block so
;; JavaScript `let` TDZ rules do not shadow RHS references.
(string-append "const " (js-id tmp) " = " (js1 init) ";\n") ... (string-append "const " (js-id tmp) " = " (js1 init) ";\n") ...
"{\n"
(string-append "let " (js-id id) " = " (js-id tmp) ";\n") ... (string-append "let " (js-id id) " = " (js-id tmp) ";\n") ...
(js body ...) (js body ...)
"}\n"
"}"))])) "}"))]))
;; ------------------------------------------------------------------------- ;; -------------------------------------------------------------------------
@@ -235,11 +239,15 @@
#'(string-append "{\n" #'(string-append "{\n"
;; Initial expressions are evaluated before the loop ;; Initial expressions are evaluated before the loop
;; variables are introduced, like named `let` in Racket. ;; variables are introduced, like named `let` in Racket.
;; The loop variables live in an inner block for the same
;; reason as ordinary `let`: avoid JavaScript TDZ shadowing.
(string-append "const " (js-id tmp) " = " (js1 init) ";\n") ... (string-append "const " (js-id tmp) " = " (js1 init) ";\n") ...
"{\n"
(string-append "let " (js-id id) " = " (js-id tmp) ";\n") ... (string-append "let " (js-id id) " = " (js-id tmp) ";\n") ...
"while (true) {\n" "while (true) {\n"
(js-loop-tail loop-name (id ...) body ...) (js-loop-tail loop-name (id ...) body ...)
"}\n" "}\n"
"}\n"
"}"))])) "}"))]))
;; ------------------------------------------------------------------------- ;; -------------------------------------------------------------------------
@@ -248,7 +256,7 @@
;; ------------------------------------------------------------------------- ;; -------------------------------------------------------------------------
(define-syntax (js1 stx) (define-syntax (js1 stx)
(syntax-case stx () (syntax-case stx (define lambda λ if set! let let* begin return quote eval)
[(_ (define (f arg ...) body ...)) #'(js-define-function f (arg ...) body ...)] [(_ (define (f arg ...) body ...)) #'(js-define-function f (arg ...) body ...)]
[(_ (define name expr)) #'(js-define-value name expr)] [(_ (define name expr)) #'(js-define-value name expr)]
[(_ (lambda (arg ...) body ...)) #'(js-lambda (arg ...) body ...)] [(_ (lambda (arg ...) body ...)) #'(js-lambda (arg ...) body ...)]
@@ -300,6 +308,8 @@
[(_ statement ...) [(_ statement ...)
(string-append (js-stmt (js1 statement)) ...)])) (string-append (js-stmt (js1 statement)) ...)]))
;; ------------------------------------------------------------------------- ;; -------------------------------------------------------------------------
;; Examples ;; Examples
;; ------------------------------------------------------------------------- ;; -------------------------------------------------------------------------
+37 -826
View File
@@ -1,842 +1,53 @@
#lang scribble/manual #lang scribble/manual
@(require scribble/core @(require (for-label racket/base
(for-label racket/base js-maker))
"../main.rkt"))
@(define (compact-items . xs) @title{js-maker}
(apply itemlist #:style 'compact xs)) @author{Hans Dijkema}
@(define (note . xs)
(nested #:style 'inset (apply list xs)))
@(define (rkt+js rkt js)
(tabular #:style 'boxed #:sep (hspace 2)
(list (list (bold "Racket source") (bold "Generated JavaScript"))
(list (verbatim rkt) (verbatim js)))))
@title{js-maker: a Syntax-Driven Racket-to-JavaScript Generator}
@author+email["Hans Dijkema" ""]
@defmodule[js-maker] @defmodule[js-maker]
@bold{js-maker} is a small, syntax-driven JavaScript generator for writing a @racketmodname[js-maker] provides a deliberately small syntax-driven macro for
practical JavaScript subset in Racket notation. It provides two macros, making JavaScript strings from a limited Racket-like surface syntax. This is a
@racket[js] and @racket[js/expression]. In the ordinary case the macros clean js-maker 3 restart based on the compact @filepath{js-transform.rkt}
expand to a string containing JavaScript source code. When the source contains implementation.
@racket[(inject racket-expr)] or its historical alias @racket[(eval racket-expr)],
the macros instead expand to a Racket expression that computes the JavaScript
source string at run time. 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 @racket[#f]-only falsiness,
sequential @racket[let*] bindings, and single evaluation of chained comparison
operands. Unsupported forms should fail during macro expansion rather than
silently emit JavaScript with different semantics.
@section{Public API}
@defform[(js form ...)]{ @defform[(js form ...)]{
Translates one or more Racket-like forms to JavaScript @emph{statement} code. Generates JavaScript statements for each @racket[form] and concatenates them.
The result is a string. This is the usual entry point for functions, classes, The generated JavaScript is returned as a string.
assignments, DOM scripts, and top-level JavaScript snippets.
@racketblock[ @racketblock[
(displayln (js
(js (define (sum-to n)
(define (square x)
(return (* x x)))))
]
emits JavaScript similar to:
@codeblock{
function square(x) {
return (x * x);
}
}
Inside function bodies, the last expression is returned automatically unless it
is already a statement form such as @racket[return], @racket[define],
@racket[set!], @racket[while], or @racket[for]. At the top level of a
@racket[js] form, however, js-maker emits statement code and does not invent an
implicit @tt{return}. This keeps the generated code valid when it is handed
directly to a WebView @tt{runJavaScript} or @tt{evaluateJavaScript} entry point,
where a top-level JavaScript @tt{return} would be an @tt{Illegal return}
syntax error. Use @racket[js/expression] when a generated JavaScript value is
needed.
For example:
@racketblock[
(let ([html "<h1>Hi</h1>"])
(displayln
(js
(let ([el (send document getElementById 'test)])
(set! (js-dot el innerHTML) (inject html))
#t))))
]
emits JavaScript similar to:
@codeblock{
{
let el = document.getElementById("test");
el.innerHTML = "<h1>Hi</h1>";
true;
}
}
}
@defform[(js/expression expr)]{
Translates a single Racket-like expression to a JavaScript @emph{expression}
string. Complex control forms are wrapped in immediately-invoked function
expressions when JavaScript has no direct expression equivalent.
@racketblock[
(displayln
(js/expression
(let loop ([i 0] [acc 0]) (let loop ([i 0] [acc 0])
(if (< i 5) (if (> i n)
(loop (+ i 1) (+ acc i)) (return acc)
acc)))) (loop (+ i 1) (+ acc i))))))]
]
The named @racket[let] is emitted as a loop when it is used as a tail-recursive
self-call.
} }
@section{Mental model} @defform[(js1 form)]{
Generates JavaScript for a single expression or syntactic form and returns it as
a string. Use this when you want the expression-level generator directly.
The generator is best understood as a source-to-source translator over syntax. @racketblock[
Most input is converted to datum form and matched against the supported surface (js1 (+ 1 2))]
language. The Racket code is normally not evaluated. The deliberate exception
is @racket[(inject racket-expr)], and the compatible older spelling
@racket[(eval racket-expr)]. These forms mark a Racket expression whose value
must be serialized as a JavaScript literal while the generated source string is
being constructed. The expression is evaluated in the lexical context of the
@racket[js] or @racket[js/expression] use. This has a few important
consequences:
@compact-items[
@item{Only forms known to js-maker are translated specially. Unknown calls are
emitted as JavaScript calls with translated arguments.}
@item{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 @tt{catch}.}
@item{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.}
@item{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.}
]
@subsection{Racket value interpolation with inject}
The form @racket[(inject racket-expr)] is the preferred interpolation escape
hatch. It evaluates @racket[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.
The older spelling @racket[(eval racket-expr)] is still accepted as an alias for
compatibility with existing code. New code should prefer @racket[inject],
because the form is not JavaScript @tt{eval} and does not evaluate JavaScript
source text.
@(rkt+js
#<<RKT
(let ([x 10]
[y 20])
(js/expression
(let ([a (inject (* x y))])
(* a a))))
RKT
#<<JS
(() => {
let a = 200;
return (a * a);
})()
JS
)
@(rkt+js
#<<RKT
(js/expression (array (inject (+ 1 2))
(inject (string-append "a" "b"))))
RKT
#<<JS
[3, "ab"]
JS
)
Neither @racket[inject] nor its alias @racket[eval] is JavaScript @tt{eval}.
To call JavaScript @tt{eval}, call the JavaScript function explicitly, for
example @racket[(send window eval "1 + 2")]. Racket-side interpolation 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 untrusted user input.
The name @racket[inject] was chosen over a name such as @racket[subst] because
it is not raw textual substitution. js-maker serializes the Racket value to a
JavaScript literal, including string escaping, booleans, numbers, lists, vectors,
hashes, symbols, keywords, characters, @racket[#f], @racket[#t], @racket[null],
and @racket[(void)].
@section{Supported expression and statement forms}
The following Racket forms are supported as either expressions, statements, or
both, depending on context:
@compact-items[
@item{@racket[begin], @racket[begin0], @racket[if], @racket[cond], and
@racket[case].}
@item{@racket[lambda] and @racket[λ], including rest and dotted formals.}
@item{@racket[define], function-style @racket[define], @racket[define-values],
@racket[set!], and explicit @racket[return].}
@item{@racket[let], @racket[let*], @racket[letrec], @racket[let-values],
@racket[let*-values], and named @racket[let].}
@item{@racket[when], @racket[unless], @racket[while], @racket[for],
@racket[for/list], @racket[for/vector], and @racket[for/fold].}
@item{@racket[with-handlers] for the generic @racket[exn?] case.}
@item{@racket[(inject racket-expr)] for Racket-side value interpolation into
JavaScript literals; @racket[(eval racket-expr)] is accepted as a
compatibility alias.}
@item{Interop forms such as @racket[send], @racket[new], @racket[js-ref],
@racket[js-dot], @racket[set-prop!], @racket[delete-prop!],
@racket[js-delete], @racket[array], @racket[object],
@racket[let-object], and @racket[define-class].}
]
The sequence forms recognized by @racket[for] and friends are
@racket[in-list], @racket[in-vector], @racket[in-string], and
@racket[in-range].
@section{Truthiness and booleans}
Racket treats only @racket[#f] as false. JavaScript treats @tt{false},
@tt{0}, @tt{""}, @tt{null}, @tt{undefined}, and @tt{NaN} as falsey. The
generator therefore uses Racket-style truth tests where necessary. For
example, @racket[if], @racket[when], @racket[unless], @racket[cond],
@racket[and], @racket[or], and @racket[filter] test against @tt{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:
@(rkt+js
#<<RKT
(js/expression (and (> x 10) (< x 15)))
RKT
#<<JS
((x > 10) && (x < 15))
JS
)
A general @racket[and] still returns the first @racket[#f] or the final value,
and a general @racket[or] still returns the first non-@racket[#f] value.
@section{Bindings and return behavior}
@racket[let] evaluates all right-hand sides before introducing the JavaScript
bindings. This avoids temporal-dead-zone bugs for Racket code such as
@racket[(let ([x x]) ...)], where the right-hand side must see an outer
binding.
@racket[let*] is emitted directly in the common sequential case:
@(rkt+js
#<<RKT
(js
(let* ([x 10]
[y (+ x x)])
(return y)))
RKT
#<<JS
{
let x = 10;
let y = (x + x);
return y;
}
JS
)
If a @racket[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 @racket[return] form emits a JavaScript @tt{return} statement in statement
context. @racket[(return)] emits @tt{return undefined;}. In expression
context, @racket[(return e)] is treated as @racket[e], which is useful for
lambda bodies written in an explicit-return style.
@section{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:
@codeblock{
(function(...args) {
return console.log(args);
})(exn);
} }
Interop forms provide direct access to JavaScript object and method syntax: @section{Supported core forms}
@compact-items[ The compact branch supports identifiers, quoted data, primitive literals,
@item{@racket[(send obj method arg ...)] emits @tt{obj.method(arg, ...)}.} function calls, infix arithmetic and comparison operators, @racket[if],
@item{@racket[(new cls arg ...)] emits @tt{new cls(arg, ...)}.} @racket[begin], @racket[return], @racket[set!], @racket[lambda],
@item{@racket[(js-ref obj key)] emits @tt{obj[key]}.} @racket[define], ordinary @racket[let], @racket[let*], and named
@item{@racket[(js-dot obj field)] emits @tt{obj.field}. It is the @racket[let].
preferred explicit form for property access in generated code.}
@item{@racket[(set! (js-dot obj field) value)] emits @tt{obj.field = value}.} Ordinary @racket[let] keeps Racket's parallel binding semantics. All right-hand
@item{@racket[(set! expr.field value)] is also accepted by the reader as sides are generated before the bound names are introduced, and the actual
@racket[(set! expr .field value)] and is emitted as a direct property JavaScript bindings are placed in an inner block so JavaScript temporal dead zone
assignment. This is mainly a convenience for DOM code such as rules cannot accidentally shadow the initializers.
@racket[(set! (send document getElementById 'test).innerHTML html)].}
@item{@racket[(set-prop! obj key value)] emits @tt{obj[key] = value}.} Named @racket[let] is compiled to a JavaScript @tt{while (true)} loop. A tail
@item{@racket[(delete-prop! obj key)] and @racket[(js-delete obj key)] emit call to the loop name is rewritten to parallel assignment of the loop variables
JavaScript @tt{delete}.} followed by @tt{continue}. This keeps the important loop semantics without
@item{@racket[(array v ...)] emits a JavaScript array literal.} reintroducing the large js-maker 2 implementation.
@item{@racket[(object key value ...)] emits a JavaScript object literal.}
]
Object destructuring is available through @racket[let-object]:
@racketblock[
(js
(define (describe person)
(let-object ([name 'name]
[age 'age 0])
person
(return (string-append name ":" (number->string age))))))
]
Classes can be generated with @racket[define-class]. Constructors may contain
simple default values:
@racketblock[
(js
(define-class Greeter
(constructor ([name "world"])
(set! (js-dot this name) name))
(method greet ()
(return (string-append "Hello " (js-dot this name))))))
]
@section{Numbers and arithmetic}
The arithmetic operators @racket[+], @racket[-], @racket[*], @racket[/],
@racket[quotient], @racket[remainder], @racket[modulo], @racket[add1],
@racket[sub1], @racket[abs], @racket[floor], @racket[ceiling], @racket[round],
@racket[max], @racket[min], @racket[sqrt], @racket[sqr], @racket[expt],
@racket[sin], @racket[cos], @racket[tan], @racket[asin], @racket[acos],
@racket[atan], @racket[log], and @racket[exp] are supported.
The predicates @racket[zero?], @racket[positive?], @racket[negative?],
@racket[even?], and @racket[odd?] are also supported.
Division is intentionally special. JavaScript evaluates @tt{10 / 0} to
@tt{Infinity}; exact Racket division by zero raises an exception. js-maker
therefore emits a run-time zero check for @racket[/]. This allows the
supported @racket[with-handlers] subset to catch the common division-by-zero
case.
@section{Comparisons and equality}
The comparison operators @racket[=], @racket[==], @racket[<], @racket[>],
@racket[<=], and @racket[>=] support n-ary comparisons. Simple chained
comparisons are emitted directly:
@(rkt+js
#<<RKT
(js/expression (< x y 10))
RKT
#<<JS
((x < y) && (y < 10))
JS
)
If a chained comparison would otherwise evaluate an intermediate expression
more than once, js-maker uses temporaries instead.
@racket[equal?] uses a pragmatic deep-equality helper based on JavaScript values
and JSON-style comparison for arrays and objects. @racket[eq?] and
@racket[eqv?] use @tt{Object.is}. This is useful for tests and simple data,
but it is not a complete implementation of Racket's equality predicates.
@section{Strings and regular expressions}
The string operations @racket[string-append], @racket[substring],
@racket[string-upcase], @racket[string-downcase], @racket[string-trim],
@racket[string-contains?], @racket[string=?], @racket[string-ci=?],
@racket[string<?], @racket[string>?], @racket[string<=?], @racket[string>=?],
@racket[number->string], @racket[symbol->string], @racket[string->symbol], and
@racket[string->number] are supported.
Racket @tt{#rx} and common @tt{#px} patterns are translated to JavaScript
@tt{RegExp} values when the syntax is compatible. The supported operations are
@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}. Known incompatible constructs such as
inline option groups and atomic groups are rejected instead of being silently
miscompiled. Byte regexps are not supported.
@section{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:
@compact-items[
@item{@racket[list], @racket[vector], @racket[list*], @racket[cons],
@racket[append].}
@item{@racket[car], @racket[cdr], @racket[first], @racket[rest],
@racket[cadr], @racket[caddr], @racket[second], @racket[third].}
@item{@racket[length], @racket[vector-length], @racket[string-length],
@racket[list-ref], @racket[vector-ref], @racket[string-ref],
@racket[list-tail], and @racket[last].}
@item{@racket[null?], @racket[empty?], @racket[pair?], @racket[list?],
and @racket[vector?].}
@item{@racket[take], @racket[drop], @racket[take-right],
@racket[drop-right], @racket[reverse], @racket[list-set], and
@racket[list-update].}
@item{@racket[map], including multiple-list @racket[map], @racket[filter],
@racket[foldl], @racket[foldr], @racket[apply], and @racket[sort].}
@item{@racket[member], @racket[memq], @racket[memv], @racket[remove], and
@racket[remove*].}
]
@racket[member] returns a tail array or @tt{false}. @racket[memq] and
@racket[memv] use @tt{Object.is}. @racket[filter] keeps every value that is not
@tt{false}, preserving Racket truthiness rather than JavaScript truthiness.
@section{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 @racket[hash], @racket[hasheq], and @racket[hasheqv].
Supported hash operations include:
@compact-items[
@item{@racket[hash], @racket[hasheq], @racket[hasheqv],
@racket[make-hash], @racket[make-immutable-hash],
@racket[make-hasheq], @racket[make-immutable-hasheq],
@racket[make-hasheqv], and @racket[make-immutable-hasheqv].}
@item{@racket[hash?], @racket[hash-ref], @racket[hash-has-key?],
@racket[hash-count], and @racket[hash-empty?].}
@item{@racket[hash-set], @racket[hash-set!], @racket[hash-remove],
@racket[hash-remove!], @racket[hash-update], and
@racket[hash-update!].}
@item{@racket[hash-clear], @racket[hash-clear!], @racket[hash-copy],
@racket[hash-copy-clear].}
@item{@racket[hash-keys], @racket[hash-values], @racket[hash->list],
@racket[hash-map], and @racket[hash-for-each].}
]
@racket[hash-ref] supports a default value and a default thunk. Mutating
operations mutate the JavaScript object representation directly. Immutable
operations create shallow copies.
@section{Exceptions}
A narrow @racket[with-handlers] subset is supported:
@racketblock[
(js
(with-handlers ([exn? (lambda (e)
(displayln (exn-message e)))])
(/ 10 0)))
]
This emits a JavaScript @tt{try}/@tt{catch}. Only the generic @racket[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
@racket[exn-message] extracts @tt{.message} when present and otherwise converts
the thrown value to a string.
This feature is useful for generated JavaScript that throws JavaScript
@tt{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.
@section{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
@racket[(g:date 2026 5 27)], @racket[(gregor:date 2026 5 27)], or
@racket[(date 2026 5 27)] is matched by the local name @racket[date]. This
keeps the generator independent of the import prefix used by the Racket module.
Supported local names include @racket[date], @racket[time], @racket[datetime],
@racket[moment], @racket[make-date], @racket[make-time],
@racket[make-datetime], @racket[make-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].
@subsection{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 @tt{Date} objects by default, because @tt{Date} always carries
timezone and instant semantics. A plain HTML @tt{input type="date"} value
such as @tt{"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:
@codeblock{
{ "$type": "gregor-date", "year": 2026, "month": 5, "day": 27 }
}
A time value is also JSON-compatible:
@codeblock{
{ "$type": "gregor-time", "hour": 13, "minute": 45,
"second": 0, "millisecond": 0 }
}
A datetime or moment is represented similarly:
@codeblock{
{ "$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
@tt{undefined}, @tt{NaN}, @tt{Infinity}, @tt{Date}, @tt{Map}, @tt{Set},
@tt{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
@tt{result} field has already been encoded. Native @tt{Date} values should be
encoded as tagged values, for example:
@codeblock{
{ "$type": "js-date", "iso": "2026-05-27T11:45:30.000Z" }
}
Use @racket[->js-date] only when native JavaScript @tt{Date} behavior is
required inside JavaScript code. Before such a value is returned across the
Racket boundary, convert it back with @racket[js-date->datetime] or let the
WebView boundary encoder tag it as @tt{"js-date"}.
@subsection{Basic construction and access}
@(rkt+js
#<<RKT
(js/expression (date 2026 5 27))
RKT
#<<JS
({"$type":"gregor-date","year":2026,"month":5,"day":27})
JS
)
@(rkt+js
#<<RKT
(js/expression (time 13 45 30))
RKT
#<<JS
({"$type":"gregor-time","hour":13,"minute":45,
"second":30,"millisecond":0})
JS
)
@(rkt+js
#<<RKT
(js/expression (datetime 2026 5 27 13 45 30))
RKT
#<<JS
({"$type":"gregor-datetime","year":2026,"month":5,"day":27,
"hour":13,"minute":45,"second":30,"millisecond":0})
JS
)
Field helpers read from these tagged objects:
@racketblock[
(js/expression
(let ([d (date 2026 5 27)])
(array (->year d) (->month d) (->day d))))
]
which evaluates to a JavaScript array equivalent to:
@codeblock{
[2026, 5, 27]
}
Predicate helpers test the tag:
@racketblock[
(js/expression
(array (date? (date 2026 5 27))
(time? (date 2026 5 27))))
]
which evaluates to:
@codeblock{
[true, false]
}
@subsection{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:
@compact-items[
@item{@tt{input type="date"}: @tt{"2026-05-27"}.}
@item{@tt{input type="time"}: @tt{"13:45"} or @tt{"13:45:30"}.}
@item{@tt{input type="datetime-local"}: @tt{"2026-05-27T13:45"} or
@tt{"2026-05-27T13:45:30"}.}
]
Examples:
@racketblock[
(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
@tt{datetime-local} strings, a Racket-side helper might look like this:
@racketblock[
(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 @racket[with-handlers] as a generic
JavaScript @tt{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 @racket[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.
@subsection{Formatting}
Formatting helpers produce simple ISO-like strings:
@racketblock[
(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.
@subsection{Native JavaScript Date interoperability}
Use @racket[->js-date] when a native JavaScript API requires a @tt{Date}
object:
@racketblock[
(js/expression
(let* ([dt (datetime 2026 5 27 13 45 30)]
[jsd (->js-date dt)])
(send jsd toISOString)))
]
Conversely, if JavaScript returns a native @tt{Date}, convert it before it
crosses the Racket boundary:
@racketblock[
(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
@tt{Date}. This keeps WebView return values predictable when they are passed
through @tt{JSON.stringify}, @tt{QVariant}, @tt{QJsonObject}, or Racket's JSON
reader.
@subsection{Limitations}
The Gregor compatibility layer is intentionally small:
@compact-items[
@item{It does not implement the full Gregor API.}
@item{Prefixes are ignored; only the local identifier name is used.}
@item{Date/time values are tagged JSON-compatible objects, not Racket structs.}
@item{Native JavaScript @tt{Date} is an explicit interop representation, not
the default representation.}
@item{Parsing and formatting are ISO-like and intentionally conservative.}
@item{Timezone, locale, calendar, duration, period, and arbitrary format-string
semantics are not modeled.}
]
@section{DOM and browser-oriented code}
js-maker can generate browser code through the JavaScript interop forms. For
example:
@racketblock[
(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.
@section{Generated code style}
Statement output is indented and block-oriented. Common cases such as simple
@racket[let*], boolean @racket[and]/@racket[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.
@section{Testing infrastructure}
The regression suite lives in @filepath{testing/}. The main entry point is
@filepath{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 @tt{d8}, JavaScriptCore @tt{jsc}, SpiderMonkey
@tt{js}, and an optional Chromium fallback. Node is the preferred default.
If no JavaScript engine is available, tests are generated and the framework
uses an explicit @tt{non-failing-javascript-stub}. The stub prints notes to
stdout, does not execute the generated JavaScript, and exits successfully.
This is intentional so package tests do not fail merely because a JavaScript
runtime is missing, especially in package-server environments. Set
@tt{JSMAKER_REQUIRE_ENGINE=1} or @tt{JSMAKER_REQUIRE_NODE=1} to make a missing
engine a hard failure.
Useful commands are:
@codeblock{
raco make main.rkt testing/jsmaker-regressions.rkt scrbl/jsmaker.scrbl
racket testing/jsmaker-regressions.rkt
raco test testing/jsmaker-regressions.rkt
}
@section{Package layout}
The package uses this layout:
@codeblock{
js-maker/
main.rkt
info.rkt
private/
testing/
demo/
scrbl/
}
@filepath{main.rkt} is the public module. @filepath{testing/} contains the
executor and regression framework. @filepath{demo/} contains generated-code
examples. @filepath{scrbl/} contains this reference and the use-case document.
The @filepath{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 @filepath{info.rkt}.
@section{Limitations and non-goals}
js-maker intentionally implements a pragmatic subset. The following are
important limitations:
@compact-items[
@item{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.}
@item{Racket's numeric tower is not implemented. JavaScript numbers are used;
exactness, rationals, complex numbers, flonum/fixnum distinctions, and
overflow behavior are not modeled.}
@item{Lists are JavaScript arrays, not chains of pairs. Dotted pair semantics
are only approximated where useful.}
@item{Hashes are JavaScript objects, not true Racket hash tables. Arbitrary
object keys and equality-mode distinctions are not preserved.}
@item{Regular expression support targets the common intersection of Racket
regexps and JavaScript @tt{RegExp}. Incompatible constructs are rejected
where known.}
@item{Exception support is limited to generic @racket[exn?] handlers over
JavaScript @tt{try}/@tt{catch}.}
@item{Gregor support is a small compatibility layer, not the full Gregor API.}
@item{The emitter generates JavaScript source strings. It is not yet an AST
optimizer or full pretty-printer.}
]
@section{Extending js-maker}
New forms are normally added in one of three places in @filepath{main.rkt}:
@compact-items[
@item{Statement forms in the statement compiler.}
@item{Expression forms in the expression compiler.}
@item{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.
@section{Further examples}
The companion @hyperlink["../usecases/index.html"]{use-case manual} contains
practical examples such as DOM manipulation, @tt{Set}, currying, object
destructuring, timers, @tt{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 @filepath{scrbl/usecases.scrbl}.
The relative link above is used instead of @racket[other-doc] because the latter
can render as an unresolved @tt{(part ... "top")} tag when the two documents are
built outside Racket's installed documentation index.
+29
View File
@@ -0,0 +1,29 @@
#lang racket/base
(require rackunit
"main.rkt")
(define ordinary
(js (define (ordinary-let x)
(let ([x 1] [y x])
(return y)))))
(define sequential
(js (define (sequential-let x)
(let* ([x 1] [y x])
(return y)))))
(define looped
(js (define (sum-to n)
(let loop ([i 0] [acc 0])
(if (> i n)
(return acc)
(loop (+ i 1) (+ acc i)))))))
(check-regexp-match #rx"function ordinary_let" ordinary)
(check-regexp-match #rx"const [A-Za-z0-9_]+ = x;" ordinary)
(check-regexp-match #rx"let y = [A-Za-z0-9_]+;" ordinary)
(check-regexp-match #rx"let y = x;" sequential)
(check-regexp-match #rx"while \\(true\\)" looped)
(check-regexp-match #rx"continue;" looped)
(check-equal? (js1 (+ 1 2)) "1 + 2")