From 2cf831c180081d053865bbcf2656d80d296a4c76 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Tue, 26 May 2026 09:42:35 +0200 Subject: [PATCH] A mostly AI coded js-maker, supervised by me. --- README.md | 152 ++- demo/dom-exercises.generated.js | 152 +++ demo/dom-exercises.rkt | 83 ++ demo/js-usecases.generated.js | 312 +++++++ demo/js-usecases.rkt | 304 ++++++ demo/show-jsmaker-output.rkt | 32 + demo/show-optimized.rkt | 16 + info.rkt | 41 + main.rkt | 1185 ++++++++++++++++++++++++ private/syntax-helpers.rkt | 42 + private/utils.rkt | 209 +++++ scrbl/.gitignore | 5 + scrbl/jsmaker.scrbl | 245 +++++ scrbl/usecases.scrbl | 584 ++++++++++++ testing/jsmaker-dom-exercises.rkt | 68 ++ testing/jsmaker-executors.rkt | 164 ++++ testing/jsmaker-program-regression.rkt | 118 +++ testing/jsmaker-regexp-regression.rkt | 24 + testing/jsmaker-regression.rkt | 110 +++ testing/jsmaker-regressions.rkt | 7 + testing/jsmaker-test-framework.rkt | 88 ++ testing/jsmaker-test-runner.rkt | 12 + testing/jsmaker-usecases.rkt | 191 ++++ 23 files changed, 4143 insertions(+), 1 deletion(-) create mode 100644 demo/dom-exercises.generated.js create mode 100644 demo/dom-exercises.rkt create mode 100644 demo/js-usecases.generated.js create mode 100644 demo/js-usecases.rkt create mode 100644 demo/show-jsmaker-output.rkt create mode 100644 demo/show-optimized.rkt create mode 100644 info.rkt create mode 100644 main.rkt create mode 100644 private/syntax-helpers.rkt create mode 100644 private/utils.rkt create mode 100644 scrbl/.gitignore create mode 100644 scrbl/jsmaker.scrbl create mode 100644 scrbl/usecases.scrbl create mode 100644 testing/jsmaker-dom-exercises.rkt create mode 100644 testing/jsmaker-executors.rkt create mode 100644 testing/jsmaker-program-regression.rkt create mode 100644 testing/jsmaker-regexp-regression.rkt create mode 100644 testing/jsmaker-regression.rkt create mode 100644 testing/jsmaker-regressions.rkt create mode 100644 testing/jsmaker-test-framework.rkt create mode 100644 testing/jsmaker-test-runner.rkt create mode 100644 testing/jsmaker-usecases.rkt diff --git a/README.md b/README.md index dcad8e3..56fc0b6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,153 @@ # js-maker -Converts simple scheme expressions to javascript for use as javascript code. \ No newline at end of file +A syntax-driven Racket-to-JavaScript macro experiment. + +## Layout + +```text +jsmaker/ + main.rkt public macro module + info.rkt package metadata and package test entry + private/ + syntax-helpers.rkt compatibility/helper material + utils.rkt compatibility/helper material + scrbl/ + jsmaker.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 +``` + +## Notes on private helpers + +Static analysis of this package layout shows that `main.rkt`, the test +infrastructure, the regression tests, demos and Scribble documentation do not +require `private/utils.rkt` or `private/syntax-helpers.rkt`. Those files are +retained as compatibility material from the source project and are omitted from +compilation and the package test entry point in `info.rkt`. + +The current public module has no dependency on Gregor or the old helper +modules. Gregor-style date/time forms are translated syntactically by +`main.rkt` into a small JavaScript-side representation. + +## 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 print warnings, but do not fail unless `JSMAKER_REQUIRE_ENGINE` or +`JSMAKER_REQUIRE_NODE` is set. + +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 +Werk aan een Racket-module/package in een versievaste buildmap. + +Belangrijk: +- Maak altijd een nieuwe submap voor de oplevering, bijvoorbeeld + /mnt/data/-build-NNN/. +- Werk niet direct in /mnt/data met losse bestanden met dezelfde naam; + voorkom versieverwarring door alles in die buildmap te kopi毛ren/patchen. +- Houd de package-structuur stabiel: + - main.rkt voor de publieke module; + - private/ voor helpers en utils; + - testing/ voor test-infrastructuur en regressietests; + - demo/ voor demonstratiebestanden; + - info.rkt voor package metadata en test entry points. +- Pas require-paden aan op die structuur voordat je test. +- Test met Racket zelf, bijvoorbeeld: + /tmp/racket/bin/raco make /main.rkt /testing/.rkt + /tmp/racket/bin/racket /testing/.rkt +- Als JavaScript nodig is, gebruik een aparte executor/test-framework module. + Tests mogen niet falen alleen omdat node/deno/bun/qjs ontbreekt; ze moeten + dan skippen met duidelijke warnings, tenzij een REQUIRE-envvar is gezet. +- Gebruik geen shell-internet voor dependencies. Als packages nodig zijn, haal + ze via de rktsndbx bootstrap/package-index flow op. +- Lever na afloop een zip van exact de geteste buildmap op. +- Rapporteer kort welke commando's zijn uitgevoerd, wat de testresultaten waren, + en welke zip het geteste resultaat bevat. +``` + + +## 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 +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. diff --git a/demo/dom-exercises.generated.js b/demo/dom-exercises.generated.js new file mode 100644 index 0000000..22ce082 --- /dev/null +++ b/demo/dom-exercises.generated.js @@ -0,0 +1,152 @@ +// exercise01 +{ + let p = document.querySelector("p"); + p.innerHTML = ((__rkt_body) => { + const __rkt_to_regexp = (__pat, __global) => { + const __flags = (__pat instanceof RegExp) + ? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('') + : (__global ? 'g' : ''); + return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags); + }; + const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x)); + const __rkt_replacement = (__s) => { + if (typeof __s !== 'string') return __s; + let __out = ''; + for (let __i = 0; __i < __s.length; __i++) { + const __ch = __s[__i]; + if (__ch === '$') { __out += '$$'; continue; } + if (__ch === '\\' && __i + 1 < __s.length) { + const __n = __s[++__i]; + if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n)); + else __out += __n; + } else __out += __ch; + } + return __out; + }; + return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement); + })((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.innerHTML).replace(__rkt_to_regexp(new RegExp("\\b\\w{9,}\\b"), true), __rkt_replacement(function(word) { + return ("" + word + ""); + }))); +} + +// exercise02 +{ + let p = document.querySelector("p"); + p.insertAdjacentHTML("afterend", "Source: ForceM Ipsum"); +} + +// exercise03 +{ + let p = document.querySelector("p"); + p.innerHTML = ((__rkt_body) => { + const __rkt_to_regexp = (__pat, __global) => { + const __flags = (__pat instanceof RegExp) + ? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('') + : (__global ? 'g' : ''); + return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags); + }; + const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x)); + const __rkt_replacement = (__s) => { + if (typeof __s !== 'string') return __s; + let __out = ''; + for (let __i = 0; __i < __s.length; __i++) { + const __ch = __s[__i]; + if (__ch === '$') { __out += '$$'; continue; } + if (__ch === '\\' && __i + 1 < __s.length) { + const __n = __s[++__i]; + if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n)); + else __out += __n; + } else __out += __ch; + } + return __out; + }; + return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement); + })((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.textContent).replace(__rkt_to_regexp(new RegExp("\\.\\s*"), true), __rkt_replacement(".
"))); +} + +// exercise04 +{ + let heading = document.querySelector("h1"); + let p = document.querySelector("p"); + let words = ((__rkt_body) => { + const __rkt_to_regexp = (__pat, __global) => { + const __flags = (__pat instanceof RegExp) + ? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('') + : (__global ? 'g' : ''); + return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags); + }; + const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x)); + const __rkt_replacement = (__s) => { + if (typeof __s !== 'string') return __s; + let __out = ''; + for (let __i = 0; __i < __s.length; __i++) { + const __ch = __s[__i]; + if (__ch === '$') { __out += '$$'; continue; } + if (__ch === '\\' && __i + 1 < __s.length) { + const __n = __s[++__i]; + if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n)); + else __out += __n; + } else __out += __ch; + } + return __out; + }; + return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement); + })((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.textContent).split(__rkt_to_regexp(new RegExp(" "), true))); + let count = (words).length; + heading.insertAdjacentHTML("afterend", ("

" + String(count) + " words

")); +} + +// exercise05 +{ + let p = document.querySelector("p"); + let step = ((__rkt_body) => { + const __rkt_to_regexp = (__pat, __global) => { + const __flags = (__pat instanceof RegExp) + ? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('') + : (__global ? 'g' : ''); + return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags); + }; + const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x)); + const __rkt_replacement = (__s) => { + if (typeof __s !== 'string') return __s; + let __out = ''; + for (let __i = 0; __i < __s.length; __i++) { + const __ch = __s[__i]; + if (__ch === '$') { __out += '$$'; continue; } + if (__ch === '\\' && __i + 1 < __s.length) { + const __n = __s[++__i]; + if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n)); + else __out += __n; + } else __out += __ch; + } + return __out; + }; + return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement); + })((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.innerHTML).replace(__rkt_to_regexp(new RegExp("\\?"), true), __rkt_replacement("馃"))); + let out = ((__rkt_body) => { + const __rkt_to_regexp = (__pat, __global) => { + const __flags = (__pat instanceof RegExp) + ? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('') + : (__global ? 'g' : ''); + return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags); + }; + const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x)); + const __rkt_replacement = (__s) => { + if (typeof __s !== 'string') return __s; + let __out = ''; + for (let __i = 0; __i < __s.length; __i++) { + const __ch = __s[__i]; + if (__ch === '$') { __out += '$$'; continue; } + if (__ch === '\\' && __i + 1 < __s.length) { + const __n = __s[++__i]; + if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n)); + else __out += __n; + } else __out += __ch; + } + return __out; + }; + return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement); + })((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(step).replace(__rkt_to_regexp(new RegExp("!"), true), __rkt_replacement("馃槻"))); + p.innerHTML = out; +} + diff --git a/demo/dom-exercises.rkt b/demo/dom-exercises.rkt new file mode 100644 index 0000000..bf4fbd7 --- /dev/null +++ b/demo/dom-exercises.rkt @@ -0,0 +1,83 @@ +#lang racket/base + +(require "../main.rkt") + +(provide exercise01 + exercise02 + exercise03 + exercise04 + exercise05 + all-dom-exercises + show-dom-exercises) + +;; JavaScript DOM Exercises 01 Tutorial: https://youtu.be/EHF7xBUAmrQ + +;; Exercise 01 +;; Highlight all words over 8 characters long in the paragraph text. +(define exercise01 + (js + (let* ([p (send document querySelector "p")]) + (set! p.innerHTML + (regexp-replace* #px"\\b\\w{9,}\\b" + p.innerHTML + (位 (word) + (string-append "" + word + ""))))))) + +;; Exercise 02 +;; Add a link back to the source of the text after the paragraph tag. +(define exercise02 + (js + (let* ([p (send document querySelector "p")]) + (send p insertAdjacentHTML + "afterend" + "Source: ForceM Ipsum")))) + +;; Exercise 03 +;; Split each new sentence on to a separate line in the paragraph text. +;; A sentence is assumed to be terminated with a period. +(define exercise03 + (js + (let* ([p (send document querySelector "p")]) + (set! p.innerHTML + (regexp-replace* #rx"\\.\\s*" p.textContent ".
"))))) + +;; Exercise 04 +;; Count the number of words in the paragraph tag and display the count +;; after the heading. Words are separated by one singular whitespace. +(define exercise04 + (js + (let* ([heading (send document querySelector "h1")] + [p (send document querySelector "p")] + [words (regexp-split #rx" " p.textContent)] + [count (length words)]) + (send heading insertAdjacentHTML + "afterend" + (string-append "

" (number->string count) " words

