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 "." (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 -#<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 #<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 - #<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))