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