"))))) + +;; Exercise 05 +;; Replace question marks with thinking faces and exclamation marks with +;; astonished faces. +(define exercise05 + (js + (let* ([p (send document querySelector "p")] + [step (regexp-replace* #rx"\\?" p.innerHTML "馃")] + [out (regexp-replace* #rx"!" step "馃槻")]) + (set! p.innerHTML out)))) + +(define all-dom-exercises + `((exercise01 . ,exercise01) + (exercise02 . ,exercise02) + (exercise03 . ,exercise03) + (exercise04 . ,exercise04) + (exercise05 . ,exercise05))) + +(define (show-dom-exercises) + (for ([entry (in-list all-dom-exercises)]) + (displayln (format "// ~a" (car entry))) + (displayln (cdr entry)) + (newline))) + +(module+ main + (show-dom-exercises)) diff --git a/demo/js-usecases.generated.js b/demo/js-usecases.generated.js new file mode 100644 index 0000000..b73489d --- /dev/null +++ b/demo/js-usecases.generated.js @@ -0,0 +1,312 @@ +// Generated by demo/js-usecases.rkt +// Each use case is wrapped in a function so snippets with return are valid. + +// random-number +function run_random_number() { +function randomBetween1And5() { + return (Math.floor((Math.random() * 5)) + 1); +} +} + +// unique-values +function run_unique_values() { +function uniqueValues(xs) { + return Array.from(new Set(xs)); +} +} + +// falsey-values +function run_falsey_values() { +function falseyValues() { + return [false, 0, "", null, undefined, NaN]; +} +} + +// currying +function run_currying() { +function add(x) { + return function(y) { + return (x + y); + }; +} +} + +// object-destructuring +function run_object_destructuring() { +function describePerson(person) { + return (() => { + const { name: name, age: age = 0 } = person; + return (name + ":" + String(age)); + })(); +} +} + +// timer-interval +function run_timer_interval() { +function startTimer() { + { + let ticks = 0; + let intervalId = false; + intervalId = setInterval(function() { + ticks = (ticks + 1); + if ((ticks === 3)) { + clearInterval(intervalId); + } + return undefined; + }, 10); + return {id: intervalId, getTicks: function() { + return ticks; + }}; + } +} +} + +// object-props +function run_object_props() { +function objectProps() { + { + let obj = {a: 1}; + let a1 = obj.a; + let a2 = obj["a"]; + return (() => { + const { a: a3 } = obj; + obj.b = 2; + obj["c"] = 3; + delete obj["a"]; + return [a1, a2, a3, obj.b, obj["c"], Object.hasOwn(obj, "a")]; + })(); + } +} +} + +// string-concat-order +function run_string_concat_order() { +function concatOrder() { + return [(1 + 2 + "3"), ("1" + 2 + 3)]; +} +} + +// freeze-vs-seal +function run_freeze_vs_seal() { +function freezeVsSeal() { + { + let frozen = Object.freeze({a: 1}); + let sealed = Object.seal({a: 1}); + frozen.a = 9; + sealed.a = 9; + delete sealed["a"]; + return [frozen.a, sealed.a, Object.isFrozen(frozen), Object.isSealed(sealed), Object.hasOwn(sealed, "a")]; + } +} +} + +// switch +function run_switch() { +function switchExample(n) { + return (() => { + { + const __case_value = n; + switch (__case_value) { + case 1: + return "one"; + break; + case 2: + case 3: + return "two-or-three"; + break; + default: + return "other"; + } + } + })(); +} +} + +// class-constructor +function run_class_constructor() { +class Greeter { + constructor(name = "world") { + this.name = name; + } + greet() { + return ("Hello " + this.name); + } +} +function classExample() { + { + let a = new Greeter(); + let b = new Greeter("Ada"); + return [a.greet(), b.greet()]; + } +} +} + +// sort-objects-by-property +function run_sort_objects_by_property() { +function sortByProperty(xs, prop) { + return xs.slice().sort(function(a, b) { + return (a[prop] - b[prop]); + }); +} +} + +// delete-array-elements +function run_delete_array_elements() { +function deleteArrayWays(xs) { + { + let a1 = xs.slice(); + let a2 = xs.slice(); + let a3 = xs.slice(); + let a4 = xs.slice(); + a1.splice(1, 1); + a2 = a2.filter(function(x, i) { + return ((i === 1) === false); + }); + a3 = a3.slice(0, 1).concat(a3.slice(2)); + delete a4[1]; + return [a1, a2, a3, [Object.hasOwn(a4, "1"), (a4).length]]; + } +} +} + +// bubble-sort +function run_bubble_sort() { +function bubbleSort(xs) { + { + let a = xs.slice(); + let n = (a).length; + while (n > 1) { + { + let i = 1; + while (i < n) { + if ((() => { + const __cmp_1 = (a)[(i - 1)]; + const __cmp_2 = (a)[i]; + return (__cmp_1 > __cmp_2); + })()) { + { + let tmp = (a)[(i - 1)]; + a[(i - 1)] = (a)[i]; + a[i] = tmp; + } + } + i = (i + 1); + } + } + n = (n - 1); + } + return a; + } +} +} + +// binary-search +function run_binary_search() { +function binarySearch(xs, target, low, high) { + if (low > high) { + return -1; + } else { + { + let mid = Math.floor((() => { + const __div_3 = (low + high); + const __div_4 = 2; + if (__div_4 === 0) throw new Error("division by zero"); + return (__div_3 / __div_4); + })()); + let value = (xs)[mid]; + const __cond_value_5 = (value === target); + if (__cond_value_5 !== false) { + return mid; + } else { + const __cond_value_6 = (value < target); + if (__cond_value_6 !== false) { + return binarySearch(xs, target, (mid + 1), high); + } else { + return binarySearch(xs, target, low, (mid - 1)); + } + } + } + } +} +} + +// map-count-occurrences +function run_map_count_occurrences() { +function countOccurrences(xs) { + { + let counts = new Map(); + for (const x of xs) { + if ((counts.has(x) !== false)) { + counts.set(x, (counts.get(x) + 1)); + } else { + counts.set(x, 1); + } + } + return Array.from(counts.entries()); + } +} +} + +// get-html-three-ways +function run_get_html_three_ways() { +function getHtmlThreeWays() { + return [document.body.innerHTML, document.querySelector("body").innerHTML, document.getElementById("root")["innerHTML"]]; +} +} + +// anagram +function run_anagram() { +function sortChars(s) { + return s.split("").sort().join(""); +} +function canArrange(stringA, stringB) { + return (() => { + const __cmp_8 = sortChars(stringA); + const __cmp_9 = sortChars(stringB); + return (__cmp_8 === __cmp_9); + })(); +} +} + +// pairs-equal-target +function run_pairs_equal_target() { +function pairsEqualTarget(xs, target) { + { + let seen = new Set(); + let used = new Set(); + let out = []; + for (const x of xs) { + { + let y = (target - x); + if (((() => { + let __and_value_11 = seen.has(y); + if (__and_value_11 === false) return false; + __and_value_11 = (used.has(x) === false); + if (__and_value_11 === false) return false; + return (used.has(y) === false); + })() !== false)) { + out.push([y, x]); + used.add(x); + used.add(y); + } else { + seen.add(x); + } + } + } + return out; + } +} +} + +// fetch-api +function run_fetch_api() { +function loadTitle(url) { + return fetch(url).then(function(response) { + return response.json(); + }).then(function(data) { + return {ok: true, title: data.title}; + }).catch(function(err) { + return {ok: false, message: err.message}; + }); +} +} diff --git a/demo/js-usecases.rkt b/demo/js-usecases.rkt new file mode 100644 index 0000000..a3a2c55 --- /dev/null +++ b/demo/js-usecases.rkt @@ -0,0 +1,304 @@ +#lang racket/base + +(require racket/list + "../main.rkt") + +(provide usecase-random-number + usecase-unique-values + usecase-falsey-values + usecase-currying + usecase-object-destructuring + usecase-timer-interval + usecase-object-props + usecase-string-concat-order + usecase-freeze-vs-seal + usecase-switch + usecase-class-constructor + usecase-sort-objects-by-property + usecase-delete-array-elements + usecase-bubble-sort + usecase-binary-search + usecase-map-count-occurrences + usecase-get-html-three-ways + usecase-anagram + usecase-pairs-equal-target + usecase-fetch-api + all-js-usecases + show-js-usecases + write-js-usecases-file) + +;; Use case 01: generate a random integer between 1 and 5. +(define usecase-random-number + (js + (define (randomBetween1And5) + (return (+ (send Math floor (* (send Math random) 5)) 1))))) + +;; Use case 02: get unique values from an array with duplicates using Set. +(define usecase-unique-values + (js + (define (uniqueValues xs) + (return (send Array from (new Set xs)))))) + +;; Use case 03: the six JavaScript falsey values. +(define usecase-falsey-values + (js + (define (falseyValues) + (return (array #f 0 "" js-null js-undefined js-NaN))))) + +;; Use case 04: currying, simple example. +(define usecase-currying + (js + (define (add x) + (return (lambda (y) + (return (+ x y))))))) + +;; Use case 05: object destructuring. +(define usecase-object-destructuring + (js + (define (describePerson person) + (let-object ([name 'name] + [age 'age 0]) + person + (return (string-append name ":" (number->string age))))))) + +;; Use case 06: get out of a timer interval with setInterval/clearInterval. +(define usecase-timer-interval + (js + (define (startTimer) + (let* ([ticks 0] + [intervalId #f]) + (set! intervalId + (setInterval (lambda () + (set! ticks (+ ticks 1)) + (when (= ticks 3) + (clearInterval intervalId))) + 10)) + (return (object 'id intervalId + 'getTicks (lambda () (return ticks)))))))) + +;; Use case 07: get/set/delete object properties. The value of a is read via +;; dot access, bracket access, and destructuring. +(define usecase-object-props + (js + (define (objectProps) + (let* ([obj (object 'a 1)] + [a1 obj.a] + [a2 (js-ref obj "a")]) + (let-object ([a3 'a]) obj + (set! obj.b 2) + (set-prop! obj "c" 3) + (delete-prop! obj "a") + (return (array a1 a2 a3 obj.b (js-ref obj "c") + (send Object hasOwn obj "a")))))))) + +;; Use case 08: string concatenation; order matters with JavaScript +. +(define usecase-string-concat-order + (js + (define (concatOrder) + (return (array (+ 1 2 "3") + (+ "1" 2 3)))))) + +;; Use case 09: Object.freeze() vs Object.seal(). +(define usecase-freeze-vs-seal + (js + (define (freezeVsSeal) + (let* ([frozen (send Object freeze (object 'a 1))] + [sealed (send Object seal (object 'a 1))]) + (set! frozen.a 9) + (set! sealed.a 9) + (delete-prop! sealed "a") + (return (array frozen.a + sealed.a + (send Object isFrozen frozen) + (send Object isSealed sealed) + (send Object hasOwn sealed "a"))))))) + +;; Use case 10: switch example. The Racket surface form is case. +(define usecase-switch + (js + (define (switchExample n) + (case n + [(1) (return "one")] + [(2 3) (return "two-or-three")] + [else (return "other")])))) + +;; Use case 11: class constructor with a default value. +(define usecase-class-constructor + (js + (define-class Greeter + (constructor ([name "world"]) + (set! this.name name)) + (method greet () + (return (string-append "Hello " this.name)))) + + (define (classExample) + (let* ([a (new Greeter)] + [b (new Greeter "Ada")]) + (return (array (send a greet) (send b greet))))))) + +;; Use case 12: sort an array of objects by a given property. +(define usecase-sort-objects-by-property + (js + (define (sortByProperty xs prop) + (return (send (send xs slice) + sort + (lambda (a b) + (return (- (js-ref a prop) (js-ref b prop))))))))) + +;; Use case 13: four ways to delete/remove an element from an array. +(define usecase-delete-array-elements + (js + (define (deleteArrayWays xs) + (let* ([a1 (send xs slice)] + [a2 (send xs slice)] + [a3 (send xs slice)] + [a4 (send xs slice)]) + ;; 1. Mutating removal with splice. + (send a1 splice 1 1) + ;; 2. Functional removal with filter. + (set! a2 (send a2 filter (lambda (x i) (return (not (= i 1)))))) + ;; 3. Rebuild with slice + concat. + (set! a3 (send (send a3 slice 0 1) concat (send a3 slice 2))) + ;; 4. delete leaves a hole and preserves length. + (delete-prop! a4 1) + (return (array a1 a2 a3 (array (send Object hasOwn a4 "1") (length a4)))))))) + +;; Use case 14: Bubble Sort. +(define usecase-bubble-sort + (js + (define (bubbleSort xs) + (let* ([a (send xs slice)] + [n (length a)]) + (while (> n 1) + (let* ([i 1]) + (while (< i n) + (when (> (list-ref a (- i 1)) (list-ref a i)) + (let* ([tmp (list-ref a (- i 1))]) + (vector-set! a (- i 1) (list-ref a i)) + (vector-set! a i tmp))) + (set! i (+ i 1)))) + (set! n (- n 1))) + (return a))))) + +;; Use case 15: Binary Search using recursion. +(define usecase-binary-search + (js + (define (binarySearch xs target low high) + (if (> low high) + (return -1) + (let* ([mid (send Math floor (/ (+ low high) 2))] + [value (list-ref xs mid)]) + (cond + [(= value target) (return mid)] + [(< value target) (return (binarySearch xs target (+ mid 1) high))] + [else (return (binarySearch xs target low (- mid 1)))])))))) + +;; Use case 16: use Map to count how often each element occurs in an array. +(define usecase-map-count-occurrences + (js + (define (countOccurrences xs) + (let* ([counts (new Map)]) + (for ([x (in-list xs)]) + (if (send counts has x) + (send counts set x (+ (send counts get x) 1)) + (send counts set x 1))) + (return (send Array from (send counts entries))))))) + +;; Use case 17: get HTML in three different ways via the DOM. +(define usecase-get-html-three-ways + (js + (define (getHtmlThreeWays) + (return (array document.body.innerHTML + (js-dot (send document querySelector "body") innerHTML) + (js-ref (send document getElementById "root") "innerHTML")))))) + +;; Use case 18: determine if stringA can be arranged into stringB. +(define usecase-anagram + (js + (define (sortChars s) + (return (send (send (send s split "") sort) join ""))) + + (define (canArrange stringA stringB) + (return (string=? (sortChars stringA) (sortChars stringB)))))) + +;; Use case 19: determine what pairs in an array equal a given value, with no +;; repeated numbers in the result pairs. +(define usecase-pairs-equal-target + (js + (define (pairsEqualTarget xs target) + (let* ([seen (new Set)] + [used (new Set)] + [out (array)]) + (for ([x (in-list xs)]) + (let* ([y (- target x)]) + (if (and (send seen has y) + (not (send used has x)) + (not (send used has y))) + (begin + (send out push (array y x)) + (send used add x) + (send used add y)) + (send seen add x)))) + (return out))))) + +;; Use case 20: fetch API, handling results and errors. The function returns a +;; Promise, which the test framework awaits. +(define usecase-fetch-api + (js + (define (loadTitle url) + (return + (send + (send + (send (fetch url) + then + (lambda (response) + (return (send response json)))) + then + (lambda (data) + (return (object 'ok #t 'title data.title)))) + catch + (lambda (err) + (return (object 'ok #f 'message err.message)))))))) + +(define all-js-usecases + `((random-number . ,usecase-random-number) + (unique-values . ,usecase-unique-values) + (falsey-values . ,usecase-falsey-values) + (currying . ,usecase-currying) + (object-destructuring . ,usecase-object-destructuring) + (timer-interval . ,usecase-timer-interval) + (object-props . ,usecase-object-props) + (string-concat-order . ,usecase-string-concat-order) + (freeze-vs-seal . ,usecase-freeze-vs-seal) + (switch . ,usecase-switch) + (class-constructor . ,usecase-class-constructor) + (sort-objects-by-property . ,usecase-sort-objects-by-property) + (delete-array-elements . ,usecase-delete-array-elements) + (bubble-sort . ,usecase-bubble-sort) + (binary-search . ,usecase-binary-search) + (map-count-occurrences . ,usecase-map-count-occurrences) + (get-html-three-ways . ,usecase-get-html-three-ways) + (anagram . ,usecase-anagram) + (pairs-equal-target . ,usecase-pairs-equal-target) + (fetch-api . ,usecase-fetch-api))) + +(define (show-js-usecases) + (for ([entry (in-list all-js-usecases)]) + (displayln (format "// ~a" (car entry))) + (displayln (cdr entry)) + (newline))) + +(define (write-js-usecases-file path) + (call-with-output-file path + #:exists 'replace + (lambda (out) + (displayln "// Generated by demo/js-usecases.rkt" out) + (displayln "// Each use case is wrapped in a function so snippets with return are valid." out) + (for ([entry (in-list all-js-usecases)]) + (fprintf out "\n// ~a\n" (car entry)) + (fprintf out "function run_~a() {\n~a\n}\n" + (regexp-replace* #rx"[^A-Za-z0-9_$]" (symbol->string (car entry)) "_") + (cdr entry)))))) + +(module+ main + (show-js-usecases)) diff --git a/demo/show-jsmaker-output.rkt b/demo/show-jsmaker-output.rkt new file mode 100644 index 0000000..3c698f7 --- /dev/null +++ b/demo/show-jsmaker-output.rkt @@ -0,0 +1,32 @@ +#lang racket/base +(require "../main.rkt") + +(define examples + (list + (cons 'expression + (js/expression + (let loop ([i 0] [acc 0]) + (if (< i 5) + (loop (+ i 1) (+ acc i)) + acc)))) + (cons 'program-t1 + (js + (set! window.myfunc + (位 (x) + (let* ((el (send document getElementById 'hi)) + (y (* x x))) + (send el setAttribute "x" (+ y ""))) + (send console log "dit set attribute x on element hi"))))) + (cons 'program-t2 + (js + (define (f x) + (if (and (> x 10) (< x 15)) + (begin (console.log x) + (return x)) + (return (* x x)))))) + (cons 'regexp + (js/expression + (regexp-match #px"([a-z]+)-([0-9]+)" "abc-123"))))) + +(for ([e (in-list examples)]) + (printf "===== ~a =====\n~a\n\n" (car e) (cdr e))) diff --git a/demo/show-optimized.rkt b/demo/show-optimized.rkt new file mode 100644 index 0000000..3963683 --- /dev/null +++ b/demo/show-optimized.rkt @@ -0,0 +1,16 @@ +#lang racket/base +(require "../main.rkt") +(displayln "--- and ---") +(displayln (js/expression (and (> x 10) (< x 15)))) +(displayln "--- t2 ---") +(displayln (js (define (f x) + (if (and (> x 10) (< x 15)) + (begin (console.log x) + (return x)) + (return (* x x)))))) +(displayln "--- let* ---") +(displayln (js (let* ((x 10) + (y (+ x x))) + (return y)))) +(displayln "--- let* tdz ---") +(displayln (js/expression (let ([x 4]) (let* ([x x] [y x]) (+ x y))))) diff --git a/info.rkt b/info.rkt new file mode 100644 index 0000000..86eb24c --- /dev/null +++ b/info.rkt @@ -0,0 +1,41 @@ +#lang info + +(define collection "jsmaker") +(define version "0.2") +(define pkg-desc "Syntax-driven Racket-to-JavaScript maker macro with regression tests.") +(define pkg-authors '(hans-dijkema)) +(define deps '("base")) +(define build-deps '("scribble-lib" "racket-doc")) + +(define scribblings + '(("scrbl/jsmaker.scrbl" () (library)) + ("scrbl/usecases.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 + '("private/utils.rkt" + "private/syntax-helpers.rkt" + "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")) + +;; The private files are compatibility/support material and have project-local +;; dependencies in downstream copies. The public module and tests do not +;; depend on them. +(define compile-omit-paths + '("private/utils.rkt" + "private/syntax-helpers.rkt")) diff --git a/main.rkt b/main.rkt new file mode 100644 index 0000000..619b462 --- /dev/null +++ b/main.rkt @@ -0,0 +1,1185 @@ +#lang racket/base + +(require (for-syntax racket/base + racket/list + racket/match + racket/string)) + +(provide js js/expression) + +;; The js macro translates a practical Racket-expression subset to JavaScript. +;; It is intentionally syntax-driven: the Racket expressions are not evaluated. + +(begin-for-syntax + (struct unsupported (message datum) #:transparent) + + (define (fail who d) + (raise-syntax-error 'js (format "unsupported ~a: ~v" who d))) + + (define (indent s [prefix " "]) + (define lines (string-split s "\n" #:trim? #f)) + (string-join (map (位 (line) (if (string=? line "") line (string-append prefix line))) lines) "\n")) + + (define (join-lines xs) + (string-join (filter (位 (x) (and x (not (string=? x "")))) xs) "\n")) + + (define (escape-js-string s) + (define out (open-output-string)) + (for ([ch (in-string s)]) + (case ch + [(#\\) (display "\\\\" out)] + [(#\") (display "\\\"" out)] + [(#\newline) (display "\\n" out)] + [(#\return) (display "\\r" out)] + [(#\tab) (display "\\t" out)] + [else (display ch out)])) + (get-output-string out)) + + (define (js-string s) (format "\"~a\"" (escape-js-string s))) + + (define reserved-js + '(break case catch class const continue debugger default delete do else export extends + finally for function if import in instanceof let new return super switch this throw + try typeof var void while with yield enum await implements package protected static + interface private public null true false undefined NaN Infinity)) + + (define (replace-suffix s suffix replacement) + (if (string-suffix? s suffix) + (string-append (substring s 0 (- (string-length s) (string-length suffix))) replacement) + s)) + + (define (id->js x) + (define s0 (cond [(symbol? x) (symbol->string x)] [(keyword? x) (keyword->string x)] [else (format "~a" x)])) + (cond + [(member s0 '("...")) (fail "identifier" x)] + [(regexp-match? #rx"^[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*$" s0) + (if (memq (string->symbol s0) reserved-js) (string-append s0 "_") s0)] + [else + (define parts (string-split s0 "." #:trim? #f)) + (define (clean part) + (define p1 (replace-suffix (replace-suffix (replace-suffix part "?" "_p") "!" "_bang") "->" "_to")) + (define p2 (regexp-replace* #rx"[^A-Za-z0-9_$]" p1 "_")) + (define p3 (if (regexp-match? #rx"^[A-Za-z_$]" p2) p2 (string-append "_" p2))) + (if (memq (string->symbol p3) reserved-js) (string-append p3 "_") p3)) + (string-join (map clean parts) ".")])) + + (define (prop->js x) + ;; Property names after a dot may legally be reserved words in modern + ;; JavaScript, for example promise.catch(...). Variable identifiers still + ;; use id->js, but method/property positions use this less restrictive + ;; mapper. + (define s0 (cond [(symbol? x) (symbol->string x)] [(keyword? x) (keyword->string x)] [else (format "~a" x)])) + (define s1 (replace-suffix (replace-suffix (replace-suffix s0 "?" "_p") "!" "_bang") "->" "_to")) + (define s2 (regexp-replace* #rx"[^A-Za-z0-9_$]" s1 "_")) + (if (regexp-match? #rx"^[A-Za-z_$]" s2) s2 (string-append "_" s2))) + + (define (compile-assignment-target target) + (match target + [(? symbol?) (id->js target)] + [(list 'js-ref obj key) (format "~a[~a]" (compile-expr obj) (compile-expr key))] + [(list 'js-dot obj (? symbol? key)) (format "~a.~a" (compile-expr obj) (prop->js key))] + [_ (fail "assignment target" target)])) + + (define (compile-delete-target obj key) + (format "delete ~a[~a]" (compile-expr obj) (compile-expr key))) + + (define (object-binding-prop->js prop) + (match prop + [(list 'quote (? symbol? s)) (prop->js s)] + [(? symbol? s) (prop->js s)] + [(? keyword? kw) (prop->js kw)] + [(? string? s) s] + [_ (fail "object destructuring property" prop)])) + + (define (compile-object-destructure-pattern bindings) + (define parts + (for/list ([b (in-list bindings)]) + (match b + [(list id prop) + (format "~a: ~a" (object-binding-prop->js prop) (id->js id))] + [(list id prop default) + (format "~a: ~a = ~a" (object-binding-prop->js prop) (id->js id) (compile-expr default))] + [_ (fail "object destructuring binding" b)]))) + (format "{ ~a }" (string-join parts ", "))) + + (define (compile-let-object bindings obj body #:return-last? [return-last? #f] #:as-expression? [as-expression? #f]) + (define content + (join-lines + (list (format "const ~a = ~a;" (compile-object-destructure-pattern bindings) (compile-expr obj)) + (compile-body body #:return-last? return-last?)))) + (if as-expression? + (format "(() => ~a)()" (block content)) + (block content))) + + (define (compile-class-member m) + (match m + [(list 'constructor formals body ...) #:when (not (and (list? formals) (andmap list? formals))) + (format "constructor(~a) ~a" (compile-formals formals) (block (compile-body body)))] + [(list 'constructor bindings body ...) #:when (and (list? bindings) (andmap list? bindings)) + (define formals + (string-join + (for/list ([b (in-list bindings)]) + (match b + [(list id default) (format "~a = ~a" (id->js id) (compile-expr default))] + [(list id) (id->js id)] + [_ (fail "constructor binding" b)])) + ", ")) + (format "constructor(~a) ~a" formals (block (compile-body body)))] + [(list 'method (? symbol? name) formals body ...) + (format "~a(~a) ~a" (prop->js name) (compile-formals formals) (block (compile-body body #:return-last? #t)))] + [_ (fail "class member" m)])) + + (define (compile-define-class name members) + (format "class ~a { +~a +}" + (id->js name) + (indent (string-join (map compile-class-member members) " +")))) + + + (define (unsupported-regexp-reason src) + ;; JavaScript RegExp is close to Racket #px for common cases, but it is + ;; not PCRE/Racket. Reject known constructs that would silently change + ;; meaning or fail cryptically in JavaScript. This is deliberately a + ;; conservative syntax check, not a full regexp parser. + (cond + [(regexp-match? #rx"\\(\\?[imsxU-]" src) + "inline regexp option groups such as (?i:...) are not translated to JavaScript flags"] + [(regexp-match? #rx"\\(\\?>" src) + "atomic groups (?>...) are not supported by JavaScript RegExp"] + [(regexp-match? #rx"\\(\\?\\(" src) + "conditional regexp groups are not supported by JavaScript RegExp"] + [(regexp-match? #rx"\\(\\?P" src) + "Python/PCRE-style named groups (?P...) are not supported by JavaScript RegExp"] + [(regexp-match? #rx"\\\\[pP]\\{" src) + "Unicode property escapes are not emitted yet; JavaScript requires careful u-flag handling"] + [else #f])) + + (define (compile-regexp-literal rx) + (cond + [(or (byte-regexp? rx) (byte-pregexp? rx)) + (fail "byte regexp literal" rx)] + [else + (define src (object-name rx)) + (define reason (unsupported-regexp-reason src)) + (when reason (fail reason rx)) + (format "new RegExp(~a)" (js-string src))])) + + (define (literal->js v) + (cond + [(void? v) "undefined"] + [(eq? v #t) "true"] + [(eq? v #f) "false"] + [(regexp? v) (compile-regexp-literal v)] + [(or (byte-regexp? v) (byte-pregexp? v)) (fail "byte regexp literal" v)] + [(number? v) (number->string v)] + [(string? v) (js-string v)] + [(char? v) (js-string (string v))] + [(symbol? v) (js-string (symbol->string v))] + [(keyword? v) (js-string (keyword->string v))] + [(null? v) "[]"] + [(pair? v) (format "[~a]" (string-join (map literal->js v) ", "))] + [(vector? v) (format "[~a]" (string-join (map literal->js (vector->list v)) ", "))] + [else (fail "literal" v)])) + + (define (block stmt) + (string-append "{\n" (indent stmt) "\n}")) + + (define (parens s) (string-append "(" s ")")) + + (define (strip-one-outer-parens s) + (if (and (positive? (string-length s)) + (char=? (string-ref s 0) #\() + (char=? (string-ref s (sub1 (string-length s))) #\))) + (substring s 1 (sub1 (string-length s))) + s)) + + (define tmp-counter 0) + + (define (fresh prefix) + (set! tmp-counter (add1 tmp-counter)) + (format "__~a_~a" prefix tmp-counter)) + + ;; Racket conditionals treat only #f as false. JavaScript would also + ;; reject 0, "", null and undefined in condition position, so every + ;; Racket test is compiled through this helper. + (define (compile-test d) + (if (boolean-expr? d) + (let ([e (compile-expr d)]) + ;; Keep IIFEs intact: (() => {...})() starts and ends with parens, + ;; but removing the first/last character makes invalid JavaScript. + (if (regexp-match? #rx"=>" e) e (strip-one-outer-parens e))) + (format "(~a !== false)" (compile-expr d)))) + + (define (compile-arg-list args) + (string-join (map id->js args) ", ")) + + (define (split-formals formals) + (cond + [(null? formals) (values '() #f)] + [(symbol? formals) (values '() formals)] + [(pair? formals) + (let loop ([xs formals] [acc '()]) + (cond + [(null? xs) (values (reverse acc) #f)] + [(symbol? xs) (values (reverse acc) xs)] + [(pair? xs) (loop (cdr xs) (cons (car xs) acc))] + [else (fail "formals" formals)]))] + [else (fail "formals" formals)])) + + (define (compile-formals formals) + (define-values (pos rest) (split-formals formals)) + (define pos-js (map id->js pos)) + (define all-js (if rest (append pos-js (list (string-append "..." (id->js rest)))) pos-js)) + (string-join all-js ", ")) + + (define (binding-id b) + (match b + [(list id _) id] + [(list id _ ...) id] + [_ (fail "binding" b)])) + + (define (binding-rhs b) + (match b + [(list _ rhs) rhs] + [_ (fail "binding" b)])) + + (define (binding-ids b) + (match b + [(list ids _) ids] + [_ (fail "value binding" b)])) + + (define (ids-pattern->js ids) + (cond + [(and (list? ids) (= (length ids) 1)) (id->js (car ids))] + [(list? ids) (format "[~a]" (compile-arg-list ids))] + [(symbol? ids) (id->js ids)] + [else (fail "binding ids" ids)])) + + (define (definition? d) + (match d + [(list 'define _ ...) #t] + [(list 'define-values _ ...) #t] + [_ #f])) + + (define (return-form? d) + (match d + [(list 'return _) #t] + [_ #f])) + + (define (compile-body body #:return-last? [return-last? #f]) + (cond + [(null? body) (if return-last? "return undefined;" "")] + [else + (define n (length body)) + (join-lines + (for/list ([form (in-list body)] [i (in-naturals)]) + (define last? (= i (sub1 n))) + (if (and return-last? last? (not (definition? form))) + (compile-return form) + (compile-stmt form))))])) + + (define (compile-return d) + (match d + [(list 'return) "return undefined;"] + [(list 'return e) (format "return ~a;" (compile-expr e))] + [(list 'begin es ...) + (compile-body es #:return-last? #t)] + [(list 'if c t e) + (format "if (~a) ~a else ~a" (compile-test c) + (block (compile-body (list t) #:return-last? #t)) + (block (compile-body (list e) #:return-last? #t)))] + [(list 'cond clauses ...) + (compile-cond clauses #:return-last? #t)] + [(list 'with-handlers clauses body ...) + (compile-with-handlers clauses body #:return-last? #t #:as-expression? #f)] + [(list 'let name bindings body ...) #:when (symbol? name) + (compile-named-let name bindings body #:return-last? #t #:as-expression? #f)] + [(list 'let bindings body ...) + (compile-let 'let bindings body #:return-last? #t #:as-expression? #f)] + [(list 'let* bindings body ...) + (compile-let 'let* bindings body #:return-last? #t #:as-expression? #f)] + [(list 'letrec bindings body ...) + (compile-let 'letrec bindings body #:return-last? #t #:as-expression? #f)] + [(list 'let-values bindings body ...) + (compile-let-values 'let-values bindings body #:return-last? #t #:as-expression? #f)] + [(list 'let*-values bindings body ...) + (compile-let-values 'let*-values bindings body #:return-last? #t #:as-expression? #f)] + [(list 'when c body ...) + (format "if (~a) ~a\nreturn undefined;" (compile-expr c) (block (compile-body body)))] + [(list 'unless c body ...) + (format "if (!(~a)) ~a\nreturn undefined;" (compile-expr c) (block (compile-body body)))] + [_ (format "return ~a;" (compile-expr d))])) + + (define (compile-stmt d) + (match d + [(list 'begin es ...) + (compile-body es)] + [(list 'begin0 first rest ...) + (join-lines (cons (compile-stmt first) (map compile-stmt rest)))] + [(list 'define (? pair? sig) body ...) + (format "function ~a(~a) ~a" (id->js (car sig)) (compile-formals (cdr sig)) (block (compile-body body #:return-last? #t)))] + [(list 'define id rhs) + (format "let ~a = ~a;" (id->js id) (compile-expr rhs))] + [(list 'define-values (list id) rhs) + (format "let ~a = ~a;" (id->js id) (compile-expr rhs))] + [(list 'define-values ids rhs) + (format "let [~a] = ~a;" (compile-arg-list ids) (compile-expr rhs))] + [(list 'set! target rhs) + (format "~a = ~a;" (compile-assignment-target target) (compile-expr rhs))] + [(list 'return) + "return undefined;"] + [(list 'return e) + (format "return ~a;" (compile-expr e))] + [(list 'while c body ...) + (format "while (~a) ~a" (compile-test c) (block (compile-body body)))] + [(list 'if c t e) + (format "if (~a) ~a else ~a" (compile-test c) (block (compile-body (list t))) (block (compile-body (list e))))] + [(list 'cond clauses ...) + (compile-cond clauses #:return-last? #f)] + [(list 'with-handlers clauses body ...) + (compile-with-handlers clauses body #:return-last? #f #:as-expression? #f)] + [(list 'case key clauses ...) + (compile-case key clauses #:return-last? #f)] + [(list 'when c body ...) + (format "if (~a) ~a" (compile-test c) (block (compile-body body)))] + [(list 'unless c body ...) + (format "if (!(~a)) ~a" (compile-test c) (block (compile-body body)))] + [(list 'let name bindings body ...) #:when (symbol? name) + (compile-named-let name bindings body #:return-last? #f #:as-expression? #f)] + [(list 'let bindings body ...) + (compile-let 'let bindings body #:return-last? #f #:as-expression? #f)] + [(list 'let* bindings body ...) + (compile-let 'let* bindings body #:return-last? #f #:as-expression? #f)] + [(list 'letrec bindings body ...) + (compile-let 'letrec bindings body #:return-last? #f #:as-expression? #f)] + [(list 'let-values bindings body ...) + (compile-let-values 'let-values bindings body #:return-last? #f #:as-expression? #f)] + [(list 'let*-values bindings body ...) + (compile-let-values 'let*-values bindings body #:return-last? #f #:as-expression? #f)] + [(list 'for clauses body ...) + (compile-for clauses body #:collect? #f #:as-expression? #f)] + [(list 'for/list clauses body ...) + (format "~a;" (compile-for clauses body #:collect? #t #:as-expression? #t))] + [(list 'for/vector clauses body ...) + (format "~a;" (compile-for clauses body #:collect? #t #:as-expression? #t))] + [(list 'vector-set! v i val) + (format "~a[~a] = ~a;" (compile-expr v) (compile-expr i) (compile-expr val))] + [(list 'set-prop! obj key val) + (format "~a[~a] = ~a;" (compile-expr obj) (compile-expr key) (compile-expr val))] + [(list 'delete-prop! obj key) + (format "~a;" (compile-delete-target obj key))] + [(list 'let-object bindings obj body ...) + (compile-let-object bindings obj body #:return-last? #f #:as-expression? #f)] + [(list 'define-class name members ...) + (compile-define-class name members)] + [(list 'send obj method args ...) + (format "~a.~a(~a);" (compile-expr obj) (prop->js method) (string-join (map compile-expr args) ", "))] + [_ + (format "~a;" (compile-expr d))])) + + (define (compile-let kind bindings body #:return-last? [return-last? #f] #:as-expression? [as-expression? #f]) + (define (body-code) (compile-body body #:return-last? return-last?)) + (define content + (case kind + [(let) + ;; Evaluate all RHS expressions before introducing the JS let bindings. + ;; This avoids JavaScript TDZ bugs for Racket code such as + ;; (let ([x x]) ...), where the RHS must see the outer x. + (define tmps (for/list ([_ (in-list bindings)]) (fresh "let_value"))) + (define rhs-stmts + (for/list ([tmp (in-list tmps)] [b (in-list bindings)]) + (format "const ~a = ~a;" tmp (compile-expr (binding-rhs b))))) + (define bind-stmts + (for/list ([tmp (in-list tmps)] [b (in-list bindings)]) + (format "let ~a = ~a;" (id->js (binding-id b)) tmp))) + (join-lines (append rhs-stmts (list (block (join-lines (append bind-stmts (list (body-code))))))))] + [(let*) + ;; Emit the common case directly: + ;; (let* ([x 10] [y (+ x x)]) body) + ;; becomes one JavaScript block with sequential let bindings. Only + ;; when a RHS mentions the same identifier that is being introduced, + ;; such as (let* ([x x]) ...), do we use a temporary to preserve + ;; Racket semantics and avoid JavaScript's temporal-dead-zone error. + (define (mentions-id? x id) + (cond + [(symbol? x) (eq? x id)] + [(pair? x) (or (mentions-id? (car x) id) (mentions-id? (cdr x) id))] + [else #f])) + (define (emit bs) + (cond + [(null? bs) (body-code)] + [else + (define b (car bs)) + (define id (binding-id b)) + (define rhs (binding-rhs b)) + (cond + [(mentions-id? rhs id) + (define tmp (fresh "let_star_value")) + (join-lines + (list (format "const ~a = ~a;" tmp (compile-expr rhs)) + (block (join-lines + (list (format "let ~a = ~a;" (id->js id) tmp) + (emit (cdr bs)))))))] + [else + (join-lines + (list (format "let ~a = ~a;" (id->js id) (compile-expr rhs)) + (emit (cdr bs))))])])) + (emit bindings)] + [(letrec) + (define decls (for/list ([b (in-list bindings)]) (format "let ~a;" (id->js (binding-id b))))) + (define sets (for/list ([b (in-list bindings)]) (format "~a = ~a;" (id->js (binding-id b)) (compile-expr (binding-rhs b))))) + (join-lines (append decls sets (list (body-code))))] + [else (fail "let kind" kind)])) + (if as-expression? + (format "(() => ~a)()" (block content)) + (block content))) + + (define (compile-let-values kind bindings body #:return-last? [return-last? #f] #:as-expression? [as-expression? #f]) + (define (body-code) (compile-body body #:return-last? return-last?)) + (define (bind-stmt ids tmp) (format "let ~a = ~a;" (ids-pattern->js ids) tmp)) + (define content + (case kind + [(let-values) + (define tmps (for/list ([_ (in-list bindings)]) (fresh "let_values"))) + (define rhs-stmts + (for/list ([tmp (in-list tmps)] [b (in-list bindings)]) + (format "const ~a = ~a;" tmp (compile-expr (binding-rhs b))))) + (define bind-stmts + (for/list ([tmp (in-list tmps)] [b (in-list bindings)]) + (bind-stmt (binding-ids b) tmp))) + (join-lines (append rhs-stmts (list (block (join-lines (append bind-stmts (list (body-code))))))))] + [(let*-values) + (define (emit bs) + (cond + [(null? bs) (body-code)] + [else + (define b (car bs)) + (define tmp (fresh "let_star_values")) + (join-lines (list (format "const ~a = ~a;" tmp (compile-expr (binding-rhs b))) + (block (join-lines (list (bind-stmt (binding-ids b) tmp) (emit (cdr bs)))))))])) + (emit bindings)] + [else (fail "let-values kind" kind)])) + (if as-expression? + (format "(() => ~a)()" (block content)) + (block content))) + + (define (compile-named-let name bindings body #:return-last? [return-last? #f] #:as-expression? [as-expression? #f]) + ;; Tail-recursive named let is emitted as a JavaScript loop. This covers + ;; the common Racket idiom: + ;; (let loop ([i 0] [acc 0]) + ;; (if (...) (loop ...) acc)) + ;; without consuming the JavaScript call stack. + (define ids (map binding-id bindings)) + (define vals (map binding-rhs bindings)) + (define loop-name (id->js name)) + (define init-stmts + (for/list ([id (in-list ids)] [v (in-list vals)]) + (format "let ~a = ~a;" (id->js id) (compile-expr v)))) + (define (tail-call? d) + (and (pair? d) (eq? (car d) name))) + (define (emit-tail d) + (match d + [(list 'begin es ...) + (compile-tail-sequence es)] + [(list 'if c t e) + (format "if (~a) ~a else ~a" + (compile-test c) + (block (emit-tail t)) + (block (emit-tail e)))] + [(list 'cond clauses ...) + (compile-cond clauses #:return-last? return-last?)] + [(? tail-call?) + (define args (cdr d)) + (unless (= (length args) (length ids)) + (fail "named let arity" d)) + (define temps (for/list ([_ (in-list args)]) (fresh "loop_arg"))) + (join-lines + (append + (for/list ([tmp (in-list temps)] [arg (in-list args)]) + (format "const ~a = ~a;" tmp (compile-expr arg))) + (for/list ([id (in-list ids)] [tmp (in-list temps)]) + (format "~a = ~a;" (id->js id) tmp)) + (list "continue;")))] + [_ + (if return-last? + (format "return ~a;" (compile-expr d)) + (format "~a; +break;" (compile-expr d)))])) + (define (compile-tail-sequence es) + (cond + [(null? es) (if return-last? "return undefined;" "break;")] + [(null? (cdr es)) (emit-tail (car es))] + [else (join-lines (append (map compile-stmt (reverse (cdr (reverse es)))) + (list (emit-tail (car (reverse es))))))])) + (define content + (join-lines (append init-stmts + (list (format "while (true) ~a" + (block (compile-tail-sequence body))))))) + (if as-expression? + (format "(() => ~a)()" (block content)) + (block content))) + + (define (compile-cond clauses #:return-last? [return-last? #f]) + (define (emit-result expr) + (if return-last? (format "return ~a;" expr) (format "~a;" expr))) + (define (emit-then test-tmp c) + (match c + [(list _ '=> proc) + (emit-result (format "~a(~a)" (compile-callee proc) test-tmp))] + [(list _) + (emit-result test-tmp)] + [(list _ body ...) + (if return-last? (compile-body body #:return-last? #t) (compile-body body))] + [_ (fail "cond clause" c)])) + (define (loop cs) + (cond + [(null? cs) (if return-last? "return undefined;" "")] + [else + (define c (car cs)) + (match c + [(list 'else body ...) + (if return-last? (compile-body body #:return-last? #t) (compile-body body))] + [(list test _ ...) + (define tmp (fresh "cond_value")) + (define else-part (loop (cdr cs))) + (define then-block (block (emit-then tmp c))) + (join-lines + (list (format "const ~a = ~a;" tmp (compile-expr test)) + (if (string=? else-part "") + (format "if (~a !== false) ~a" tmp then-block) + (format "if (~a !== false) ~a else ~a" tmp then-block (block else-part)))))] + [_ (fail "cond clause" c)])])) + (loop clauses)) + + (define (compile-case key clauses #:return-last? [return-last? #f]) + (define key-js (compile-expr key)) + (define tmp "__case_value") + (define (compile-clause c) + (match c + [(list 'else body ...) + (string-append "default:\n" (indent (compile-body body #:return-last? return-last?)))] + [(list datums body ...) + (define tests (for/list ([d (in-list datums)]) (format "case ~a:" (literal->js d)))) + (string-append (string-join tests "\n") "\n" (indent (compile-body body #:return-last? return-last?)) "\n break;")] + [_ (fail "case clause" c)])) + (format "{\n const ~a = ~a;\n switch (~a) {\n~a\n }\n}" + tmp key-js tmp (indent (string-join (map compile-clause clauses) "\n") " "))) + + (define (compile-if-expr c t e) + (format "(~a ? ~a : ~a)" (compile-test c) (compile-expr t) (compile-expr e))) + + (define (compile-begin-expr es) + (format "(() => ~a)()" (block (compile-body es #:return-last? #t)))) + + (define (compile-hash args) + (unless (even? (length args)) (fail "hash expression with odd number of arguments" args)) + (define pairs + (for/list ([k (in-list args)] [v (in-list (cdr args))] [i (in-naturals)] #:when (even? i)) + (define key + (match k + [(list 'quote (? symbol? s)) (symbol->string s)] + [(? keyword? kw) (keyword->string kw)] + [(? symbol? s) (symbol->string s)] + [(? string? s) s] + [_ #f])) + (if key + (format "~a: ~a" (if (regexp-match? #rx"^[A-Za-z_$][A-Za-z0-9_$]*$" key) key (js-string key)) (compile-expr v)) + (format "[~a]: ~a" (compile-expr k) (compile-expr v))))) + (format "{~a}" (string-join pairs ", "))) + + (define (atomic-expr? x) + (or (boolean? x) + (number? x) + (string? x) + (char? x) + (keyword? x) + (regexp? x) + (and (symbol? x) (not (memq x '(#%app quote quasiquote lambda 位 if begin let let* letrec cond case for for/list for/vector for/fold values return while)))) + (and (pair? x) (eq? (car x) 'quote)))) + + (define (boolean-expr? x) + (or (boolean? x) + (and (pair? x) + (let ([op (car x)]) + (or (memq op '(< > <= >= = == equal? eq? eqv? != not-equal? + zero? positive? negative? even? odd? + 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)) + (and (eq? op 'and) (andmap boolean-expr? (cdr x))) + (and (eq? op 'or) (andmap boolean-expr? (cdr x)))))))) + + (define (compile-pairwise args make-test) + (cond + [(or (null? args) (null? (cdr args))) "true"] + [(andmap atomic-expr? args) + (define compiled (map compile-expr args)) + (define tests + (for/list ([a (in-list compiled)] [b (in-list (cdr compiled))]) + (make-test a b))) + (if (null? (cdr tests)) (car tests) (parens (string-join tests " && ")))] + [else + (define temps (for/list ([_ (in-list args)]) (fresh "cmp"))) + (define bindings + (for/list ([tmp (in-list temps)] [arg (in-list args)]) + (format "const ~a = ~a;" tmp (compile-expr arg)))) + (define tests + (for/list ([a (in-list temps)] [b (in-list (cdr temps))]) + (make-test a b))) + (format "(() => ~a)()" + (block (join-lines (append bindings + (list (format "return ~a;" (string-join tests " && ")))))))])) + + (define (compile-comparison jsop args) + (compile-pairwise args (位 (a b) (format "(~a ~a ~a)" a jsop b)))) + + (define (compile-object-is args) + (compile-pairwise args (位 (a b) (format "Object.is(~a, ~a)" a b)))) + + (define (compile-deep-equal args) + (compile-pairwise + args + (位 (a b) + (format "(Object.is(~a, ~a) || JSON.stringify(~a) === JSON.stringify(~a))" a b a b)))) + + (define (compile-and args) + (cond + [(null? args) "true"] + [(null? (cdr args)) (compile-expr (car args))] + [(andmap boolean-expr? args) + (parens (string-join (map compile-expr args) " && "))] + [else + (define tmp (fresh "and_value")) + (define non-last (reverse (cdr (reverse args)))) + (define last-arg (car (reverse args))) + (define steps + (append + (list (format "let ~a = ~a;" tmp (compile-expr (car non-last))) + (format "if (~a === false) return false;" tmp)) + (for/list ([arg (in-list (cdr non-last))]) + (format "~a = ~a; +if (~a === false) return false;" tmp (compile-expr arg) tmp)) + (list (format "return ~a;" (compile-expr last-arg))))) + (format "(() => ~a)()" (block (join-lines steps)))])) + + (define (compile-or args) + (cond + [(null? args) "false"] + [(andmap boolean-expr? args) + (parens (string-join (map compile-expr args) " || "))] + [else + (define tmp (fresh "or_value")) + (define steps + (for/list ([arg (in-list args)]) + (format "~a = ~a; +if (~a !== false) return ~a;" tmp (compile-expr arg) tmp tmp))) + (format "(() => ~a)()" + (block (join-lines (append (list (format "let ~a = false;" tmp)) + steps + (list "return false;")))))])) + + (define (compile-sequence d) + (match d + [(list 'in-list xs) (compile-expr xs)] + [(list 'in-vector xs) (compile-expr xs)] + [(list 'in-string xs) (format "Array.from(~a)" (compile-expr xs))] + [(list 'in-range end) + (format "((__end) => { const __out = []; for (let __i = 0; __i < __end; __i++) __out.push(__i); return __out; })(~a)" + (compile-expr end))] + [(list 'in-range start end) + (format "((__start, __end) => { const __out = []; for (let __i = __start; __i < __end; __i++) __out.push(__i); return __out; })(~a, ~a)" + (compile-expr start) (compile-expr end))] + [(list 'in-range start end step) + (format "((__start, __end, __step) => { const __out = []; for (let __i = __start; (__step >= 0 ? __i < __end : __i > __end); __i += __step) __out.push(__i); return __out; })(~a, ~a, ~a)" + (compile-expr start) (compile-expr end) (compile-expr step))] + [_ (compile-expr d)])) + + (define (compile-body-as-expr body) + (cond + [(null? body) "undefined"] + [(null? (cdr body)) (compile-expr (car body))] + [else (compile-begin-expr body)])) + + (define (compile-callee f) + ;; A lambda in callee position must be emitted as a function expression, + ;; not as a function declaration statement. Parenthesizing compound + ;; callees is harmless and prevents invalid code such as: + ;; function(...args) { ... }(__exn) + (define f-js (compile-expr f)) + (if (symbol? f) f-js (parens f-js))) + + (define (compile-for clauses body #:collect? [collect? #f] #:as-expression? [as-expression? #t]) + (define out (fresh "for_out")) + (define (emit clauses*) + (cond + [(null? clauses*) + (if collect? + (format "~a.push(~a);" out (compile-body-as-expr body)) + (compile-body body))] + [else + (define c (car clauses*)) + (cond + [(eq? c '#:when) + (match (cdr clauses*) + [(list test more ...) (format "if (~a) ~a" (compile-test test) (block (emit more)))] + [_ (fail "for #:when clause" clauses*)])] + [(eq? c '#:unless) + (match (cdr clauses*) + [(list test more ...) (format "if (!(~a)) ~a" (compile-test test) (block (emit more)))] + [_ (fail "for #:unless clause" clauses*)])] + [else + (match c + [(list id seq) + (format "for (const ~a of ~a) ~a" (id->js id) (compile-sequence seq) (block (emit (cdr clauses*))))] + [_ (fail "for clause" c)])])])) + (define content + (if collect? + (join-lines (list (format "const ~a = [];" out) (emit clauses) (format "return ~a;" out))) + (emit clauses))) + (cond + [as-expression? (format "(() => ~a)()" (block content))] + [else content])) + + (define (compile-for-fold bindings clauses body) + (define ids (map binding-id bindings)) + (define init-stmts + (for/list ([b (in-list bindings)]) + (format "let ~a = ~a;" (id->js (binding-id b)) (compile-expr (binding-rhs b))))) + (define body-expr (compile-body-as-expr body)) + (define update + (if (= (length ids) 1) + (format "~a = ~a;" (id->js (car ids)) body-expr) + (format "[~a] = ~a;" (compile-arg-list ids) body-expr))) + (define (emit clauses*) + (cond + [(null? clauses*) update] + [else + (define c (car clauses*)) + (cond + [(eq? c '#:when) + (match (cdr clauses*) + [(list test more ...) (format "if (~a) ~a" (compile-test test) (block (emit more)))] + [_ (fail "for/fold #:when clause" clauses*)])] + [(eq? c '#:unless) + (match (cdr clauses*) + [(list test more ...) (format "if (!(~a)) ~a" (compile-test test) (block (emit more)))] + [_ (fail "for/fold #:unless clause" clauses*)])] + [else + (match c + [(list id seq) + (format "for (const ~a of ~a) ~a" (id->js id) (compile-sequence seq) (block (emit (cdr clauses*))))] + [_ (fail "for/fold clause" c)])])])) + (format "(() => ~a)()" + (block (join-lines (append init-stmts + (list (emit clauses) + (if (= (length ids) 1) + (format "return ~a;" (id->js (car ids))) + (format "return [~a];" (compile-arg-list ids))))))))) + + (define (regexp-arg->js pat) + (format "__rkt_to_regexp(~a, false)" (compile-expr pat))) + + (define (regexp-arg->js/global pat) + (format "__rkt_to_regexp(~a, true)" (compile-expr pat))) + + (define (inline-regexp-runtime expr) + (format "((__rkt_body) => {\n const __rkt_to_regexp = (__pat, __global) => {\n const __flags = (__pat instanceof RegExp)\n ? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('')\n : (__global ? 'g' : '');\n return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags);\n };\n const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x));\n const __rkt_replacement = (__s) => {\n if (typeof __s !== 'string') return __s;\n let __out = '';\n for (let __i = 0; __i < __s.length; __i++) {\n const __ch = __s[__i];\n if (__ch === '$') { __out += '$$'; continue; }\n if (__ch === '\\\\' && __i + 1 < __s.length) {\n const __n = __s[++__i];\n if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n));\n else __out += __n;\n } else __out += __ch;\n }\n return __out;\n };\n return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement);\n})((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => ~a)" expr)) + + (define (compile-regexp-match args) + (match args + [(list pat s) + (inline-regexp-runtime + (format "__rkt_match_array(String(~a).match(~a))" (compile-expr s) (regexp-arg->js pat)))] + [(list pat s start) + (inline-regexp-runtime + (format "__rkt_match_array(String(~a).slice(~a).match(~a))" + (compile-expr s) (compile-expr start) (regexp-arg->js pat)))] + [_ (fail "regexp-match arity" args)])) + + (define (compile-regexp-match? args) + (match args + [(list pat s) + (inline-regexp-runtime + (format "~a.test(String(~a))" (regexp-arg->js pat) (compile-expr s)))] + [(list pat s start) + (inline-regexp-runtime + (format "~a.test(String(~a).slice(~a))" (regexp-arg->js pat) (compile-expr s) (compile-expr start)))] + [_ (fail "regexp-match? arity" args)])) + + (define (compile-regexp-match* args) + (match args + [(list pat s) + (inline-regexp-runtime + (format "Array.from(String(~a).matchAll(~a), (__m) => __m[0])" + (compile-expr s) (regexp-arg->js/global pat)))] + [(list pat s start) + (inline-regexp-runtime + (format "Array.from(String(~a).slice(~a).matchAll(~a), (__m) => __m[0])" + (compile-expr s) (compile-expr start) (regexp-arg->js/global pat)))] + [_ (fail "regexp-match* arity" args)])) + + (define (compile-regexp-match-positions args) + (match args + [(list pat s) + (inline-regexp-runtime + (format "((__s, __rx) => { const __m = __rx.exec(__s); if (__m === null) return false; return __m.map((__x, __i) => { if (__x === undefined) return false; const __start = __m.indices ? __m.indices[__i][0] : (__i === 0 ? __m.index : __s.indexOf(__x, __m.index)); return [__start, __start + __x.length]; }); })(String(~a), (() => { const __rx = ~a; return new RegExp(__rx.source, Array.from(new Set((__rx.flags.replace(/g/g, '') + 'd').split(''))).join('')); })())" + (compile-expr s) (regexp-arg->js pat)))] + [_ (fail "regexp-match-positions arity" args)])) + + (define (compile-regexp-split args) + (match args + [(list pat s) + (inline-regexp-runtime + (format "String(~a).split(~a)" (compile-expr s) (regexp-arg->js/global pat)))] + [_ (fail "regexp-split arity" args)])) + + (define (compile-regexp-replace args #:all? all?) + (match args + [(list pat s repl) + (inline-regexp-runtime + (format "String(~a).replace(~a, __rkt_replacement(~a))" + (compile-expr s) + ((if all? regexp-arg->js/global regexp-arg->js) pat) + (compile-expr repl)))] + [_ (fail (if all? "regexp-replace* arity" "regexp-replace arity") args)])) + + (define (compile-regexp-quote args) + (match args + [(list s) + (format "String(~a).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')" (compile-expr s))] + [_ (fail "regexp-quote arity" args)])) + + + (define (handler-predicate-supported? pred) + ;; Deliberately narrow: js-maker only models one JavaScript catch channel. + ;; It therefore supports generic exn? handlers, not the Racket exception + ;; predicate hierarchy. + (eq? pred 'exn?)) + + (define (compile-with-handlers clauses body #:return-last? [return-last? #t] #:as-expression? [as-expression? #t]) + (unless (and (list? clauses) (andmap list? clauses)) + (fail "with-handlers clauses" clauses)) + (define catch-var (fresh "exn")) + (define (emit-handler cs) + (cond + [(null? cs) (format "throw ~a;" catch-var)] + [else + (match (car cs) + [(list (? symbol? pred) handler) + (unless (handler-predicate-supported? pred) + (fail "with-handlers predicate; only generic exn? is supported" pred)) + (define call (format "~a(~a)" (compile-callee handler) catch-var)) + (if return-last? (format "return ~a;" call) (format "~a;" call))] + [_ (fail "with-handlers clause" (car cs))])])) + (define content + (format "try ~a catch (~a) ~a" + (block (compile-body body #:return-last? return-last?)) + catch-var + (block (emit-handler clauses)))) + (if as-expression? (format "(() => ~a)()" (block content)) content)) + + (define (identifier-local-name op) + ;; Gregor is commonly imported with a prefix, but that prefix is not + ;; stable in user code. This helper strips an arbitrary prefix ending in + ;; ':' and matches on the local binding name. It is intentionally only + ;; used by the small Gregor compatibility table below. + (define s (symbol->string op)) + (define parts (string-split s ":")) + (if (null? parts) s (car (reverse parts)))) + + (define (inline-gregor-runtime expr) + ;; Small JS-side value model for Gregor-like date/time values. Plain dates + ;; and times remain timezone-independent tagged objects. Explicit + ;; ->js-date conversion yields a native JavaScript Date. + (format "((__rkt_body) => {\n const __pad = (__n, __w = 2) => String(__n).padStart(__w, '0');\n const __date = (__y, __m, __d) => ({__gregor: 'date', year: Number(__y), month: Number(__m), day: Number(__d)});\n const __time = (__h, __m, __s = 0) => ({__gregor: 'time', hour: Number(__h), minute: Number(__m), second: Number(__s)});\n const __datetime = (__y, __mo, __d, __h = 0, __mi = 0, __s = 0) => ({__gregor: 'datetime', year: Number(__y), month: Number(__mo), day: Number(__d), hour: Number(__h), minute: Number(__mi), second: Number(__s)});\n const __parse_date = (__s) => { const __m = String(__s).match(/^(\\d{4})-(\\d{2})-(\\d{2})$/); if (!__m) throw new Error('invalid date: ' + __s); return __date(__m[1], __m[2], __m[3]); };\n const __parse_time = (__s) => { const __m = String(__s).match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/); if (!__m) throw new Error('invalid time: ' + __s); return __time(__m[1], __m[2], __m[3] || 0); };\n const __parse_datetime = (__s) => { const __m = String(__s).match(/^(\\d{4})-(\\d{2})-(\\d{2})[T ](\\d{2}):(\\d{2})(?::(\\d{2}))?(?:Z|[+-]\\d{2}:?\\d{2})?$/); if (!__m) throw new Error('invalid datetime: ' + __s); return __datetime(__m[1], __m[2], __m[3], __m[4], __m[5], __m[6] || 0); };\n const __from_js_date = (__x) => __datetime(__x.getFullYear(), __x.getMonth() + 1, __x.getDate(), __x.getHours(), __x.getMinutes(), __x.getSeconds());\n const __to_js_date = (__x) => { if (__x instanceof Date) return __x; if (__x.__gregor === 'date') return new Date(__x.year, __x.month - 1, __x.day); if (__x.__gregor === 'time') return new Date(1970, 0, 1, __x.hour, __x.minute, __x.second || 0); return new Date(__x.year, __x.month - 1, __x.day, __x.hour || 0, __x.minute || 0, __x.second || 0); };\n const __date_string = (__x) => `${__pad(__x.year, 4)}-${__pad(__x.month)}-${__pad(__x.day)}`;\n const __time_string = (__x) => `${__pad(__x.hour)}:${__pad(__x.minute)}:${__pad(__x.second || 0)}`;\n const __datetime_string = (__x) => `${__pad(__x.year, 4)}-${__pad(__x.month)}-${__pad(__x.day)}T${__pad(__x.hour || 0)}:${__pad(__x.minute || 0)}:${__pad(__x.second || 0)}`;\n const __kind = (__x) => (__x && __x.__gregor) || (__x instanceof Date ? 'js-date' : false);\n const __get = (__x, __field) => { if (__x instanceof Date) { const __m = {year: __x.getFullYear(), month: __x.getMonth() + 1, day: __x.getDate(), hour: __x.getHours(), minute: __x.getMinutes(), second: __x.getSeconds()}; return __m[__field]; } return __x[__field]; };\n return __rkt_body({date: __date, time: __time, datetime: __datetime, parseDate: __parse_date, parseTime: __parse_time, parseDateTime: __parse_datetime, fromJSDate: __from_js_date, toJSDate: __to_js_date, dateString: __date_string, timeString: __time_string, dateTimeString: __datetime_string, kind: __kind, get: __get});\n})((__g) => ~a)" expr)) + + (define (compile-gregor op args) + (define lname (identifier-local-name op)) + (define (one who) (match args [(list x) x] [_ (fail who args)])) + (define (one/two who) (match args [(list x) x] [(list x _fmt) x] [_ (fail who args)])) + (define (date3 who) + (match args [(list y m d) (inline-gregor-runtime (format "__g.date(~a, ~a, ~a)" (compile-expr y) (compile-expr m) (compile-expr d)))] [_ (fail who args)])) + (define (time2/3 who) + (match args + [(list h m) (inline-gregor-runtime (format "__g.time(~a, ~a, 0)" (compile-expr h) (compile-expr m)))] + [(list h m s) (inline-gregor-runtime (format "__g.time(~a, ~a, ~a)" (compile-expr h) (compile-expr m) (compile-expr s)))] + [_ (fail who args)])) + (define (dt who) + (match args + [(list y mo d h mi) (inline-gregor-runtime (format "__g.datetime(~a, ~a, ~a, ~a, ~a, 0)" (compile-expr y) (compile-expr mo) (compile-expr d) (compile-expr h) (compile-expr mi)))] + [(list y mo d h mi s) (inline-gregor-runtime (format "__g.datetime(~a, ~a, ~a, ~a, ~a, ~a)" (compile-expr y) (compile-expr mo) (compile-expr d) (compile-expr h) (compile-expr mi) (compile-expr s)))] + [_ (fail who args)])) + (case (string->symbol lname) + [(string->date parse-date) (inline-gregor-runtime (format "__g.parseDate(~a)" (compile-expr (one/two op))))] + [(date->string) (inline-gregor-runtime (format "__g.dateString(~a)" (compile-expr (one op))))] + [(string->time parse-time) (inline-gregor-runtime (format "__g.parseTime(~a)" (compile-expr (one/two op))))] + [(time->string) (inline-gregor-runtime (format "__g.timeString(~a)" (compile-expr (one op))))] + [(string->datetime parse-moment parse-datetime) (inline-gregor-runtime (format "__g.parseDateTime(~a)" (compile-expr (one/two op))))] + [(datetime->string moment->string) (inline-gregor-runtime (format "__g.dateTimeString(~a)" (compile-expr (one op))))] + [(date make-date) (date3 op)] + [(time make-time) (time2/3 op)] + [(datetime moment make-datetime make-moment) (dt op)] + [(date?) (inline-gregor-runtime (format "(__g.kind(~a) === 'date')" (compile-expr (one op))))] + [(time?) (inline-gregor-runtime (format "(__g.kind(~a) === 'time')" (compile-expr (one op))))] + [(datetime? moment?) (inline-gregor-runtime (format "(__g.kind(~a) === 'datetime' || __g.kind(~a) === 'js-date')" (compile-expr (one op)) (compile-expr (one op))))] + [(->year year) (inline-gregor-runtime (format "__g.get(~a, 'year')" (compile-expr (one op))))] + [(->month month) (inline-gregor-runtime (format "__g.get(~a, 'month')" (compile-expr (one op))))] + [(->day day) (inline-gregor-runtime (format "__g.get(~a, 'day')" (compile-expr (one op))))] + [(->hours hours) (inline-gregor-runtime (format "__g.get(~a, 'hour')" (compile-expr (one op))))] + [(->minutes minutes) (inline-gregor-runtime (format "__g.get(~a, 'minute')" (compile-expr (one op))))] + [(->seconds seconds) (inline-gregor-runtime (format "__g.get(~a, 'second')" (compile-expr (one op))))] + [(->js-date) (inline-gregor-runtime (format "__g.toJSDate(~a)" (compile-expr (one op))))] + [(js-date->datetime) (inline-gregor-runtime (format "__g.fromJSDate(~a)" (compile-expr (one op))))] + [else #f])) + + (define (compile-division args) + ;; JavaScript produces Infinity for 10 / 0, while Racket raises an + ;; exception for exact division by zero. This subset uses a runtime + ;; zero-check so with-handlers can catch the common division-by-zero case. + (when (null? args) (fail "empty / operator" args)) + (define temps (for/list ([_ (in-list args)]) (fresh "div"))) + (define bindings + (for/list ([tmp (in-list temps)] [arg (in-list args)]) + (format "const ~a = ~a;" tmp (compile-expr arg)))) + (define denominators (if (= (length temps) 1) temps (cdr temps))) + (define checks + (for/list ([tmp (in-list denominators)]) + (format "if (~a === 0) throw new Error(\"division by zero\");" tmp))) + (define result + (if (= (length temps) 1) + (format "(1 / ~a)" (car temps)) + (parens (string-join temps " / ")))) + (format "(() => ~a)()" + (block (join-lines (append bindings checks (list (format "return ~a;" result))))))) + + (define (compile-operator op args) + (or (compile-gregor op args) + (let () + (define (binary jsop [empty #f] [single-prefix #f]) + (cond + [(null? args) (or empty (fail "empty operator" op))] + [(and (= (length args) 1) single-prefix) (format "(~a~a)" single-prefix (compile-expr (car args)))] + [else (parens (string-join (map compile-expr args) (format " ~a " jsop)))])) + (case op + [(+) (if (null? args) "0" (binary "+"))] + [(*) (if (null? args) "1" (binary "*"))] + [(-) (binary "-" #f "-")] + [(/) (compile-division args)] + [(quotient) (format "Math.trunc(~a / ~a)" (compile-expr (car args)) (compile-expr (cadr args)))] + [(remainder modulo) (binary "%")] + [(add1) (format "(~a + 1)" (compile-expr (car args)))] + [(sub1) (format "(~a - 1)" (compile-expr (car args)))] + [(abs) (format "Math.abs(~a)" (compile-expr (car args)))] + [(floor) (format "Math.floor(~a)" (compile-expr (car args)))] + [(ceiling) (format "Math.ceil(~a)" (compile-expr (car args)))] + [(round) (format "Math.round(~a)" (compile-expr (car args)))] + [(max) (format "Math.max(~a)" (string-join (map compile-expr args) ", "))] + [(min) (format "Math.min(~a)" (string-join (map compile-expr args) ", "))] + [(sqrt) (format "Math.sqrt(~a)" (compile-expr (car args)))] + [(sqr) (format "((__x) => __x * __x)(~a)" (compile-expr (car args)))] + [(expt) (format "Math.pow(~a, ~a)" (compile-expr (car args)) (compile-expr (cadr args)))] + [(sin cos tan asin acos atan log exp) (format "Math.~a(~a)" (symbol->string op) (string-join (map compile-expr args) ", "))] + [(zero?) (format "(~a === 0)" (compile-expr (car args)))] + [(positive?) (format "(~a > 0)" (compile-expr (car args)))] + [(negative?) (format "(~a < 0)" (compile-expr (car args)))] + [(even?) (format "(~a % 2 === 0)" (compile-expr (car args)))] + [(odd?) (format "(~a % 2 !== 0)" (compile-expr (car args)))] + [(= ==) (compile-comparison "===" args)] + [(equal?) (compile-deep-equal args)] + [(eq? eqv?) (compile-object-is args)] + [(!= not-equal?) (format "!(~a)" (compile-deep-equal args))] + [(< > <= >=) (compile-comparison (symbol->string op) args)] + [(and) (compile-and args)] + [(or) (compile-or args)] + [(not) (format "(~a === false)" (compile-expr (car args)))] + [(list vector) (format "[~a]" (string-join (map compile-expr 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) ", ")))] + [(car first) (format "~a[0]" (parens (compile-expr (car args))))] + [(cdr rest) (format "~a.slice(1)" (parens (compile-expr (car args))))] + [(cadr second) (format "~a[1]" (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))))] + [(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? 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)))] + [(boolean?) (format "typeof ~a === \"boolean\"" (compile-expr (car args)))] + [(symbol?) (format "typeof ~a === \"string\"" (compile-expr (car args)))] + [(void) "undefined"] + [(values) (if (= (length args) 1) (compile-expr (car args)) (format "[~a]" (string-join (map compile-expr args) ", ")))] + [(string-append) (if (null? args) "\"\"" (binary "+"))] + [(substring) + (match args + [(list s start) (format "~a.substring(~a)" (parens (compile-expr s)) (compile-expr start))] + [(list s start end) (format "~a.substring(~a, ~a)" (parens (compile-expr s)) (compile-expr start) (compile-expr end))] + [_ (fail "substring" args)])] + [(string-upcase) (format "~a.toUpperCase()" (parens (compile-expr (car args))))] + [(string-downcase) (format "~a.toLowerCase()" (parens (compile-expr (car args))))] + [(string-trim) (format "~a.trim()" (parens (compile-expr (car args))))] + [(string-contains?) (format "~a.includes(~a)" (parens (compile-expr (car args))) (compile-expr (cadr args)))] + [(regexp? pregexp?) (format "(~a instanceof RegExp || typeof ~a === \"string\")" (compile-expr (car args)) (compile-expr (car args)))] + [(regexp-match) (compile-regexp-match args)] + [(regexp-match?) (compile-regexp-match? args)] + [(regexp-match*) (compile-regexp-match* args)] + [(regexp-match-positions) (compile-regexp-match-positions args)] + [(regexp-split) (compile-regexp-split args)] + [(regexp-replace) (compile-regexp-replace args #:all? #f)] + [(regexp-replace*) (compile-regexp-replace args #:all? #t)] + [(regexp-quote) (compile-regexp-quote args)] + [(string=? string-ci=?) (compile-comparison "===" args)] + [(string? string<=? string>=?) + (compile-comparison (case op [(string?) ">"] [(string<=?) "<="] [(string>=?) ">="]) args)] + [(number->string symbol->string string->symbol) (format "String(~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) ", "))] + [(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-has-key?) (format "Object.prototype.hasOwnProperty.call(~a, ~a)" (compile-expr (car args)) (compile-expr (cadr args)))] + [(hash-keys) (format "Object.keys(~a)" (compile-expr (car args)))] + [(hash-values) (format "Object.values(~a)" (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)))] + [(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)))] + [(map) + (match args + [(list f xs) (format "~a.map((__x) => ~a(__x))" (parens (compile-expr xs)) (compile-expr f))] + [(list f xs more ...) + (format "((__arrays) => __arrays[0].map((_, __i) => ~a(...__arrays.map((__a) => __a[__i]))))([~a])" + (compile-expr f) (string-join (map compile-expr (cons xs more)) ", "))] + [_ (fail "map" args)])] + [(filter) (format "~a.filter((__x) => (~a(__x) !== false))" (parens (compile-expr (cadr args))) (compile-expr (car args)))] + [(foldl) (format "~a.reduce((__acc, __x) => ~a(__x, __acc), ~a)" (parens (compile-expr (caddr args))) (compile-expr (car args)) (compile-expr (cadr args)))] + [(foldr) (format "~a.reduceRight((__acc, __x) => ~a(__x, __acc), ~a)" (parens (compile-expr (caddr args))) (compile-expr (car args)) (compile-expr (cadr args)))] + [(apply) + (match args + [(list f last-arg) (format "~a(...~a)" (compile-expr f) (compile-expr last-arg))] + [(list f fixed ... last-arg) (format "~a(~a, ...~a)" (compile-expr f) (string-join (map compile-expr fixed) ", ") (compile-expr last-arg))] + [_ (fail "apply" args)])] + [(error) (format "(() => { throw new Error(~a); })()" (if (null? args) "\"error\"" (compile-expr (car args))))] + [(raise) (format "(() => { throw ~a; })()" (if (null? args) "new Error(\"raise\")" (compile-expr (car args))))] + [(exn?) (if (null? args) "true" "true")] + [(exn-message) (format "String((~a && ~a.message !== undefined) ? ~a.message : ~a)" (compile-expr (car args)) (compile-expr (car args)) (compile-expr (car args)) (compile-expr (car args)))] + [else #f])))) + + (define (compile-call f args) + (or (and (symbol? f) (compile-operator f args)) + (match f + ['send (match args + [(list obj method more ...) + (format "~a.~a(~a)" (compile-expr obj) (prop->js method) (string-join (map compile-expr more) ", "))] + [_ (fail "send" args)])] + ['new (match args + [(list cls more ...) (format "new ~a(~a)" (compile-expr cls) (string-join (map compile-expr more) ", "))] + [_ (fail "new" args)])] + ['js-ref (match args + [(list obj key) (format "~a[~a]" (compile-expr obj) (compile-expr key))] + [_ (fail "js-ref" args)])] + ['js-dot (match args + [(list obj (? symbol? key)) (format "~a.~a" (compile-expr obj) (prop->js key))] + [_ (fail "js-dot" args)])] + ['js-delete (match args + [(list obj key) (parens (compile-delete-target obj key))] + [_ (fail "js-delete" args)])] + ['array (format "[~a]" (string-join (map compile-expr args) ", "))] + ['object (compile-hash args)] + [_ (format "~a(~a)" (compile-callee f) (string-join (map compile-expr args) ", "))]))) + + (define (compile-expr d) + (match d + [(? boolean?) (if d "true" "false")] + [(? regexp?) (compile-regexp-literal d)] + [(? byte-regexp?) (fail "byte regexp literal" d)] + [(? number?) (number->string d)] + [(? string?) (js-string d)] + [(? char?) (js-string (string d))] + [(? keyword?) (js-string (keyword->string d))] + ['js-null "null"] + ['js-undefined "undefined"] + ['js-NaN "NaN"] + [(? symbol?) (id->js d)] + [(list 'quote v) (literal->js v)] + [(list 'quasiquote v) (literal->js v)] + [(list 'lambda formals body ...) + (format "function(~a) ~a" (compile-formals formals) (block (compile-body body #:return-last? #t)))] + [(list '位 formals body ...) + (format "function(~a) ~a" (compile-formals formals) (block (compile-body body #:return-last? #t)))] + [(list 'if c t e) (compile-if-expr c t e)] + [(list 'begin es ...) (compile-begin-expr es)] + [(list 'while c body ...) + (format "(() => ~a)()" + (block (join-lines + (list (format "while (~a) ~a" (compile-test c) (block (compile-body body))) + "return undefined;"))))] + [(list 'begin0 first rest ...) + (define tmp "__begin0_value") + (format "(() => ~a)()" (block (join-lines (append (list (format "const ~a = ~a;" tmp (compile-expr first))) + (map compile-stmt rest) + (list (format "return ~a;" tmp))))))] + [(list 'let name bindings body ...) #:when (symbol? name) + (compile-named-let name bindings body #:return-last? #t #:as-expression? #t)] + [(list 'let bindings body ...) + (compile-let 'let bindings body #:return-last? #t #:as-expression? #t)] + [(list 'let* bindings body ...) + (compile-let 'let* bindings body #:return-last? #t #:as-expression? #t)] + [(list 'letrec bindings body ...) + (compile-let 'letrec bindings body #:return-last? #t #:as-expression? #t)] + [(list 'let-values bindings body ...) + (compile-let-values 'let-values bindings body #:return-last? #t #:as-expression? #t)] + [(list 'let*-values bindings body ...) + (compile-let-values 'let*-values bindings body #:return-last? #t #:as-expression? #t)] + [(list 'cond clauses ...) + (format "(() => ~a)()" (block (compile-cond clauses #:return-last? #t)))] + [(list 'case key clauses ...) + (format "(() => ~a)()" (block (compile-case key clauses #:return-last? #t)))] + [(list 'with-handlers clauses body ...) + (compile-with-handlers clauses body #:return-last? #t #:as-expression? #t)] + [(list 'for/list clauses body ...) + (compile-for clauses body #:collect? #t #:as-expression? #t)] + [(list 'for/vector clauses body ...) + (compile-for clauses body #:collect? #t #:as-expression? #t)] + [(list 'for clauses body ...) + (compile-for clauses body #:collect? #f #:as-expression? #t)] + [(list 'for/fold bindings clauses body ...) + (compile-for-fold bindings clauses body)] + [(list 'when c body ...) + (format "(() => ~a)()" (block (compile-return `(when ,c ,@body))))] + [(list 'unless c body ...) + (format "(() => ~a)()" (block (compile-return `(unless ,c ,@body))))] + [(list 'let-object bindings obj body ...) + (compile-let-object bindings obj body #:return-last? #t #:as-expression? #t)] + [(list 'js-delete obj key) + (parens (compile-delete-target obj key))] + [(list 'set-prop! obj key val) + (format "(~a[~a] = ~a)" (compile-expr obj) (compile-expr key) (compile-expr val))] + [(list 'set! target rhs) + (format "(~a = ~a)" (compile-assignment-target target) (compile-expr rhs))] + [(list 'return) "undefined"] + [(list 'return e) (compile-expr e)] + [(list f args ...) (compile-call f args)] + [_ (literal->js d)])) + + (define (compile-top forms) + (compile-body forms #:return-last? #f))) + +(define-syntax (js stx) + (syntax-case stx () + [(_ form ...) + (datum->syntax stx (compile-top (syntax->datum #'(form ...))))])) + +(define-syntax (js/expression stx) + (syntax-case stx () + [(_ form) + (datum->syntax stx (compile-expr (syntax->datum #'form)))])) diff --git a/private/syntax-helpers.rkt b/private/syntax-helpers.rkt new file mode 100644 index 0000000..5ff7537 --- /dev/null +++ b/private/syntax-helpers.rkt @@ -0,0 +1,42 @@ +#lang racket/base + +(require racket/string + "utils.rkt" + ) +(provide symbol?? + symstr + symstr-eval + is-if?) + + +(define (symbol?? a) + (let ((r (symbol? a))) + r)) + +(define (symstr x) + (cond + ((list? x) + (string-append "[ " + (string-join (map symstr-eval x) ", ") + " ]")) + ((vector? x) + (symstr (vector->list x))) + (else + (let ((r (format "~a" x))) + (let ((r* (if (string-prefix? r "(quote") + (let ((s (substring r 7))) + (substring s 0 (- (string-length s) 1))) + r))) + r*))) + ) + ) + + +(define (symstr-eval x) + (cond + ((string? x) (format "\"~a\"" (esc-double-quote x))) + (else (symstr x)))) + +(define (is-if? x) + (displayln x) + (eq? x 'if)) \ No newline at end of file diff --git a/private/utils.rkt b/private/utils.rkt new file mode 100644 index 0000000..f7ef90d --- /dev/null +++ b/private/utils.rkt @@ -0,0 +1,209 @@ +#lang racket/base + +(require racket/string + racket/port + racket/contract + json + (prefix-in g: gregor) + (prefix-in g: gregor/time) + gregor-utils + racket-sprintf + simple-log + ) + +(provide while + until + get-lib-path + do-for + esc-quote + esc-double-quote + fromJson + mk-js-array + js-code + kv-1 + kv-2 + make-kv + kv? + list-of-kv? + list-of-symbol? + list-of? + string->time + time->string + string->date + date->string + string->datetime + datetime->string + + dbg-webview + err-webview + info-webview + warn-webview + fatal-webview + sync-log-webview + + ) + + +(sl-def-log webview) + +(define-syntax while +(syntax-rules () + ((_ cond body ...) + (letrec ((while-f (lambda (last-result) + (if cond + (let ((last-result (begin + body + ...))) + (while-f last-result)) + last-result)))) + (while-f #f)) + ) + )) + +(define-syntax until +(syntax-rules () + ((_ cond body ...) + (letrec ((until-f (lambda (last-result) + (if cond + last-result + (let ((last-reult (begin + body + ...))) + (until-f last-result)))))) + (until-f #f))))) + +(define-syntax do-for +(syntax-rules () + ((_ (init cond next) body ...) + (begin + init + (letrec ((do-for-f (lamba () + (if cond + (begin + (begin + body + ...) + next + (do-for-f)))))) + (do-for-f)))))) + +(define (get-lib-path lib) +(let ((platform (system-type))) + (cond + [(eq? platform 'windows) + (let ((try1 (build-path (current-directory) ".." "lib" "dll" lib)) + (try2 (build-path (current-directory) "lib" "dll" lib))) + (if (file-exists? try1) + try1 + try2) + )] + [else + (error (format "Install the shared library: ~a" lib))] + ))) + +(define (esc-quote str) + (string-replace (string-replace str "\\" "\\\\") "'" "\\'")) + +(define (esc-double-quote str) + (string-replace (string-replace str "\\" "\\\\") "\"" "\\\"")) + +(define (fromJson str) + (with-input-from-string str read-json)) + +(define (mk-js-array l) + (if (list-of-kv? l) + (string-append "[ " (string-join (map (位 (e) (mk-js-array e)) l) ", ") " ]") + (if (list? l) + (string-append "[ " (string-join (map (位 (e) (format "'~a'" + (esc-quote (format "~a" e)))) l) ", ") " ]") + (if (pair? l) + (format "[ '~a', '~a' ]" (car l) (cdr l)) + (format "[ '~a' ]" (esc-quote (format "~a" l))))))) + +(define (js-code . a) + (define (code* l) + (if (null? l) + "" + (string-append (car l) "\n" (code* (cdr l))) + ) + ) + (code* a)) + +(define (kv? e) + (or + (and (list? e) (= (length e) 2) (symbol? (car e))) + (and (pair? e) (symbol? (car e))))) + +(define/contract (kv-1 e) + (-> kv? symbol?) + (car e)) + +(define/contract (kv-2 e) + (-> kv? any/c) + (if (list? e) + (cadr e) + (cdr e))) + +(define/contract (make-kv k v) + (-> symbol? any/c kv?) + (if (list? v) + (list k v) + (cons k v))) + +(define (list-of? pred? l) + (define (all-pred? l) + (if (null? l) + #t + (if (pred? (car l)) + (all-pred? (cdr l)) + #f))) + (if (list? l) + (all-pred? l) + #f)) + +(define (list-of-kv? l) + (list-of? kv? l)) + +(define (list-of-symbol? l) + (list-of? symbol? l)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Date / Time conversion +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (string->time s) + (with-handlers ([exn:fail? + (位 (e) (g:parse-time s "HH:mm"))]) + (g:parse-time s "HH:mm:ss"))) + +(define (time->string t) + (unless (or (g:time? t) (g:datetime? t) (g:moment? t)) + (error "set! - gregor time?, moment? or datetime? expected")) + (sprintf "%02d:%02d:%02d" (g:->hours t) (g:->minutes t) (g:->seconds t))) + +(define (string->datetime s) + (with-handlers ([exn:fail? + (位 (e) (g:parse-moment s "yyyy-MM-dd'T'HH:mm:ss"))]) + (g:parse-moment s "yyyy-MM-dd'T'HH:mm"))) + +(define (datetime->string dt) + (when (racket-date? dt) + (datetime->string date->moment dt)) + (unless (or (g:datetime? dt) (g:moment? dt) (g:date? dt) (g:time? dt)) + (error "set! - gregor time? , date?, datetime? or moment? expected")) + (sprintf "%04d:%02d:%02dT%02d:%02d:%02d" + (g:->year dt) (g:->month dt) (g:->day dt) + (g:->hours dt) (g:->minutes dt) (g:->seconds dt)) + ) + +(define (string->date d) + (g:parse-date d "yyyy-MM-dd")) + +(define (date->string d) + (when (racket-date? d) + (date->string (date->moment d))) + (unless (or (g:date? d) (g:moment? d) (g:datetime? d)) + (error "set! - gregor date expected")) + (sprintf "%04d-%02d-%02d" (g:->year d) (g:->month d) (g:->day d))) + + diff --git a/scrbl/.gitignore b/scrbl/.gitignore new file mode 100644 index 0000000..e681bac --- /dev/null +++ b/scrbl/.gitignore @@ -0,0 +1,5 @@ +*.html +*.js +*.css +*.bak +*.scrbl~ diff --git a/scrbl/jsmaker.scrbl b/scrbl/jsmaker.scrbl new file mode 100644 index 0000000..147bc7b --- /dev/null +++ b/scrbl/jsmaker.scrbl @@ -0,0 +1,245 @@ +#lang scribble/manual + +@(require (for-label racket/base + racket/list + racket/string + "../main.rkt")) + +@title{jsmaker} +@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. + +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. + +@section{Public API} + +@defform[(js form ...)]{ +Translates one or more Racket forms to JavaScript statement code. The result +is a string. + +@racketblock[ +(js + (define (f x) + (if (and (> x 10) (< x 15)) + (begin + (console.log x) + (return x)) + (return (* x x))))) +] + +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. + +@racketblock[ +(js/expression + (let loop ([i 0] [acc 0]) + (if (< i 5) + (loop (+ i 1) (+ acc i)) + acc))) +] + +Tail-recursive named @racket[let] loops are lowered to JavaScript +@tt{while (true)} loops when the recursive call is in tail position. +} + +@section{Supported expression subset} + +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 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]. + +@section{Truthiness and boolean simplification} + +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: + +@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{ +((x < y) && (y < 10)) +} + +When an intermediate expression might have side effects, the generator uses +temporaries so the expression is evaluated once and in order. + +@section{Regular expressions} + +Racket @tt{#rx} and common @tt{#px} patterns are translated to +JavaScript @tt{RegExp} values where the syntax is compatible. The generator +supports @racket[regexp?], @racket[pregexp?], @racket[regexp-match], +@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}. + +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{Exceptions} + +A small @racket[with-handlers] subset is supported: + +@racketblock[ +(js/expression + (with-handlers ([exn? (lambda (e) + (string-append "caught:" (exn-message e)))]) + (error "boom"))) +] + +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. + +@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]. + +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]. + +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. + +@section{JavaScript interop forms} + +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: + +@codeblock{ +raco test jsmaker/testing/jsmaker-regressions.rkt +} + +@section{Private compatibility files} + +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}. + +@section{Limitations} + +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. + +@section{Use-case documentation} + +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. diff --git a/scrbl/usecases.scrbl b/scrbl/usecases.scrbl new file mode 100644 index 0000000..c23631d --- /dev/null +++ b/scrbl/usecases.scrbl @@ -0,0 +1,584 @@ +#lang scribble/manual + +@(require scribble/core + (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 (tested s) + (nested #:style 'inset (bold "Tested behavior: ") s)) + +@title{jsmaker JavaScript Use Cases} +@author+email["Hans Dijkema" ""] + +@defmodule[jsmaker/demo/js-usecases] + +This document describes the practical JavaScript use cases in +@filepath{demo/js-usecases.rkt}. Each implementation is written as a Racket +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 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}. + +@section{Running the examples} + +Generate the JavaScript examples with: + +@codeblock{ +racket demo/js-usecases.rkt +} + +Run the use-case regression tests with: + +@codeblock{ +racket testing/jsmaker-usecases.rkt +} + +@section{Use cases} + +@subsection{1. Random number between 1 and 5} + +@(side-by-side +#<string age)))))) +RKT +#< { + const { name: name, age: age = 0 } = person; + return (name + ":" + String(age)); + })(); +} +JS +) + +@(tested "The object { name: \"Ada\", age: 37 } is rendered as \"Ada:37\".") + +@subsection{6. Escaping a timer interval} + +@(side-by-side +#< { + const { a: a3 } = obj; + obj.b = 2; + obj["c"] = 3; + delete obj["a"]; + return [a1, a2, a3, obj.b, obj["c"], Object.hasOwn(obj, "a")]; + })(); +} +JS +) + +@(tested "The three reads produce 1, the two writes produce 2 and 3, and the deleted property is absent.") + +@subsection{8. String concatenation order} + +@(side-by-side +#< n 1) + (let* ([i 1]) + (while (< i n) + (when (> (list-ref a (- i 1)) (list-ref a i)) + (let* ([tmp (list-ref a (- i 1))]) + (vector-set! a (- i 1) (list-ref a i)) + (vector-set! a i tmp))) + (set! i (+ i 1)))) + (set! n (- n 1))) + (return a)))) +RKT +#< 1)) { + let i = 1; + while ((i < n)) { + if ((a[(i - 1)] > a[i])) { + let tmp = a[(i - 1)]; + a[(i - 1)] = a[i]; + a[i] = tmp; + } + i = (i + 1); + } + n = (n - 1); + } + return a; +} +JS +) + +@(tested "Sorting [5, 1, 4, 2, 8] yields [1, 2, 4, 5, 8].") + +@subsection{15. Recursive binary search} + +@(side-by-side +#< low high) + (return -1) + (let* ([mid (send Math floor (/ (+ low high) 2))] + [value (list-ref xs mid)]) + (cond + [(= value target) (return mid)] + [(< value target) (return (binarySearch xs target (+ mid 1) high))] + [else (return (binarySearch xs target low (- mid 1)))]))))) +RKT +#< high)) return -1; + let mid = Math.floor(((low + high) / 2)); + let value = xs[mid]; + if ((value === target)) return mid; + if ((value < target)) return binarySearch(xs, target, (mid + 1), high); + return binarySearch(xs, target, low, (mid - 1)); +} +JS +) + +@(tested "Searching 7 returns index 3; searching 4 returns -1.") + +@subsection{16. Count occurrences with Map} + +@(side-by-side +#<]*>/g, ''); }, + set textContent(v) { this.innerHTML = String(v); }, + insertAdjacentHTML: (position, html) => { state.afterParagraph.push([position, html]); } +}; +const heading = { + insertAdjacentHTML: (position, html) => { state.afterHeading.push([position, html]); } +}; +const document = { + querySelector: (selector) => { + if (selector === 'p') return paragraph; + if (selector === 'h1') return heading; + return false; + } +}; +JS + (jsexpr->string paragraph-text))) + +(define tests + (list + (js-program-test + 'dom-ex01-highlight-long-words + exercise01 + "paragraph.innerHTML" + (jsexpr->string "Short extraordinary words remain.") + #:preamble (dom-preamble "Short extraordinary words remain.")) + + (js-program-test + 'dom-ex02-add-source-link + exercise02 + "state.afterParagraph" + (jsexpr->string '(("afterend" "Source: ForceM Ipsum"))) + #:preamble (dom-preamble "Force ipsum text.")) + + (js-program-test + 'dom-ex03-split-sentences + exercise03 + "paragraph.innerHTML" + (jsexpr->string "First sentence.
Second sentence.
Third.
") + #:preamble (dom-preamble "First sentence. Second sentence. Third.")) + + (js-program-test + 'dom-ex04-count-words-after-heading + exercise04 + "state.afterHeading" + (jsexpr->string '(("afterend" "

