Files
js-maker/scrbl/jsmaker.scrbl
T

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.