js-maker simpeler
This commit is contained in:
@@ -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.
|
|
||||||
|
|||||||
@@ -2,36 +2,16 @@
|
|||||||
|
|
||||||
(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"))
|
|
||||||
|
|||||||
@@ -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
@@ -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[
|
|
||||||
(displayln
|
|
||||||
(js
|
|
||||||
(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])
|
|
||||||
(if (< i 5)
|
|
||||||
(loop (+ i 1) (+ acc i))
|
|
||||||
acc))))
|
|
||||||
]
|
|
||||||
|
|
||||||
The named @racket[let] is emitted as a loop when it is used as a tail-recursive
|
|
||||||
self-call.
|
|
||||||
}
|
|
||||||
|
|
||||||
@section{Mental model}
|
|
||||||
|
|
||||||
The generator is best understood as a source-to-source translator over syntax.
|
|
||||||
Most input is converted to datum form and matched against the supported surface
|
|
||||||
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:
|
|
||||||
|
|
||||||
@compact-items[
|
|
||||||
@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}. It is the
|
|
||||||
preferred explicit form for property access in generated code.}
|
|
||||||
@item{@racket[(set! (js-dot obj field) value)] emits @tt{obj.field = value}.}
|
|
||||||
@item{@racket[(set! expr.field value)] is also accepted by the reader as
|
|
||||||
@racket[(set! expr .field value)] and is emitted as a direct property
|
|
||||||
assignment. This is mainly a convenience for DOM code such as
|
|
||||||
@racket[(set! (send document getElementById 'test).innerHTML html)].}
|
|
||||||
@item{@racket[(set-prop! obj key value)] emits @tt{obj[key] = value}.}
|
|
||||||
@item{@racket[(delete-prop! obj key)] and @racket[(js-delete obj key)] emit
|
|
||||||
JavaScript @tt{delete}.}
|
|
||||||
@item{@racket[(array v ...)] emits a JavaScript array literal.}
|
|
||||||
@item{@racket[(object key value ...)] emits a JavaScript object literal.}
|
|
||||||
]
|
|
||||||
|
|
||||||
Object destructuring is available through @racket[let-object]:
|
|
||||||
|
|
||||||
@racketblock[
|
@racketblock[
|
||||||
(js
|
(js
|
||||||
(define (describe person)
|
(define (sum-to n)
|
||||||
(let-object ([name 'name]
|
(let loop ([i 0] [acc 0])
|
||||||
[age 'age 0])
|
(if (> i n)
|
||||||
person
|
(return acc)
|
||||||
(return (string-append name ":" (number->string age))))))
|
(loop (+ i 1) (+ acc i))))))]
|
||||||
]
|
|
||||||
|
|
||||||
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:
|
@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.
|
||||||
|
|
||||||
@codeblock{
|
@racketblock[
|
||||||
{ "$type": "gregor-time", "hour": 13, "minute": 45,
|
(js1 (+ 1 2))]
|
||||||
"second": 0, "millisecond": 0 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
A datetime or moment is represented similarly:
|
@section{Supported core forms}
|
||||||
|
|
||||||
@codeblock{
|
The compact branch supports identifiers, quoted data, primitive literals,
|
||||||
{ "$type": "gregor-datetime", "year": 2026, "month": 5, "day": 27,
|
function calls, infix arithmetic and comparison operators, @racket[if],
|
||||||
"hour": 13, "minute": 45, "second": 30, "millisecond": 0 }
|
@racket[begin], @racket[return], @racket[set!], @racket[lambda],
|
||||||
}
|
@racket[define], ordinary @racket[let], @racket[let*], and named
|
||||||
|
@racket[let].
|
||||||
This convention is important for WebView integration. Generated JavaScript may
|
|
||||||
freely use native JavaScript values internally, but values that cross back to
|
Ordinary @racket[let] keeps Racket's parallel binding semantics. All right-hand
|
||||||
Racket should be JSON-compatible. Non-JSON JavaScript values such as
|
sides are generated before the bound names are introduced, and the actual
|
||||||
@tt{undefined}, @tt{NaN}, @tt{Infinity}, @tt{Date}, @tt{Map}, @tt{Set},
|
JavaScript bindings are placed in an inner block so JavaScript temporal dead zone
|
||||||
@tt{Error}, DOM nodes, and functions should be converted explicitly before
|
rules cannot accidentally shadow the initializers.
|
||||||
returning them to Racket.
|
|
||||||
|
Named @racket[let] is compiled to a JavaScript @tt{while (true)} loop. A tail
|
||||||
In a WebView bridge this usually means that the JavaScript wrapper around the
|
call to the loop name is rewritten to parallel assignment of the loop variables
|
||||||
user code should return a JSON string, or at least a plain JSON object, whose
|
followed by @tt{continue}. This keeps the important loop semantics without
|
||||||
@tt{result} field has already been encoded. Native @tt{Date} values should be
|
reintroducing the large js-maker 2 implementation.
|
||||||
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.
|
|
||||||
|
|||||||
@@ -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")
|
||||||
Reference in New Issue
Block a user