4 words

"))) + #:preamble (dom-preamble "These are four words")) + + (js-program-test + 'dom-ex05-replace-punctuation-faces + exercise05 + "paragraph.innerHTML" + (jsexpr->string "Really馃 Yes馃槻 No馃") + #:preamble (dom-preamble "Really? Yes! No?")))) + +(define engine (find-js-engine)) +(run-jsmaker-regression 'jsmaker-dom-exercises tests "/tmp/jsmaker-dom-exercises.js" #:engine engine) diff --git a/testing/jsmaker-executors.rkt b/testing/jsmaker-executors.rkt new file mode 100644 index 0000000..c525511 --- /dev/null +++ b/testing/jsmaker-executors.rkt @@ -0,0 +1,164 @@ +#lang racket/base + +(require racket/file + racket/list + racket/match + racket/port + racket/string) + +(provide js-engine? + js-engine-name + js-engine-path + js-engine-kind + js-engine-version + js-run-result? + js-run-result-engine + js-run-result-status + js-run-result-stdout + js-run-result-stderr + js-run-result-success? + known-js-engine-names + find-js-engine + run-js-file) + +(struct js-engine (name path kind version-args) #:transparent) +(struct js-run-result (engine status stdout stderr) #:transparent) + +(define (path-string/non-empty? v) + (and (string? v) (not (string=? v "")))) + +(define (executable-file? p) + (and p (file-exists? p))) + +(define (getenv/path name) + (define v (getenv name)) + (and (path-string/non-empty? v) (string->path v))) + +(define engine-specs + ;; name aliases kind version-args extra-paths + '((node ("node" "nodejs") plain ("--version") + ("/opt/nvm/versions/node/v22.16.0/bin/node" "/usr/local/bin/node" "/usr/bin/node")) + (deno ("deno") plain ("--version") ()) + (bun ("bun") plain ("--version") ()) + (qjs ("qjs" "quickjs") plain ("-v") ()) + (d8 ("d8") plain ("--version") ()) + (jsc ("jsc") plain ("--version") ()) + (js ("js") plain ("--version") ()) + (chromium ("chromium" "chromium-browser" "google-chrome" "google-chrome-stable") chromium ("--version") + ("/usr/bin/chromium" "/usr/bin/chromium-browser" "/usr/bin/google-chrome")))) + +(define (known-js-engine-names) + (map car engine-specs)) + +(define (lookup-engine-spec name) + (define wanted (string-downcase (format "~a" name))) + (for/first ([spec (in-list engine-specs)] + #:when (or (string=? wanted (symbol->string (car spec))) + (member wanted (cadr spec)))) + spec)) + +(define (candidate-paths spec) + (match spec + [(list name aliases kind version-args extra-paths) + (append + (if (eq? name 'node) + (filter values (list (getenv/path "JSMAKER_NODE"))) + '()) + (filter values (list (getenv/path "JSMAKER_ENGINE_PATH"))) + (filter values (map find-executable-path aliases)) + (map string->path extra-paths))])) + +(define (make-engine-from-spec spec) + (match spec + [(list name aliases kind version-args extra-paths) + (for/first ([p (in-list (candidate-paths spec))] + #:when (executable-file? p)) + (js-engine name p kind version-args))])) + +(define (find-js-engine [requested (or (getenv "JSMAKER_ENGINE") "auto")]) + (define req (string-downcase requested)) + (cond + [(or (string=? req "") (string=? req "auto")) + (define browser-fallback? (getenv "JSMAKER_BROWSER_FALLBACK")) + (or (for*/first ([spec (in-list engine-specs)] + #:unless (and (not browser-fallback?) (eq? (car spec) 'chromium)) + [engine (in-value (make-engine-from-spec spec))] + #:when engine) + engine) + #f)] + [else + (define spec (lookup-engine-spec req)) + (and spec (make-engine-from-spec spec))])) + +(define (capture-timeout-seconds) + (define v (getenv "JSMAKER_ENGINE_TIMEOUT_SECONDS")) + (cond + [(and (path-string/non-empty? v) (string->number v)) => values] + [else 15])) + +(define (capture path args) + (with-handlers ([exn:fail? (lambda (e) (values 127 "" (exn-message e)))]) + (define-values (proc stdout stdin stderr) + (apply subprocess #f #f #f path args)) + (define waiter (thread (lambda () (subprocess-wait proc)))) + (define finished? (sync/timeout (capture-timeout-seconds) waiter)) + (unless finished? + (subprocess-kill proc #t) + (thread-wait waiter)) + (values (if finished? (subprocess-status proc) 124) + (port->string stdout) + (string-append (port->string stderr) + (if finished? "" "\nJavaScript engine timed out and was killed.\n"))))) + +(define (js-engine-version engine) + (define-values (status stdout stderr) + (capture (js-engine-path engine) (js-engine-version-args engine))) + (define s (string-trim (if (string=? stdout "") stderr stdout))) + (and (zero? status) (not (string=? s "")) s)) + +(define (plain-run-args engine js-path) + (case (js-engine-name engine) + [(deno) (list "run" "--quiet" js-path)] + [else (list js-path)])) + +(define (copy-js-as-browser-html js-path) + (define html-path (build-path (find-system-path 'temp-dir) + (format "jsmaker-browser-~a.html" (current-inexact-milliseconds)))) + (define js (file->string js-path)) + (call-with-output-file html-path + #:exists 'replace + (lambda (out) + (displayln "
" out)
+      (displayln "" out)))
+  html-path)
+
+(define (chromium-run engine js-path)
+  (define html-path (copy-js-as-browser-html js-path))
+  (define url (string-append "file://" (path->string html-path)))
+  (define args (list "--headless" "--disable-gpu" "--no-sandbox" "--dump-dom" url))
+  (define-values (status stdout stderr) (capture (js-engine-path engine) args))
+  (define browser-fail? (regexp-match? #rx"data-jsmaker-status=\"fail\"" stdout))
+  (js-run-result engine (if browser-fail? 1 status) stdout stderr))
+
+(define (run-js-file engine js-path)
+  (case (js-engine-kind engine)
+    [(chromium) (chromium-run engine js-path)]
+    [else
+     (define-values (status stdout stderr)
+       (capture (js-engine-path engine) (plain-run-args engine js-path)))
+     (js-run-result engine status stdout stderr)]))
+
+(define (js-run-result-success? result)
+  (zero? (js-run-result-status result)))
diff --git a/testing/jsmaker-program-regression.rkt b/testing/jsmaker-program-regression.rkt
new file mode 100644
index 0000000..58473df
--- /dev/null
+++ b/testing/jsmaker-program-regression.rkt
@@ -0,0 +1,118 @@
+#lang racket/base
+
+(require "../main.rkt"
+         "jsmaker-executors.rkt"
+         "jsmaker-test-framework.rkt")
+
+(define dom-preamble
+  (string-append
+   "const window = {};\n"
+   "const logs = [];\n"
+   "const attrs = {};\n"
+   "const console = { log: (...xs) => logs.push(xs.length === 1 ? xs[0] : xs) };\n"
+   "const document = {\n"
+   "  getElementById: (id) => id === 'hi'\n"
+   "    ? { setAttribute: (k, v) => { attrs[k] = v; }, getAttribute: (k) => attrs[k] || '' }\n"
+   "    : false\n"
+   "};"))
+
+(define t1-program
+  (js (set! window.myfunc (位 (x)
+                            (let* ((el (send document getElementById 'hi))
+                                   (old (send el getAttribute "x"))
+                                   (y (+ old (* x x))))
+                              (send el setAttribute "x" (+ y "")))
+                            (send console log "dit set attribute x on element hi")))))
+
+(define t2-program
+  (js (define (f x)
+        (if (and (> x 10) (< x 15))
+            (begin (console.log x)
+                   (return x))
+            (return (* x x))))))
+
+(define t3-program
+  (js (define (f x y z)
+        (send console log (cons x (cons y (list z))))
+        (let* ((l (cons x (cons y (list z)))))
+          (return (send l map (位 (a) (return (+ a 10)))))))))
+
+(define tail-loop-expression
+  (js/expression (let loop ([i 0] [s 0])
+                   (if (< i 5)
+                       (loop (+ i 1) (+ s i))
+                       s))))
+
+(unless (regexp-match? #rx"while \\(true\\)" tail-loop-expression)
+  (error 'jsmaker-program-regression
+         "tail-recursive named let was not lowered to while(true): ~a"
+         tail-loop-expression))
+
+(unless (regexp-match? #rx"\n  " t1-program)
+  (error 'jsmaker-program-regression
+         "generated statement code does not appear to contain indentation: ~a"
+         t1-program))
+
+(unless (regexp-match? #rx"if \\(\\(x > 10\\) && \\(x < 15\\)\\)" t2-program)
+  (error 'jsmaker-program-regression
+         "boolean and should compile directly in if test: ~a"
+         t2-program))
+
+(define program-tests
+  (list
+   (js-program-test
+    'dom-style-set-attribute
+    t1-program
+    "(() => { attrs.x = 'old:'; window.myfunc(4); return [attrs.x, logs]; })()"
+    "[\"old:16\",[\"dit set attribute x on element hi\"]]"
+    #:preamble dom-preamble)
+
+   (js-program-test
+    'define-if-return-console
+    t2-program
+    "(() => { const a = f(12); const b = f(5); return [a, b, logs]; })()"
+    "[12,25,[12]]"
+    #:preamble "const logs = []; const console = { log: (...xs) => logs.push(xs.length === 1 ? xs[0] : xs) };" )
+
+   (js-program-test
+    'define-send-map-return
+    t3-program
+    "(() => { const r = f(1, 2, 3); return [r, logs]; })()"
+    "[[11,12,13],[[1,2,3]]]"
+    #:preamble "const logs = []; const console = { log: (...xs) => logs.push(xs.length === 1 ? xs[0] : xs) };" )
+
+   (js-expression-test 'named-let-tail-big
+                       (js/expression (let loop ([i 0] [s 0])
+                                        (if (< i 100000)
+                                            (loop (+ i 1) (+ s 1))
+                                            s)))
+                       "100000")
+
+   (js-expression-test 'named-let-tail-accumulator tail-loop-expression "10")
+
+   (js-expression-test 'explicit-while-form
+                       (js/expression (let ([i 0] [s 0])
+                                        (while (< i 4)
+                                          (set! s (+ s i))
+                                          (set! i (+ i 1)))
+                                        s))
+                       "6")
+
+   (js-expression-test 'bare-return
+                       (js/expression ((lambda (x) (if x (return) (return 7))) #t))
+                       "undefined")
+
+   (js-expression-test 'implicit-last-expression-return
+                       (js/expression ((lambda (x) (+ x 10)) 5))
+                       "15")
+
+   (js-program-test
+    'with-handlers-rest-lambda-statement
+    (js (with-handlers ([exn? (位 args (displayln (length args)))])
+          (/ 10 0)))
+    "logs"
+    "[1]"
+    #:preamble "const logs = []; const console = { log: (...xs) => logs.push(xs.length === 1 ? xs[0] : xs) };")))
+
+(define engine (find-js-engine))
+(run-jsmaker-regression 'jsmaker-program-regression program-tests "/tmp/jsmaker-program-regression.js" #:engine engine)
diff --git a/testing/jsmaker-regexp-regression.rkt b/testing/jsmaker-regexp-regression.rkt
new file mode 100644
index 0000000..f846837
--- /dev/null
+++ b/testing/jsmaker-regexp-regression.rkt
@@ -0,0 +1,24 @@
+#lang racket/base
+
+(require "../main.rkt"
+         "jsmaker-executors.rkt"
+         "jsmaker-test-framework.rkt")
+
+(define tests
+  (list
+   (js-expression-test 'regexp-literal-test (js/expression (regexp-match? #rx"a+" "baac")) "true")
+   (js-expression-test 'regexp-match-basic (js/expression (regexp-match #rx"a+" "baac")) "[\"aa\"]")
+   (js-expression-test 'regexp-match-captures (js/expression (regexp-match #rx"(a)(b)?" "a")) "[\"a\",\"a\",false]")
+   (js-expression-test 'pregexp-match-digits (js/expression (regexp-match #px"([a-z]+)-([0-9]+)" "abc-123")) "[\"abc-123\",\"abc\",\"123\"]")
+   (js-expression-test 'regexp-match-star (js/expression (regexp-match* #rx"a+" "baacaa")) "[\"aa\",\"aa\"]")
+   (js-expression-test 'regexp-split (js/expression (regexp-split #rx"," "a,b,,c")) "[\"a\",\"b\",\"\",\"c\"]")
+   (js-expression-test 'regexp-replace (js/expression (regexp-replace #rx"a" "banana" "X")) "\"bXnana\"")
+   (js-expression-test 'regexp-replace-star (js/expression (regexp-replace* #rx"a" "banana" "X")) "\"bXnXnX\"")
+   (js-expression-test 'regexp-replace-backref (js/expression (regexp-replace #rx"([a-z]+)-([0-9]+)" "abc-123" "\\2/\\1")) "\"123/abc\"")
+   (js-expression-test 'regexp-replace-full (js/expression (regexp-replace #rx"a+" "baac" "[\\0]")) "\"b[aa]c\"")
+   (js-expression-test 'regexp-pattern-string (js/expression (regexp-match "a+" "baac")) "[\"aa\"]")
+   (js-expression-test 'regexp-quote (js/expression (regexp-quote "a+b*c?")) "\"a\\\\+b\\\\*c\\\\?\"")
+   (js-expression-test 'regexp-match-positions (js/expression (regexp-match-positions #rx"(a)(b)?" "xa")) "[[1,2],[1,2],false]")))
+
+(define engine (find-js-engine))
+(run-jsmaker-regression 'jsmaker-regexp-regression tests "/tmp/jsmaker-regexp-regression.js" #:engine engine)
diff --git a/testing/jsmaker-regression.rkt b/testing/jsmaker-regression.rkt
new file mode 100644
index 0000000..bb41b73
--- /dev/null
+++ b/testing/jsmaker-regression.rkt
@@ -0,0 +1,110 @@
+#lang racket/base
+
+(require racket/string
+         "../main.rkt"
+         "jsmaker-executors.rkt"
+         "jsmaker-test-framework.rkt")
+
+(define direct-and (js/expression (and (> x 10) (< x 15))))
+(unless (string=? direct-and "((x > 10) && (x < 15))")
+  (error 'jsmaker-regression "expected direct && generation, got: ~a" direct-and))
+
+(define direct-or (js/expression (or (< x 10) (> x 20))))
+(unless (string=? direct-or "((x < 10) || (x > 20))")
+  (error 'jsmaker-regression "expected direct || generation, got: ~a" direct-or))
+
+(define direct-chain (js/expression (< x y 10)))
+(unless (string=? direct-chain "((x < y) && (y < 10))")
+  (error 'jsmaker-regression "expected direct pairwise comparison, got: ~a" direct-chain))
+
+(define effectful-chain (js/expression (< x (f y) 10)))
+(unless (regexp-match? #rx"__cmp" effectful-chain)
+  (error 'jsmaker-regression "effectful chained comparison should use temporaries, got: ~a" effectful-chain))
+
+(define simple-let-star
+  (js (let* ((x 10)
+             (y (+ x x)))
+        (return y))))
+
+(define with-handlers-rest-lambda-program
+  (js (with-handlers ([exn? (位 args (displayln args))])
+        (/ 10 0))))
+(unless (regexp-match? #rx"\\(function\\(\\.\\.\\.args\\)" with-handlers-rest-lambda-program)
+  (error 'jsmaker-regression
+         "with-handlers rest-lambda handler should be parenthesized in callee position, got: ~a"
+         with-handlers-rest-lambda-program))
+(unless (not (regexp-match? #rx"catch[^{]*\\{\\n  function\\(" with-handlers-rest-lambda-program))
+  (error 'jsmaker-regression
+         "with-handlers emitted a function declaration in statement position, got: ~a"
+         with-handlers-rest-lambda-program))
+(unless (regexp-match? #rx"let x = 10;" simple-let-star)
+  (error 'jsmaker-regression "simple let* should emit direct let for x, got: ~a" simple-let-star))
+(unless (regexp-match? #rx"let y = .*x.*x.*;" simple-let-star)
+  (error 'jsmaker-regression "simple let* should emit direct dependent let for y, got: ~a" simple-let-star))
+(unless (not (regexp-match? #rx"__let_star_value" simple-let-star))
+  (error 'jsmaker-regression "simple let* should not use tempvars, got: ~a" simple-let-star))
+
+(define tests
+  (list
+   (js-expression-test 'if-zero (js/expression (if 0 1 2)) "1")
+   (js-expression-test 'if-false (js/expression (if #f 1 2)) "2")
+   (js-expression-test 'and-false (js/expression (and #f 5)) "false")
+   (js-expression-test 'and-zero (js/expression (and 0 5)) "5")
+   (js-expression-test 'or-empty-string (js/expression (or "" 5)) "\"\"")
+   (js-expression-test 'direct-and-boolean (format "((x) => ~a)(12)" direct-and) "true")
+   (js-expression-test 'direct-or-boolean (format "((x) => ~a)(5)" direct-or) "true")
+   (js-expression-test 'chain-lt (js/expression (< 1 2 3)) "true")
+   (js-expression-test 'chain-lt-false (js/expression (< 1 3 2)) "false")
+   (js-expression-test 'let-expr (js/expression (let ([x 2] [y 3]) (+ x y))) "5")
+   (js-expression-test 'let-star-sequential-binding (js/expression (let* ([x 10] [y (+ x x)]) y)) "20")
+   (js-expression-test 'let-star-dependent-shadowing (js/expression (let ([x 4]) (let* ([x x] [y x]) (+ x y)))) "8")
+   (js-expression-test 'named-let (js/expression (let loop ([i 0] [s 0]) (if (< i 5) (loop (+ i 1) (+ s i)) s))) "10")
+   (js-expression-test 'for-list (js/expression (for/list ([x (in-range 5)] #:when (odd? x)) (* x x))) "[1,9]")
+   (js-expression-test 'for-fold (js/expression (for/fold ([s 0]) ([x (in-list (list 1 2 3))]) (+ x s))) "6")
+   (js-expression-test 'map-filter (js/expression (filter (lambda (x) (> x 2)) (map (lambda (x) (+ x 1)) (list 1 2 3)))) "[3,4]")
+   (js-expression-test 'hash-ref (js/expression (hash-ref (hash 'a 1 'b 2) 'b)) "2")
+   (js-expression-test 'substring (js/expression (substring "abcdef" 1 4)) "\"bcd\"")
+   (js-expression-test 'equal-list (js/expression (equal? (list 1 2) (list 1 2))) "true")
+   (js-expression-test 'cond-test-only (js/expression (cond [0] [else 2])) "0")
+   (js-expression-test 'cond-arrow (js/expression (cond [(+ 1 2) => (lambda (x) (+ x 10))] [else 0])) "13")
+   (js-expression-test 'cond-false-arrow (js/expression (cond [#f => (lambda (x) (+ x 10))] [else 7])) "7")
+   (js-expression-test 'rest-lambda (js/expression ((lambda xs (length xs)) 1 2 3)) "3")
+   (js-expression-test 'dotted-lambda (js/expression ((lambda (a . xs) (+ a (length xs))) 10 20 30)) "12")
+   (js-expression-test 'let-values-one (js/expression (let-values ([(x) 5]) (+ x 1))) "6")
+   (js-expression-test 'let-values-many (js/expression (let-values ([(x y) (values 2 3)]) (+ x y))) "5")
+   (js-expression-test 'let-tdz (js/expression (let ([x 4]) (let ([x x]) (+ x 1)))) "5")
+   (js-expression-test 'let-star-tdz (js/expression (let ([x 4]) (let* ([x x] [y x]) (+ x y)))) "8")
+   (js-expression-test 'division-normal
+                       (js/expression (/ 20 2 2))
+                       "5")
+   (js-expression-test 'with-handlers-division-by-zero
+                       (js/expression (with-handlers ([exn? (lambda args (string-append "caught:" (exn-message (car args))))])
+                                        (/ 10 0)))
+                       "\"caught:division by zero\"")
+   (js-expression-test 'with-handlers-generic-exn
+                       (js/expression (with-handlers ([exn? (lambda (e) (string-append "caught:" (exn-message e)))])
+                                        (error "boom")))
+                       "\"caught:boom\"")
+   (js-expression-test 'with-handlers-no-error
+                       (js/expression (with-handlers ([exn? (lambda (e) 99)])
+                                        (+ 20 22)))
+                       "42")
+   (js-expression-test 'gregor-prefix-date-string
+                       (js/expression (date->string (foo:date 2026 5 25)))
+                       "\"2026-05-25\"")
+   (js-expression-test 'gregor-prefix-time-string
+                       (js/expression (time->string (bar:time 8 9 10)))
+                       "\"08:09:10\"")
+   (js-expression-test 'gregor-prefix-moment-fields
+                       (js/expression (list (baz:->year (baz:parse-moment "2026-05-25T08:09:10" "yyyy-MM-dd'T'HH:mm:ss"))
+                                            (baz:->month (baz:parse-moment "2026-05-25T08:09:10"))
+                                            (baz:->day (baz:parse-moment "2026-05-25T08:09:10"))))
+                       "[2026,5,25]")
+   (js-expression-test 'gregor-js-date-conversion
+                       (js/expression (list (q:->year (js-date->datetime (q:->js-date (q:date 2026 5 25))))
+                                            (q:date? (q:date 2026 5 25))
+                                            (q:moment? (q:moment 2026 5 25 8 9 10))))
+                       "[2026,true,true]")))
+
+(define engine (find-js-engine))
+(run-jsmaker-regression 'jsmaker-core-regression tests "/tmp/jsmaker-core-regression.js" #:engine engine)
diff --git a/testing/jsmaker-regressions.rkt b/testing/jsmaker-regressions.rkt
new file mode 100644
index 0000000..1c37bbf
--- /dev/null
+++ b/testing/jsmaker-regressions.rkt
@@ -0,0 +1,7 @@
+#lang racket/base
+
+(require "jsmaker-regression.rkt"
+         "jsmaker-regexp-regression.rkt"
+         "jsmaker-program-regression.rkt"
+         "jsmaker-dom-exercises.rkt"
+         "jsmaker-usecases.rkt")
diff --git a/testing/jsmaker-test-framework.rkt b/testing/jsmaker-test-framework.rkt
new file mode 100644
index 0000000..7522bfd
--- /dev/null
+++ b/testing/jsmaker-test-framework.rkt
@@ -0,0 +1,88 @@
+#lang racket/base
+
+(require racket/format
+         racket/string
+         "jsmaker-executors.rkt")
+
+(provide js-expression-test
+         js-program-test
+         write-js-test-file
+         run-jsmaker-regression
+         warning-line)
+
+(define (warning-line who fmt . args)
+  (displayln (string-append "WARNING: " (apply format fmt args))
+             (current-error-port)))
+
+(define (js-expression-test name expr expected)
+  (list name expr expected))
+
+(define (js-program-test name program check-expr expected #:preamble [preamble ""])
+  (list name
+        (format "(() => {~a~a~areturn (~a); })()"
+                (if (string=? preamble "") "" (string-append "\n" preamble "\n"))
+                program
+                (if (regexp-match? #rx"\n$" program) "" "\n")
+                check-expr)
+        expected))
+
+(define (write-js-test-file js-path tests)
+  (call-with-output-file js-path
+    #:exists 'replace
+    (lambda (out)
+      (displayln "const tests = [];" out)
+      (displayln "const __normalize = (v) => v === undefined ? 'undefined' : JSON.stringify(v);" out)
+      (displayln "const __print = (...xs) => {" out)
+      (displayln "  if (typeof console !== 'undefined' && console.log) console.log(...xs);" out)
+      (displayln "  else if (typeof print === 'function') print(xs.join(' '));" out)
+      (displayln "};" out)
+      (for ([t tests])
+        (define name (symbol->string (car t)))
+        (define expr (cadr t))
+        (define expected (caddr t))
+        (fprintf out "tests.push([~s, () => (~a), ~s]);\n" name expr expected))
+      (displayln "(async () => {" out)
+      (displayln "  let failed = 0;" out)
+      (displayln "  for (const [name, thunk, expected] of tests) {" out)
+      (displayln "    let value;" out)
+      (displayln "    try { value = await Promise.resolve(thunk()); }" out)
+      (displayln "    catch (e) { value = { __thrown: String(e && e.message !== undefined ? e.message : e) }; }" out)
+      (displayln "    const actual = __normalize(value);" out)
+      (displayln "    const ok = actual === expected;" out)
+      (displayln "    __print((ok ? 'ok' : 'FAIL') + '\\t' + name + '\\t' + actual);" out)
+      (displayln "    if (!ok) failed++;" out)
+      (displayln "  }" out)
+      (displayln "  if (failed !== 0) throw new Error(failed + ' jsmaker test(s) failed');" out)
+      (displayln "})().catch((e) => {" out)
+      (displayln "  __print(e && e.stack ? e.stack : String(e));" out)
+      (displayln "  if (typeof process !== 'undefined' && process.exit) process.exit(1);" out)
+      (displayln "  throw e;" out)
+      (displayln "});" out))))
+
+(define (describe-engine engine)
+  (define version (or (js-engine-version engine) "unknown version"))
+  (format "~a at ~a (~a)"
+          (js-engine-name engine)
+          (path->string (js-engine-path engine))
+          version))
+
+(define (run-jsmaker-regression who tests js-path #:engine [engine (find-js-engine)])
+  (write-js-test-file js-path tests)
+  (cond
+    [engine
+     (printf "~a: using JavaScript engine ~a\n" who (describe-engine engine))
+     (define result (run-js-file engine js-path))
+     (display (js-run-result-stdout result))
+     (unless (string=? (js-run-result-stderr result) "")
+       (display (js-run-result-stderr result) (current-error-port)))
+     (unless (js-run-result-success? result)
+       (error who "JavaScript regression failed with ~a; see ~a"
+              (js-engine-name engine) js-path))]
+    [else
+     (warning-line who "No JavaScript engine was found; regression tests were generated but not executed.")
+     (warning-line who "Generated JavaScript test file: ~a" js-path)
+     (warning-line who "Tried engines: ~a" (string-join (map symbol->string (known-js-engine-names)) ", "))
+     (warning-line who "Set JSMAKER_ENGINE to auto/node/deno/bun/qjs/d8/jsc/js/chromium or set JSMAKER_ENGINE_PATH.")
+     (warning-line who "For backwards compatibility, JSMAKER_NODE can point to a Node executable.")
+     (when (or (getenv "JSMAKER_REQUIRE_ENGINE") (getenv "JSMAKER_REQUIRE_NODE"))
+       (error who "JavaScript engine required by environment setting"))]))
diff --git a/testing/jsmaker-test-runner.rkt b/testing/jsmaker-test-runner.rkt
new file mode 100644
index 0000000..793cc51
--- /dev/null
+++ b/testing/jsmaker-test-runner.rkt
@@ -0,0 +1,12 @@
+#lang racket/base
+
+(require "jsmaker-test-framework.rkt")
+
+(provide run-jsmaker-node-regression)
+
+;; Compatibility wrapper for the older single-engine test runner name.
+;; It now delegates to the generic framework/executor layer.  When no engine
+;; is available, the framework generates the JavaScript test file and reports a
+;; skip/warning unless JSMAKER_REQUIRE_ENGINE or JSMAKER_REQUIRE_NODE is set.
+(define (run-jsmaker-node-regression who tests js-path)
+  (run-jsmaker-regression who tests js-path))
diff --git a/testing/jsmaker-usecases.rkt b/testing/jsmaker-usecases.rkt
new file mode 100644
index 0000000..b158cdd
--- /dev/null
+++ b/testing/jsmaker-usecases.rkt
@@ -0,0 +1,191 @@
+#lang racket/base
+
+(require json
+         "../main.rkt"
+         "jsmaker-test-framework.rkt"
+         "jsmaker-executors.rkt"
+         "../demo/js-usecases.rkt")
+
+(define timer-preamble
+  #<A

" }; +const root = { innerHTML: "

A

" }; +const document = { + body, + querySelector: (selector) => selector === "body" ? body : false, + getElementById: (id) => id === "root" ? root : false +}; +JS + ) + +(define fetch-preamble + #< Promise.resolve({ title: "Done" }) + }); + } + return Promise.reject(new Error("network")); +} +JS + ) + +(define tests + (list + (js-program-test + 'use-random-number-1-to-5 + usecase-random-number + (js/expression (randomBetween1And5)) + (jsexpr->string 5) + #:preamble "Math.random = () => 0.80;") + + (js-program-test + 'use-unique-values-set + usecase-unique-values + (js/expression (uniqueValues (array 1 2 2 3 1))) + (jsexpr->string '(1 2 3))) + + (js-program-test + 'use-six-falsey-values + usecase-falsey-values + (js/expression (send (falseyValues) map (lambda (x) (return (Boolean x))))) + (jsexpr->string '(#f #f #f #f #f #f))) + + (js-program-test + 'use-currying-simple + usecase-currying + (js/expression ((add 2) 3)) + (jsexpr->string 5)) + + (js-program-test + 'use-object-destructuring + usecase-object-destructuring + (js/expression (describePerson (object 'name "Ada" 'age 37))) + (jsexpr->string "Ada:37")) + + (js-program-test + 'use-timer-clear-interval + usecase-timer-interval + (js/expression + (let* ([t (startTimer)]) + (runInterval t.id 5) + (array t.id (send t getTicks) (js-dot (js-ref intervals t.id) active)))) + (jsexpr->string '(1 3 #f)) + #:preamble timer-preamble) + + (js-program-test + 'use-object-get-set-delete-prop + usecase-object-props + (js/expression (objectProps)) + (jsexpr->string '(1 1 1 2 3 #f))) + + (js-program-test + 'use-string-concat-order + usecase-string-concat-order + (js/expression (concatOrder)) + (jsexpr->string '("33" "123"))) + + (js-program-test + 'use-freeze-vs-seal + usecase-freeze-vs-seal + (js/expression (freezeVsSeal)) + (jsexpr->string '(1 9 #t #t #t))) + + (js-program-test + 'use-switch-example + usecase-switch + (js/expression (array (switchExample 1) (switchExample 2) (switchExample 9))) + (jsexpr->string '("one" "two-or-three" "other"))) + + (js-program-test + 'use-class-constructor-default + usecase-class-constructor + (js/expression (classExample)) + (jsexpr->string '("Hello world" "Hello Ada"))) + + (js-program-test + 'use-sort-objects-by-property + usecase-sort-objects-by-property + (js/expression + (send (sortByProperty (array (object 'age 30) (object 'age 20) (object 'age 25)) "age") + map + (lambda (x) (return x.age)))) + (jsexpr->string '(20 25 30))) + + (js-program-test + 'use-delete-array-elements-four-ways + usecase-delete-array-elements + (js/expression (deleteArrayWays (array "a" "b" "c"))) + (jsexpr->string '(("a" "c") ("a" "c") ("a" "c") (#f 3)))) + + (js-program-test + 'use-bubble-sort + usecase-bubble-sort + (js/expression (bubbleSort (array 5 1 4 2 8))) + (jsexpr->string '(1 2 4 5 8))) + + (js-program-test + 'use-binary-search-recursive + usecase-binary-search + (js/expression + (array (binarySearch (array 1 3 5 7 9) 7 0 4) + (binarySearch (array 1 3 5 7 9) 4 0 4))) + (jsexpr->string '(3 -1))) + + (js-program-test + 'use-map-count-occurrences + usecase-map-count-occurrences + (js/expression (countOccurrences (array "a" "b" "a" "c" "b" "a"))) + (jsexpr->string '(("a" 3) ("b" 2) ("c" 1)))) + + (js-program-test + 'use-get-html-three-ways + usecase-get-html-three-ways + (js/expression (getHtmlThreeWays)) + (jsexpr->string '("

A

" "

A

" "

A

")) + #:preamble dom-preamble) + + (js-program-test + 'use-anagram + usecase-anagram + (js/expression (array (canArrange "listen" "silent") + (canArrange "abc" "abd"))) + (jsexpr->string '(#t #f))) + + (js-program-test + 'use-pairs-equal-target + usecase-pairs-equal-target + (js/expression (pairsEqualTarget (array 1 2 3 4 3 5) 6)) + (jsexpr->string '((2 4) (3 3) (1 5)))) + + (js-program-test + 'use-fetch-api-results-errors + usecase-fetch-api + (js/expression (send Promise all (array (loadTitle "/ok") (loadTitle "/fail")))) + "[{\"ok\":true,\"title\":\"Done\"},{\"ok\":false,\"message\":\"network\"}]" + #:preamble fetch-preamble))) + +(define engine (find-js-engine)) +(run-jsmaker-regression 'jsmaker-usecases tests "/tmp/jsmaker-usecases.js" #:engine engine)