From a7acdc61401a3c2301ea8fcc7e7f00ae1404ef9c Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Wed, 27 May 2026 13:24:55 +0200 Subject: [PATCH] more tests and cases and documentation --- README.md | 4 + info.rkt | 4 +- main.rkt | 134 ++++- scrbl/jsmaker.scrbl | 854 ++++++++++++++++++++++------ scrbl/usecases.scrbl | 20 +- testing/jsmaker-hash-regression.rkt | 146 +++++ testing/jsmaker-list-regression.rkt | 99 ++++ testing/jsmaker-regressions.rkt | 4 +- 8 files changed, 1068 insertions(+), 197 deletions(-) create mode 100644 testing/jsmaker-hash-regression.rkt create mode 100644 testing/jsmaker-list-regression.rkt diff --git a/README.md b/README.md index 56fc0b6..96b4e94 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/info.rkt b/info.rkt index 86eb24c..862167a 100644 --- a/info.rkt +++ b/info.rkt @@ -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 diff --git a/main.rkt b/main.rkt index 619b462..081b668 100644 --- a/main.rkt +++ b/main.rkt @@ -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>=? - 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))] diff --git a/scrbl/jsmaker.scrbl b/scrbl/jsmaker.scrbl index 147bc7b..e19eb34 100644 --- a/scrbl/jsmaker.scrbl +++ b/scrbl/jsmaker.scrbl @@ -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 (square x) + (return (* x x))))) +] + +emits JavaScript similar to: + +@codeblock{ +function square(x) { + return (x * x); +} +} + +Inside function bodies, the last expression is returned automatically unless it +is already a statement form such as @racket[return], @racket[define], +@racket[set!], @racket[while], or @racket[for]. +} + +@defform[(js/expression expr)]{ +Translates a single Racket-like expression to a JavaScript @emph{expression} +string. Complex control forms are wrapped in immediately-invoked function +expressions when JavaScript has no direct expression equivalent. + +@racketblock[ +(displayln + (js/expression + (let loop ([i 0] [acc 0]) + (if (< i 5) + (loop (+ i 1) (+ acc i)) + acc)))) +] + +The named @racket[let] is emitted as a loop when it is used as a tail-recursive +self-call. +} + +@section{Mental model} + +The generator is best understood as a source-to-source translator over syntax. +The input is converted to datum form and matched against the supported surface +language. The Racket code is not evaluated. This has a few important +consequences: + +@compact-items[ + @item{Only forms known to js-maker are translated specially. Unknown calls are + emitted as JavaScript calls with translated arguments.} + @item{Identifiers are mapped to JavaScript identifiers. Reserved words are + avoided in variable position, while method/property names may use modern + JavaScript reserved property names such as @tt{catch}.} + @item{Macros from other Racket modules are not expanded by js-maker. If a + macro should be supported, add an explicit js-maker form or compile the + expanded shape.} + @item{The output is source text, not a JavaScript AST. Statement output is + pretty-printed and indented, but some expression output uses inline + helper functions/IIFEs to preserve semantics.} +] + +@section{Supported expression and statement forms} + +The following Racket forms are supported as either expressions, statements, or +both, depending on context: + +@compact-items[ + @item{@racket[begin], @racket[begin0], @racket[if], @racket[cond], and + @racket[case].} + @item{@racket[lambda] and @racket[λ], including rest and dotted formals.} + @item{@racket[define], function-style @racket[define], @racket[define-values], + @racket[set!], and explicit @racket[return].} + @item{@racket[let], @racket[let*], @racket[letrec], @racket[let-values], + @racket[let*-values], and named @racket[let].} + @item{@racket[when], @racket[unless], @racket[while], @racket[for], + @racket[for/list], @racket[for/vector], and @racket[for/fold].} + @item{@racket[with-handlers] for the generic @racket[exn?] case.} + @item{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 +#< x 10) (< x 15))) +RKT +#< 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 +#< x 10) (< x 15)) - (begin - (console.log x) - (return x)) - (return (* x x))))) + (define (describe person) + (let-object ([name 'name] + [age 'age 0]) + person + (return (string-append name ":" (number->string age)))))) ] -The generated JavaScript is statement-oriented: - -@codeblock{ -function f(x) { - if ((x > 10) && (x < 15)) { - console.log(x); - return x; - } else { - return (x * x); - } -} -} - -Inside function bodies, the final expression is returned automatically unless -an explicit @racket[return] form is used. -} - -@defform[(js/expression form)]{ -Translates a single Racket expression to a JavaScript expression string. +Classes can be generated with @racket[define-class]. Constructors may contain +simple default values: @racketblock[ -(js/expression - (let loop ([i 0] [acc 0]) - (if (< i 5) - (loop (+ i 1) (+ acc i)) - acc))) +(js + (define-class Greeter + (constructor ([name "world"]) + (set! (js-dot this name) name)) + (method greet () + (return (string-append "Hello " (js-dot this name)))))) ] -Tail-recursive named @racket[let] loops are lowered to JavaScript -@tt{while (true)} loops when the recursive call is in tail position. -} +@section{Numbers and arithmetic} -@section{Supported expression subset} +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 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 predicates @racket[zero?], @racket[positive?], @racket[negative?], +@racket[even?], and @racket[odd?] are also supported. -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]. +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{Truthiness and boolean simplification} +@section{Comparisons and equality} -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 comparison operators @racket[=], @racket[==], @racket[<], @racket[>], +@racket[<=], and @racket[>=] support n-ary comparisons. Simple chained +comparisons are emitted directly: -@racketblock[ -(js/expression (and (> x 10) (< x 15))) -] - -emits: - -@codeblock{ -((x > 10) && (x < 15)) -} - -A chained comparison such as @racket[(< x y 10)] is emitted directly when all -reused operands are simple: - -@codeblock{ +@(rkt+js +#<?], @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 +#<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 "" + word + "")))))) +] + +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. diff --git a/scrbl/usecases.scrbl b/scrbl/usecases.scrbl index c23631d..368622d 100644 --- a/scrbl/usecases.scrbl +++ b/scrbl/usecases.scrbl @@ -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}. diff --git a/testing/jsmaker-hash-regression.rkt b/testing/jsmaker-hash-regression.rkt new file mode 100644 index 0000000..d6c22ac --- /dev/null +++ b/testing/jsmaker-hash-regression.rkt @@ -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 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) diff --git a/testing/jsmaker-regressions.rkt b/testing/jsmaker-regressions.rkt index 1c37bbf..ea483e1 100644 --- a/testing/jsmaker-regressions.rkt +++ b/testing/jsmaker-regressions.rkt @@ -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")