diff --git a/README.md b/README.md index cb76d21..2acb247 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,12 @@ # js-maker -A syntax-driven Racket-to-JavaScript macro module. +A compact Racket-to-JavaScript string maker macro. -This js-maker module was started as part of racket-webview, and has since -evolved under supervision of the author using ChatGPT as AI agent. +This js-maker 3 package is a clean restart from `js-transform.rkt`. It exports +only: -## Layout +- `js` +- `js1` -```text -js-maker/ - main.rkt public macro module - info.rkt package metadata and package test entry - scrbl/ - js-maker.scrbl Scribble documentation - testing/ - jsmaker-executors.rkt JavaScript engine discovery/execution - jsmaker-test-framework.rkt JS regression framework - jsmaker-test-runner.rkt old-name compatibility wrapper - jsmaker-regression.rkt core expression tests - jsmaker-regexp-regression.rkt regexp tests - jsmaker-program-regression.rkt larger program tests - jsmaker-regressions.rkt aggregate test entry - demo/ - show-jsmaker-output.rkt - show-optimized.rkt -``` - -## Added language support - -This package includes conservative support for: - -- `(with-handlers ([exn? handler]) body ...)`, translated to JavaScript - `try`/`catch`. Only generic `exn?` predicates are accepted. -- Gregor-style local names such as `date`, `time`, `moment`, `parse-date`, - `parse-time`, `parse-moment`, `date->string`, `time->string`, `->year`, - `->month`, `->day`, `->hours`, `->minutes`, `->seconds`, `->js-date`, and - `js-date->datetime`. Import prefixes are deliberately not hardcoded; the - compiler matches on the local identifier name after any `prefix:` part. - -## Run tests - -From the directory above `jsmaker`: - -```bash -raco make jsmaker/main.rkt jsmaker/testing/jsmaker-regressions.rkt \ - jsmaker/scrbl/jsmaker.scrbl -racket jsmaker/testing/jsmaker-regressions.rkt -raco test -p jsmaker -``` - -The test framework looks for JavaScript engines such as `node`, `deno`, -`bun`, `qjs`, `d8`, `jsc`, `js`. Chromium is only used when explicitly selected -or when `JSMAKER_BROWSER_FALLBACK=1` is set. - -When no JavaScript engine is available, the tests generate the JavaScript test -files and use an explicit `non-failing-javascript-stub`. The stub prints notes -to stdout, does not execute the generated JavaScript, and succeeds unless -`JSMAKER_REQUIRE_ENGINE` or `JSMAKER_REQUIRE_NODE` is set. This avoids package -server failures caused solely by a missing JavaScript runtime. - -Useful environment variables: - -```bash -JSMAKER_ENGINE=auto|node|deno|bun|qjs|d8|jsc|js|chromium -JSMAKER_ENGINE_PATH=/path/to/executable -JSMAKER_NODE=/path/to/node -JSMAKER_REQUIRE_ENGINE=1 -JSMAKER_ENGINE_TIMEOUT_SECONDS=15 -JSMAKER_BROWSER_FALLBACK=1 -``` - -## Suggested start prompt for future work - -Use this prompt when asking ChatGPT to make a new Racket module or extend this -one: - -```text -Work on a Racket module/package in a versioned build directory. - -Important: -- Always create a new subdirectory for the deliverable, for example - /mnt/data/-build-NNN/. -- Do not work directly in /mnt/data with loose files that have the same names; - avoid version confusion by copying and patching everything inside that build - directory. -- Keep the package structure stable: - - main.rkt for the public module; - - testing/ for test infrastructure and regression tests; - - demo/ for demonstration files; - - info.rkt for package metadata and test entry points. -- Adjust require paths to that structure before testing. -- Test with Racket itself, for example: - /tmp/racket/bin/raco make /main.rkt /testing/.rkt - /tmp/racket/bin/racket /testing/.rkt -- If JavaScript execution is needed, use a separate executor/test-framework - module. Tests must not fail solely because node/deno/bun/qjs is missing; in - that case they should use an explicit non-failing JavaScript stub that reports - this to stdout, unless a REQUIRE environment variable is set. -- Do not use shell-based internet access for dependencies. If packages are - needed, fetch them through the rktsndbx bootstrap/package-index flow. -- Deliver a zip file containing exactly the tested build directory. -- Briefly report which commands were run, what the test results were, and which - zip contains the tested result. -``` - - -## Latest tested fix - -This build includes the `with-handlers` callee-position fix for inline lambda -handlers, including rest-argument handlers such as `(lambda args ...)`. It also -fixes top-level `js` statement-context handling in general: `js` now emits -program/statement text and does not invent an implicit top-level `return`. This -keeps snippets valid when they are passed directly to WebView -`runJavaScript`/`evaluateJavaScript`, where a top-level JavaScript `return` is -a syntax error. Use `js/expression` when a generated JavaScript value is needed. -The build also adds a Racket-like division-by-zero runtime check for `/`, so the -generic `exn?` handler subset can catch `(/ 10 0)`. - -## JavaScript use case demos - -The package includes a larger set of JavaScript use case snippets in -`demo/js-usecases.rkt`. They are written in the Racket surface syntax accepted -by `js` and compiled to JavaScript by the macro. The generated JavaScript is -also written to `demo/js-usecases.generated.js`. - -The corresponding regression tests live in `testing/jsmaker-usecases.rkt` and -are included by `testing/jsmaker-regressions.rkt`. The test framework now awaits -Promise-valued tests, so asynchronous examples such as the Fetch API can be -checked with Node as well. - -Covered use cases include random numbers, `Set`, JavaScript falsey values, -currying, object destructuring, `setInterval`/`clearInterval`, object property -get/set/delete, string concatenation order, `Object.freeze`/`Object.seal`, -switch/case, classes with constructor defaults, sorting objects, array deletion -techniques, Bubble Sort, recursive Binary Search, `Map` counting, DOM HTML -access, anagram checks, pair-sum checks, and Fetch API result/error handling. - -## Use-case documentation - -The file `scrbl/usecases.scrbl` documents the JavaScript use cases from -`demo/js-usecases.rkt`. Each use case is shown as Racket/js-maker source next -to representative generated JavaScript, followed by the behavior covered by the -regression test. - -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. +There is deliberately no `js/expression` compatibility macro in this branch. +Use `js1` when the expression-level generator is needed directly. diff --git a/info.rkt b/info.rkt index 8f1314f..701e494 100644 --- a/info.rkt +++ b/info.rkt @@ -2,36 +2,16 @@ (define collection "js-maker") (define version "0.3") -(define license 'MIT) -(define pkg-desc "Syntax-driven Racket-to-JavaScript maker macro.") +(define license 'MIT) +(define pkg-desc "Small syntax-driven Racket-to-JavaScript maker macro.") (define pkg-authors '(hnmdijkema)) (define deps '("base")) -(define build-deps '("scribble-lib" "racket-doc")) +(define build-deps '("scribble-lib" "racket-doc" "rackunit-lib")) (define scribblings - '(("scrbl/js-maker.scrbl" () (library)) - ("scrbl/usecases.scrbl" () (library)) - ) - ) + '(("scrbl/js-maker.scrbl" () (library)))) -;; Running the package test suite should invoke exactly the maintained -;; regression entry point. The regression framework itself skips JavaScript -;; execution with warnings when no JavaScript engine is available, unless -;; JSMAKER_REQUIRE_ENGINE or JSMAKER_REQUIRE_NODE is set. -(define test-include-paths '("testing/jsmaker-regressions.rkt")) - -;; These files are supporting/reference files in this package layout and are -;; not part of the package test entry point. -(define test-omit-paths - '("demo/show-jsmaker-output.rkt" - "demo/show-optimized.rkt" - "testing/jsmaker-executors.rkt" - "testing/jsmaker-test-framework.rkt" - "testing/jsmaker-test-runner.rkt" - "testing/jsmaker-regression.rkt" - "testing/jsmaker-regexp-regression.rkt" - "testing/jsmaker-program-regression.rkt" - "testing/jsmaker-dom-exercises.rkt" - "testing/jsmaker-usecases.rkt" - "testing/jsmaker-list-regression.rkt" - "testing/jsmaker-hash-regression.rkt")) +;; js-maker 3 is a clean restart. The old demo/testing tree from the larger +;; branch is intentionally not shipped. The maintained package test suite is +;; this compact smoke test. +(define test-include-paths '("smoke-test.rkt")) diff --git a/main.rkt b/main.rkt index 7e47898..a5f1cb1 100644 --- a/main.rkt +++ b/main.rkt @@ -173,9 +173,13 @@ ;; First evaluate all RHS expressions into temporary names. ;; This preserves ordinary Racket/Scheme `let` scoping: ;; newly bound names are not visible to other RHS expressions. + ;; The real bindings are introduced in an inner block so + ;; JavaScript `let` TDZ rules do not shadow RHS references. (string-append "const " (js-id tmp) " = " (js1 init) ";\n") ... + "{\n" (string-append "let " (js-id id) " = " (js-id tmp) ";\n") ... (js body ...) + "}\n" "}"))])) ;; ------------------------------------------------------------------------- @@ -235,11 +239,15 @@ #'(string-append "{\n" ;; Initial expressions are evaluated before the loop ;; variables are introduced, like named `let` in Racket. + ;; The loop variables live in an inner block for the same + ;; reason as ordinary `let`: avoid JavaScript TDZ shadowing. (string-append "const " (js-id tmp) " = " (js1 init) ";\n") ... + "{\n" (string-append "let " (js-id id) " = " (js-id tmp) ";\n") ... "while (true) {\n" (js-loop-tail loop-name (id ...) body ...) "}\n" + "}\n" "}"))])) ;; ------------------------------------------------------------------------- @@ -248,7 +256,7 @@ ;; ------------------------------------------------------------------------- (define-syntax (js1 stx) - (syntax-case stx () + (syntax-case stx (define lambda λ if set! let let* begin return quote eval) [(_ (define (f arg ...) body ...)) #'(js-define-function f (arg ...) body ...)] [(_ (define name expr)) #'(js-define-value name expr)] [(_ (lambda (arg ...) body ...)) #'(js-lambda (arg ...) body ...)] @@ -300,6 +308,8 @@ [(_ statement ...) (string-append (js-stmt (js1 statement)) ...)])) + + ;; ------------------------------------------------------------------------- ;; Examples ;; ------------------------------------------------------------------------- diff --git a/scrbl/js-maker.scrbl b/scrbl/js-maker.scrbl index a680ae1..7ea1630 100644 --- a/scrbl/js-maker.scrbl +++ b/scrbl/js-maker.scrbl @@ -1,842 +1,53 @@ #lang scribble/manual -@(require scribble/core - (for-label racket/base - "../main.rkt")) +@(require (for-label racket/base + js-maker)) -@(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" ""] +@title{js-maker} +@author{Hans Dijkema} @defmodule[js-maker] -@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]. In the ordinary case the macros -expand to a string containing JavaScript source code. When the source contains -@racket[(inject racket-expr)] or its historical alias @racket[(eval racket-expr)], -the macros instead expand to a Racket expression that computes the JavaScript -source string at run time. The generated JavaScript can then be embedded in a -page, written to a file, tested with Node or another JavaScript engine, or used -as part of a larger code-generation workflow. - -The package is deliberately not a full Racket compiler. It recognizes a -well-defined set of Racket-like forms and maps them to JavaScript while trying -to preserve important Racket conventions such as @racket[#f]-only falsiness, -sequential @racket[let*] bindings, and single evaluation of chained comparison -operands. Unsupported forms should fail during macro expansion rather than -silently emit JavaScript with different semantics. - -@section{Public API} +@racketmodname[js-maker] provides a deliberately small syntax-driven macro for +making JavaScript strings from a limited Racket-like surface syntax. This is a +clean js-maker 3 restart based on the compact @filepath{js-transform.rkt} +implementation. @defform[(js form ...)]{ -Translates one or more Racket-like forms to JavaScript @emph{statement} code. -The result is a string. This is the usual entry point for functions, classes, -assignments, DOM scripts, and top-level JavaScript snippets. - -@racketblock[ -(displayln - (js - (define (square x) - (return (* x x))))) -] - -emits JavaScript similar to: - -@codeblock{ -function square(x) { - return (x * x); -} -} - -Inside function bodies, the last expression is returned automatically unless it -is already a statement form such as @racket[return], @racket[define], -@racket[set!], @racket[while], or @racket[for]. At the top level of a -@racket[js] form, however, js-maker emits statement code and does not invent an -implicit @tt{return}. This keeps the generated code valid when it is handed -directly to a WebView @tt{runJavaScript} or @tt{evaluateJavaScript} entry point, -where a top-level JavaScript @tt{return} would be an @tt{Illegal return} -syntax error. Use @racket[js/expression] when a generated JavaScript value is -needed. - -For example: - -@racketblock[ -(let ([html "

