more tests and cases and documentation
This commit is contained in:
@@ -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
|
`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
|
only for small test-harness preambles such as fake timers, fake DOM objects, and
|
||||||
fake fetch.
|
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.
|
||||||
|
|||||||
@@ -31,7 +31,9 @@
|
|||||||
"testing/jsmaker-regexp-regression.rkt"
|
"testing/jsmaker-regexp-regression.rkt"
|
||||||
"testing/jsmaker-program-regression.rkt"
|
"testing/jsmaker-program-regression.rkt"
|
||||||
"testing/jsmaker-dom-exercises.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
|
;; The private files are compatibility/support material and have project-local
|
||||||
;; dependencies in downstream copies. The public module and tests do not
|
;; dependencies in downstream copies. The public module and tests do not
|
||||||
|
|||||||
@@ -589,6 +589,64 @@ break;" (compile-expr d)))]))
|
|||||||
(format "[~a]: ~a" (compile-expr k) (compile-expr v)))))
|
(format "[~a]: ~a" (compile-expr k) (compile-expr v)))))
|
||||||
(format "{~a}" (string-join pairs ", ")))
|
(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)
|
(define (atomic-expr? x)
|
||||||
(or (boolean? x)
|
(or (boolean? x)
|
||||||
(number? x)
|
(number? x)
|
||||||
@@ -608,7 +666,7 @@ break;" (compile-expr d)))]))
|
|||||||
null? empty? pair? list? vector? number? real? integer?
|
null? empty? pair? list? vector? number? real? integer?
|
||||||
string? boolean? symbol? regexp? pregexp? regexp-match?
|
string? boolean? symbol? regexp? pregexp? regexp-match?
|
||||||
string=? string-ci=? string<? string>? string<=? string>=?
|
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 'and) (andmap boolean-expr? (cdr x)))
|
||||||
(and (eq? op 'or) (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)]
|
[(or) (compile-or args)]
|
||||||
[(not) (format "(~a === false)" (compile-expr (car args)))]
|
[(not) (format "(~a === false)" (compile-expr (car args)))]
|
||||||
[(list vector) (format "[~a]" (string-join (map compile-expr 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)))]
|
[(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) ", ")))]
|
[(append) (if (null? args) "[]" (format "[].concat(~a)" (string-join (map compile-expr args) ", ")))]
|
||||||
[(car first) (format "~a[0]" (parens (compile-expr (car 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))))]
|
[(caddr third) (format "~a[2]" (parens (compile-expr (car args))))]
|
||||||
[(length vector-length string-length) (format "~a.length" (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)))]
|
[(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))))]
|
[(list-tail) (format "~a.slice(~a)" (parens (compile-expr (car args))) (compile-expr (cadr args)))]
|
||||||
[(pair?) (format "Array.isArray(~a) && ~a.length > 0" (compile-expr (car args)) (parens (compile-expr (car 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)))]
|
[(list? vector?) (format "Array.isArray(~a)" (compile-expr (car args)))]
|
||||||
[(number? real? integer?) (format "typeof ~a === \"number\"" (compile-expr (car args)))]
|
[(number? real? integer?) (format "typeof ~a === \"number\"" (compile-expr (car args)))]
|
||||||
[(string?) (format "typeof ~a === \"string\"" (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)))]
|
[(string->number) (format "Number(~a)" (compile-expr (car args)))]
|
||||||
[(displayln) (format "console.log(~a)" (string-join (map compile-expr args) ", "))]
|
[(displayln) (format "console.log(~a)" (string-join (map compile-expr args) ", "))]
|
||||||
[(display) (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 hasheq hasheqv) (compile-hash args)]
|
||||||
[(hash-ref)
|
[(make-hash make-immutable-hash make-hasheq make-immutable-hasheq make-hasheqv make-immutable-hasheqv)
|
||||||
(match args
|
(compile-make-hash args)]
|
||||||
[(list h k) (format "~a[~a]" (parens (compile-expr h)) (compile-expr k))]
|
[(hash?) (format "((__h) => __h !== null && typeof __h === 'object' && !Array.isArray(__h))(~a)" (compile-expr (car args)))]
|
||||||
[(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))]
|
[(hash-ref) (compile-hash-ref args)]
|
||||||
[_ (fail "hash-ref" args)])]
|
[(hash-set) (compile-hash-set args)]
|
||||||
[(hash-set) (format "Object.assign({}, ~a, { [~a]: ~a })" (compile-expr (car args)) (compile-expr (cadr args)) (compile-expr (caddr 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-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-keys) (format "Object.keys(~a)" (compile-expr (car args)))]
|
||||||
[(hash-values) (format "Object.values(~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))))]
|
[(reverse) (format "[...~a].reverse()" (parens (compile-expr (car args))))]
|
||||||
[(take) (format "~a.slice(0, ~a)" (parens (compile-expr (car args))) (compile-expr (cadr 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)))]
|
[(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)
|
[(map)
|
||||||
(match args
|
(match args
|
||||||
[(list f xs) (format "~a.map((__x) => ~a(__x))" (parens (compile-expr xs)) (compile-expr f))]
|
[(list f xs) (format "~a.map((__x) => ~a(__x))" (parens (compile-expr xs)) (compile-expr f))]
|
||||||
|
|||||||
+656
-162
@@ -1,125 +1,290 @@
|
|||||||
#lang scribble/manual
|
#lang scribble/manual
|
||||||
|
|
||||||
@(require (for-label racket/base
|
@(require scribble/core
|
||||||
racket/list
|
(for-label racket/base
|
||||||
racket/string
|
|
||||||
"../main.rkt"))
|
"../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" ""]
|
@author+email["Hans Dijkema" ""]
|
||||||
|
|
||||||
@defmodule[jsmaker]
|
@defmodule[jsmaker]
|
||||||
|
|
||||||
The @racketmodname[jsmaker] collection provides two syntax forms that
|
@bold{js-maker} is a small, syntax-driven JavaScript generator for writing a
|
||||||
translate a practical subset of Racket expressions to JavaScript source code.
|
practical JavaScript subset in Racket notation. It provides two macros,
|
||||||
The translation is syntax-driven: the Racket expression is not evaluated, but
|
@racket[js] and @racket[js/expression]. Both macros run at expansion time and
|
||||||
is inspected by the macro and emitted as a JavaScript string.
|
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
|
The package is deliberately not a full Racket compiler. It recognizes a
|
||||||
and predictable source generator for small pieces of JavaScript, including
|
well-defined set of Racket-like forms and maps them to JavaScript while trying
|
||||||
callbacks, browser-facing functions, simple data processing, regular
|
to preserve important Racket conventions such as @racket[#f]-only falsiness,
|
||||||
expressions, and some date/time helper forms.
|
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}
|
@section{Public API}
|
||||||
|
|
||||||
@defform[(js form ...)]{
|
@defform[(js form ...)]{
|
||||||
Translates one or more Racket forms to JavaScript statement code. The result
|
Translates one or more Racket-like forms to JavaScript @emph{statement} code.
|
||||||
is a string.
|
The result is a string. This is the usual entry point for functions, classes,
|
||||||
|
assignments, DOM scripts, and top-level JavaScript snippets.
|
||||||
|
|
||||||
@racketblock[
|
@racketblock[
|
||||||
(js
|
(displayln
|
||||||
(define (f x)
|
(js
|
||||||
(if (and (> x 10) (< x 15))
|
(define (square x)
|
||||||
(begin
|
|
||||||
(console.log x)
|
|
||||||
(return x))
|
|
||||||
(return (* x x)))))
|
(return (* x x)))))
|
||||||
]
|
]
|
||||||
|
|
||||||
The generated JavaScript is statement-oriented:
|
emits JavaScript similar to:
|
||||||
|
|
||||||
@codeblock{
|
@codeblock{
|
||||||
function f(x) {
|
function square(x) {
|
||||||
if ((x > 10) && (x < 15)) {
|
|
||||||
console.log(x);
|
|
||||||
return x;
|
|
||||||
} else {
|
|
||||||
return (x * x);
|
return (x * x);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Inside function bodies, the final expression is returned automatically unless
|
Inside function bodies, the last expression is returned automatically unless it
|
||||||
an explicit @racket[return] form is used.
|
is already a statement form such as @racket[return], @racket[define],
|
||||||
|
@racket[set!], @racket[while], or @racket[for].
|
||||||
}
|
}
|
||||||
|
|
||||||
@defform[(js/expression form)]{
|
@defform[(js/expression expr)]{
|
||||||
Translates a single Racket expression to a JavaScript expression string.
|
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[
|
@racketblock[
|
||||||
(js/expression
|
(displayln
|
||||||
|
(js/expression
|
||||||
(let loop ([i 0] [acc 0])
|
(let loop ([i 0] [acc 0])
|
||||||
(if (< i 5)
|
(if (< i 5)
|
||||||
(loop (+ i 1) (+ acc i))
|
(loop (+ i 1) (+ acc i))
|
||||||
acc)))
|
acc))))
|
||||||
]
|
]
|
||||||
|
|
||||||
Tail-recursive named @racket[let] loops are lowered to JavaScript
|
The named @racket[let] is emitted as a loop when it is used as a tail-recursive
|
||||||
@tt{while (true)} loops when the recursive call is in tail position.
|
self-call.
|
||||||
}
|
}
|
||||||
|
|
||||||
@section{Supported expression subset}
|
@section{Mental model}
|
||||||
|
|
||||||
The supported subset includes literals, identifiers, function calls,
|
The generator is best understood as a source-to-source translator over syntax.
|
||||||
@racket[lambda], @racket[λ], @racket[define], @racket[set!], @racket[if],
|
The input is converted to datum form and matched against the supported surface
|
||||||
@racket[begin], @racket[begin0], @racket[cond], @racket[case], @racket[let],
|
language. The Racket code is not evaluated. This has a few important
|
||||||
@racket[let*], @racket[letrec], @racket[let-values], @racket[let*-values],
|
consequences:
|
||||||
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
|
@compact-items[
|
||||||
forms used in the regression suite are also supported. Examples include
|
@item{Only forms known to js-maker are translated specially. Unknown calls are
|
||||||
@racket[+], @racket[-], @racket[*], @racket[/], @racket[quotient],
|
emitted as JavaScript calls with translated arguments.}
|
||||||
@racket[remainder], @racket[<], @racket[<=], @racket[>], @racket[>=],
|
@item{Identifiers are mapped to JavaScript identifiers. Reserved words are
|
||||||
@racket[=], @racket[equal?], @racket[and], @racket[or], @racket[not],
|
avoided in variable position, while method/property names may use modern
|
||||||
@racket[list], @racket[vector], @racket[cons], @racket[append],
|
JavaScript reserved property names such as @tt{catch}.}
|
||||||
@racket[map], @racket[filter], @racket[foldl], @racket[foldr],
|
@item{Macros from other Racket modules are not expanded by js-maker. If a
|
||||||
@racket[substring], @racket[string-append], @racket[hash], and
|
macro should be supported, add an explicit js-maker form or compile the
|
||||||
@racket[hash-ref].
|
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
|
The following Racket forms are supported as either expressions, statements, or
|
||||||
false. The generator therefore preserves Racket truthiness where needed.
|
both, depending on context:
|
||||||
When a form is known to produce a JavaScript boolean, the generator emits
|
|
||||||
simpler JavaScript. For example:
|
@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[
|
@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{
|
@racketblock[
|
||||||
((x > 10) && (x < 15))
|
(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
|
@section{Numbers and arithmetic}
|
||||||
reused operands are simple:
|
|
||||||
|
|
||||||
@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))
|
((x < y) && (y < 10))
|
||||||
}
|
JS
|
||||||
|
)
|
||||||
|
|
||||||
When an intermediate expression might have side effects, the generator uses
|
If a chained comparison would otherwise evaluate an intermediate expression
|
||||||
temporaries so the expression is evaluated once and in order.
|
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
|
@section{Strings and regular expressions}
|
||||||
JavaScript @tt{RegExp} values where the syntax is compatible. The generator
|
|
||||||
supports @racket[regexp?], @racket[pregexp?], @racket[regexp-match],
|
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?], @racket[regexp-match*],
|
||||||
@racket[regexp-match-positions], @racket[regexp-split],
|
@racket[regexp-match-positions], @racket[regexp-split],
|
||||||
@racket[regexp-replace], @racket[regexp-replace*], and
|
@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
|
Match results are normalized to Racket-like values: a failed match becomes
|
||||||
@tt{false}, successful matches become JavaScript arrays, and an unmatched
|
@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
|
@section{Lists, vectors, and array-backed sequences}
|
||||||
such as inline option groups and atomic groups are rejected instead of being
|
|
||||||
silently miscompiled.
|
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}
|
@section{Exceptions}
|
||||||
|
|
||||||
A small @racket[with-handlers] subset is supported:
|
A narrow @racket[with-handlers] subset is supported:
|
||||||
|
|
||||||
@racketblock[
|
@racketblock[
|
||||||
(js/expression
|
(js
|
||||||
(with-handlers ([exn? (lambda (e)
|
(with-handlers ([exn? (lambda (e)
|
||||||
(string-append "caught:" (exn-message e)))])
|
(displayln (exn-message e)))])
|
||||||
(error "boom")))
|
(/ 10 0)))
|
||||||
]
|
]
|
||||||
|
|
||||||
The generated JavaScript uses @tt{try}/@tt{catch}. Only a generic
|
This emits a JavaScript @tt{try}/@tt{catch}. Only the generic @racket[exn?]
|
||||||
@racket[exn?] predicate is supported. Handler procedures are emitted as
|
predicate is supported. More specific Racket exception predicates are rejected
|
||||||
function expressions in callee position, so rest-argument handlers such as
|
because JavaScript has a single catch channel and no Racket exception
|
||||||
@racket[(lambda args ...)] are valid JavaScript. Division by zero in
|
hierarchy. The handler is called with the JavaScript thrown value. The helper
|
||||||
@racket[/] is checked at run time and throws a JavaScript @tt{Error}, which
|
@racket[exn-message] extracts @tt{.message} when present and otherwise converts
|
||||||
this subset can catch with @racket[with-handlers]. The generator does not
|
the thrown value to a string.
|
||||||
model Racket's exception hierarchy, continuable exceptions, exception marks,
|
|
||||||
or the full exact/inexact numeric distinction.
|
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}
|
@section{Gregor-style date and time helpers}
|
||||||
|
|
||||||
The generator includes a small JavaScript-side representation for a subset of
|
The generator includes a small JavaScript-side value model for a subset of
|
||||||
Gregor-style date/time operations. Prefixes are not hardcoded. A call such as
|
Gregor-style date and time operations. This layer is intended for browser
|
||||||
@racket[(g:date 2026 5 25)], @racket[(gregor:date 2026 5 25)], or
|
input/output code and for JSON-safe communication between generated JavaScript
|
||||||
@racket[(date 2026 5 25)] is matched by the local name @racket[date].
|
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],
|
Supported local names include @racket[date], @racket[time], @racket[datetime],
|
||||||
@racket[moment], @racket[parse-date], @racket[parse-time],
|
@racket[moment], @racket[make-date], @racket[make-time],
|
||||||
@racket[parse-datetime], @racket[parse-moment], @racket[string->date],
|
@racket[make-datetime], @racket[make-moment], @racket[parse-date],
|
||||||
@racket[string->time], @racket[string->datetime], @racket[date->string],
|
@racket[parse-time], @racket[parse-datetime], @racket[parse-moment],
|
||||||
@racket[time->string], @racket[datetime->string], @racket[moment->string],
|
@racket[string->date], @racket[string->time], @racket[string->datetime],
|
||||||
@racket[date?], @racket[time?], @racket[datetime?], @racket[moment?],
|
@racket[date->string], @racket[time->string], @racket[datetime->string],
|
||||||
@racket[->year], @racket[->month], @racket[->day], @racket[->hours],
|
@racket[moment->string], @racket[date?], @racket[time?],
|
||||||
@racket[->minutes], @racket[->seconds], @racket[->js-date], and
|
@racket[datetime?], @racket[moment?], @racket[->year], @racket[->month],
|
||||||
@racket[js-date->datetime].
|
@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
|
@subsection{Representation and the JavaScript/Racket boundary}
|
||||||
native @tt{Date} objects, avoiding accidental timezone shifts. Use
|
|
||||||
@racket[->js-date] when a native JavaScript @tt{Date} is desired.
|
|
||||||
|
|
||||||
@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:
|
A date value is therefore JSON-compatible data:
|
||||||
|
|
||||||
@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{
|
@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
|
@codeblock{
|
||||||
source project. The current public @racketmodname[jsmaker] module, demos, and
|
{ "$type": "gregor-time", "hour": 13, "minute": 45,
|
||||||
regression tests do not require these helper files. They are kept in the
|
"second": 0, "millisecond": 0 }
|
||||||
package layout for compatibility and are omitted from compilation and the test
|
}
|
||||||
entry point in @filepath{info.rkt}.
|
|
||||||
|
|
||||||
@section{Limitations}
|
A datetime or moment is represented similarly:
|
||||||
|
|
||||||
This package is not a full Racket compiler. It does not expand arbitrary
|
@codeblock{
|
||||||
Racket macros, implement modules, contracts, classes, continuations, parameters,
|
{ "$type": "gregor-datetime", "year": 2026, "month": 5, "day": 27,
|
||||||
or the full numeric tower. Unsupported forms should fail explicitly rather than
|
"hour": 13, "minute": 45, "second": 30, "millisecond": 0 }
|
||||||
silently generate JavaScript with different semantics.
|
}
|
||||||
|
|
||||||
@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
|
In a WebView bridge this usually means that the JavaScript wrapper around the
|
||||||
@other-doc['(lib "jsmaker/scrbl/usecases.scrbl")]. That document shows each
|
user code should return a JSON string, or at least a plain JSON object, whose
|
||||||
use case as Racket/js-maker source next to representative generated
|
@tt{result} field has already been encoded. Native @tt{Date} values should be
|
||||||
JavaScript, and lists the behavior covered by the regression test.
|
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
@@ -4,10 +4,17 @@
|
|||||||
(for-label racket/base
|
(for-label racket/base
|
||||||
"../main.rkt"))
|
"../main.rkt"))
|
||||||
|
|
||||||
@(define (side-by-side racket-source js-source)
|
@(define (code-box title source)
|
||||||
(tabular #:style 'boxed #:sep (hspace 2)
|
(tabular #:style 'boxed
|
||||||
(list (list (bold "Racket / js-maker") (bold "Generated JavaScript"))
|
(list (list (bold title))
|
||||||
(list (verbatim racket-source) (verbatim js-source)))))
|
(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)
|
@(define (tested s)
|
||||||
(nested #:style 'inset (bold "Tested behavior: ") 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
|
executing that JavaScript with the configured test executor. The corresponding
|
||||||
tests are in @filepath{testing/jsmaker-usecases.rkt}.
|
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
|
The tests intentionally use @racket[js/expression] for their calls wherever
|
||||||
possible. Raw JavaScript remains only in small harness preambles, such as fake
|
possible. Raw JavaScript remains only in small harness preambles, such as fake
|
||||||
@tt{setInterval}, fake DOM objects, and fake @tt{fetch}.
|
@tt{setInterval}, fake DOM objects, and fake @tt{fetch}.
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -4,4 +4,6 @@
|
|||||||
"jsmaker-regexp-regression.rkt"
|
"jsmaker-regexp-regression.rkt"
|
||||||
"jsmaker-program-regression.rkt"
|
"jsmaker-program-regression.rkt"
|
||||||
"jsmaker-dom-exercises.rkt"
|
"jsmaker-dom-exercises.rkt"
|
||||||
"jsmaker-usecases.rkt")
|
"jsmaker-usecases.rkt"
|
||||||
|
"jsmaker-list-regression.rkt"
|
||||||
|
"jsmaker-hash-regression.rkt")
|
||||||
|
|||||||
Reference in New Issue
Block a user