From 823130e3ac63b5b9bbdd2038b6b5f72d6e44e486 Mon Sep 17 00:00:00 2001
From: Hans Dijkema
Date: Mon, 8 Jun 2026 12:55:08 +0200
Subject: [PATCH] Alles aangepast.
---
Makefile | 24 +-
README.md | 17 +-
demo/dom-exercises.generated.js | 155 +------
demo/dom-exercises.rkt | 85 +---
demo/js-usecases.generated.js | 327 +------------
demo/js-usecases.rkt | 309 +------------
demo/show-jsmaker-output.rkt | 37 +-
demo/show-optimized.rkt | 25 +-
info.rkt | 16 +-
main.rkt | 2 +-
scrbl/js-maker.scrbl | 101 ++--
scrbl/usecases.scrbl | 613 ++-----------------------
smoke-test.rkt | 29 --
testing/jsmaker-dom-exercises.rkt | 82 +---
testing/jsmaker-executors.rkt | 164 +------
testing/jsmaker-hash-regression.rkt | 154 +------
testing/jsmaker-list-regression.rkt | 110 +----
testing/jsmaker-program-regression.rkt | 149 ++----
testing/jsmaker-regexp-regression.rkt | 33 +-
testing/jsmaker-regression.rkt | 223 ++-------
testing/jsmaker-regressions.rkt | 25 +-
testing/jsmaker-test-framework.rkt | 137 ++----
testing/jsmaker-test-runner.rkt | 13 +-
testing/jsmaker-usecases.rkt | 209 +--------
24 files changed, 418 insertions(+), 2621 deletions(-)
delete mode 100644 smoke-test.rkt
diff --git a/Makefile b/Makefile
index ab14ad3..e748317 100644
--- a/Makefile
+++ b/Makefile
@@ -1,22 +1,12 @@
-RACKET ?= racket
-RACO ?= raco
+RACO ?= raco
-COLLECTION := js-maker
-
-.PHONY: all test docs clean very-clean
-
-all:
- $(RACO) make main.rkt testing/jsmaker-regressions.rkt scrbl/js-maker.scrbl scrbl/usecases.scrbl
+.PHONY: test setup docs
test:
- $(RACKET) testing/jsmaker-regressions.rkt
+ $(RACO) test -p js-maker
+
+setup:
+ $(RACO) setup js-maker
docs:
- $(RACO) scribble --htmls --dest rendered-docs scrbl/js-maker.scrbl scrbl/usecases.scrbl
-
-clean:
- find . -type d -name compiled -prune -exec rm -rf {} +
- find . -type f -name '*~' -delete
- find . -type f -name '*.bak' -delete
- rm -rf rendered-docs
-
+ $(RACO) scribble --html --dest doc scrbl/js-maker.scrbl scrbl/usecases.scrbl
diff --git a/README.md b/README.md
index 2acb247..36961d8 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,13 @@
# js-maker
-A compact Racket-to-JavaScript string maker macro.
+js-maker is a deliberately small syntax-driven Racket-to-JavaScript string
+maker. The public API is intentionally one macro:
-This js-maker 3 package is a clean restart from `js-transform.rkt`. It exports
-only:
+```racket
+(require js-maker)
+(js (define (square x) (return (* x x))))
+```
-- `js`
-- `js1`
-
-There is deliberately no `js/expression` compatibility macro in this branch.
-Use `js1` when the expression-level generator is needed directly.
+js-maker 3 keeps the implementation compact and supports ordinary `let`,
+`let*`, and tail-recursive named `let` loops while preserving Racket binding
+semantics. Demos are in `demo/`; regression tests are in `testing/`.
diff --git a/demo/dom-exercises.generated.js b/demo/dom-exercises.generated.js
index 22ce082..06b9562 100644
--- a/demo/dom-exercises.generated.js
+++ b/demo/dom-exercises.generated.js
@@ -1,152 +1,9 @@
-// exercise01
+let title = document.getElementById("title");
+title.innerHTML = "Hello from js-maker";
+title.addEventListener("click", function (evt) {
{
- 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;
+console.log("clicked");
+return true;
}
+});
diff --git a/demo/dom-exercises.rkt b/demo/dom-exercises.rkt
index bf4fbd7..78e5510 100644
--- a/demo/dom-exercises.rkt
+++ b/demo/dom-exercises.rkt
@@ -2,82 +2,17 @@
(require "../main.rkt")
-(provide exercise01
- exercise02
- exercise03
- exercise04
- exercise05
- all-dom-exercises
- show-dom-exercises)
+(provide generated-js)
-;; JavaScript DOM Exercises 01 Tutorial: https://youtu.be/EHF7xBUAmrQ
-
-;; Exercise 01
-;; Highlight all words over 8 characters long in the paragraph text.
-(define exercise01
+(define generated-js
(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)))
+ (define title (send document getElementById "title"))
+ (set! (js-dot title innerHTML) "Hello from js-maker")
+ (send title addEventListener "click"
+ (lambda (evt)
+ (begin
+ (send console log "clicked")
+ (return #t))))))
(module+ main
- (show-dom-exercises))
+ (display generated-js))
diff --git a/demo/js-usecases.generated.js b/demo/js-usecases.generated.js
index b73489d..c0efa8b 100644
--- a/demo/js-usecases.generated.js
+++ b/demo/js-usecases.generated.js
@@ -1,312 +1,25 @@
-// 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);
+let answer = 42;
+function square(x) {
+return x * x;
+}
+function sum_to(n) {
+{
+const i7 = 0;
+const acc8 = 0;
+{
+let i = i7;
+let acc = acc8;
+while (true) {
+if (i > n) {
+return acc;
+} else {
+const i11 = i + 1;
+const acc12 = acc + i;
+i = i11;
+acc = acc12;
+continue;
}
}
-
-// 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
index a3a2c55..b017a50 100644
--- a/demo/js-usecases.rkt
+++ b/demo/js-usecases.rkt
@@ -1,304 +1,19 @@
#lang racket/base
-(require racket/list
- "../main.rkt")
+(require "../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)
+(provide generated-js)
-;; Use case 01: generate a random integer between 1 and 5.
-(define usecase-random-number
+(define generated-js
(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))))))
+ (define answer 42)
+ (define (square x)
+ (return (* x x)))
+ (define (sum-to n)
+ (let loop ([i 0] [acc 0])
+ (if (> i n)
+ (return acc)
+ (loop (+ i 1) (+ acc i)))))))
(module+ main
- (show-js-usecases))
+ (display generated-js))
diff --git a/demo/show-jsmaker-output.rkt b/demo/show-jsmaker-output.rkt
index 3c698f7..220bf0d 100644
--- a/demo/show-jsmaker-output.rkt
+++ b/demo/show-jsmaker-output.rkt
@@ -1,32 +1,11 @@
#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")))))
+(require (prefix-in use: "js-usecases.rkt")
+ (prefix-in dom: "dom-exercises.rkt"))
-(for ([e (in-list examples)])
- (printf "===== ~a =====\n~a\n\n" (car e) (cdr e)))
+(module+ main
+ (displayln ";; js-usecases")
+ (display use:generated-js)
+ (newline)
+ (displayln ";; dom-exercises")
+ (display dom:generated-js))
diff --git a/demo/show-optimized.rkt b/demo/show-optimized.rkt
index 3963683..78a0dbf 100644
--- a/demo/show-optimized.rkt
+++ b/demo/show-optimized.rkt
@@ -1,16 +1,13 @@
#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)))))
+
+;; There is no separate optimizer in js-maker 3. This demo shows the compact
+;; named-let loop output produced directly by the `js` macro.
+(module+ main
+ (display
+ (js (define (factorial n)
+ (let loop ([i n] [acc 1])
+ (if (<= i 1)
+ (return acc)
+ (loop (- i 1) (* acc i))))))))
diff --git a/info.rkt b/info.rkt
index 701e494..4962e84 100644
--- a/info.rkt
+++ b/info.rkt
@@ -3,15 +3,19 @@
(define collection "js-maker")
(define version "0.3")
(define license 'MIT)
-(define pkg-desc "Small syntax-driven Racket-to-JavaScript maker macro.")
+(define pkg-desc "A small syntax-driven Racket-to-JavaScript maker macro.")
(define pkg-authors '(hnmdijkema))
(define deps '("base"))
(define build-deps '("scribble-lib" "racket-doc" "rackunit-lib"))
+(define tags '("javascript" "macro" "racket"))
(define scribblings
- '(("scrbl/js-maker.scrbl" () (library))))
+ '(("scrbl/js-maker.scrbl" () (library))
+ ("scrbl/usecases.scrbl" () (library))))
-;; js-maker 3 is a clean restart. The old demo/testing tree from the larger
-;; branch is intentionally not shipped. The maintained package test suite is
-;; this compact smoke test.
-(define test-include-paths '("smoke-test.rkt"))
+;; Keep the test entry point explicit. The supporting regression modules are
+;; required by this runner and compile during package setup.
+(define test-include-paths '("testing/jsmaker-regressions.rkt"))
+(define test-omit-paths
+ '("testing/jsmaker-executors.rkt"
+ "testing/jsmaker-test-framework.rkt"))
diff --git a/main.rkt b/main.rkt
index a5f1cb1..205f291 100644
--- a/main.rkt
+++ b/main.rkt
@@ -3,7 +3,7 @@
(require racket/string
(for-syntax racket/base))
-(provide js js1)
+(provide js)
;; A deliberately small Racket/Scheme-to-JavaScript string maker.
;;
diff --git a/scrbl/js-maker.scrbl b/scrbl/js-maker.scrbl
index 7ea1630..3c942ea 100644
--- a/scrbl/js-maker.scrbl
+++ b/scrbl/js-maker.scrbl
@@ -1,53 +1,84 @@
#lang scribble/manual
-@(require (for-label racket/base
- js-maker))
+@(require (for-label racket/base js-maker))
@title{js-maker}
@author{Hans Dijkema}
@defmodule[js-maker]
-@racketmodname[js-maker] provides a deliberately small syntax-driven macro for
-making JavaScript strings from a limited Racket-like surface syntax. This is a
-clean js-maker 3 restart based on the compact @filepath{js-transform.rkt}
-implementation.
+@emph{js-maker} is a deliberately small syntax-driven JavaScript string maker.
+It provides one public macro, @racket[js]. The helper machinery used to render
+subexpressions is private to the package.
+
+@section{Public API}
@defform[(js form ...)]{
-Generates JavaScript statements for each @racket[form] and concatenates them.
-The generated JavaScript is returned as a string.
+Produces a JavaScript string for the supplied forms. Each top-level form is
+rendered as a JavaScript statement. The macro is intended for small generated
+snippets, demos, and controlled code generation; it is not a complete Racket to
+JavaScript compiler.
+
+Examples:
@racketblock[
-(js
- (define (sum-to n)
- (let loop ([i 0] [acc 0])
- (if (> i n)
- (return acc)
- (loop (+ i 1) (+ acc i))))))]
-}
-
-@defform[(js1 form)]{
-Generates JavaScript for a single expression or syntactic form and returns it as
-a string. Use this when you want the expression-level generator directly.
-
-@racketblock[
-(js1 (+ 1 2))]
+(js (+ 1 2))
+(js (define answer 42))
+(js (define (square x)
+ (return (* x x))))
+]
}
@section{Supported core forms}
-The compact branch supports identifiers, quoted data, primitive literals,
-function calls, infix arithmetic and comparison operators, @racket[if],
-@racket[begin], @racket[return], @racket[set!], @racket[lambda],
-@racket[define], ordinary @racket[let], @racket[let*], and named
-@racket[let].
+The compact js-maker 3 implementation supports:
-Ordinary @racket[let] keeps Racket's parallel binding semantics. All right-hand
-sides are generated before the bound names are introduced, and the actual
-JavaScript bindings are placed in an inner block so JavaScript temporal dead zone
-rules cannot accidentally shadow the initializers.
+@itemlist[
+ @item{@racket[define] for values and functions.}
+ @item{@racket[lambda] and @racket[lambda]-style generated JavaScript functions.}
+ @item{@racket[if], @racket[begin], @racket[set!], and explicit @racket[return].}
+ @item{Ordinary @racket[let] with parallel binding semantics.}
+ @item{@racket[let*] with sequential binding semantics.}
+ @item{Named @racket[let] in tail-recursive loop style.}
+ @item{Common infix operators such as @racket[+], @racket[-], @racket[*],
+ @racket[/], comparisons, @racket[and], @racket[or], and @racket[not].}
+ @item{@racket[list], @racket[cons], @racket[send], @racket[js-dot], and
+ @racket[new].}
+]
-Named @racket[let] is compiled to a JavaScript @tt{while (true)} loop. A tail
-call to the loop name is rewritten to parallel assignment of the loop variables
-followed by @tt{continue}. This keeps the important loop semantics without
-reintroducing the large js-maker 2 implementation.
+@section{Let semantics}
+
+Ordinary @racket[let] evaluates all right-hand sides before introducing the new
+bindings. js-maker preserves that behavior by emitting temporary JavaScript
+constants and then opening a nested block for the real @tt{let} bindings.
+This avoids JavaScript temporal-dead-zone shadowing when a bound name is also
+used by a right-hand side.
+
+@racketblock[
+(js (define (ordinary-let x)
+ (let ([x 1] [y x])
+ (return y))))
+]
+
+The generated JavaScript returns the original argument as the value of
+@tt{y}, matching Racket's ordinary @racket[let] semantics.
+
+@section{Named let}
+
+Named @racket[let] is emitted as a @tt{while (true)} loop. A tail call to the
+loop name is translated to parallel update assignments followed by
+@tt{continue}. The intended style is statement-oriented and uses explicit
+@racket[return] for terminating branches.
+
+@racketblock[
+(js (define (sum-to n)
+ (let loop ([i 0] [acc 0])
+ (if (> i n)
+ (return acc)
+ (loop (+ i 1) (+ acc i))))))
+]
+
+@section{Package layout}
+
+The package includes small demos under @filepath{demo/} and a regression suite
+under @filepath{testing/}. The public API remains just @racket[js].
diff --git a/scrbl/usecases.scrbl b/scrbl/usecases.scrbl
index f4acefa..d42b261 100644
--- a/scrbl/usecases.scrbl
+++ b/scrbl/usecases.scrbl
@@ -1,596 +1,43 @@
#lang scribble/manual
-@(require scribble/core
- (for-label racket/base
- "../main.rkt"))
+@(require (for-label racket/base js-maker))
-@(define (code-box title source)
- (tabular #:style 'boxed
- (list (list (bold title))
- (list (verbatim source)))))
+@title{js-maker use cases}
-@(define (code-pair racket-source js-source)
- (list
- (code-box "Racket / js-maker" racket-source)
- (code-box "Generated JavaScript" js-source)))
+@section{Generating a small function}
-@(define side-by-side code-pair)
+@racketblock[
+(js (define (square x)
+ (return (* x x))))
+]
-@(define (tested s)
- (nested #:style 'inset (bold "Tested behavior: ") s))
+This produces a JavaScript function declaration. Racket identifiers are mapped
+to JavaScript-friendly names by replacing unsupported characters with
+underscores.
-@title{jsmaker JavaScript Use Cases}
-@author+email["Hans Dijkema" "hans@dijkewijk.nl"]
+@section{Generating a loop}
-@defmodule[js-maker/demo/js-usecases]
+@racketblock[
+(js (define (sum-to n)
+ (let loop ([i 0] [acc 0])
+ (if (> i n)
+ (return acc)
+ (loop (+ i 1) (+ acc i))))))
+]
-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 named @racket[let] form is useful for simple loops while keeping ordinary
+Racket binding semantics for the initial values and loop updates.
-The examples are shown vertically: first the Racket/js-maker source, then the
-generated JavaScript. Each code fragment is shown in a boxed documentation
-cell, preserving the light shaded background of the earlier side-by-side
-layout while avoiding narrow, wrapped code columns.
+@section{Generating DOM-style calls}
-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
-#< i n)
- (return acc)
- (loop (+ i 1) (+ acc i)))))))
-
-(check-regexp-match #rx"function ordinary_let" ordinary)
-(check-regexp-match #rx"const [A-Za-z0-9_]+ = x;" ordinary)
-(check-regexp-match #rx"let y = [A-Za-z0-9_]+;" ordinary)
-(check-regexp-match #rx"let y = x;" sequential)
-(check-regexp-match #rx"while \\(true\\)" looped)
-(check-regexp-match #rx"continue;" looped)
-(check-equal? (js1 (+ 1 2)) "1 + 2")
diff --git a/testing/jsmaker-dom-exercises.rkt b/testing/jsmaker-dom-exercises.rkt
index 9c1b9f8..ca89933 100644
--- a/testing/jsmaker-dom-exercises.rkt
+++ b/testing/jsmaker-dom-exercises.rkt
@@ -1,69 +1,25 @@
#lang racket/base
-(require json
- "jsmaker-test-framework.rkt"
- "jsmaker-executors.rkt"
- "../demo/dom-exercises.rkt")
+(require rackunit
+ "../main.rkt"
+ "jsmaker-test-framework.rkt")
-(define (dom-preamble paragraph-text)
- (format #<]*>/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)))
+(provide dom-tests)
-(define (side-effect-expr preamble program check-expr)
- (format "(() => {~a
-(function() {
-~a
-})();
-return (~a);
-})()"
- preamble
- program
- check-expr))
+(define dom-snippet
+ (js
+ (define title (send document getElementById "title"))
+ (set! (js-dot title innerHTML) "Hello")
+ (send title addEventListener "click" (lambda (evt) (return #t)))))
-(define tests
- (list
- (js-expression-test
- 'dom-ex01-highlight-long-words
- (side-effect-expr (dom-preamble "Short extraordinary words remain.") exercise01 "paragraph.innerHTML")
- (jsexpr->string "Short extraordinary words remain."))
+(define dom-tests
+ (test-suite
+ "DOM-like JavaScript generation"
+ (test-case "send and js-dot generate method calls and property assignment"
+ (check-js-contains? dom-snippet "document.getElementById(\"title\")")
+ (check-js-contains? dom-snippet "title.innerHTML = \"Hello\";")
+ (check-js-contains? dom-snippet "title.addEventListener"))))
- (js-expression-test
- 'dom-ex02-add-source-link
- (side-effect-expr (dom-preamble "Force ipsum text.") exercise02 "state.afterParagraph")
- (jsexpr->string '(("afterend" "Source: ForceM Ipsum"))))
-
- (js-expression-test
- 'dom-ex03-split-sentences
- (side-effect-expr (dom-preamble "First sentence. Second sentence. Third.") exercise03 "paragraph.innerHTML")
- (jsexpr->string "First sentence.
Second sentence.
Third.
"))
-
- (js-expression-test
- 'dom-ex04-count-words-after-heading
- (side-effect-expr (dom-preamble "These are four words") exercise04 "state.afterHeading")
- (jsexpr->string '(("afterend" "4 words
"))))
-
- (js-expression-test
- 'dom-ex05-replace-punctuation-faces
- (side-effect-expr (dom-preamble "Really? Yes! No?") exercise05 "paragraph.innerHTML")
- (jsexpr->string "Really馃 Yes馃槻 No馃"))))
-
-(define engine (find-js-engine))
-(run-jsmaker-regression 'jsmaker-dom-exercises tests "/tmp/jsmaker-dom-exercises.js" #:engine engine)
+(module+ test
+ (require rackunit/text-ui)
+ (run-tests dom-tests))
diff --git a/testing/jsmaker-executors.rkt b/testing/jsmaker-executors.rkt
index c525511..210869f 100644
--- a/testing/jsmaker-executors.rkt
+++ b/testing/jsmaker-executors.rkt
@@ -1,164 +1,4 @@
#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)))
+(require "jsmaker-test-framework.rkt")
+(provide node-available? run-js/trimmed)
diff --git a/testing/jsmaker-hash-regression.rkt b/testing/jsmaker-hash-regression.rkt
index d6c22ac..effe67d 100644
--- a/testing/jsmaker-hash-regression.rkt
+++ b/testing/jsmaker-hash-regression.rkt
@@ -1,146 +1,18 @@
#lang racket/base
-(require "../main.rkt"
- "jsmaker-executors.rkt"
+(require rackunit
+ "../main.rkt"
"jsmaker-test-framework.rkt")
-(define tests
- (list
- (js-expression-test 'hash-literal-ref-count
- (js/expression
- (let ([h (hash 'a 1 'b 2)])
- (list (hash-ref h 'a)
- (hash-ref h 'b)
- (hash-count h)
- (hash-empty? h)
- (hash? h))))
- "[1,2,2,false,true]")
- (js-expression-test 'make-hash-from-assoc-list
- (js/expression
- (let ([h (make-hash (list (cons 'a 1)
- (cons 'b 2)))])
- (list (hash-ref h 'a)
- (hash-ref h 'b))))
- "[1,2]")
- (js-expression-test 'make-hash-empty-and-set-bang
- (js/expression
- (let ([h (make-hash)])
- (hash-set! h 'a 1)
- (hash-set! h 'b 2)
- (list (hash-ref h 'a)
- (hash-ref h 'b)
- (hash-count h))))
- "[1,2,2]")
- (js-expression-test 'hash-ref-default-value-and-thunk
- (js/expression
- (let ([h (hash 'a 1)])
- (list (hash-ref h 'missing 42)
- (hash-ref h 'other (lambda () 99)))))
- "[42,99]")
- (js-expression-test 'hash-has-key
- (js/expression
- (let ([h (hash 'a #f 'b 2)])
- (list (hash-has-key? h 'a)
- (hash-has-key? h 'c)
- (hash-ref h 'a 'fallback))))
- "[true,false,false]")
- (js-expression-test 'hash-set-immutable-copy
- (js/expression
- (let* ([h (hash 'a 1)]
- [h2 (hash-set h 'b 2)])
- (list (hash-has-key? h 'b)
- (hash-ref h2 'a)
- (hash-ref h2 'b))))
- "[false,1,2]")
- (js-expression-test 'hash-remove-immutable-copy
- (js/expression
- (let* ([h (hash 'a 1 'b 2)]
- [h2 (hash-remove h 'a)])
- (list (hash-has-key? h 'a)
- (hash-has-key? h2 'a)
- (hash-ref h2 'b))))
- "[true,false,2]")
- (js-expression-test 'hash-remove-bang
- (js/expression
- (let ([h (make-hash (list (cons 'a 1)
- (cons 'b 2)))])
- (hash-remove! h 'a)
- (list (hash-has-key? h 'a)
- (hash-ref h 'b)
- (hash-count h))))
- "[false,2,1]")
- (js-expression-test 'hash-update-immutable
- (js/expression
- (let* ([h (hash 'a 1)]
- [h2 (hash-update h 'a (lambda (x) (+ x 10)))]
- [h3 (hash-update h2 'b (lambda (x) (+ x 1)) 40)])
- (list (hash-ref h 'a)
- (hash-ref h2 'a)
- (hash-ref h3 'b))))
- "[1,11,41]")
- (js-expression-test 'hash-update-bang
- (js/expression
- (let ([h (make-hash (list (cons 'a 1)))])
- (hash-update! h 'a (lambda (x) (+ x 10)))
- (hash-update! h 'b (lambda (x) (+ x 1)) 40)
- (list (hash-ref h 'a)
- (hash-ref h 'b))))
- "[11,41]")
- (js-expression-test 'hash-clear-and-clear-bang
- (js/expression
- (let* ([h (make-hash (list (cons 'a 1)
- (cons 'b 2)))]
- [h2 (hash-clear h)])
- (hash-clear! h)
- (list (hash-empty? h)
- (hash-empty? h2))))
- "[true,true]")
- (js-expression-test 'hash-copy-and-copy-clear
- (js/expression
- (let* ([h (hash 'a 1)]
- [h2 (hash-copy h)]
- [h3 (hash-copy-clear h)])
- (hash-set! h2 'b 2)
- (list (hash-has-key? h 'b)
- (hash-ref h2 'b)
- (hash-empty? h3))))
- "[false,2,true]")
- (js-expression-test 'hash-keys-values-list
- (js/expression
- (let ([h (hash 'a 1 'b 2)])
- (list (hash-keys h)
- (hash-values h)
- (hash->list h))))
- "[[\"a\",\"b\"],[1,2],[[\"a\",1],[\"b\",2]]]" )
- (js-expression-test 'hash-map
- (js/expression
- (let ([h (hash 'a 1 'b 2)])
- (hash-map h (lambda (k v) (string-append k ":" (number->string v))))))
- "[\"a:1\",\"b:2\"]")
- (js-expression-test 'hash-for-each
- (js/expression
- (let ([h (hash 'a 1 'b 2)]
- [out (list)])
- (hash-for-each h (lambda (k v)
- (set! out (append out (list (string-append k (number->string v)))))))
- out))
- "[\"a1\",\"b2\"]")
- (js-expression-test 'hasheq-and-make-immutable-hash
- (js/expression
- (let* ([h (hasheq 'a 1 'b 2)]
- [h2 (make-immutable-hash (list (cons 'c 3)))])
- (list (hash-ref h 'a)
- (hash-ref h2 'c))))
- "[1,3]")
- (js-expression-test 'hash-composition-pipeline
- (js/expression
- (let* ([h (make-hash)]
- [xs (list 'a 'b 'a 'c 'b 'a)])
- (for ([x xs])
- (hash-update! h x (lambda (n) (+ n 1)) 0))
- (sort (hash-map h (lambda (k v) (list k v)))
- (lambda (a b) (string (car a) (car b))))))
- "[[\"a\",3],[\"b\",2],[\"c\",1]]")))
+(provide object-tests)
-(define engine (find-js-engine))
-(run-jsmaker-regression 'jsmaker-hash-regression tests "/tmp/jsmaker-hash-regression.js" #:engine engine)
+(define object-tests
+ (test-suite
+ "object construction regression tests"
+ (test-case "new and method calls"
+ (check-js-equal? (js (new Date)) "new Date();\n")
+ (check-js-equal? (js (send console log "ok")) "console.log(\"ok\");\n"))))
+
+(module+ test
+ (require rackunit/text-ui)
+ (run-tests object-tests))
diff --git a/testing/jsmaker-list-regression.rkt b/testing/jsmaker-list-regression.rkt
index 120be18..1d66aa5 100644
--- a/testing/jsmaker-list-regression.rkt
+++ b/testing/jsmaker-list-regression.rkt
@@ -1,99 +1,21 @@
#lang racket/base
-(require "../main.rkt"
- "jsmaker-executors.rkt"
+(require rackunit
+ "../main.rkt"
"jsmaker-test-framework.rkt")
-(define tests
- (list
- (js-expression-test 'list-literal
- (js/expression (list 1 2 3))
- "[1,2,3]")
- (js-expression-test 'cons-front
- (js/expression (cons 1 (list 2 3)))
- "[1,2,3]")
- (js-expression-test 'list-star-with-tail
- (js/expression (list* 1 2 (list 3 4)))
- "[1,2,3,4]")
- (js-expression-test 'append-no-args
- (js/expression (append))
- "[]")
- (js-expression-test 'append-many
- (js/expression (append (list 1) (list 2 3) (list) (list 4)))
- "[1,2,3,4]")
- (js-expression-test 'car-cdr-cadr-caddr
- (js/expression (list (car (list 10 20 30))
- (cdr (list 10 20 30))
- (cadr (list 10 20 30))
- (caddr (list 10 20 30))))
- "[10,[20,30],20,30]")
- (js-expression-test 'length-and-predicates
- (js/expression (list (length (list 1 2 3))
- (null? (list))
- (empty? (list 1))
- (pair? (list 1))
- (list? (list 1 2))))
- "[3,true,false,true,true]")
- (js-expression-test 'list-ref-tail-last
- (js/expression (list (list-ref (list "a" "b" "c") 1)
- (list-tail (list 1 2 3 4) 2)
- (last (list 1 2 3 4))))
- "[\"b\",[3,4],4]")
- (js-expression-test 'take-drop-left-right
- (js/expression (list (take (list 1 2 3 4 5) 3)
- (drop (list 1 2 3 4 5) 2)
- (take-right (list 1 2 3 4 5) 2)
- (drop-right (list 1 2 3 4 5) 2)))
- "[[1,2,3],[3,4,5],[4,5],[1,2,3]]")
- (js-expression-test 'reverse-list
- (js/expression (reverse (list 1 2 3)))
- "[3,2,1]")
- (js-expression-test 'map-single-list
- (js/expression (map (lambda (x) (* x x)) (list 1 2 3 4)))
- "[1,4,9,16]")
- (js-expression-test 'map-multiple-lists
- (js/expression (map (lambda (x y) (+ x y))
- (list 1 2 3)
- (list 10 20 30)))
- "[11,22,33]")
- (js-expression-test 'filter-racket-truthiness
- (js/expression (filter (lambda (x) x) (list #f 0 "" 3)))
- "[0,\"\",3]")
- (js-expression-test 'filter-predicate
- (js/expression (filter (lambda (x) (> x 2)) (list 1 2 3 4)))
- "[3,4]")
- (js-expression-test 'foldl-cons
- (js/expression (foldl (lambda (x acc) (cons x acc)) (list) (list 1 2 3)))
- "[3,2,1]")
- (js-expression-test 'foldr-cons
- (js/expression (foldr (lambda (x acc) (cons x acc)) (list) (list 1 2 3)))
- "[1,2,3]")
- (js-expression-test 'member-tail-and-false
- (js/expression (list (member 2 (list 1 2 3 2))
- (member 9 (list 1 2 3))))
- "[[2,3,2],false]")
- (js-expression-test 'remove-first
- (js/expression (remove 2 (list 1 2 3 2)))
- "[1,3,2]")
- (js-expression-test 'remove-star
- (js/expression (remove* (list 2 4) (list 1 2 3 4 2 5)))
- "[1,3,5]")
- (js-expression-test 'list-set-update-immutable
- (js/expression (let* ([xs (list 1 2 3)]
- [ys (list-set xs 1 20)]
- [zs (list-update xs 2 (lambda (x) (+ x 100)))])
- (list xs ys zs)))
- "[[1,2,3],[1,20,3],[1,2,103]]")
- (js-expression-test 'sort-with-predicate
- (js/expression (sort (list 5 1 4 2 3) (lambda (a b) (< a b))))
- "[1,2,3,4,5]")
- (js-expression-test 'list-composition-pipeline
- (js/expression
- (let* ([xs (append (list 1 2) (list 3 4 5))]
- [ys (filter (lambda (x) (odd? x)) xs)]
- [zs (map (lambda (x) (* x 10)) ys)])
- (take zs 2)))
- "[10,30]")))
+(provide list-tests)
-(define engine (find-js-engine))
-(run-jsmaker-regression 'jsmaker-list-regression tests "/tmp/jsmaker-list-regression.js" #:engine engine)
+(define list-tests
+ (test-suite
+ "list and quoted datum generation"
+ (test-case "list and cons"
+ (check-js-equal? (js (list 1 2 3)) "[1, 2, 3];\n")
+ (check-js-equal? (js (cons 1 (list 2 3))) "[1].concat([2, 3]);\n"))
+ (test-case "quoted data"
+ (check-js-equal? (js (quote alpha)) "\"alpha\";\n")
+ (check-js-equal? (js (quote (1 2 x))) "[1, 2, \"x\"];\n"))))
+
+(module+ test
+ (require rackunit/text-ui)
+ (run-tests list-tests))
diff --git a/testing/jsmaker-program-regression.rkt b/testing/jsmaker-program-regression.rkt
index 58473df..e913682 100644
--- a/testing/jsmaker-program-regression.rkt
+++ b/testing/jsmaker-program-regression.rkt
@@ -1,118 +1,51 @@
#lang racket/base
-(require "../main.rkt"
- "jsmaker-executors.rkt"
+(require rackunit
+ "../main.rkt"
"jsmaker-test-framework.rkt")
-(define dom-preamble
+(provide program-tests)
+
+(define ordinary-let-program
(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"
- "};"))
+ (js (define (ordinary-let x)
+ (let ([x 1] [y x])
+ (return y))))
+ "console.log(ordinary_let(99));\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 sequential-let-program
+ (string-append
+ (js (define (sequential-let x)
+ (let* ([x 1] [y x])
+ (return y))))
+ "console.log(sequential_let(99));\n"))
-(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 named-let-program
+ (string-append
+ (js (define (sum-to n)
+ (let loop ([i 0] [acc 0])
+ (if (> i n)
+ (return acc)
+ (loop (+ i 1) (+ acc i))))))
+ "console.log(sum_to(10));\n"))
(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)
+ (test-suite
+ "generated JavaScript program behavior"
+ (test-case "ordinary let uses parallel Racket binding semantics"
+ (check-js-contains? ordinary-let-program "const")
+ (check-js-contains? ordinary-let-program "let x")
+ (when (node-available?)
+ (check-equal? (run-js/trimmed ordinary-let-program) "99")))
+ (test-case "let* uses sequential binding semantics"
+ (when (node-available?)
+ (check-equal? (run-js/trimmed sequential-let-program) "1")))
+ (test-case "named let compiles to a while loop with parallel updates"
+ (check-js-contains? named-let-program "while (true)")
+ (check-js-contains? named-let-program "continue;")
+ (when (node-available?)
+ (check-equal? (run-js/trimmed named-let-program) "55")))))
- (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)
+(module+ test
+ (require rackunit/text-ui)
+ (run-tests program-tests))
diff --git a/testing/jsmaker-regexp-regression.rkt b/testing/jsmaker-regexp-regression.rkt
index f846837..c085d21 100644
--- a/testing/jsmaker-regexp-regression.rkt
+++ b/testing/jsmaker-regexp-regression.rkt
@@ -1,24 +1,19 @@
#lang racket/base
-(require "../main.rkt"
- "jsmaker-executors.rkt"
+(require rackunit
+ "../main.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]")))
+(provide regexp-tests)
-(define engine (find-js-engine))
-(run-jsmaker-regression 'jsmaker-regexp-regression tests "/tmp/jsmaker-regexp-regression.js" #:engine engine)
+(define regexp-tests
+ (test-suite
+ "string escaping regression tests"
+ (test-case "strings are JavaScript escaped"
+ (check-js-equal? (js "a\"b") "\"a\\\"b\";\n")
+ (check-js-equal? (js "a\\b") "\"a\\\\b\";\n")
+ (check-js-equal? (js "a\nb") "\"a\\nb\";\n"))))
+
+(module+ test
+ (require rackunit/text-ui)
+ (run-tests regexp-tests))
diff --git a/testing/jsmaker-regression.rkt b/testing/jsmaker-regression.rkt
index 6f0c629..1a2c2f7 100644
--- a/testing/jsmaker-regression.rkt
+++ b/testing/jsmaker-regression.rkt
@@ -1,204 +1,35 @@
#lang racket/base
-(require racket/string
- json
+(require rackunit
+ racket/runtime-path
"../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))
+(provide regression-tests)
-(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-runtime-path main-module "../main.rkt")
-(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 regression-tests
+ (test-suite
+ "core js macro output"
+ (test-case "the public API exports js only"
+ (check-exn exn:fail? (lambda () (dynamic-require main-module 'js1)))
+ (check-exn exn:fail? (lambda () (dynamic-require main-module 'js/expression))))
+ (test-case "arithmetic and boolean expressions can be emitted as statements"
+ (check-js-equal? (js (+ 1 2)) "1 + 2;\n")
+ (check-js-equal? (js (and a b)) "a && b;\n")
+ (check-js-equal? (js (not ready)) "!(ready);\n"))
+ (test-case "value and function definitions"
+ (check-js-equal? (js (define answer 42)) "let answer = 42;\n")
+ (check-js-contains?
+ (js (define (square x) (return (* x x))))
+ "function square(x)"))
+ (test-case "conditionals and begin blocks"
+ (define out (js (if (> x 0) (return x) (return 0))))
+ (check-js-contains? out "if (x > 0)")
+ (check-js-contains? out "return x;")
+ (check-js-contains? (js (begin (set! x 1) (return x))) "x = 1;"))))
-(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-syntax with-id->el
- (syntax-rules ()
- ((_ id el expr)
- (js (let ((el (send document getElementById (eval id))))
- expr)))))
-
-(define (runtime-eval-macro-function id html)
- (with-id->el id el
- (begin
- (set! (js-dot el innerHTML) (eval html))
- #t)))
-
-(define runtime-eval-macro-program
- (runtime-eval-macro-function 't "BEE"))
-(unless (and (regexp-match? #rx"getElementById\\(\"t\"\\)" runtime-eval-macro-program)
- (regexp-match? #rx"innerHTML = \"BEE\"" runtime-eval-macro-program)
- (not (regexp-match? #rx"return true;" runtime-eval-macro-program)))
- (error 'jsmaker-regression
- "runtime eval inside a user macro should keep use-site lexical bindings and should not emit an implicit top-level return, got: ~a"
- runtime-eval-macro-program))
-
-(define-syntax with-id->el/inject
- (syntax-rules ()
- ((_ id el expr)
- (js (let ((el (send document getElementById (inject id))))
- expr)))))
-
-(define (runtime-inject-macro-function id html)
- (with-id->el/inject id el
- (begin
- (set! (js-dot el innerHTML) (inject html))
- #t)))
-
-(define runtime-inject-macro-program
- (runtime-inject-macro-function 't "BEE"))
-(unless (and (regexp-match? #rx"getElementById\\(\"t\"\\)" runtime-inject-macro-program)
- (regexp-match? #rx"innerHTML = \"BEE\"" runtime-inject-macro-program)
- (not (regexp-match? #rx"return true;" runtime-inject-macro-program)))
- (error 'jsmaker-regression
- "runtime inject inside a user macro should keep use-site lexical bindings and should not emit an implicit top-level return, got: ~a"
- runtime-inject-macro-program))
-
-(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 webview-set-value-program
- (let ([val "2026-05-28"])
- (with-id->el/inject 'inp2 el
- (if (or (= (js-dot el type) "checkbox")
- (= (js-dot el type) "radio"))
- (begin
- (set! (js-dot el checked) (inject (if (eq? val #f) #f #t)))
- #t)
- (begin
- (set! (js-dot el value) (inject val))
- #t)))))
-
-(unless (not (regexp-match? #rx"return true;" webview-set-value-program))
- (error 'jsmaker-regression
- "top-level WebView-style js must not emit branch-level return true statements, got: ~a"
- webview-set-value-program))
-
-(define webview-top-level-vm-smoke
- (format #< {
- if (typeof require !== 'function') return true;
- const vm = require('vm');
- const el = { type: 'text', value: '', checked: false };
- const context = { document: { getElementById: (id) => el } };
- vm.runInNewContext(~a, context);
- return JSON.stringify([el.value, el.checked]) === ~a;
-})()
-JS
- (jsexpr->string webview-set-value-program)
- (jsexpr->string "[\"2026-05-28\",false]")))
-
-(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 'compile-time-eval-number
- (js/expression (eval (+ 1 2)))
- "3")
- (js-expression-test 'compile-time-eval-data
- (js/expression (array (eval (list 1 2 3))
- (eval (string-append "a" "b"))))
- "[[1,2,3],\"ab\"]")
- (js-expression-test 'webview-top-level-script-vm
- webview-top-level-vm-smoke
- "true")
- (let ([x 10]
- [y 20])
- (js-expression-test 'runtime-eval-lexical-let
- (js/expression (let ([a (eval (* x y))]) (* a a)))
- "40000"))
- (let ([x 10]
- [y 20])
- (js-expression-test 'runtime-inject-lexical-let
- (js/expression (let ([a (inject (* x y))]) (* a a)))
- "40000"))
- (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)
+(module+ test
+ (require rackunit/text-ui)
+ (run-tests regression-tests))
diff --git a/testing/jsmaker-regressions.rkt b/testing/jsmaker-regressions.rkt
index ea483e1..e07022d 100644
--- a/testing/jsmaker-regressions.rkt
+++ b/testing/jsmaker-regressions.rkt
@@ -1,9 +1,28 @@
#lang racket/base
-(require "jsmaker-regression.rkt"
- "jsmaker-regexp-regression.rkt"
+(require rackunit
+ rackunit/text-ui
+ "jsmaker-regression.rkt"
"jsmaker-program-regression.rkt"
"jsmaker-dom-exercises.rkt"
- "jsmaker-usecases.rkt"
"jsmaker-list-regression.rkt"
+ "jsmaker-regexp-regression.rkt"
+ "jsmaker-usecases.rkt"
"jsmaker-hash-regression.rkt")
+
+(define all-tests
+ (test-suite
+ "js-maker 3 regression suite"
+ regression-tests
+ program-tests
+ dom-tests
+ list-tests
+ regexp-tests
+ usecase-tests
+ object-tests))
+
+(module+ main
+ (void (run-tests all-tests)))
+
+(module+ test
+ (void (run-tests all-tests)))
diff --git a/testing/jsmaker-test-framework.rkt b/testing/jsmaker-test-framework.rkt
index 1b5f427..15854b6 100644
--- a/testing/jsmaker-test-framework.rkt
+++ b/testing/jsmaker-test-framework.rkt
@@ -1,98 +1,57 @@
#lang racket/base
-(require racket/format
+(require rackunit
racket/string
- "jsmaker-executors.rkt")
+ racket/file
+ racket/system)
-(provide js-expression-test
- js-program-test
- write-js-test-file
- run-jsmaker-regression
- notice-line
- warning-line)
+(provide check-js-equal?
+ check-js-contains?
+ check-js-matches?
+ node-available?
+ run-js/trimmed)
-(define (notice-line who fmt . args)
- (displayln (string-append "NOTE: " (apply format fmt args))))
+(define-check (check-js-equal? actual expected)
+ (check-equal? actual expected))
-(define (warning-line who fmt . args)
- (displayln (string-append "WARNING: " (apply format fmt args))
- (current-error-port)))
+(define-check (check-js-contains? actual needle)
+ (check-true (string-contains? actual needle)
+ (format "expected generated JavaScript to contain ~s, got:\n~a" needle actual)))
-(define (js-expression-test name expr expected)
- (list name expr expected))
+(define-check (check-js-matches? actual pattern)
+ (check-true (regexp-match? pattern actual)
+ (format "expected generated JavaScript to match ~s, got:\n~a" pattern actual)))
-(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 (node-available?)
+ (and (find-executable-path "node") #t))
-(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
- (if (or (getenv "JSMAKER_REQUIRE_ENGINE") (getenv "JSMAKER_REQUIRE_NODE"))
- (begin
- (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.")
- (error who "JavaScript engine required by environment setting"))
- (begin
- (notice-line who "No JavaScript engine was found; using non-failing-javascript-stub.")
- (notice-line who "Generated JavaScript test file: ~a" js-path)
- (notice-line who "Generated tests were not executed by a JavaScript runtime.")
- (notice-line who "Set JSMAKER_REQUIRE_ENGINE=1 to make a missing JavaScript engine fail.")))]))
+(define (run-js/trimmed program)
+ (define node (find-executable-path "node"))
+ (unless node
+ (error 'run-js/trimmed "node is not available"))
+ (define source-path (make-temporary-file "js-maker-test-~a.js"))
+ (define out-path (make-temporary-file "js-maker-test-out-~a.txt"))
+ (define err-path (make-temporary-file "js-maker-test-err-~a.txt"))
+ (dynamic-wind
+ void
+ (lambda ()
+ (call-with-output-file source-path #:exists 'truncate
+ (lambda (out) (display program out)))
+ (define exit-code
+ (call-with-output-file out-path #:exists 'truncate
+ (lambda (out)
+ (call-with-output-file err-path #:exists 'truncate
+ (lambda (err)
+ (parameterize ([current-output-port out]
+ [current-error-port err])
+ (system*/exit-code node source-path)))))))
+ (define stdout (file->string out-path))
+ (define stderr (file->string err-path))
+ (unless (zero? exit-code)
+ (error 'run-js/trimmed
+ "node failed with exit code ~a\nstdout:\n~a\nstderr:\n~a\nprogram:\n~a"
+ exit-code stdout stderr program))
+ (string-trim stdout))
+ (lambda ()
+ (for ([path (list source-path out-path err-path)])
+ (with-handlers ([exn:fail? void]) (delete-file path))))))
diff --git a/testing/jsmaker-test-runner.rkt b/testing/jsmaker-test-runner.rkt
index 0ebd09e..16a7b6c 100644
--- a/testing/jsmaker-test-runner.rkt
+++ b/testing/jsmaker-test-runner.rkt
@@ -1,13 +1,2 @@
#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 uses an
-;; explicit non-failing-javascript-stub 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))
+(require "jsmaker-regressions.rkt")
diff --git a/testing/jsmaker-usecases.rkt b/testing/jsmaker-usecases.rkt
index b158cdd..9bde3ae 100644
--- a/testing/jsmaker-usecases.rkt
+++ b/testing/jsmaker-usecases.rkt
@@ -1,191 +1,32 @@
#lang racket/base
-(require json
+(require rackunit
"../main.rkt"
- "jsmaker-test-framework.rkt"
- "jsmaker-executors.rkt"
- "../demo/js-usecases.rkt")
+ "jsmaker-test-framework.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 counter-program
+ (string-append
+ (js (define (make-counter start)
+ (let ([value start])
+ (return (lambda ()
+ (begin
+ (set! value (+ value 1))
+ (return value)))))))
+ "const c = make_counter(5);\n"
+ "console.log(c());\n"
+ "console.log(c());\n"))
-(define fetch-preamble
- #< Promise.resolve({ title: "Done" })
- });
- }
- return Promise.reject(new Error("network"));
-}
-JS
- )
+(define usecase-tests
+ (test-suite
+ "small use cases"
+ (test-case "closures and set!"
+ (check-js-contains? counter-program "function make_counter(start)")
+ (check-js-contains? counter-program "value = value + 1;")
+ (when (node-available?)
+ (check-equal? (run-js/trimmed counter-program) "6\n7")))))
-(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)
+(module+ test
+ (require rackunit/text-ui)
+ (run-tests usecase-tests))