Hi

"]) - (displayln - (js - (let ([el (send document getElementById 'test)]) - (set! (js-dot el innerHTML) (inject html)) - #t)))) -] - -emits JavaScript similar to: - -@codeblock{ -{ - let el = document.getElementById("test"); - el.innerHTML = "

Hi

"; - true; -} -} -} - -@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. -Most input is converted to datum form and matched against the supported surface -language. The Racket code is normally not evaluated. The deliberate exception -is @racket[(inject racket-expr)], and the compatible older spelling -@racket[(eval racket-expr)]. These forms mark a Racket expression whose value -must be serialized as a JavaScript literal while the generated source string is -being constructed. The expression is evaluated in the lexical context of the -@racket[js] or @racket[js/expression] use. This has a few important -consequences: - -@compact-items[ - @item{Only forms known to js-maker are translated specially. Unknown calls are - emitted as JavaScript calls with translated arguments.} - @item{Identifiers are mapped to JavaScript identifiers. Reserved words are - avoided in variable position, while method/property names may use modern - JavaScript reserved property names such as @tt{catch}.} - @item{Macros from other Racket modules are not expanded by js-maker. If a - macro should be supported, add an explicit js-maker form or compile the - expanded shape.} - @item{The output is source text, not a JavaScript AST. Statement output is - pretty-printed and indented, but some expression output uses inline - helper functions/IIFEs to preserve semantics.} -] - -@subsection{Racket value interpolation with inject} - -The form @racket[(inject racket-expr)] is the preferred interpolation escape -hatch. It evaluates @racket[racket-expr] as Racket in the use-site lexical -context and then splices the resulting value into the JavaScript source as a -literal. This makes surrounding Racket bindings visible to the interpolation -expression. - -The older spelling @racket[(eval racket-expr)] is still accepted as an alias for -compatibility with existing code. New code should prefer @racket[inject], -because the form is not JavaScript @tt{eval} and does not evaluate JavaScript -source text. - -@(rkt+js -#< { - let a = 200; - return (a * a); -})() -JS -) - -@(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 -#<string age)))))) -] - -Classes can be generated with @racket[define-class]. Constructors may contain -simple default values: - -@racketblock[ -(js - (define-class Greeter - (constructor ([name "world"]) - (set! (js-dot this name) name)) - (method greet () - (return (string-append "Hello " (js-dot this name)))))) -] - -@section{Numbers and arithmetic} - -The arithmetic operators @racket[+], @racket[-], @racket[*], @racket[/], -@racket[quotient], @racket[remainder], @racket[modulo], @racket[add1], -@racket[sub1], @racket[abs], @racket[floor], @racket[ceiling], @racket[round], -@racket[max], @racket[min], @racket[sqrt], @racket[sqr], @racket[expt], -@racket[sin], @racket[cos], @racket[tan], @racket[asin], @racket[acos], -@racket[atan], @racket[log], and @racket[exp] are supported. - -The predicates @racket[zero?], @racket[positive?], @racket[negative?], -@racket[even?], and @racket[odd?] are also supported. - -Division is intentionally special. JavaScript evaluates @tt{10 / 0} to -@tt{Infinity}; exact Racket division by zero raises an exception. js-maker -therefore emits a run-time zero check for @racket[/]. This allows the -supported @racket[with-handlers] subset to catch the common division-by-zero -case. - -@section{Comparisons and equality} - -The comparison operators @racket[=], @racket[==], @racket[<], @racket[>], -@racket[<=], and @racket[>=] support n-ary comparisons. Simple chained -comparisons are emitted directly: - -@(rkt+js -#<?], @racket[string<=?], @racket[string>=?], -@racket[number->string], @racket[symbol->string], @racket[string->symbol], and -@racket[string->number] are supported. - -Racket @tt{#rx} and common @tt{#px} patterns are translated to JavaScript -@tt{RegExp} values when the syntax is compatible. The supported operations are -@racket[regexp?], @racket[pregexp?], @racket[regexp-match], -@racket[regexp-match?], @racket[regexp-match*], -@racket[regexp-match-positions], @racket[regexp-split], -@racket[regexp-replace], @racket[regexp-replace*], and -@racket[regexp-quote]. - -Match results are normalized to Racket-like values: a failed match becomes -@tt{false}, successful matches become JavaScript arrays, and an unmatched -optional capture becomes @tt{false}. Known incompatible constructs such as -inline option groups and atomic groups are rejected instead of being silently -miscompiled. Byte regexps are not supported. - -@section{Lists, vectors, and array-backed sequences} - -Lists and vectors are represented as JavaScript arrays. This is the most useful -mapping for JavaScript interoperability, but it is not the same as Racket's -linked-pair representation. - -Supported list/vector operations include: - -@compact-items[ - @item{@racket[list], @racket[vector], @racket[list*], @racket[cons], - @racket[append].} - @item{@racket[car], @racket[cdr], @racket[first], @racket[rest], - @racket[cadr], @racket[caddr], @racket[second], @racket[third].} - @item{@racket[length], @racket[vector-length], @racket[string-length], - @racket[list-ref], @racket[vector-ref], @racket[string-ref], - @racket[list-tail], and @racket[last].} - @item{@racket[null?], @racket[empty?], @racket[pair?], @racket[list?], - and @racket[vector?].} - @item{@racket[take], @racket[drop], @racket[take-right], - @racket[drop-right], @racket[reverse], @racket[list-set], and - @racket[list-update].} - @item{@racket[map], including multiple-list @racket[map], @racket[filter], - @racket[foldl], @racket[foldr], @racket[apply], and @racket[sort].} - @item{@racket[member], @racket[memq], @racket[memv], @racket[remove], and - @racket[remove*].} -] - -@racket[member] returns a tail array or @tt{false}. @racket[memq] and -@racket[memv] use @tt{Object.is}. @racket[filter] keeps every value that is not -@tt{false}, preserving Racket truthiness rather than JavaScript truthiness. - -@section{Hashes} - -Hashes are represented as plain JavaScript objects. This supports common -symbol/string-keyed data well, but does not implement Racket's arbitrary key -semantics, custom equality, mutation contracts, weak hashes, or the differences -between @racket[hash], @racket[hasheq], and @racket[hasheqv]. - -Supported hash operations include: - -@compact-items[ - @item{@racket[hash], @racket[hasheq], @racket[hasheqv], - @racket[make-hash], @racket[make-immutable-hash], - @racket[make-hasheq], @racket[make-immutable-hasheq], - @racket[make-hasheqv], and @racket[make-immutable-hasheqv].} - @item{@racket[hash?], @racket[hash-ref], @racket[hash-has-key?], - @racket[hash-count], and @racket[hash-empty?].} - @item{@racket[hash-set], @racket[hash-set!], @racket[hash-remove], - @racket[hash-remove!], @racket[hash-update], and - @racket[hash-update!].} - @item{@racket[hash-clear], @racket[hash-clear!], @racket[hash-copy], - @racket[hash-copy-clear].} - @item{@racket[hash-keys], @racket[hash-values], @racket[hash->list], - @racket[hash-map], and @racket[hash-for-each].} -] - -@racket[hash-ref] supports a default value and a default thunk. Mutating -operations mutate the JavaScript object representation directly. Immutable -operations create shallow copies. - -@section{Exceptions} - -A narrow @racket[with-handlers] subset is supported: - -@racketblock[ -(js - (with-handlers ([exn? (lambda (e) - (displayln (exn-message e)))]) - (/ 10 0))) -] - -This emits a JavaScript @tt{try}/@tt{catch}. Only the generic @racket[exn?] -predicate is supported. More specific Racket exception predicates are rejected -because JavaScript has a single catch channel and no Racket exception -hierarchy. The handler is called with the JavaScript thrown value. The helper -@racket[exn-message] extracts @tt{.message} when present and otherwise converts -the thrown value to a string. - -This feature is useful for generated JavaScript that throws JavaScript -@tt{Error} objects, including js-maker's division-by-zero check. It does not -model exception marks, continuable exceptions, parameterization, or Racket's -full exception hierarchy. - -@section{Gregor-style date and time helpers} - -The generator includes a small JavaScript-side value model for a subset of -Gregor-style date and time operations. This layer is intended for browser -input/output code and for JSON-safe communication between generated JavaScript -and Racket. It is not a complete implementation of Gregor. - -Prefixes are intentionally not hardcoded. A call such as -@racket[(g:date 2026 5 27)], @racket[(gregor:date 2026 5 27)], or -@racket[(date 2026 5 27)] is matched by the local name @racket[date]. This -keeps the generator independent of the import prefix used by the Racket module. - -Supported local names include @racket[date], @racket[time], @racket[datetime], -@racket[moment], @racket[make-date], @racket[make-time], -@racket[make-datetime], @racket[make-moment], @racket[parse-date], -@racket[parse-time], @racket[parse-datetime], @racket[parse-moment], -@racket[string->date], @racket[string->time], @racket[string->datetime], -@racket[date->string], @racket[time->string], @racket[datetime->string], -@racket[moment->string], @racket[date?], @racket[time?], -@racket[datetime?], @racket[moment?], @racket[->year], @racket[->month], -@racket[->day], @racket[->hours], @racket[->minutes], @racket[->seconds], -@racket[->js-date], and @racket[js-date->datetime]. - -@subsection{Representation and the JavaScript/Racket boundary} - -Plain dates, times, datetimes, and moments are represented as tagged -JavaScript objects. They are deliberately not represented as native -JavaScript @tt{Date} objects by default, because @tt{Date} always carries -timezone and instant semantics. A plain HTML @tt{input type="date"} value -such as @tt{"2026-05-27"} should not accidentally shift to another day when -serialized or interpreted in a different timezone. - -A date value is therefore JSON-compatible data: - -@codeblock{ -{ "$type": "gregor-date", "year": 2026, "month": 5, "day": 27 } + (define (sum-to n) + (let loop ([i 0] [acc 0]) + (if (> i n) + (return acc) + (loop (+ i 1) (+ acc i))))))] } -A time value is also JSON-compatible: +@defform[(js1 form)]{ +Generates JavaScript for a single expression or syntactic form and returns it as +a string. Use this when you want the expression-level generator directly. -@codeblock{ -{ "$type": "gregor-time", "hour": 13, "minute": 45, - "second": 0, "millisecond": 0 } +@racketblock[ +(js1 (+ 1 2))] } -A datetime or moment is represented similarly: - -@codeblock{ -{ "$type": "gregor-datetime", "year": 2026, "month": 5, "day": 27, - "hour": 13, "minute": 45, "second": 30, "millisecond": 0 } -} - -This convention is important for WebView integration. Generated JavaScript may -freely use native JavaScript values internally, but values that cross back to -Racket should be JSON-compatible. Non-JSON JavaScript values such as -@tt{undefined}, @tt{NaN}, @tt{Infinity}, @tt{Date}, @tt{Map}, @tt{Set}, -@tt{Error}, DOM nodes, and functions should be converted explicitly before -returning them to Racket. - -In a WebView bridge this usually means that the JavaScript wrapper around the -user code should return a JSON string, or at least a plain JSON object, whose -@tt{result} field has already been encoded. Native @tt{Date} values should be -encoded as tagged values, for example: - -@codeblock{ -{ "$type": "js-date", "iso": "2026-05-27T11:45:30.000Z" } -} - -Use @racket[->js-date] only when native JavaScript @tt{Date} behavior is -required inside JavaScript code. Before such a value is returned across the -Racket boundary, convert it back with @racket[js-date->datetime] or let the -WebView boundary encoder tag it as @tt{"js-date"}. - -@subsection{Basic construction and access} - -@(rkt+js -#<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 and the framework -uses an explicit @tt{non-failing-javascript-stub}. The stub prints notes to -stdout, does not execute the generated JavaScript, and exits successfully. -This is intentional so package tests do not fail merely because a JavaScript -runtime is missing, especially in package-server environments. Set -@tt{JSMAKER_REQUIRE_ENGINE=1} or @tt{JSMAKER_REQUIRE_NODE=1} to make a missing -engine a hard failure. - -Useful commands are: - -@codeblock{ -raco make main.rkt testing/jsmaker-regressions.rkt scrbl/jsmaker.scrbl -racket testing/jsmaker-regressions.rkt -raco test testing/jsmaker-regressions.rkt -} - -@section{Package layout} - -The package uses this layout: - -@codeblock{ -js-maker/ - main.rkt - info.rkt - private/ - testing/ - demo/ - scrbl/ -} - -@filepath{main.rkt} is the public module. @filepath{testing/} contains the -executor and regression framework. @filepath{demo/} contains generated-code -examples. @filepath{scrbl/} contains this reference and the use-case document. -The @filepath{private/} directory contains compatibility/helper material from -the source project. The current public module and tests do not depend on those -private files; they are kept for downstream compatibility and are omitted from -compilation and the package test entry point in @filepath{info.rkt}. - -@section{Limitations and non-goals} - -js-maker intentionally implements a pragmatic subset. The following are -important limitations: - -@compact-items[ - @item{It does not compile arbitrary Racket programs. There is no module - compiler, macro expander, contract compiler, class compiler, - continuation implementation, parameter model, or place/thread model.} - @item{Racket's numeric tower is not implemented. JavaScript numbers are used; - exactness, rationals, complex numbers, flonum/fixnum distinctions, and - overflow behavior are not modeled.} - @item{Lists are JavaScript arrays, not chains of pairs. Dotted pair semantics - are only approximated where useful.} - @item{Hashes are JavaScript objects, not true Racket hash tables. Arbitrary - object keys and equality-mode distinctions are not preserved.} - @item{Regular expression support targets the common intersection of Racket - regexps and JavaScript @tt{RegExp}. Incompatible constructs are rejected - where known.} - @item{Exception support is limited to generic @racket[exn?] handlers over - JavaScript @tt{try}/@tt{catch}.} - @item{Gregor support is a small compatibility layer, not the full Gregor API.} - @item{The emitter generates JavaScript source strings. It is not yet an AST - optimizer or full pretty-printer.} -] - -@section{Extending js-maker} - -New forms are normally added in one of three places in @filepath{main.rkt}: - -@compact-items[ - @item{Statement forms in the statement compiler.} - @item{Expression forms in the expression compiler.} - @item{Function/operator mappings in the operator table.} -] - -Every extension should include a regression test that compiles the Racket form, -runs the generated JavaScript with the executor, and checks the result. When a -new feature needs JavaScript environment support, keep that support in the test -harness rather than hiding raw JavaScript inside the feature implementation. - -@section{Further examples} - -The companion @hyperlink["../usecases/index.html"]{use-case manual} contains -practical examples such as DOM manipulation, @tt{Set}, currying, object -destructuring, timers, @tt{fetch}, sorting, binary search, and map-based -counting. Each use case shows the Racket/js-maker source, representative -JavaScript output, and the behavior tested by the regression suite. - -The source file for that companion manual is @filepath{scrbl/usecases.scrbl}. -The relative link above is used instead of @racket[other-doc] because the latter -can render as an unresolved @tt{(part ... "top")} tag when the two documents are -built outside Racket's installed documentation index. +@section{Supported core forms} + +The compact branch supports identifiers, quoted data, primitive literals, +function calls, infix arithmetic and comparison operators, @racket[if], +@racket[begin], @racket[return], @racket[set!], @racket[lambda], +@racket[define], ordinary @racket[let], @racket[let*], and named +@racket[let]. + +Ordinary @racket[let] keeps Racket's parallel binding semantics. All right-hand +sides are generated before the bound names are introduced, and the actual +JavaScript bindings are placed in an inner block so JavaScript temporal dead zone +rules cannot accidentally shadow the initializers. + +Named @racket[let] is compiled to a JavaScript @tt{while (true)} loop. A tail +call to the loop name is rewritten to parallel assignment of the loop variables +followed by @tt{continue}. This keeps the important loop semantics without +reintroducing the large js-maker 2 implementation. diff --git a/smoke-test.rkt b/smoke-test.rkt new file mode 100644 index 0000000..95d4408 --- /dev/null +++ b/smoke-test.rkt @@ -0,0 +1,29 @@ +#lang racket/base + +(require rackunit + "main.rkt") + +(define ordinary + (js (define (ordinary-let x) + (let ([x 1] [y x]) + (return y))))) + +(define sequential + (js (define (sequential-let x) + (let* ([x 1] [y x]) + (return y))))) + +(define looped + (js (define (sum-to n) + (let loop ([i 0] [acc 0]) + (if (> i n) + (return acc) + (loop (+ i 1) (+ acc i))))))) + +(check-regexp-match #rx"function ordinary_let" ordinary) +(check-regexp-match #rx"const [A-Za-z0-9_]+ = x;" ordinary) +(check-regexp-match #rx"let y = [A-Za-z0-9_]+;" ordinary) +(check-regexp-match #rx"let y = x;" sequential) +(check-regexp-match #rx"while \\(true\\)" looped) +(check-regexp-match #rx"continue;" looped) +(check-equal? (js1 (+ 1 2)) "1 + 2")