A mostly AI coded js-maker, supervised by me.

This commit is contained in:
2026-05-26 09:42:35 +02:00
parent 8e8afc321b
commit 2cf831c180
23 changed files with 4143 additions and 1 deletions
+151 -1
View File
@@ -1,3 +1,153 @@
# js-maker
Converts simple scheme expressions to javascript for use as javascript code.
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/<project>-build-NNN/<collection-name>.
- 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 <collection>/main.rkt <collection>/testing/<tests>.rkt
/tmp/racket/bin/racket <collection>/testing/<tests>.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.
+152
View File
@@ -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 ("<span style=\"background: yellow\">" + word + "</span>");
})));
}
// exercise02
{
let p = document.querySelector("p");
p.insertAdjacentHTML("afterend", "<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>");
}
// 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(".<br>")));
}
// 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", ("<p>" + String(count) + " words</p>"));
}
// 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;
}
+83
View File
@@ -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 "<span style=\"background: yellow\">"
word
"</span>")))))))
;; 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"
"<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>"))))
;; 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 ".<br>")))))
;; 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 "<p>" (number->string count) " words</p>")))))
;; 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))
+312
View File
@@ -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};
});
}
}
+304
View File
@@ -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))
+32
View File
@@ -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)))
+16
View File
@@ -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)))))
+41
View File
@@ -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"))
+1185
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -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))
+209
View File
@@ -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)))
+5
View File
@@ -0,0 +1,5 @@
*.html
*.js
*.css
*.bak
*.scrbl~
+245
View File
@@ -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.
+584
View File
@@ -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
#<<RKT
(js
(define (randomBetween1And5)
(return (+ (send Math floor (* (send Math random) 5)) 1))))
RKT
#<<JS
function randomBetween1And5() {
return (Math.floor((Math.random() * 5)) + 1);
}
JS
)
@(tested "With Math.random fixed at 0.80, the generated function returns 5.")
@subsection{2. Unique values with Set}
@(side-by-side
#<<RKT
(js
(define (uniqueValues xs)
(return (send Array from (new Set xs)))))
RKT
#<<JS
function uniqueValues(xs) {
return Array.from(new Set(xs));
}
JS
)
@(tested "The array [1, 2, 2, 3, 1] becomes [1, 2, 3].")
@subsection{3. Six falsey JavaScript values}
@(side-by-side
#<<RKT
(js
(define (falseyValues)
(return (array #f 0 "" js-null js-undefined js-NaN))))
RKT
#<<JS
function falseyValues() {
return [false, 0, "", null, undefined, NaN];
}
JS
)
@(tested "Mapping JavaScript Boolean over the returned values yields six false values.")
@subsection{4. Currying}
@(side-by-side
#<<RKT
(js
(define (add x)
(return (lambda (y)
(return (+ x y))))))
RKT
#<<JS
function add(x) {
return function(y) {
return (x + y);
};
}
JS
)
@(tested "The generated call add(2)(3) returns 5; the test call itself is produced with js/expression.")
@subsection{5. Object destructuring}
@(side-by-side
#<<RKT
(js
(define (describePerson person)
(let-object ([name 'name]
[age 'age 0])
person
(return (string-append name ":" (number->string age))))))
RKT
#<<JS
function describePerson(person) {
return (() => {
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
#<<RKT
(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)))))))
RKT
#<<JS
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; }};
}
}
JS
)
@(tested "A fake timer runs the callback five times, but clearInterval stops it at tick 3.")
@subsection{7. Get, set, and delete object properties}
@(side-by-side
#<<RKT
(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")))))))
RKT
#<<JS
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")];
})();
}
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
#<<RKT
(js
(define (concatOrder)
(return (array (+ 1 2 "3")
(+ "1" 2 3)))))
RKT
#<<JS
function concatOrder() {
return [(1 + 2 + "3"), ("1" + 2 + 3)];
}
JS
)
@(tested "The generated function returns [\"33\", \"123\"].")
@subsection{9. Object.freeze versus Object.seal}
@(side-by-side
#<<RKT
(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"))))))
RKT
#<<JS
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")];
}
JS
)
@(tested "The frozen value stays 1; the sealed value changes to 9 but remains present.")
@subsection{10. Switch example}
@(side-by-side
#<<RKT
(js
(define (switchExample n)
(case n
[(1) (return "one")]
[(2 3) (return "two-or-three")]
[else (return "other")]))))
RKT
#<<JS
function switchExample(n) {
switch (n) {
case 1: return "one";
case 2:
case 3: return "two-or-three";
default: return "other";
}
}
JS
)
@(tested "The generated function maps 1, 2, and 9 to the three expected branches.")
@subsection{11. Class constructor with a default value}
@(side-by-side
#<<RKT
(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))))))
RKT
#<<JS
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()];
}
JS
)
@(tested "The default constructor value and explicit constructor argument both work.")
@subsection{12. Sort objects by a property}
@(side-by-side
#<<RKT
(js
(define (sortByProperty xs prop)
(return (send (send xs slice)
sort
(lambda (a b)
(return (- (js-ref a prop) (js-ref b prop))))))))
RKT
#<<JS
function sortByProperty(xs, prop) {
return xs.slice().sort(function(a, b) {
return (a[prop] - b[prop]);
});
}
JS
)
@(tested "Sorting age objects by age yields [20, 25, 30].")
@subsection{13. Four ways to delete or remove an array element}
@(side-by-side
#<<RKT
(js
(define (deleteArrayWays xs)
(let* ([a1 (send xs slice)]
[a2 (send xs slice)]
[a3 (send xs slice)]
[a4 (send xs slice)])
(send a1 splice 1 1)
(set! a2 (send a2 filter (lambda (x i) (return (not (= i 1))))))
(set! a3 (send (send a3 slice 0 1) concat (send a3 slice 2)))
(delete-prop! a4 1)
(return (array a1 a2 a3
(array (send Object hasOwn a4 "1") (length a4)))))))
RKT
#<<JS
function deleteArrayWays(xs) {
let a1 = xs.slice(), a2 = xs.slice(), a3 = xs.slice(), a4 = xs.slice();
a1.splice(1, 1);
a2 = a2.filter(function(x, i) { return !(i === 1); });
a3 = a3.slice(0, 1).concat(a3.slice(2));
delete a4[1];
return [a1, a2, a3, [Object.hasOwn(a4, "1"), a4.length]];
}
JS
)
@(tested "splice, filter, and slice/concat remove the item; delete leaves a hole and preserves length.")
@subsection{14. Bubble sort}
@(side-by-side
#<<RKT
(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))))
RKT
#<<JS
function bubbleSort(xs) {
let a = xs.slice();
let n = a.length;
while ((n > 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
#<<RKT
(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)))])))))
RKT
#<<JS
function binarySearch(xs, target, low, high) {
if ((low > 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
#<<RKT
(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))))))
RKT
#<<JS
function countOccurrences(xs) {
let counts = new Map();
for (const x of xs) {
if (counts.has(x)) counts.set(x, (counts.get(x) + 1));
else counts.set(x, 1);
}
return Array.from(counts.entries());
}
JS
)
@(tested "The array [\"a\", \"b\", \"a\", \"c\", \"b\", \"a\"] yields [[\"a\",3],[\"b\",2],[\"c\",1]].")
@subsection{17. Get HTML in three ways}
@(side-by-side
#<<RKT
(js
(define (getHtmlThreeWays)
(return (array document.body.innerHTML
(js-dot (send document querySelector "body") innerHTML)
(js-ref (send document getElementById "root") "innerHTML")))))
RKT
#<<JS
function getHtmlThreeWays() {
return [document.body.innerHTML,
document.querySelector("body").innerHTML,
document.getElementById("root")["innerHTML"]];
}
JS
)
@(tested "A fake DOM returns the same HTML string through all three access forms.")
@subsection{18. Anagram / rearrangement check}
@(side-by-side
#<<RKT
(js
(define (sortChars s)
(return (send (send (send s split "") sort) join "")))
(define (canArrange stringA stringB)
(return (string=? (sortChars stringA) (sortChars stringB)))))
RKT
#<<JS
function sortChars(s) {
return s.split("").sort().join("");
}
function canArrange(stringA, stringB) {
return (sortChars(stringA) === sortChars(stringB));
}
JS
)
@(tested "listen/silent returns true; abc/abd returns false.")
@subsection{19. Pairs equal to a target, without repeats}
@(side-by-side
#<<RKT
(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))))
RKT
#<<JS
function pairsEqualTarget(xs, target) {
let seen = new Set();
let used = new Set();
let out = [];
for (const x of xs) {
let y = (target - x);
if (seen.has(y) && !(used.has(x)) && !(used.has(y))) {
out.push([y, x]); used.add(x); used.add(y);
} else {
seen.add(x);
}
}
return out;
}
JS
)
@(tested "For [1, 2, 3, 4, 3, 5] and target 6 the returned pairs are [[2,4],[3,3],[1,5]].")
@subsection{20. Fetch API with result and error handling}
@(side-by-side
#<<RKT
(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)))))))
RKT
#<<JS
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}; });
}
JS
)
@(tested "A fake fetch resolves /ok to {ok:true,title:\"Done\"} and rejects /fail to {ok:false,message:\"network\"}.")
+68
View File
@@ -0,0 +1,68 @@
#lang racket/base
(require json
"jsmaker-test-framework.rkt"
"jsmaker-executors.rkt"
"../demo/dom-exercises.rkt")
(define (dom-preamble paragraph-text)
(format #<<JS
const state = { afterParagraph: [], afterHeading: [] };
const paragraph = {
innerHTML: ~a,
get textContent() { return String(this.innerHTML).replace(/<[^>]*>/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 <span style=\"background: yellow\">extraordinary</span> words remain.")
#:preamble (dom-preamble "Short extraordinary words remain."))
(js-program-test
'dom-ex02-add-source-link
exercise02
"state.afterParagraph"
(jsexpr->string '(("afterend" "<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>")))
#:preamble (dom-preamble "Force ipsum text."))
(js-program-test
'dom-ex03-split-sentences
exercise03
"paragraph.innerHTML"
(jsexpr->string "First sentence.<br>Second sentence.<br>Third.<br>")
#:preamble (dom-preamble "First sentence. Second sentence. Third."))
(js-program-test
'dom-ex04-count-words-after-heading
exercise04
"state.afterHeading"
(jsexpr->string '(("afterend" "<p>4 words</p>")))
#: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)
+164
View File
@@ -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 "<!doctype html><meta charset=\"utf-8\"><pre id=\"out\"></pre>" out)
(displayln "<script>" out)
(displayln "const __out = document.getElementById('out');" out)
(displayln "const print = (...xs) => { __out.append(xs.join(' ') + '\\n'); };" out)
(displayln "console.log = (...xs) => print(...xs);" out)
(displayln "console.error = (...xs) => print(...xs);" out)
(displayln "try {" out)
(display js out)
(displayln "\ndocument.documentElement.setAttribute('data-jsmaker-status', 'ok');" out)
(displayln "} catch (e) {" out)
(displayln " print('ERROR\\t' + (e && e.stack ? e.stack : String(e)));" out)
(displayln " document.documentElement.setAttribute('data-jsmaker-status', 'fail');" out)
(displayln "}" out)
(displayln "</script>" 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)))
+118
View File
@@ -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)
+24
View File
@@ -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)
+110
View File
@@ -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)
+7
View File
@@ -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")
+88
View File
@@ -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"))]))
+12
View File
@@ -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))
+191
View File
@@ -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
#<<JS
const intervals = {};
let nextIntervalId = 1;
function setInterval(cb, ms) {
const id = nextIntervalId++;
intervals[id] = { cb, ms, active: true };
return id;
}
function clearInterval(id) {
if (intervals[id]) intervals[id].active = false;
}
function runInterval(id, times) {
for (let i = 0; i < times; i++) {
if (!intervals[id] || !intervals[id].active) break;
intervals[id].cb();
}
}
JS
)
(define dom-preamble
#<<JS
const body = { innerHTML: "<p>A</p>" };
const root = { innerHTML: "<p>A</p>" };
const document = {
body,
querySelector: (selector) => selector === "body" ? body : false,
getElementById: (id) => id === "root" ? root : false
};
JS
)
(define fetch-preamble
#<<JS
function fetch(url) {
if (url === "/ok") {
return Promise.resolve({
json: () => 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 '("<p>A</p>" "<p>A</p>" "<p>A</p>"))
#: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)