more tests and cases and documentation

This commit is contained in:
2026-05-27 13:24:55 +02:00
parent 2cf831c180
commit a7acdc6140
8 changed files with 1068 additions and 197 deletions
+4
View File
@@ -151,3 +151,7 @@ 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.
+3 -1
View File
@@ -31,7 +31,9 @@
"testing/jsmaker-regexp-regression.rkt"
"testing/jsmaker-program-regression.rkt"
"testing/jsmaker-dom-exercises.rkt"
"testing/jsmaker-usecases.rkt"))
"testing/jsmaker-usecases.rkt"
"testing/jsmaker-list-regression.rkt"
"testing/jsmaker-hash-regression.rkt"))
;; The private files are compatibility/support material and have project-local
;; dependencies in downstream copies. The public module and tests do not
+123 -11
View File
@@ -589,6 +589,64 @@ break;" (compile-expr d)))]))
(format "[~a]: ~a" (compile-expr k) (compile-expr v)))))
(format "{~a}" (string-join pairs ", ")))
(define (compile-make-hash args)
(match args
['() "{}"]
[(list entries) (format "Object.fromEntries(~a)" (compile-expr entries))]
[_ (fail "make-hash arity" args)]))
(define (compile-hash-ref args)
(match args
[(list h k)
(format "~a[~a]" (parens (compile-expr h)) (compile-expr k))]
[(list h k default)
(format "((__h, __k, __default) => Object.prototype.hasOwnProperty.call(__h, __k) ? __h[__k] : (typeof __default === 'function' ? __default() : __default))(~a, ~a, ~a)"
(compile-expr h) (compile-expr k) (compile-expr default))]
[_ (fail "hash-ref" args)]))
(define (compile-hash-set args #:mutate? [mutate? #f])
(match args
[(list h k v)
(if mutate?
(format "((__h, __k, __v) => { __h[__k] = __v; return undefined; })(~a, ~a, ~a)"
(compile-expr h) (compile-expr k) (compile-expr v))
(format "Object.assign({}, ~a, { [~a]: ~a })"
(compile-expr h) (compile-expr k) (compile-expr v)))]
[_ (fail (if mutate? "hash-set!" "hash-set") args)]))
(define (compile-hash-remove args #:mutate? [mutate? #f])
(match args
[(list h k)
(if mutate?
(format "((__h, __k) => { delete __h[__k]; return undefined; })(~a, ~a)"
(compile-expr h) (compile-expr k))
(format "((__h, __k) => { const __out = Object.assign({}, __h); delete __out[__k]; return __out; })(~a, ~a)"
(compile-expr h) (compile-expr k)))]
[_ (fail (if mutate? "hash-remove!" "hash-remove") args)]))
(define (compile-hash-update args #:mutate? [mutate? #f])
(match args
[(list h k f)
(format "((__h, __k, __f) => { if (!Object.prototype.hasOwnProperty.call(__h, __k)) throw new Error('hash-update: no value found for key'); ~a; return ~a; })(~a, ~a, ~a)"
(if mutate? "__h[__k] = __f(__h[__k])" "const __out = Object.assign({}, __h); __out[__k] = __f(__h[__k])")
(if mutate? "undefined" "__out")
(compile-expr h) (compile-expr k) (compile-expr f))]
[(list h k f default)
(format "((__h, __k, __f, __default) => { const __old = Object.prototype.hasOwnProperty.call(__h, __k) ? __h[__k] : (typeof __default === 'function' ? __default() : __default); ~a; return ~a; })(~a, ~a, ~a, ~a)"
(if mutate? "__h[__k] = __f(__old)" "const __out = Object.assign({}, __h); __out[__k] = __f(__old)")
(if mutate? "undefined" "__out")
(compile-expr h) (compile-expr k) (compile-expr f) (compile-expr default))]
[_ (fail (if mutate? "hash-update!" "hash-update") args)]))
(define (compile-hash-clear args #:mutate? [mutate? #f])
(match args
[(list h)
(if mutate?
(format "((__h) => { for (const __k of Object.keys(__h)) delete __h[__k]; return undefined; })(~a)"
(compile-expr h))
"{}")]
[_ (fail (if mutate? "hash-clear!" "hash-clear") args)]))
(define (atomic-expr? x)
(or (boolean? x)
(number? x)
@@ -608,7 +666,7 @@ break;" (compile-expr d)))]))
null? empty? pair? list? vector? number? real? integer?
string? boolean? symbol? regexp? pregexp? regexp-match?
string=? string-ci=? string<? string>? string<=? string>=?
hash-has-key? not))
hash? hash-has-key? not))
(and (eq? op 'and) (andmap boolean-expr? (cdr x)))
(and (eq? op 'or) (andmap boolean-expr? (cdr x))))))))
@@ -996,6 +1054,14 @@ if (~a !== false) return ~a;" tmp (compile-expr arg) tmp tmp)))
[(or) (compile-or args)]
[(not) (format "(~a === false)" (compile-expr (car args)))]
[(list vector) (format "[~a]" (string-join (map compile-expr args) ", "))]
[(list*)
(cond
[(null? args) "[]"]
[(null? (cdr args)) (compile-expr (car args))]
[else
(define fixed (reverse (cdr (reverse args))))
(define last-arg (car (reverse args)))
(format "[~a].concat(~a)" (string-join (map compile-expr fixed) ", ") (compile-expr last-arg))])]
[(cons) (format "[~a].concat(~a)" (compile-expr (car args)) (compile-expr (cadr args)))]
[(append) (if (null? args) "[]" (format "[].concat(~a)" (string-join (map compile-expr args) ", ")))]
[(car first) (format "~a[0]" (parens (compile-expr (car args))))]
@@ -1004,8 +1070,22 @@ if (~a !== false) return ~a;" tmp (compile-expr arg) tmp tmp)))
[(caddr third) (format "~a[2]" (parens (compile-expr (car args))))]
[(length vector-length string-length) (format "~a.length" (parens (compile-expr (car args))))]
[(list-ref vector-ref string-ref) (format "~a[~a]" (parens (compile-expr (car args))) (compile-expr (cadr args)))]
[(null? empty?) (format "Array.isArray(~a) && ~a.length === 0" (compile-expr (car args)) (parens (compile-expr (car args))))]
[(pair?) (format "Array.isArray(~a) && ~a.length > 0" (compile-expr (car args)) (parens (compile-expr (car args))))]
[(list-tail) (format "~a.slice(~a)" (parens (compile-expr (car args))) (compile-expr (cadr args)))]
[(last) (format "((__xs) => __xs[__xs.length - 1])(~a)" (compile-expr (car args)))]
[(list-set)
(match args
[(list xs i v)
(format "((__xs, __i, __v) => { const __out = __xs.slice(); __out[__i] = __v; return __out; })(~a, ~a, ~a)"
(compile-expr xs) (compile-expr i) (compile-expr v))]
[_ (fail "list-set" args)])]
[(list-update)
(match args
[(list xs i f)
(format "((__xs, __i, __f) => { const __out = __xs.slice(); __out[__i] = __f(__out[__i]); return __out; })(~a, ~a, ~a)"
(compile-expr xs) (compile-expr i) (compile-expr f))]
[_ (fail "list-update" args)])]
[(null? empty?) (format "((__xs) => Array.isArray(__xs) && __xs.length === 0)(~a)" (compile-expr (car args)))]
[(pair?) (format "((__xs) => Array.isArray(__xs) && __xs.length > 0)(~a)" (compile-expr (car args)))]
[(list? vector?) (format "Array.isArray(~a)" (compile-expr (car args)))]
[(number? real? integer?) (format "typeof ~a === \"number\"" (compile-expr (car args)))]
[(string?) (format "typeof ~a === \"string\"" (compile-expr (car args)))]
@@ -1039,20 +1119,52 @@ if (~a !== false) return ~a;" tmp (compile-expr arg) tmp tmp)))
[(string->number) (format "Number(~a)" (compile-expr (car args)))]
[(displayln) (format "console.log(~a)" (string-join (map compile-expr args) ", "))]
[(display) (format "console.log(~a)" (string-join (map compile-expr args) ", "))]
[(hash) (compile-hash args)]
[(hash-ref)
(match args
[(list h k) (format "~a[~a]" (parens (compile-expr h)) (compile-expr k))]
[(list h k default) (format "((__h, __k) => Object.prototype.hasOwnProperty.call(__h, __k) ? __h[__k] : ~a)(~a, ~a)" (compile-expr default) (compile-expr h) (compile-expr k))]
[_ (fail "hash-ref" args)])]
[(hash-set) (format "Object.assign({}, ~a, { [~a]: ~a })" (compile-expr (car args)) (compile-expr (cadr args)) (compile-expr (caddr args)))]
[(hash hasheq hasheqv) (compile-hash args)]
[(make-hash make-immutable-hash make-hasheq make-immutable-hasheq make-hasheqv make-immutable-hasheqv)
(compile-make-hash args)]
[(hash?) (format "((__h) => __h !== null && typeof __h === 'object' && !Array.isArray(__h))(~a)" (compile-expr (car args)))]
[(hash-ref) (compile-hash-ref args)]
[(hash-set) (compile-hash-set args)]
[(hash-set!) (compile-hash-set args #:mutate? #t)]
[(hash-remove) (compile-hash-remove args)]
[(hash-remove!) (compile-hash-remove args #:mutate? #t)]
[(hash-update) (compile-hash-update args)]
[(hash-update!) (compile-hash-update args #:mutate? #t)]
[(hash-clear) (compile-hash-clear args)]
[(hash-clear!) (compile-hash-clear args #:mutate? #t)]
[(hash-copy) (format "Object.assign({}, ~a)" (compile-expr (car args)))]
[(hash-copy-clear) "{}"]
[(hash-has-key?) (format "Object.prototype.hasOwnProperty.call(~a, ~a)" (compile-expr (car args)) (compile-expr (cadr args)))]
[(hash-count) (format "Object.keys(~a).length" (compile-expr (car args)))]
[(hash-empty?) (format "Object.keys(~a).length === 0" (compile-expr (car args)))]
[(hash-keys) (format "Object.keys(~a)" (compile-expr (car args)))]
[(hash-values) (format "Object.values(~a)" (compile-expr (car args)))]
[(hash->list) (format "Object.entries(~a)" (compile-expr (car args)))]
[(hash-map) (format "Object.entries(~a).map(([__k, __v]) => ~a(__k, __v))" (compile-expr (car args)) (compile-expr (cadr args)))]
[(hash-for-each) (format "((__h, __f) => { Object.entries(__h).forEach(([__k, __v]) => __f(__k, __v)); return undefined; })(~a, ~a)" (compile-expr (car args)) (compile-expr (cadr args)))]
[(reverse) (format "[...~a].reverse()" (parens (compile-expr (car args))))]
[(take) (format "~a.slice(0, ~a)" (parens (compile-expr (car args))) (compile-expr (cadr args)))]
[(drop) (format "~a.slice(~a)" (parens (compile-expr (car args))) (compile-expr (cadr args)))]
[(member memq memv) (format "~a.includes(~a)" (parens (compile-expr (cadr args))) (compile-expr (car args)))]
[(take-right) (format "((__xs, __n) => __xs.slice(Math.max(0, __xs.length - __n)))(~a, ~a)" (compile-expr (car args)) (compile-expr (cadr args)))]
[(drop-right) (format "((__xs, __n) => __xs.slice(0, Math.max(0, __xs.length - __n)))(~a, ~a)" (compile-expr (car args)) (compile-expr (cadr args)))]
[(member)
(format "((__v, __xs) => { const __i = __xs.findIndex((__x) => Object.is(__x, __v) || JSON.stringify(__x) === JSON.stringify(__v)); return __i < 0 ? false : __xs.slice(__i); })(~a, ~a)"
(compile-expr (car args)) (compile-expr (cadr args)))]
[(memq memv)
(format "((__v, __xs) => { const __i = __xs.findIndex((__x) => Object.is(__x, __v)); return __i < 0 ? false : __xs.slice(__i); })(~a, ~a)"
(compile-expr (car args)) (compile-expr (cadr args)))]
[(remove)
(format "((__v, __xs) => { let __done = false; return __xs.filter((__x) => { const __same = Object.is(__x, __v) || JSON.stringify(__x) === JSON.stringify(__v); if (!__done && __same) { __done = true; return false; } return true; }); })(~a, ~a)"
(compile-expr (car args)) (compile-expr (cadr args)))]
[(remove*)
(format "((__vs, __xs) => __xs.filter((__x) => !__vs.some((__v) => Object.is(__x, __v) || JSON.stringify(__x) === JSON.stringify(__v))))(~a, ~a)"
(compile-expr (car args)) (compile-expr (cadr args)))]
[(sort)
(match args
[(list xs less?)
(format "((__xs, __less) => __xs.slice().sort((__a, __b) => __less(__a, __b) !== false ? -1 : (__less(__b, __a) !== false ? 1 : 0)))(~a, ~a)"
(compile-expr xs) (compile-expr less?))]
[_ (fail "sort" args)])]
[(map)
(match args
[(list f xs) (format "~a.map((__x) => ~a(__x))" (parens (compile-expr xs)) (compile-expr f))]
+655 -161
View File
@@ -1,125 +1,290 @@
#lang scribble/manual
@(require (for-label racket/base
racket/list
racket/string
@(require scribble/core
(for-label racket/base
"../main.rkt"))
@title{jsmaker}
@(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" ""]
@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.
@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 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.
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 forms to JavaScript statement code. The result
is a string.
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 (f x)
(if (and (> x 10) (< x 15))
(begin
(console.log x)
(return x))
(define (square x)
(return (* x x)))))
]
The generated JavaScript is statement-oriented:
emits JavaScript similar to:
@codeblock{
function f(x) {
if ((x > 10) && (x < 15)) {
console.log(x);
return x;
} else {
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].
}
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.
@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)))
acc))))
]
Tail-recursive named @racket[let] loops are lowered to JavaScript
@tt{while (true)} loops when the recursive call is in tail position.
The named @racket[let] is emitted as a loop when it is used as a tail-recursive
self-call.
}
@section{Supported expression subset}
@section{Mental model}
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 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 not evaluated. This has a few important
consequences:
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].
@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.}
]
@section{Truthiness and boolean simplification}
@section{Supported expression and statement forms}
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:
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{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/expression (and (> x 10) (< x 15)))
(js
(define (describe person)
(let-object ([name 'name]
[age 'age 0])
person
(return (string-append name ":" (number->string age))))))
]
emits:
Classes can be generated with @racket[define-class]. Constructors may contain
simple default values:
@codeblock{
((x > 10) && (x < 15))
}
@racketblock[
(js
(define-class Greeter
(constructor ([name "world"])
(set! (js-dot this name) name))
(method greet ()
(return (string-append "Hello " (js-dot this name))))))
]
A chained comparison such as @racket[(< x y 10)] is emitted directly when all
reused operands are simple:
@section{Numbers and arithmetic}
@codeblock{
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
)
When an intermediate expression might have side effects, the generator uses
temporaries so the expression is evaluated once and in order.
If a chained comparison would otherwise evaluate an intermediate expression
more than once, js-maker uses temporaries instead.
@section{Regular expressions}
@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.
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],
@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
@@ -127,119 +292,448 @@ supports @racket[regexp?], @racket[pregexp?], @racket[regexp-match],
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}.
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.
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{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 small @racket[with-handlers] subset is supported:
A narrow @racket[with-handlers] subset is supported:
@racketblock[
(js/expression
(js
(with-handlers ([exn? (lambda (e)
(string-append "caught:" (exn-message e)))])
(error "boom")))
(displayln (exn-message e)))])
(/ 10 0)))
]
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.
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 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].
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[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].
@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].
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.
@subsection{Representation and the JavaScript/Racket boundary}
@section{JavaScript interop forms}
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.
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:
A date value is therefore JSON-compatible data:
@codeblock{
raco test jsmaker/testing/jsmaker-regressions.rkt
{ "$type": "gregor-date", "year": 2026, "month": 5, "day": 27 }
}
@section{Private compatibility files}
A time value is also JSON-compatible:
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}.
@codeblock{
{ "$type": "gregor-time", "hour": 13, "minute": 45,
"second": 0, "millisecond": 0 }
}
@section{Limitations}
A datetime or moment is represented similarly:
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.
@codeblock{
{ "$type": "gregor-datetime", "year": 2026, "month": 5, "day": 27,
"hour": 13, "minute": 45, "second": 30, "millisecond": 0 }
}
@section{Use-case documentation}
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.
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.
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 document @other-doc['(lib "jsmaker/scrbl/usecases.scrbl")]
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.
+16 -4
View File
@@ -4,10 +4,17 @@
(for-label racket/base
"../main.rkt"))
@(define (side-by-side racket-source js-source)
(tabular #:style 'boxed #:sep (hspace 2)
(list (list (bold "Racket / js-maker") (bold "Generated JavaScript"))
(list (verbatim racket-source) (verbatim js-source)))))
@(define (code-box title source)
(tabular #:style 'boxed
(list (list (bold title))
(list (verbatim source)))))
@(define (code-pair racket-source js-source)
(list
(code-box "Racket / js-maker" racket-source)
(code-box "Generated JavaScript" js-source)))
@(define side-by-side code-pair)
@(define (tested s)
(nested #:style 'inset (bold "Tested behavior: ") s))
@@ -23,6 +30,11 @@ snippet using @racket[js] and is tested by compiling it to JavaScript and
executing that JavaScript with the configured test executor. The corresponding
tests are in @filepath{testing/jsmaker-usecases.rkt}.
The examples are shown vertically: first the Racket/js-maker source, then the
generated JavaScript. Each code fragment is shown in a boxed documentation
cell, preserving the light shaded background of the earlier side-by-side
layout while avoiding narrow, wrapped code columns.
The tests intentionally use @racket[js/expression] for their calls wherever
possible. Raw JavaScript remains only in small harness preambles, such as fake
@tt{setInterval}, fake DOM objects, and fake @tt{fetch}.
+146
View File
@@ -0,0 +1,146 @@
#lang racket/base
(require "../main.rkt"
"jsmaker-executors.rkt"
"jsmaker-test-framework.rkt")
(define tests
(list
(js-expression-test 'hash-literal-ref-count
(js/expression
(let ([h (hash 'a 1 'b 2)])
(list (hash-ref h 'a)
(hash-ref h 'b)
(hash-count h)
(hash-empty? h)
(hash? h))))
"[1,2,2,false,true]")
(js-expression-test 'make-hash-from-assoc-list
(js/expression
(let ([h (make-hash (list (cons 'a 1)
(cons 'b 2)))])
(list (hash-ref h 'a)
(hash-ref h 'b))))
"[1,2]")
(js-expression-test 'make-hash-empty-and-set-bang
(js/expression
(let ([h (make-hash)])
(hash-set! h 'a 1)
(hash-set! h 'b 2)
(list (hash-ref h 'a)
(hash-ref h 'b)
(hash-count h))))
"[1,2,2]")
(js-expression-test 'hash-ref-default-value-and-thunk
(js/expression
(let ([h (hash 'a 1)])
(list (hash-ref h 'missing 42)
(hash-ref h 'other (lambda () 99)))))
"[42,99]")
(js-expression-test 'hash-has-key
(js/expression
(let ([h (hash 'a #f 'b 2)])
(list (hash-has-key? h 'a)
(hash-has-key? h 'c)
(hash-ref h 'a 'fallback))))
"[true,false,false]")
(js-expression-test 'hash-set-immutable-copy
(js/expression
(let* ([h (hash 'a 1)]
[h2 (hash-set h 'b 2)])
(list (hash-has-key? h 'b)
(hash-ref h2 'a)
(hash-ref h2 'b))))
"[false,1,2]")
(js-expression-test 'hash-remove-immutable-copy
(js/expression
(let* ([h (hash 'a 1 'b 2)]
[h2 (hash-remove h 'a)])
(list (hash-has-key? h 'a)
(hash-has-key? h2 'a)
(hash-ref h2 'b))))
"[true,false,2]")
(js-expression-test 'hash-remove-bang
(js/expression
(let ([h (make-hash (list (cons 'a 1)
(cons 'b 2)))])
(hash-remove! h 'a)
(list (hash-has-key? h 'a)
(hash-ref h 'b)
(hash-count h))))
"[false,2,1]")
(js-expression-test 'hash-update-immutable
(js/expression
(let* ([h (hash 'a 1)]
[h2 (hash-update h 'a (lambda (x) (+ x 10)))]
[h3 (hash-update h2 'b (lambda (x) (+ x 1)) 40)])
(list (hash-ref h 'a)
(hash-ref h2 'a)
(hash-ref h3 'b))))
"[1,11,41]")
(js-expression-test 'hash-update-bang
(js/expression
(let ([h (make-hash (list (cons 'a 1)))])
(hash-update! h 'a (lambda (x) (+ x 10)))
(hash-update! h 'b (lambda (x) (+ x 1)) 40)
(list (hash-ref h 'a)
(hash-ref h 'b))))
"[11,41]")
(js-expression-test 'hash-clear-and-clear-bang
(js/expression
(let* ([h (make-hash (list (cons 'a 1)
(cons 'b 2)))]
[h2 (hash-clear h)])
(hash-clear! h)
(list (hash-empty? h)
(hash-empty? h2))))
"[true,true]")
(js-expression-test 'hash-copy-and-copy-clear
(js/expression
(let* ([h (hash 'a 1)]
[h2 (hash-copy h)]
[h3 (hash-copy-clear h)])
(hash-set! h2 'b 2)
(list (hash-has-key? h 'b)
(hash-ref h2 'b)
(hash-empty? h3))))
"[false,2,true]")
(js-expression-test 'hash-keys-values-list
(js/expression
(let ([h (hash 'a 1 'b 2)])
(list (hash-keys h)
(hash-values h)
(hash->list h))))
"[[\"a\",\"b\"],[1,2],[[\"a\",1],[\"b\",2]]]" )
(js-expression-test 'hash-map
(js/expression
(let ([h (hash 'a 1 'b 2)])
(hash-map h (lambda (k v) (string-append k ":" (number->string v))))))
"[\"a:1\",\"b:2\"]")
(js-expression-test 'hash-for-each
(js/expression
(let ([h (hash 'a 1 'b 2)]
[out (list)])
(hash-for-each h (lambda (k v)
(set! out (append out (list (string-append k (number->string v)))))))
out))
"[\"a1\",\"b2\"]")
(js-expression-test 'hasheq-and-make-immutable-hash
(js/expression
(let* ([h (hasheq 'a 1 'b 2)]
[h2 (make-immutable-hash (list (cons 'c 3)))])
(list (hash-ref h 'a)
(hash-ref h2 'c))))
"[1,3]")
(js-expression-test 'hash-composition-pipeline
(js/expression
(let* ([h (make-hash)]
[xs (list 'a 'b 'a 'c 'b 'a)])
(for ([x xs])
(hash-update! h x (lambda (n) (+ n 1)) 0))
(sort (hash-map h (lambda (k v) (list k v)))
(lambda (a b) (string<? (car a) (car b))))))
"[[\"a\",3],[\"b\",2],[\"c\",1]]")))
(define engine (find-js-engine))
(run-jsmaker-regression 'jsmaker-hash-regression tests "/tmp/jsmaker-hash-regression.js" #:engine engine)
+99
View File
@@ -0,0 +1,99 @@
#lang racket/base
(require "../main.rkt"
"jsmaker-executors.rkt"
"jsmaker-test-framework.rkt")
(define tests
(list
(js-expression-test 'list-literal
(js/expression (list 1 2 3))
"[1,2,3]")
(js-expression-test 'cons-front
(js/expression (cons 1 (list 2 3)))
"[1,2,3]")
(js-expression-test 'list-star-with-tail
(js/expression (list* 1 2 (list 3 4)))
"[1,2,3,4]")
(js-expression-test 'append-no-args
(js/expression (append))
"[]")
(js-expression-test 'append-many
(js/expression (append (list 1) (list 2 3) (list) (list 4)))
"[1,2,3,4]")
(js-expression-test 'car-cdr-cadr-caddr
(js/expression (list (car (list 10 20 30))
(cdr (list 10 20 30))
(cadr (list 10 20 30))
(caddr (list 10 20 30))))
"[10,[20,30],20,30]")
(js-expression-test 'length-and-predicates
(js/expression (list (length (list 1 2 3))
(null? (list))
(empty? (list 1))
(pair? (list 1))
(list? (list 1 2))))
"[3,true,false,true,true]")
(js-expression-test 'list-ref-tail-last
(js/expression (list (list-ref (list "a" "b" "c") 1)
(list-tail (list 1 2 3 4) 2)
(last (list 1 2 3 4))))
"[\"b\",[3,4],4]")
(js-expression-test 'take-drop-left-right
(js/expression (list (take (list 1 2 3 4 5) 3)
(drop (list 1 2 3 4 5) 2)
(take-right (list 1 2 3 4 5) 2)
(drop-right (list 1 2 3 4 5) 2)))
"[[1,2,3],[3,4,5],[4,5],[1,2,3]]")
(js-expression-test 'reverse-list
(js/expression (reverse (list 1 2 3)))
"[3,2,1]")
(js-expression-test 'map-single-list
(js/expression (map (lambda (x) (* x x)) (list 1 2 3 4)))
"[1,4,9,16]")
(js-expression-test 'map-multiple-lists
(js/expression (map (lambda (x y) (+ x y))
(list 1 2 3)
(list 10 20 30)))
"[11,22,33]")
(js-expression-test 'filter-racket-truthiness
(js/expression (filter (lambda (x) x) (list #f 0 "" 3)))
"[0,\"\",3]")
(js-expression-test 'filter-predicate
(js/expression (filter (lambda (x) (> x 2)) (list 1 2 3 4)))
"[3,4]")
(js-expression-test 'foldl-cons
(js/expression (foldl (lambda (x acc) (cons x acc)) (list) (list 1 2 3)))
"[3,2,1]")
(js-expression-test 'foldr-cons
(js/expression (foldr (lambda (x acc) (cons x acc)) (list) (list 1 2 3)))
"[1,2,3]")
(js-expression-test 'member-tail-and-false
(js/expression (list (member 2 (list 1 2 3 2))
(member 9 (list 1 2 3))))
"[[2,3,2],false]")
(js-expression-test 'remove-first
(js/expression (remove 2 (list 1 2 3 2)))
"[1,3,2]")
(js-expression-test 'remove-star
(js/expression (remove* (list 2 4) (list 1 2 3 4 2 5)))
"[1,3,5]")
(js-expression-test 'list-set-update-immutable
(js/expression (let* ([xs (list 1 2 3)]
[ys (list-set xs 1 20)]
[zs (list-update xs 2 (lambda (x) (+ x 100)))])
(list xs ys zs)))
"[[1,2,3],[1,20,3],[1,2,103]]")
(js-expression-test 'sort-with-predicate
(js/expression (sort (list 5 1 4 2 3) (lambda (a b) (< a b))))
"[1,2,3,4,5]")
(js-expression-test 'list-composition-pipeline
(js/expression
(let* ([xs (append (list 1 2) (list 3 4 5))]
[ys (filter (lambda (x) (odd? x)) xs)]
[zs (map (lambda (x) (* x 10)) ys)])
(take zs 2)))
"[10,30]")))
(define engine (find-js-engine))
(run-jsmaker-regression 'jsmaker-list-regression tests "/tmp/jsmaker-list-regression.js" #:engine engine)
+3 -1
View File
@@ -4,4 +4,6 @@
"jsmaker-regexp-regression.rkt"
"jsmaker-program-regression.rkt"
"jsmaker-dom-exercises.rkt"
"jsmaker-usecases.rkt")
"jsmaker-usecases.rkt"
"jsmaker-list-regression.rkt"
"jsmaker-hash-regression.rkt")