smalle changes
This commit is contained in:
@@ -0,0 +1,788 @@
|
||||
#lang scribble/manual
|
||||
|
||||
@(require scribble/core
|
||||
(for-label racket/base
|
||||
"../main.rkt"))
|
||||
|
||||
@(define (compact-items . xs)
|
||||
(apply itemlist #:style 'compact xs))
|
||||
|
||||
@(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" "hans@dijkewijk.nl"]
|
||||
|
||||
@defmodule[jsmaker]
|
||||
|
||||
@bold{js-maker} is a small, syntax-driven JavaScript generator for writing a
|
||||
practical JavaScript subset in Racket notation. It provides two macros,
|
||||
@racket[js] and @racket[js/expression]. Both macros run at expansion time and
|
||||
return JavaScript source code as a string. The generated JavaScript can then be
|
||||
embedded in a page, written to a file, tested with Node or another JavaScript
|
||||
engine, or used as part of a larger code-generation workflow.
|
||||
|
||||
The package is deliberately not a full Racket compiler. It recognizes a
|
||||
well-defined set of Racket-like forms and maps them to JavaScript while trying
|
||||
to preserve important Racket conventions such as @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 ...)]{
|
||||
Translates one or more Racket-like forms to JavaScript @emph{statement} code.
|
||||
The result is a string. This is the usual entry point for functions, classes,
|
||||
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].
|
||||
}
|
||||
|
||||
@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.
|
||||
The input is converted to datum form and matched against the supported surface
|
||||
language. The Racket code is normally not evaluated. The deliberate exception
|
||||
is @racket[(eval racket-expr)], which interpolates a Racket value into the
|
||||
JavaScript source text. The expression is evaluated in the lexical context of
|
||||
the @racket[js] or @racket[js/expression] use and its value is emitted as a
|
||||
JavaScript literal. 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 eval}
|
||||
|
||||
The form @racket[(eval racket-expr)] is an interpolation escape hatch inherited
|
||||
from the original transformer. 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.
|
||||
|
||||
@(rkt+js
|
||||
#<<RKT
|
||||
(let ([x 10]
|
||||
[y 20])
|
||||
(js (let ([a (eval (* x y))])
|
||||
(return (* a a)))))
|
||||
RKT
|
||||
#<<JS
|
||||
{
|
||||
let a = 200;
|
||||
return (a * a);
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(rkt+js
|
||||
#<<RKT
|
||||
(js/expression (array (eval (+ 1 2))
|
||||
(eval (string-append "a" "b"))))
|
||||
RKT
|
||||
#<<JS
|
||||
[3, "ab"]
|
||||
JS
|
||||
)
|
||||
|
||||
This is not JavaScript @tt{eval}. To call JavaScript @tt{eval}, call the
|
||||
JavaScript function explicitly, for example @racket[(send window eval "1 + 2")].
|
||||
Racket-side @racket[eval] is best used for constants, generated literal data,
|
||||
and small configuration values that are known while the JavaScript source is
|
||||
being constructed. It should not be used for run-time browser state, DOM access,
|
||||
or user input.
|
||||
|
||||
@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[(eval racket-expr)] for Racket-side value interpolation into JavaScript literals.}
|
||||
@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}.}
|
||||
@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[
|
||||
(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 but execution is
|
||||
skipped with clear warnings and a successful exit status. This is intentional
|
||||
so package tests do not fail merely because a JavaScript runtime is missing.
|
||||
Set @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.
|
||||
Reference in New Issue
Block a user