Alles aangepast.

This commit is contained in:
2026-06-08 12:55:08 +02:00
parent a9610e6e0c
commit 823130e3ac
24 changed files with 418 additions and 2621 deletions
+6 -16
View File
@@ -1,22 +1,12 @@
RACKET ?= racket
RACO ?= raco RACO ?= raco
COLLECTION := js-maker .PHONY: test setup docs
.PHONY: all test docs clean very-clean
all:
$(RACO) make main.rkt testing/jsmaker-regressions.rkt scrbl/js-maker.scrbl scrbl/usecases.scrbl
test: test:
$(RACKET) testing/jsmaker-regressions.rkt $(RACO) test -p js-maker
setup:
$(RACO) setup js-maker
docs: docs:
$(RACO) scribble --htmls --dest rendered-docs scrbl/js-maker.scrbl scrbl/usecases.scrbl $(RACO) scribble --html --dest doc 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
+9 -8
View File
@@ -1,12 +1,13 @@
# js-maker # 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 ```racket
only: (require js-maker)
(js (define (square x) (return (* x x))))
```
- `js` js-maker 3 keeps the implementation compact and supports ordinary `let`,
- `js1` `let*`, and tail-recursive named `let` loops while preserving Racket binding
semantics. Demos are in `demo/`; regression tests are in `testing/`.
There is deliberately no `js/expression` compatibility macro in this branch.
Use `js1` when the expression-level generator is needed directly.
+6 -149
View File
@@ -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"); console.log("clicked");
p.innerHTML = ((__rkt_body) => { return true;
const __rkt_to_regexp = (__pat, __global) => {
const __flags = (__pat instanceof RegExp)
? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('')
: (__global ? 'g' : '');
return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags);
};
const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x));
const __rkt_replacement = (__s) => {
if (typeof __s !== 'string') return __s;
let __out = '';
for (let __i = 0; __i < __s.length; __i++) {
const __ch = __s[__i];
if (__ch === '$') { __out += '$$'; continue; }
if (__ch === '\\' && __i + 1 < __s.length) {
const __n = __s[++__i];
if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n));
else __out += __n;
} else __out += __ch;
}
return __out;
};
return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement);
})((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.innerHTML).replace(__rkt_to_regexp(new RegExp("\\b\\w{9,}\\b"), true), __rkt_replacement(function(word) {
return ("<span style=\"background: yellow\">" + word + "</span>");
})));
}
// exercise02
{
let p = document.querySelector("p");
p.insertAdjacentHTML("afterend", "<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>");
}
// exercise03
{
let p = document.querySelector("p");
p.innerHTML = ((__rkt_body) => {
const __rkt_to_regexp = (__pat, __global) => {
const __flags = (__pat instanceof RegExp)
? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('')
: (__global ? 'g' : '');
return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags);
};
const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x));
const __rkt_replacement = (__s) => {
if (typeof __s !== 'string') return __s;
let __out = '';
for (let __i = 0; __i < __s.length; __i++) {
const __ch = __s[__i];
if (__ch === '$') { __out += '$$'; continue; }
if (__ch === '\\' && __i + 1 < __s.length) {
const __n = __s[++__i];
if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n));
else __out += __n;
} else __out += __ch;
}
return __out;
};
return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement);
})((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.textContent).replace(__rkt_to_regexp(new RegExp("\\.\\s*"), true), __rkt_replacement(".<br>")));
}
// exercise04
{
let heading = document.querySelector("h1");
let p = document.querySelector("p");
let words = ((__rkt_body) => {
const __rkt_to_regexp = (__pat, __global) => {
const __flags = (__pat instanceof RegExp)
? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('')
: (__global ? 'g' : '');
return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags);
};
const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x));
const __rkt_replacement = (__s) => {
if (typeof __s !== 'string') return __s;
let __out = '';
for (let __i = 0; __i < __s.length; __i++) {
const __ch = __s[__i];
if (__ch === '$') { __out += '$$'; continue; }
if (__ch === '\\' && __i + 1 < __s.length) {
const __n = __s[++__i];
if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n));
else __out += __n;
} else __out += __ch;
}
return __out;
};
return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement);
})((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.textContent).split(__rkt_to_regexp(new RegExp(" "), true)));
let count = (words).length;
heading.insertAdjacentHTML("afterend", ("<p>" + String(count) + " words</p>"));
}
// exercise05
{
let p = document.querySelector("p");
let step = ((__rkt_body) => {
const __rkt_to_regexp = (__pat, __global) => {
const __flags = (__pat instanceof RegExp)
? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('')
: (__global ? 'g' : '');
return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags);
};
const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x));
const __rkt_replacement = (__s) => {
if (typeof __s !== 'string') return __s;
let __out = '';
for (let __i = 0; __i < __s.length; __i++) {
const __ch = __s[__i];
if (__ch === '$') { __out += '$$'; continue; }
if (__ch === '\\' && __i + 1 < __s.length) {
const __n = __s[++__i];
if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n));
else __out += __n;
} else __out += __ch;
}
return __out;
};
return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement);
})((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.innerHTML).replace(__rkt_to_regexp(new RegExp("\\?"), true), __rkt_replacement("🤔")));
let out = ((__rkt_body) => {
const __rkt_to_regexp = (__pat, __global) => {
const __flags = (__pat instanceof RegExp)
? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('')
: (__global ? 'g' : '');
return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags);
};
const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x));
const __rkt_replacement = (__s) => {
if (typeof __s !== 'string') return __s;
let __out = '';
for (let __i = 0; __i < __s.length; __i++) {
const __ch = __s[__i];
if (__ch === '$') { __out += '$$'; continue; }
if (__ch === '\\' && __i + 1 < __s.length) {
const __n = __s[++__i];
if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n));
else __out += __n;
} else __out += __ch;
}
return __out;
};
return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement);
})((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(step).replace(__rkt_to_regexp(new RegExp("!"), true), __rkt_replacement("😲")));
p.innerHTML = out;
} }
});
+10 -75
View File
@@ -2,82 +2,17 @@
(require "../main.rkt") (require "../main.rkt")
(provide exercise01 (provide generated-js)
exercise02
exercise03
exercise04
exercise05
all-dom-exercises
show-dom-exercises)
;; JavaScript DOM Exercises 01 Tutorial: https://youtu.be/EHF7xBUAmrQ (define generated-js
;; Exercise 01
;; Highlight all words over 8 characters long in the paragraph text.
(define exercise01
(js (js
(let* ([p (send document querySelector "p")]) (define title (send document getElementById "title"))
(set! p.innerHTML (set! (js-dot title innerHTML) "Hello from js-maker")
(regexp-replace* #px"\\b\\w{9,}\\b" (send title addEventListener "click"
p.innerHTML (lambda (evt)
(λ (word) (begin
(string-append "<span style=\"background: yellow\">" (send console log "clicked")
word (return #t))))))
"</span>")))))))
;; Exercise 02
;; Add a link back to the source of the text after the paragraph tag.
(define exercise02
(js
(let* ([p (send document querySelector "p")])
(send p insertAdjacentHTML
"afterend"
"<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>"))))
;; Exercise 03
;; Split each new sentence on to a separate line in the paragraph text.
;; A sentence is assumed to be terminated with a period.
(define exercise03
(js
(let* ([p (send document querySelector "p")])
(set! p.innerHTML
(regexp-replace* #rx"\\.\\s*" p.textContent ".<br>")))))
;; Exercise 04
;; Count the number of words in the paragraph tag and display the count
;; after the heading. Words are separated by one singular whitespace.
(define exercise04
(js
(let* ([heading (send document querySelector "h1")]
[p (send document querySelector "p")]
[words (regexp-split #rx" " p.textContent)]
[count (length words)])
(send heading insertAdjacentHTML
"afterend"
(string-append "<p>" (number->string count) " words</p>")))))
;; Exercise 05
;; Replace question marks with thinking faces and exclamation marks with
;; astonished faces.
(define exercise05
(js
(let* ([p (send document querySelector "p")]
[step (regexp-replace* #rx"\\?" p.innerHTML "🤔")]
[out (regexp-replace* #rx"!" step "😲")])
(set! p.innerHTML out))))
(define all-dom-exercises
`((exercise01 . ,exercise01)
(exercise02 . ,exercise02)
(exercise03 . ,exercise03)
(exercise04 . ,exercise04)
(exercise05 . ,exercise05)))
(define (show-dom-exercises)
(for ([entry (in-list all-dom-exercises)])
(displayln (format "// ~a" (car entry)))
(displayln (cdr entry))
(newline)))
(module+ main (module+ main
(show-dom-exercises)) (display generated-js))
+16 -303
View File
@@ -1,312 +1,25 @@
// Generated by demo/js-usecases.rkt let answer = 42;
// Each use case is wrapped in a function so snippets with return are valid. function square(x) {
return x * x;
// random-number
function run_random_number() {
function randomBetween1And5() {
return (Math.floor((Math.random() * 5)) + 1);
} }
} function sum_to(n) {
// 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; const i7 = 0;
let intervalId = false; const acc8 = 0;
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 i = i7;
let a1 = obj.a; let acc = acc8;
let a2 = obj["a"]; while (true) {
return (() => { if (i > n) {
const { a: a3 } = obj; return acc;
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 { } else {
{ const i11 = i + 1;
let mid = Math.floor((() => { const acc12 = acc + i;
const __div_3 = (low + high); i = i11;
const __div_4 = 2; acc = acc12;
if (__div_4 === 0) throw new Error("division by zero"); continue;
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};
});
}
}
+12 -297
View File
@@ -1,304 +1,19 @@
#lang racket/base #lang racket/base
(require racket/list (require "../main.rkt")
"../main.rkt")
(provide usecase-random-number (provide generated-js)
usecase-unique-values
usecase-falsey-values
usecase-currying
usecase-object-destructuring
usecase-timer-interval
usecase-object-props
usecase-string-concat-order
usecase-freeze-vs-seal
usecase-switch
usecase-class-constructor
usecase-sort-objects-by-property
usecase-delete-array-elements
usecase-bubble-sort
usecase-binary-search
usecase-map-count-occurrences
usecase-get-html-three-ways
usecase-anagram
usecase-pairs-equal-target
usecase-fetch-api
all-js-usecases
show-js-usecases
write-js-usecases-file)
;; Use case 01: generate a random integer between 1 and 5. (define generated-js
(define usecase-random-number
(js (js
(define (randomBetween1And5) (define answer 42)
(return (+ (send Math floor (* (send Math random) 5)) 1))))) (define (square x)
(return (* x x)))
;; Use case 02: get unique values from an array with duplicates using Set. (define (sum-to n)
(define usecase-unique-values (let loop ([i 0] [acc 0])
(js (if (> i n)
(define (uniqueValues xs) (return acc)
(return (send Array from (new Set xs)))))) (loop (+ i 1) (+ acc i)))))))
;; Use case 03: the six JavaScript falsey values.
(define usecase-falsey-values
(js
(define (falseyValues)
(return (array #f 0 "" js-null js-undefined js-NaN)))))
;; Use case 04: currying, simple example.
(define usecase-currying
(js
(define (add x)
(return (lambda (y)
(return (+ x y)))))))
;; Use case 05: object destructuring.
(define usecase-object-destructuring
(js
(define (describePerson person)
(let-object ([name 'name]
[age 'age 0])
person
(return (string-append name ":" (number->string age)))))))
;; Use case 06: get out of a timer interval with setInterval/clearInterval.
(define usecase-timer-interval
(js
(define (startTimer)
(let* ([ticks 0]
[intervalId #f])
(set! intervalId
(setInterval (lambda ()
(set! ticks (+ ticks 1))
(when (= ticks 3)
(clearInterval intervalId)))
10))
(return (object 'id intervalId
'getTicks (lambda () (return ticks))))))))
;; Use case 07: get/set/delete object properties. The value of a is read via
;; dot access, bracket access, and destructuring.
(define usecase-object-props
(js
(define (objectProps)
(let* ([obj (object 'a 1)]
[a1 obj.a]
[a2 (js-ref obj "a")])
(let-object ([a3 'a]) obj
(set! obj.b 2)
(set-prop! obj "c" 3)
(delete-prop! obj "a")
(return (array a1 a2 a3 obj.b (js-ref obj "c")
(send Object hasOwn obj "a"))))))))
;; Use case 08: string concatenation; order matters with JavaScript +.
(define usecase-string-concat-order
(js
(define (concatOrder)
(return (array (+ 1 2 "3")
(+ "1" 2 3))))))
;; Use case 09: Object.freeze() vs Object.seal().
(define usecase-freeze-vs-seal
(js
(define (freezeVsSeal)
(let* ([frozen (send Object freeze (object 'a 1))]
[sealed (send Object seal (object 'a 1))])
(set! frozen.a 9)
(set! sealed.a 9)
(delete-prop! sealed "a")
(return (array frozen.a
sealed.a
(send Object isFrozen frozen)
(send Object isSealed sealed)
(send Object hasOwn sealed "a")))))))
;; Use case 10: switch example. The Racket surface form is case.
(define usecase-switch
(js
(define (switchExample n)
(case n
[(1) (return "one")]
[(2 3) (return "two-or-three")]
[else (return "other")]))))
;; Use case 11: class constructor with a default value.
(define usecase-class-constructor
(js
(define-class Greeter
(constructor ([name "world"])
(set! this.name name))
(method greet ()
(return (string-append "Hello " this.name))))
(define (classExample)
(let* ([a (new Greeter)]
[b (new Greeter "Ada")])
(return (array (send a greet) (send b greet)))))))
;; Use case 12: sort an array of objects by a given property.
(define usecase-sort-objects-by-property
(js
(define (sortByProperty xs prop)
(return (send (send xs slice)
sort
(lambda (a b)
(return (- (js-ref a prop) (js-ref b prop)))))))))
;; Use case 13: four ways to delete/remove an element from an array.
(define usecase-delete-array-elements
(js
(define (deleteArrayWays xs)
(let* ([a1 (send xs slice)]
[a2 (send xs slice)]
[a3 (send xs slice)]
[a4 (send xs slice)])
;; 1. Mutating removal with splice.
(send a1 splice 1 1)
;; 2. Functional removal with filter.
(set! a2 (send a2 filter (lambda (x i) (return (not (= i 1))))))
;; 3. Rebuild with slice + concat.
(set! a3 (send (send a3 slice 0 1) concat (send a3 slice 2)))
;; 4. delete leaves a hole and preserves length.
(delete-prop! a4 1)
(return (array a1 a2 a3 (array (send Object hasOwn a4 "1") (length a4))))))))
;; Use case 14: Bubble Sort.
(define usecase-bubble-sort
(js
(define (bubbleSort xs)
(let* ([a (send xs slice)]
[n (length a)])
(while (> n 1)
(let* ([i 1])
(while (< i n)
(when (> (list-ref a (- i 1)) (list-ref a i))
(let* ([tmp (list-ref a (- i 1))])
(vector-set! a (- i 1) (list-ref a i))
(vector-set! a i tmp)))
(set! i (+ i 1))))
(set! n (- n 1)))
(return a)))))
;; Use case 15: Binary Search using recursion.
(define usecase-binary-search
(js
(define (binarySearch xs target low high)
(if (> low high)
(return -1)
(let* ([mid (send Math floor (/ (+ low high) 2))]
[value (list-ref xs mid)])
(cond
[(= value target) (return mid)]
[(< value target) (return (binarySearch xs target (+ mid 1) high))]
[else (return (binarySearch xs target low (- mid 1)))]))))))
;; Use case 16: use Map to count how often each element occurs in an array.
(define usecase-map-count-occurrences
(js
(define (countOccurrences xs)
(let* ([counts (new Map)])
(for ([x (in-list xs)])
(if (send counts has x)
(send counts set x (+ (send counts get x) 1))
(send counts set x 1)))
(return (send Array from (send counts entries)))))))
;; Use case 17: get HTML in three different ways via the DOM.
(define usecase-get-html-three-ways
(js
(define (getHtmlThreeWays)
(return (array document.body.innerHTML
(js-dot (send document querySelector "body") innerHTML)
(js-ref (send document getElementById "root") "innerHTML"))))))
;; Use case 18: determine if stringA can be arranged into stringB.
(define usecase-anagram
(js
(define (sortChars s)
(return (send (send (send s split "") sort) join "")))
(define (canArrange stringA stringB)
(return (string=? (sortChars stringA) (sortChars stringB))))))
;; Use case 19: determine what pairs in an array equal a given value, with no
;; repeated numbers in the result pairs.
(define usecase-pairs-equal-target
(js
(define (pairsEqualTarget xs target)
(let* ([seen (new Set)]
[used (new Set)]
[out (array)])
(for ([x (in-list xs)])
(let* ([y (- target x)])
(if (and (send seen has y)
(not (send used has x))
(not (send used has y)))
(begin
(send out push (array y x))
(send used add x)
(send used add y))
(send seen add x))))
(return out)))))
;; Use case 20: fetch API, handling results and errors. The function returns a
;; Promise, which the test framework awaits.
(define usecase-fetch-api
(js
(define (loadTitle url)
(return
(send
(send
(send (fetch url)
then
(lambda (response)
(return (send response json))))
then
(lambda (data)
(return (object 'ok #t 'title data.title))))
catch
(lambda (err)
(return (object 'ok #f 'message err.message))))))))
(define all-js-usecases
`((random-number . ,usecase-random-number)
(unique-values . ,usecase-unique-values)
(falsey-values . ,usecase-falsey-values)
(currying . ,usecase-currying)
(object-destructuring . ,usecase-object-destructuring)
(timer-interval . ,usecase-timer-interval)
(object-props . ,usecase-object-props)
(string-concat-order . ,usecase-string-concat-order)
(freeze-vs-seal . ,usecase-freeze-vs-seal)
(switch . ,usecase-switch)
(class-constructor . ,usecase-class-constructor)
(sort-objects-by-property . ,usecase-sort-objects-by-property)
(delete-array-elements . ,usecase-delete-array-elements)
(bubble-sort . ,usecase-bubble-sort)
(binary-search . ,usecase-binary-search)
(map-count-occurrences . ,usecase-map-count-occurrences)
(get-html-three-ways . ,usecase-get-html-three-ways)
(anagram . ,usecase-anagram)
(pairs-equal-target . ,usecase-pairs-equal-target)
(fetch-api . ,usecase-fetch-api)))
(define (show-js-usecases)
(for ([entry (in-list all-js-usecases)])
(displayln (format "// ~a" (car entry)))
(displayln (cdr entry))
(newline)))
(define (write-js-usecases-file path)
(call-with-output-file path
#:exists 'replace
(lambda (out)
(displayln "// Generated by demo/js-usecases.rkt" out)
(displayln "// Each use case is wrapped in a function so snippets with return are valid." out)
(for ([entry (in-list all-js-usecases)])
(fprintf out "\n// ~a\n" (car entry))
(fprintf out "function run_~a() {\n~a\n}\n"
(regexp-replace* #rx"[^A-Za-z0-9_$]" (symbol->string (car entry)) "_")
(cdr entry))))))
(module+ main (module+ main
(show-js-usecases)) (display generated-js))
+8 -29
View File
@@ -1,32 +1,11 @@
#lang racket/base #lang racket/base
(require "../main.rkt")
(define examples (require (prefix-in use: "js-usecases.rkt")
(list (prefix-in dom: "dom-exercises.rkt"))
(cons 'expression
(js/expression
(let loop ([i 0] [acc 0])
(if (< i 5)
(loop (+ i 1) (+ acc i))
acc))))
(cons 'program-t1
(js
(set! window.myfunc
(λ (x)
(let* ((el (send document getElementById 'hi))
(y (* x x)))
(send el setAttribute "x" (+ y "")))
(send console log "dit set attribute x on element hi")))))
(cons 'program-t2
(js
(define (f x)
(if (and (> x 10) (< x 15))
(begin (console.log x)
(return x))
(return (* x x))))))
(cons 'regexp
(js/expression
(regexp-match #px"([a-z]+)-([0-9]+)" "abc-123")))))
(for ([e (in-list examples)]) (module+ main
(printf "===== ~a =====\n~a\n\n" (car e) (cdr e))) (displayln ";; js-usecases")
(display use:generated-js)
(newline)
(displayln ";; dom-exercises")
(display dom:generated-js))
+11 -14
View File
@@ -1,16 +1,13 @@
#lang racket/base #lang racket/base
(require "../main.rkt") (require "../main.rkt")
(displayln "--- and ---")
(displayln (js/expression (and (> x 10) (< x 15)))) ;; There is no separate optimizer in js-maker 3. This demo shows the compact
(displayln "--- t2 ---") ;; named-let loop output produced directly by the `js` macro.
(displayln (js (define (f x) (module+ main
(if (and (> x 10) (< x 15)) (display
(begin (console.log x) (js (define (factorial n)
(return x)) (let loop ([i n] [acc 1])
(return (* x x)))))) (if (<= i 1)
(displayln "--- let* ---") (return acc)
(displayln (js (let* ((x 10) (loop (- i 1) (* acc i))))))))
(y (+ x x)))
(return y))))
(displayln "--- let* tdz ---")
(displayln (js/expression (let ([x 4]) (let* ([x x] [y x]) (+ x y)))))
+10 -6
View File
@@ -3,15 +3,19 @@
(define collection "js-maker") (define collection "js-maker")
(define version "0.3") (define version "0.3")
(define license 'MIT) (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 pkg-authors '(hnmdijkema))
(define deps '("base")) (define deps '("base"))
(define build-deps '("scribble-lib" "racket-doc" "rackunit-lib")) (define build-deps '("scribble-lib" "racket-doc" "rackunit-lib"))
(define tags '("javascript" "macro" "racket"))
(define scribblings (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 ;; Keep the test entry point explicit. The supporting regression modules are
;; branch is intentionally not shipped. The maintained package test suite is ;; required by this runner and compile during package setup.
;; this compact smoke test. (define test-include-paths '("testing/jsmaker-regressions.rkt"))
(define test-include-paths '("smoke-test.rkt")) (define test-omit-paths
'("testing/jsmaker-executors.rkt"
"testing/jsmaker-test-framework.rkt"))
+1 -1
View File
@@ -3,7 +3,7 @@
(require racket/string (require racket/string
(for-syntax racket/base)) (for-syntax racket/base))
(provide js js1) (provide js)
;; A deliberately small Racket/Scheme-to-JavaScript string maker. ;; A deliberately small Racket/Scheme-to-JavaScript string maker.
;; ;;
+66 -35
View File
@@ -1,53 +1,84 @@
#lang scribble/manual #lang scribble/manual
@(require (for-label racket/base @(require (for-label racket/base js-maker))
js-maker))
@title{js-maker} @title{js-maker}
@author{Hans Dijkema} @author{Hans Dijkema}
@defmodule[js-maker] @defmodule[js-maker]
@racketmodname[js-maker] provides a deliberately small syntax-driven macro for @emph{js-maker} is a deliberately small syntax-driven JavaScript string maker.
making JavaScript strings from a limited Racket-like surface syntax. This is a It provides one public macro, @racket[js]. The helper machinery used to render
clean js-maker 3 restart based on the compact @filepath{js-transform.rkt} subexpressions is private to the package.
implementation.
@section{Public API}
@defform[(js form ...)]{ @defform[(js form ...)]{
Generates JavaScript statements for each @racket[form] and concatenates them. Produces a JavaScript string for the supplied forms. Each top-level form is
The generated JavaScript is returned as a string. 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[ @racketblock[
(js (js (+ 1 2))
(define (sum-to n) (js (define answer 42))
(let loop ([i 0] [acc 0]) (js (define (square x)
(if (> i n) (return (* x x))))
(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))]
} }
@section{Supported core forms} @section{Supported core forms}
The compact branch supports identifiers, quoted data, primitive literals, The compact js-maker 3 implementation supports:
function calls, infix arithmetic and comparison operators, @racket[if],
@racket[begin], @racket[return], @racket[set!], @racket[lambda],
@racket[define], ordinary @racket[let], @racket[let*], and named
@racket[let].
Ordinary @racket[let] keeps Racket's parallel binding semantics. All right-hand @itemlist[
sides are generated before the bound names are introduced, and the actual @item{@racket[define] for values and functions.}
JavaScript bindings are placed in an inner block so JavaScript temporal dead zone @item{@racket[lambda] and @racket[lambda]-style generated JavaScript functions.}
rules cannot accidentally shadow the initializers. @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 @section{Let semantics}
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 Ordinary @racket[let] evaluates all right-hand sides before introducing the new
reintroducing the large js-maker 2 implementation. 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].
+30 -583
View File
@@ -1,596 +1,43 @@
#lang scribble/manual #lang scribble/manual
@(require scribble/core @(require (for-label racket/base js-maker))
(for-label racket/base
"../main.rkt"))
@(define (code-box title source) @title{js-maker use cases}
(tabular #:style 'boxed
(list (list (bold title))
(list (verbatim source)))))
@(define (code-pair racket-source js-source) @section{Generating a small function}
(list
(code-box "Racket / js-maker" racket-source)
(code-box "Generated JavaScript" js-source)))
@(define side-by-side code-pair) @racketblock[
(js (define (square x)
(return (* x x))))
]
@(define (tested s) This produces a JavaScript function declaration. Racket identifiers are mapped
(nested #:style 'inset (bold "Tested behavior: ") s)) to JavaScript-friendly names by replacing unsupported characters with
underscores.
@title{jsmaker JavaScript Use Cases} @section{Generating a loop}
@author+email["Hans Dijkema" "hans@dijkewijk.nl"]
@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 The named @racket[let] form is useful for simple loops while keeping ordinary
@filepath{demo/js-usecases.rkt}. Each implementation is written as a Racket Racket binding semantics for the initial values and loop updates.
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 examples are shown vertically: first the Racket/js-maker source, then the @section{Generating DOM-style calls}
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.
The tests intentionally use @racket[js/expression] for their calls wherever @racketblock[
possible. Raw JavaScript remains only in small harness preambles, such as fake
@tt{setInterval}, fake DOM objects, and fake @tt{fetch}.
@section{Running the examples}
Generate the JavaScript examples with:
@codeblock{
racket demo/js-usecases.rkt
}
Run the use-case regression tests with:
@codeblock{
racket testing/jsmaker-usecases.rkt
}
@section{Use cases}
@subsection{1. Random number between 1 and 5}
@(side-by-side
#<<RKT
(js (js
(define (randomBetween1And5) (define title (send document getElementById "title"))
(return (+ (send Math floor (* (send Math random) 5)) 1)))) (set! (js-dot title innerHTML) "Hello")
RKT (send title addEventListener "click"
#<<JS (lambda (evt) (return #t))))
function randomBetween1And5() { ]
return (Math.floor((Math.random() * 5)) + 1);
}
JS
)
@(tested "With Math.random fixed at 0.80, the generated function returns 5.") This is string generation only. The generated JavaScript must still be run in a
JavaScript environment that provides the referenced objects, such as
@subsection{2. Unique values with Set} @tt{document}.
@(side-by-side
#<<RKT
(js
(define (uniqueValues xs)
(return (send Array from (new Set xs)))))
RKT
#<<JS
function uniqueValues(xs) {
return Array.from(new Set(xs));
}
JS
)
@(tested "The array [1, 2, 2, 3, 1] becomes [1, 2, 3].")
@subsection{3. Six falsey JavaScript values}
@(side-by-side
#<<RKT
(js
(define (falseyValues)
(return (array #f 0 "" js-null js-undefined js-NaN))))
RKT
#<<JS
function falseyValues() {
return [false, 0, "", null, undefined, NaN];
}
JS
)
@(tested "Mapping JavaScript Boolean over the returned values yields six false values.")
@subsection{4. Currying}
@(side-by-side
#<<RKT
(js
(define (add x)
(return (lambda (y)
(return (+ x y))))))
RKT
#<<JS
function add(x) {
return function(y) {
return (x + y);
};
}
JS
)
@(tested "The generated call add(2)(3) returns 5; the test call itself is produced with js/expression.")
@subsection{5. Object destructuring}
@(side-by-side
#<<RKT
(js
(define (describePerson person)
(let-object ([name 'name]
[age 'age 0])
person
(return (string-append name ":" (number->string age))))))
RKT
#<<JS
function describePerson(person) {
return (() => {
const { name: name, age: age = 0 } = person;
return (name + ":" + String(age));
})();
}
JS
)
@(tested "The object { name: \"Ada\", age: 37 } is rendered as \"Ada:37\".")
@subsection{6. Escaping a timer interval}
@(side-by-side
#<<RKT
(js
(define (startTimer)
(let* ([ticks 0]
[intervalId #f])
(set! intervalId
(setInterval (lambda ()
(set! ticks (+ ticks 1))
(when (= ticks 3)
(clearInterval intervalId)))
10))
(return (object 'id intervalId
'getTicks (lambda () (return ticks)))))))
RKT
#<<JS
function startTimer() {
{
let ticks = 0;
let intervalId = false;
intervalId = setInterval(function() {
ticks = (ticks + 1);
if ((ticks === 3)) clearInterval(intervalId);
return undefined;
}, 10);
return {id: intervalId, getTicks: function() { return ticks; }};
}
}
JS
)
@(tested "A fake timer runs the callback five times, but clearInterval stops it at tick 3.")
@subsection{7. Get, set, and delete object properties}
@(side-by-side
#<<RKT
(js
(define (objectProps)
(let* ([obj (object 'a 1)]
[a1 obj.a]
[a2 (js-ref obj "a")])
(let-object ([a3 'a]) obj
(set! obj.b 2)
(set-prop! obj "c" 3)
(delete-prop! obj "a")
(return (array a1 a2 a3 obj.b (js-ref obj "c")
(send Object hasOwn obj "a")))))))
RKT
#<<JS
function objectProps() {
let obj = {a: 1};
let a1 = obj.a;
let a2 = obj["a"];
return (() => {
const { a: a3 } = obj;
obj.b = 2;
obj["c"] = 3;
delete obj["a"];
return [a1, a2, a3, obj.b, obj["c"], Object.hasOwn(obj, "a")];
})();
}
JS
)
@(tested "The three reads produce 1, the two writes produce 2 and 3, and the deleted property is absent.")
@subsection{8. String concatenation order}
@(side-by-side
#<<RKT
(js
(define (concatOrder)
(return (array (+ 1 2 "3")
(+ "1" 2 3)))))
RKT
#<<JS
function concatOrder() {
return [(1 + 2 + "3"), ("1" + 2 + 3)];
}
JS
)
@(tested "The generated function returns [\"33\", \"123\"].")
@subsection{9. Object.freeze versus Object.seal}
@(side-by-side
#<<RKT
(js
(define (freezeVsSeal)
(let* ([frozen (send Object freeze (object 'a 1))]
[sealed (send Object seal (object 'a 1))])
(set! frozen.a 9)
(set! sealed.a 9)
(delete-prop! sealed "a")
(return (array frozen.a sealed.a
(send Object isFrozen frozen)
(send Object isSealed sealed)
(send Object hasOwn sealed "a"))))))
RKT
#<<JS
function freezeVsSeal() {
let frozen = Object.freeze({a: 1});
let sealed = Object.seal({a: 1});
frozen.a = 9;
sealed.a = 9;
delete sealed["a"];
return [frozen.a, sealed.a, Object.isFrozen(frozen),
Object.isSealed(sealed), Object.hasOwn(sealed, "a")];
}
JS
)
@(tested "The frozen value stays 1; the sealed value changes to 9 but remains present.")
@subsection{10. Switch example}
@(side-by-side
#<<RKT
(js
(define (switchExample n)
(case n
[(1) (return "one")]
[(2 3) (return "two-or-three")]
[else (return "other")]))))
RKT
#<<JS
function switchExample(n) {
switch (n) {
case 1: return "one";
case 2:
case 3: return "two-or-three";
default: return "other";
}
}
JS
)
@(tested "The generated function maps 1, 2, and 9 to the three expected branches.")
@subsection{11. Class constructor with a default value}
@(side-by-side
#<<RKT
(js
(define-class Greeter
(constructor ([name "world"])
(set! this.name name))
(method greet ()
(return (string-append "Hello " this.name))))
(define (classExample)
(let* ([a (new Greeter)]
[b (new Greeter "Ada")])
(return (array (send a greet) (send b greet))))))
RKT
#<<JS
class Greeter {
constructor(name = "world") {
this.name = name;
}
greet() {
return ("Hello " + this.name);
}
}
function classExample() {
let a = new Greeter();
let b = new Greeter("Ada");
return [a.greet(), b.greet()];
}
JS
)
@(tested "The default constructor value and explicit constructor argument both work.")
@subsection{12. Sort objects by a property}
@(side-by-side
#<<RKT
(js
(define (sortByProperty xs prop)
(return (send (send xs slice)
sort
(lambda (a b)
(return (- (js-ref a prop) (js-ref b prop))))))))
RKT
#<<JS
function sortByProperty(xs, prop) {
return xs.slice().sort(function(a, b) {
return (a[prop] - b[prop]);
});
}
JS
)
@(tested "Sorting age objects by age yields [20, 25, 30].")
@subsection{13. Four ways to delete or remove an array element}
@(side-by-side
#<<RKT
(js
(define (deleteArrayWays xs)
(let* ([a1 (send xs slice)]
[a2 (send xs slice)]
[a3 (send xs slice)]
[a4 (send xs slice)])
(send a1 splice 1 1)
(set! a2 (send a2 filter (lambda (x i) (return (not (= i 1))))))
(set! a3 (send (send a3 slice 0 1) concat (send a3 slice 2)))
(delete-prop! a4 1)
(return (array a1 a2 a3
(array (send Object hasOwn a4 "1") (length a4)))))))
RKT
#<<JS
function deleteArrayWays(xs) {
let a1 = xs.slice(), a2 = xs.slice(), a3 = xs.slice(), a4 = xs.slice();
a1.splice(1, 1);
a2 = a2.filter(function(x, i) { return !(i === 1); });
a3 = a3.slice(0, 1).concat(a3.slice(2));
delete a4[1];
return [a1, a2, a3, [Object.hasOwn(a4, "1"), a4.length]];
}
JS
)
@(tested "splice, filter, and slice/concat remove the item; delete leaves a hole and preserves length.")
@subsection{14. Bubble sort}
@(side-by-side
#<<RKT
(js
(define (bubbleSort xs)
(let* ([a (send xs slice)]
[n (length a)])
(while (> n 1)
(let* ([i 1])
(while (< i n)
(when (> (list-ref a (- i 1)) (list-ref a i))
(let* ([tmp (list-ref a (- i 1))])
(vector-set! a (- i 1) (list-ref a i))
(vector-set! a i tmp)))
(set! i (+ i 1))))
(set! n (- n 1)))
(return a))))
RKT
#<<JS
function bubbleSort(xs) {
let a = xs.slice();
let n = a.length;
while ((n > 1)) {
let i = 1;
while ((i < n)) {
if ((a[(i - 1)] > a[i])) {
let tmp = a[(i - 1)];
a[(i - 1)] = a[i];
a[i] = tmp;
}
i = (i + 1);
}
n = (n - 1);
}
return a;
}
JS
)
@(tested "Sorting [5, 1, 4, 2, 8] yields [1, 2, 4, 5, 8].")
@subsection{15. Recursive binary search}
@(side-by-side
#<<RKT
(js
(define (binarySearch xs target low high)
(if (> low high)
(return -1)
(let* ([mid (send Math floor (/ (+ low high) 2))]
[value (list-ref xs mid)])
(cond
[(= value target) (return mid)]
[(< value target) (return (binarySearch xs target (+ mid 1) high))]
[else (return (binarySearch xs target low (- mid 1)))])))))
RKT
#<<JS
function binarySearch(xs, target, low, high) {
if ((low > high)) return -1;
let mid = Math.floor(((low + high) / 2));
let value = xs[mid];
if ((value === target)) return mid;
if ((value < target)) return binarySearch(xs, target, (mid + 1), high);
return binarySearch(xs, target, low, (mid - 1));
}
JS
)
@(tested "Searching 7 returns index 3; searching 4 returns -1.")
@subsection{16. Count occurrences with Map}
@(side-by-side
#<<RKT
(js
(define (countOccurrences xs)
(let* ([counts (new Map)])
(for ([x (in-list xs)])
(if (send counts has x)
(send counts set x (+ (send counts get x) 1))
(send counts set x 1)))
(return (send Array from (send counts entries))))))
RKT
#<<JS
function countOccurrences(xs) {
let counts = new Map();
for (const x of xs) {
if (counts.has(x)) counts.set(x, (counts.get(x) + 1));
else counts.set(x, 1);
}
return Array.from(counts.entries());
}
JS
)
@(tested "The array [\"a\", \"b\", \"a\", \"c\", \"b\", \"a\"] yields [[\"a\",3],[\"b\",2],[\"c\",1]].")
@subsection{17. Get HTML in three ways}
@(side-by-side
#<<RKT
(js
(define (getHtmlThreeWays)
(return (array document.body.innerHTML
(js-dot (send document querySelector "body") innerHTML)
(js-ref (send document getElementById "root") "innerHTML")))))
RKT
#<<JS
function getHtmlThreeWays() {
return [document.body.innerHTML,
document.querySelector("body").innerHTML,
document.getElementById("root")["innerHTML"]];
}
JS
)
@(tested "A fake DOM returns the same HTML string through all three access forms.")
@subsection{18. Anagram / rearrangement check}
@(side-by-side
#<<RKT
(js
(define (sortChars s)
(return (send (send (send s split "") sort) join "")))
(define (canArrange stringA stringB)
(return (string=? (sortChars stringA) (sortChars stringB)))))
RKT
#<<JS
function sortChars(s) {
return s.split("").sort().join("");
}
function canArrange(stringA, stringB) {
return (sortChars(stringA) === sortChars(stringB));
}
JS
)
@(tested "listen/silent returns true; abc/abd returns false.")
@subsection{19. Pairs equal to a target, without repeats}
@(side-by-side
#<<RKT
(js
(define (pairsEqualTarget xs target)
(let* ([seen (new Set)]
[used (new Set)]
[out (array)])
(for ([x (in-list xs)])
(let* ([y (- target x)])
(if (and (send seen has y)
(not (send used has x))
(not (send used has y)))
(begin
(send out push (array y x))
(send used add x)
(send used add y))
(send seen add x))))
(return out))))
RKT
#<<JS
function pairsEqualTarget(xs, target) {
let seen = new Set();
let used = new Set();
let out = [];
for (const x of xs) {
let y = (target - x);
if (seen.has(y) && !(used.has(x)) && !(used.has(y))) {
out.push([y, x]); used.add(x); used.add(y);
} else {
seen.add(x);
}
}
return out;
}
JS
)
@(tested "For [1, 2, 3, 4, 3, 5] and target 6 the returned pairs are [[2,4],[3,3],[1,5]].")
@subsection{20. Fetch API with result and error handling}
@(side-by-side
#<<RKT
(js
(define (loadTitle url)
(return
(send
(send
(send (fetch url)
then
(lambda (response)
(return (send response json))))
then
(lambda (data)
(return (object 'ok #t 'title data.title))))
catch
(lambda (err)
(return (object 'ok #f 'message err.message)))))))
RKT
#<<JS
function loadTitle(url) {
return fetch(url)
.then(function(response) { return response.json(); })
.then(function(data) { return {ok: true, title: data.title}; })
.catch(function(err) { return {ok: false, message: err.message}; });
}
JS
)
@(tested "A fake fetch resolves /ok to {ok:true,title:\"Done\"} and rejects /fail to {ok:false,message:\"network\"}.")
-29
View File
@@ -1,29 +0,0 @@
#lang racket/base
(require rackunit
"main.rkt")
(define ordinary
(js (define (ordinary-let x)
(let ([x 1] [y x])
(return y)))))
(define sequential
(js (define (sequential-let x)
(let* ([x 1] [y x])
(return y)))))
(define looped
(js (define (sum-to n)
(let loop ([i 0] [acc 0])
(if (> i n)
(return acc)
(loop (+ i 1) (+ acc i)))))))
(check-regexp-match #rx"function ordinary_let" ordinary)
(check-regexp-match #rx"const [A-Za-z0-9_]+ = x;" ordinary)
(check-regexp-match #rx"let y = [A-Za-z0-9_]+;" ordinary)
(check-regexp-match #rx"let y = x;" sequential)
(check-regexp-match #rx"while \\(true\\)" looped)
(check-regexp-match #rx"continue;" looped)
(check-equal? (js1 (+ 1 2)) "1 + 2")
+19 -63
View File
@@ -1,69 +1,25 @@
#lang racket/base #lang racket/base
(require json (require rackunit
"jsmaker-test-framework.rkt" "../main.rkt"
"jsmaker-executors.rkt" "jsmaker-test-framework.rkt")
"../demo/dom-exercises.rkt")
(define (dom-preamble paragraph-text) (provide dom-tests)
(format #<<JS
const state = { afterParagraph: [], afterHeading: [] };
const paragraph = {
innerHTML: ~a,
get textContent() { return String(this.innerHTML).replace(/<[^>]*>/g, ''); },
set textContent(v) { this.innerHTML = String(v); },
insertAdjacentHTML: (position, html) => { state.afterParagraph.push([position, html]); }
};
const heading = {
insertAdjacentHTML: (position, html) => { state.afterHeading.push([position, html]); }
};
const document = {
querySelector: (selector) => {
if (selector === 'p') return paragraph;
if (selector === 'h1') return heading;
return false;
}
};
JS
(jsexpr->string paragraph-text)))
(define (side-effect-expr preamble program check-expr) (define dom-snippet
(format "(() => {~a (js
(function() { (define title (send document getElementById "title"))
~a (set! (js-dot title innerHTML) "Hello")
})(); (send title addEventListener "click" (lambda (evt) (return #t)))))
return (~a);
})()"
preamble
program
check-expr))
(define tests (define dom-tests
(list (test-suite
(js-expression-test "DOM-like JavaScript generation"
'dom-ex01-highlight-long-words (test-case "send and js-dot generate method calls and property assignment"
(side-effect-expr (dom-preamble "Short extraordinary words remain.") exercise01 "paragraph.innerHTML") (check-js-contains? dom-snippet "document.getElementById(\"title\")")
(jsexpr->string "Short <span style=\"background: yellow\">extraordinary</span> words remain.")) (check-js-contains? dom-snippet "title.innerHTML = \"Hello\";")
(check-js-contains? dom-snippet "title.addEventListener"))))
(js-expression-test (module+ test
'dom-ex02-add-source-link (require rackunit/text-ui)
(side-effect-expr (dom-preamble "Force ipsum text.") exercise02 "state.afterParagraph") (run-tests dom-tests))
(jsexpr->string '(("afterend" "<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>"))))
(js-expression-test
'dom-ex03-split-sentences
(side-effect-expr (dom-preamble "First sentence. Second sentence. Third.") exercise03 "paragraph.innerHTML")
(jsexpr->string "First sentence.<br>Second sentence.<br>Third.<br>"))
(js-expression-test
'dom-ex04-count-words-after-heading
(side-effect-expr (dom-preamble "These are four words") exercise04 "state.afterHeading")
(jsexpr->string '(("afterend" "<p>4 words</p>"))))
(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)
+2 -162
View File
@@ -1,164 +1,4 @@
#lang racket/base #lang racket/base
(require racket/file (require "jsmaker-test-framework.rkt")
racket/list (provide node-available? run-js/trimmed)
racket/match
racket/port
racket/string)
(provide js-engine?
js-engine-name
js-engine-path
js-engine-kind
js-engine-version
js-run-result?
js-run-result-engine
js-run-result-status
js-run-result-stdout
js-run-result-stderr
js-run-result-success?
known-js-engine-names
find-js-engine
run-js-file)
(struct js-engine (name path kind version-args) #:transparent)
(struct js-run-result (engine status stdout stderr) #:transparent)
(define (path-string/non-empty? v)
(and (string? v) (not (string=? v ""))))
(define (executable-file? p)
(and p (file-exists? p)))
(define (getenv/path name)
(define v (getenv name))
(and (path-string/non-empty? v) (string->path v)))
(define engine-specs
;; name aliases kind version-args extra-paths
'((node ("node" "nodejs") plain ("--version")
("/opt/nvm/versions/node/v22.16.0/bin/node" "/usr/local/bin/node" "/usr/bin/node"))
(deno ("deno") plain ("--version") ())
(bun ("bun") plain ("--version") ())
(qjs ("qjs" "quickjs") plain ("-v") ())
(d8 ("d8") plain ("--version") ())
(jsc ("jsc") plain ("--version") ())
(js ("js") plain ("--version") ())
(chromium ("chromium" "chromium-browser" "google-chrome" "google-chrome-stable") chromium ("--version")
("/usr/bin/chromium" "/usr/bin/chromium-browser" "/usr/bin/google-chrome"))))
(define (known-js-engine-names)
(map car engine-specs))
(define (lookup-engine-spec name)
(define wanted (string-downcase (format "~a" name)))
(for/first ([spec (in-list engine-specs)]
#:when (or (string=? wanted (symbol->string (car spec)))
(member wanted (cadr spec))))
spec))
(define (candidate-paths spec)
(match spec
[(list name aliases kind version-args extra-paths)
(append
(if (eq? name 'node)
(filter values (list (getenv/path "JSMAKER_NODE")))
'())
(filter values (list (getenv/path "JSMAKER_ENGINE_PATH")))
(filter values (map find-executable-path aliases))
(map string->path extra-paths))]))
(define (make-engine-from-spec spec)
(match spec
[(list name aliases kind version-args extra-paths)
(for/first ([p (in-list (candidate-paths spec))]
#:when (executable-file? p))
(js-engine name p kind version-args))]))
(define (find-js-engine [requested (or (getenv "JSMAKER_ENGINE") "auto")])
(define req (string-downcase requested))
(cond
[(or (string=? req "") (string=? req "auto"))
(define browser-fallback? (getenv "JSMAKER_BROWSER_FALLBACK"))
(or (for*/first ([spec (in-list engine-specs)]
#:unless (and (not browser-fallback?) (eq? (car spec) 'chromium))
[engine (in-value (make-engine-from-spec spec))]
#:when engine)
engine)
#f)]
[else
(define spec (lookup-engine-spec req))
(and spec (make-engine-from-spec spec))]))
(define (capture-timeout-seconds)
(define v (getenv "JSMAKER_ENGINE_TIMEOUT_SECONDS"))
(cond
[(and (path-string/non-empty? v) (string->number v)) => values]
[else 15]))
(define (capture path args)
(with-handlers ([exn:fail? (lambda (e) (values 127 "" (exn-message e)))])
(define-values (proc stdout stdin stderr)
(apply subprocess #f #f #f path args))
(define waiter (thread (lambda () (subprocess-wait proc))))
(define finished? (sync/timeout (capture-timeout-seconds) waiter))
(unless finished?
(subprocess-kill proc #t)
(thread-wait waiter))
(values (if finished? (subprocess-status proc) 124)
(port->string stdout)
(string-append (port->string stderr)
(if finished? "" "\nJavaScript engine timed out and was killed.\n")))))
(define (js-engine-version engine)
(define-values (status stdout stderr)
(capture (js-engine-path engine) (js-engine-version-args engine)))
(define s (string-trim (if (string=? stdout "") stderr stdout)))
(and (zero? status) (not (string=? s "")) s))
(define (plain-run-args engine js-path)
(case (js-engine-name engine)
[(deno) (list "run" "--quiet" js-path)]
[else (list js-path)]))
(define (copy-js-as-browser-html js-path)
(define html-path (build-path (find-system-path 'temp-dir)
(format "jsmaker-browser-~a.html" (current-inexact-milliseconds))))
(define js (file->string js-path))
(call-with-output-file html-path
#:exists 'replace
(lambda (out)
(displayln "<!doctype html><meta charset=\"utf-8\"><pre id=\"out\"></pre>" out)
(displayln "<script>" out)
(displayln "const __out = document.getElementById('out');" out)
(displayln "const print = (...xs) => { __out.append(xs.join(' ') + '\\n'); };" out)
(displayln "console.log = (...xs) => print(...xs);" out)
(displayln "console.error = (...xs) => print(...xs);" out)
(displayln "try {" out)
(display js out)
(displayln "\ndocument.documentElement.setAttribute('data-jsmaker-status', 'ok');" out)
(displayln "} catch (e) {" out)
(displayln " print('ERROR\\t' + (e && e.stack ? e.stack : String(e)));" out)
(displayln " document.documentElement.setAttribute('data-jsmaker-status', 'fail');" out)
(displayln "}" out)
(displayln "</script>" out)))
html-path)
(define (chromium-run engine js-path)
(define html-path (copy-js-as-browser-html js-path))
(define url (string-append "file://" (path->string html-path)))
(define args (list "--headless" "--disable-gpu" "--no-sandbox" "--dump-dom" url))
(define-values (status stdout stderr) (capture (js-engine-path engine) args))
(define browser-fail? (regexp-match? #rx"data-jsmaker-status=\"fail\"" stdout))
(js-run-result engine (if browser-fail? 1 status) stdout stderr))
(define (run-js-file engine js-path)
(case (js-engine-kind engine)
[(chromium) (chromium-run engine js-path)]
[else
(define-values (status stdout stderr)
(capture (js-engine-path engine) (plain-run-args engine js-path)))
(js-run-result engine status stdout stderr)]))
(define (js-run-result-success? result)
(zero? (js-run-result-status result)))
+13 -141
View File
@@ -1,146 +1,18 @@
#lang racket/base #lang racket/base
(require "../main.rkt" (require rackunit
"jsmaker-executors.rkt" "../main.rkt"
"jsmaker-test-framework.rkt") "jsmaker-test-framework.rkt")
(define tests (provide object-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]]")))
(define engine (find-js-engine)) (define object-tests
(run-jsmaker-regression 'jsmaker-hash-regression tests "/tmp/jsmaker-hash-regression.js" #:engine engine) (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))
+16 -94
View File
@@ -1,99 +1,21 @@
#lang racket/base #lang racket/base
(require "../main.rkt" (require rackunit
"jsmaker-executors.rkt" "../main.rkt"
"jsmaker-test-framework.rkt") "jsmaker-test-framework.rkt")
(define tests (provide list-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]")))
(define engine (find-js-engine)) (define list-tests
(run-jsmaker-regression 'jsmaker-list-regression tests "/tmp/jsmaker-list-regression.js" #:engine engine) (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))
+41 -108
View File
@@ -1,118 +1,51 @@
#lang racket/base #lang racket/base
(require "../main.rkt" (require rackunit
"jsmaker-executors.rkt" "../main.rkt"
"jsmaker-test-framework.rkt") "jsmaker-test-framework.rkt")
(define dom-preamble (provide program-tests)
(define ordinary-let-program
(string-append (string-append
"const window = {};\n" (js (define (ordinary-let x)
"const logs = [];\n" (let ([x 1] [y x])
"const attrs = {};\n" (return y))))
"const console = { log: (...xs) => logs.push(xs.length === 1 ? xs[0] : xs) };\n" "console.log(ordinary_let(99));\n"))
"const document = {\n"
" getElementById: (id) => id === 'hi'\n"
" ? { setAttribute: (k, v) => { attrs[k] = v; }, getAttribute: (k) => attrs[k] || '' }\n"
" : false\n"
"};"))
(define t1-program (define sequential-let-program
(js (set! window.myfunc (λ (x) (string-append
(let* ((el (send document getElementById 'hi)) (js (define (sequential-let x)
(old (send el getAttribute "x")) (let* ([x 1] [y x])
(y (+ old (* x x)))) (return y))))
(send el setAttribute "x" (+ y ""))) "console.log(sequential_let(99));\n"))
(send console log "dit set attribute x on element hi")))))
(define t2-program (define named-let-program
(js (define (f x) (string-append
(if (and (> x 10) (< x 15)) (js (define (sum-to n)
(begin (console.log x) (let loop ([i 0] [acc 0])
(return x)) (if (> i n)
(return (* x x)))))) (return acc)
(loop (+ i 1) (+ acc i))))))
(define t3-program "console.log(sum_to(10));\n"))
(js (define (f x y z)
(send console log (cons x (cons y (list z))))
(let* ((l (cons x (cons y (list z)))))
(return (send l map (λ (a) (return (+ a 10)))))))))
(define tail-loop-expression
(js/expression (let loop ([i 0] [s 0])
(if (< i 5)
(loop (+ i 1) (+ s i))
s))))
(unless (regexp-match? #rx"while \\(true\\)" tail-loop-expression)
(error 'jsmaker-program-regression
"tail-recursive named let was not lowered to while(true): ~a"
tail-loop-expression))
(unless (regexp-match? #rx"\n " t1-program)
(error 'jsmaker-program-regression
"generated statement code does not appear to contain indentation: ~a"
t1-program))
(unless (regexp-match? #rx"if \\(\\(x > 10\\) && \\(x < 15\\)\\)" t2-program)
(error 'jsmaker-program-regression
"boolean and should compile directly in if test: ~a"
t2-program))
(define program-tests (define program-tests
(list (test-suite
(js-program-test "generated JavaScript program behavior"
'dom-style-set-attribute (test-case "ordinary let uses parallel Racket binding semantics"
t1-program (check-js-contains? ordinary-let-program "const")
"(() => { attrs.x = 'old:'; window.myfunc(4); return [attrs.x, logs]; })()" (check-js-contains? ordinary-let-program "let x")
"[\"old:16\",[\"dit set attribute x on element hi\"]]" (when (node-available?)
#:preamble dom-preamble) (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 (module+ test
'define-if-return-console (require rackunit/text-ui)
t2-program (run-tests program-tests))
"(() => { 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)
+14 -19
View File
@@ -1,24 +1,19 @@
#lang racket/base #lang racket/base
(require "../main.rkt" (require rackunit
"jsmaker-executors.rkt" "../main.rkt"
"jsmaker-test-framework.rkt") "jsmaker-test-framework.rkt")
(define tests (provide regexp-tests)
(list
(js-expression-test 'regexp-literal-test (js/expression (regexp-match? #rx"a+" "baac")) "true")
(js-expression-test 'regexp-match-basic (js/expression (regexp-match #rx"a+" "baac")) "[\"aa\"]")
(js-expression-test 'regexp-match-captures (js/expression (regexp-match #rx"(a)(b)?" "a")) "[\"a\",\"a\",false]")
(js-expression-test 'pregexp-match-digits (js/expression (regexp-match #px"([a-z]+)-([0-9]+)" "abc-123")) "[\"abc-123\",\"abc\",\"123\"]")
(js-expression-test 'regexp-match-star (js/expression (regexp-match* #rx"a+" "baacaa")) "[\"aa\",\"aa\"]")
(js-expression-test 'regexp-split (js/expression (regexp-split #rx"," "a,b,,c")) "[\"a\",\"b\",\"\",\"c\"]")
(js-expression-test 'regexp-replace (js/expression (regexp-replace #rx"a" "banana" "X")) "\"bXnana\"")
(js-expression-test 'regexp-replace-star (js/expression (regexp-replace* #rx"a" "banana" "X")) "\"bXnXnX\"")
(js-expression-test 'regexp-replace-backref (js/expression (regexp-replace #rx"([a-z]+)-([0-9]+)" "abc-123" "\\2/\\1")) "\"123/abc\"")
(js-expression-test 'regexp-replace-full (js/expression (regexp-replace #rx"a+" "baac" "[\\0]")) "\"b[aa]c\"")
(js-expression-test 'regexp-pattern-string (js/expression (regexp-match "a+" "baac")) "[\"aa\"]")
(js-expression-test 'regexp-quote (js/expression (regexp-quote "a+b*c?")) "\"a\\\\+b\\\\*c\\\\?\"")
(js-expression-test 'regexp-match-positions (js/expression (regexp-match-positions #rx"(a)(b)?" "xa")) "[[1,2],[1,2],false]")))
(define engine (find-js-engine)) (define regexp-tests
(run-jsmaker-regression 'jsmaker-regexp-regression tests "/tmp/jsmaker-regexp-regression.js" #:engine engine) (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))
+27 -196
View File
@@ -1,204 +1,35 @@
#lang racket/base #lang racket/base
(require racket/string (require rackunit
json racket/runtime-path
"../main.rkt" "../main.rkt"
"jsmaker-executors.rkt"
"jsmaker-test-framework.rkt") "jsmaker-test-framework.rkt")
(define direct-and (js/expression (and (> x 10) (< x 15)))) (provide regression-tests)
(unless (string=? direct-and "((x > 10) && (x < 15))")
(error 'jsmaker-regression "expected direct && generation, got: ~a" direct-and))
(define direct-or (js/expression (or (< x 10) (> x 20)))) (define-runtime-path main-module "../main.rkt")
(unless (string=? direct-or "((x < 10) || (x > 20))")
(error 'jsmaker-regression "expected direct || generation, got: ~a" direct-or))
(define direct-chain (js/expression (< x y 10))) (define regression-tests
(unless (string=? direct-chain "((x < y) && (y < 10))") (test-suite
(error 'jsmaker-regression "expected direct pairwise comparison, got: ~a" direct-chain)) "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))) (module+ test
(unless (regexp-match? #rx"__cmp" effectful-chain) (require rackunit/text-ui)
(error 'jsmaker-regression "effectful chained comparison should use temporaries, got: ~a" effectful-chain)) (run-tests regression-tests))
(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 #<<JS
(() => {
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)
+22 -3
View File
@@ -1,9 +1,28 @@
#lang racket/base #lang racket/base
(require "jsmaker-regression.rkt" (require rackunit
"jsmaker-regexp-regression.rkt" rackunit/text-ui
"jsmaker-regression.rkt"
"jsmaker-program-regression.rkt" "jsmaker-program-regression.rkt"
"jsmaker-dom-exercises.rkt" "jsmaker-dom-exercises.rkt"
"jsmaker-usecases.rkt"
"jsmaker-list-regression.rkt" "jsmaker-list-regression.rkt"
"jsmaker-regexp-regression.rkt"
"jsmaker-usecases.rkt"
"jsmaker-hash-regression.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)))
+47 -88
View File
@@ -1,98 +1,57 @@
#lang racket/base #lang racket/base
(require racket/format (require rackunit
racket/string racket/string
"jsmaker-executors.rkt") racket/file
racket/system)
(provide js-expression-test (provide check-js-equal?
js-program-test check-js-contains?
write-js-test-file check-js-matches?
run-jsmaker-regression node-available?
notice-line run-js/trimmed)
warning-line)
(define (notice-line who fmt . args) (define-check (check-js-equal? actual expected)
(displayln (string-append "NOTE: " (apply format fmt args)))) (check-equal? actual expected))
(define (warning-line who fmt . args) (define-check (check-js-contains? actual needle)
(displayln (string-append "WARNING: " (apply format fmt args)) (check-true (string-contains? actual needle)
(current-error-port))) (format "expected generated JavaScript to contain ~s, got:\n~a" needle actual)))
(define (js-expression-test name expr expected) (define-check (check-js-matches? actual pattern)
(list name expr expected)) (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 ""]) (define (node-available?)
(list name (and (find-executable-path "node") #t))
(format "(() => {~a~a~areturn (~a); })()"
(if (string=? preamble "") "" (string-append "\n" preamble "\n"))
program
(if (regexp-match? #rx"\n$" program) "" "\n")
check-expr)
expected))
(define (write-js-test-file js-path tests) (define (run-js/trimmed program)
(call-with-output-file js-path (define node (find-executable-path "node"))
#:exists 'replace (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) (lambda (out)
(displayln "const tests = [];" out) (call-with-output-file err-path #:exists 'truncate
(displayln "const __normalize = (v) => v === undefined ? 'undefined' : JSON.stringify(v);" out) (lambda (err)
(displayln "const __print = (...xs) => {" out) (parameterize ([current-output-port out]
(displayln " if (typeof console !== 'undefined' && console.log) console.log(...xs);" out) [current-error-port err])
(displayln " else if (typeof print === 'function') print(xs.join(' '));" out) (system*/exit-code node source-path)))))))
(displayln "};" out) (define stdout (file->string out-path))
(for ([t tests]) (define stderr (file->string err-path))
(define name (symbol->string (car t))) (unless (zero? exit-code)
(define expr (cadr t)) (error 'run-js/trimmed
(define expected (caddr t)) "node failed with exit code ~a\nstdout:\n~a\nstderr:\n~a\nprogram:\n~a"
(fprintf out "tests.push([~s, () => (~a), ~s]);\n" name expr expected)) exit-code stdout stderr program))
(displayln "(async () => {" out) (string-trim stdout))
(displayln " let failed = 0;" out) (lambda ()
(displayln " for (const [name, thunk, expected] of tests) {" out) (for ([path (list source-path out-path err-path)])
(displayln " let value;" out) (with-handlers ([exn:fail? void]) (delete-file path))))))
(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.")))]))
+1 -12
View File
@@ -1,13 +1,2 @@
#lang racket/base #lang racket/base
(require "jsmaker-regressions.rkt")
(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))
+25 -184
View File
@@ -1,191 +1,32 @@
#lang racket/base #lang racket/base
(require json (require rackunit
"../main.rkt" "../main.rkt"
"jsmaker-test-framework.rkt" "jsmaker-test-framework.rkt")
"jsmaker-executors.rkt"
"../demo/js-usecases.rkt")
(define timer-preamble (provide usecase-tests)
#<<JS
const intervals = {};
let nextIntervalId = 1;
function setInterval(cb, ms) {
const id = nextIntervalId++;
intervals[id] = { cb, ms, active: true };
return id;
}
function clearInterval(id) {
if (intervals[id]) intervals[id].active = false;
}
function runInterval(id, times) {
for (let i = 0; i < times; i++) {
if (!intervals[id] || !intervals[id].active) break;
intervals[id].cb();
}
}
JS
)
(define dom-preamble (define counter-program
#<<JS (string-append
const body = { innerHTML: "<p>A</p>" }; (js (define (make-counter start)
const root = { innerHTML: "<p>A</p>" }; (let ([value start])
const document = { (return (lambda ()
body, (begin
querySelector: (selector) => selector === "body" ? body : false, (set! value (+ value 1))
getElementById: (id) => id === "root" ? root : false (return value)))))))
}; "const c = make_counter(5);\n"
JS "console.log(c());\n"
) "console.log(c());\n"))
(define fetch-preamble (define usecase-tests
#<<JS (test-suite
function fetch(url) { "small use cases"
if (url === "/ok") { (test-case "closures and set!"
return Promise.resolve({ (check-js-contains? counter-program "function make_counter(start)")
json: () => Promise.resolve({ title: "Done" }) (check-js-contains? counter-program "value = value + 1;")
}); (when (node-available?)
} (check-equal? (run-js/trimmed counter-program) "6\n7")))))
return Promise.reject(new Error("network"));
}
JS
)
(define tests (module+ test
(list (require rackunit/text-ui)
(js-program-test (run-tests usecase-tests))
'use-random-number-1-to-5
usecase-random-number
(js/expression (randomBetween1And5))
(jsexpr->string 5)
#:preamble "Math.random = () => 0.80;")
(js-program-test
'use-unique-values-set
usecase-unique-values
(js/expression (uniqueValues (array 1 2 2 3 1)))
(jsexpr->string '(1 2 3)))
(js-program-test
'use-six-falsey-values
usecase-falsey-values
(js/expression (send (falseyValues) map (lambda (x) (return (Boolean x)))))
(jsexpr->string '(#f #f #f #f #f #f)))
(js-program-test
'use-currying-simple
usecase-currying
(js/expression ((add 2) 3))
(jsexpr->string 5))
(js-program-test
'use-object-destructuring
usecase-object-destructuring
(js/expression (describePerson (object 'name "Ada" 'age 37)))
(jsexpr->string "Ada:37"))
(js-program-test
'use-timer-clear-interval
usecase-timer-interval
(js/expression
(let* ([t (startTimer)])
(runInterval t.id 5)
(array t.id (send t getTicks) (js-dot (js-ref intervals t.id) active))))
(jsexpr->string '(1 3 #f))
#:preamble timer-preamble)
(js-program-test
'use-object-get-set-delete-prop
usecase-object-props
(js/expression (objectProps))
(jsexpr->string '(1 1 1 2 3 #f)))
(js-program-test
'use-string-concat-order
usecase-string-concat-order
(js/expression (concatOrder))
(jsexpr->string '("33" "123")))
(js-program-test
'use-freeze-vs-seal
usecase-freeze-vs-seal
(js/expression (freezeVsSeal))
(jsexpr->string '(1 9 #t #t #t)))
(js-program-test
'use-switch-example
usecase-switch
(js/expression (array (switchExample 1) (switchExample 2) (switchExample 9)))
(jsexpr->string '("one" "two-or-three" "other")))
(js-program-test
'use-class-constructor-default
usecase-class-constructor
(js/expression (classExample))
(jsexpr->string '("Hello world" "Hello Ada")))
(js-program-test
'use-sort-objects-by-property
usecase-sort-objects-by-property
(js/expression
(send (sortByProperty (array (object 'age 30) (object 'age 20) (object 'age 25)) "age")
map
(lambda (x) (return x.age))))
(jsexpr->string '(20 25 30)))
(js-program-test
'use-delete-array-elements-four-ways
usecase-delete-array-elements
(js/expression (deleteArrayWays (array "a" "b" "c")))
(jsexpr->string '(("a" "c") ("a" "c") ("a" "c") (#f 3))))
(js-program-test
'use-bubble-sort
usecase-bubble-sort
(js/expression (bubbleSort (array 5 1 4 2 8)))
(jsexpr->string '(1 2 4 5 8)))
(js-program-test
'use-binary-search-recursive
usecase-binary-search
(js/expression
(array (binarySearch (array 1 3 5 7 9) 7 0 4)
(binarySearch (array 1 3 5 7 9) 4 0 4)))
(jsexpr->string '(3 -1)))
(js-program-test
'use-map-count-occurrences
usecase-map-count-occurrences
(js/expression (countOccurrences (array "a" "b" "a" "c" "b" "a")))
(jsexpr->string '(("a" 3) ("b" 2) ("c" 1))))
(js-program-test
'use-get-html-three-ways
usecase-get-html-three-ways
(js/expression (getHtmlThreeWays))
(jsexpr->string '("<p>A</p>" "<p>A</p>" "<p>A</p>"))
#:preamble dom-preamble)
(js-program-test
'use-anagram
usecase-anagram
(js/expression (array (canArrange "listen" "silent")
(canArrange "abc" "abd")))
(jsexpr->string '(#t #f)))
(js-program-test
'use-pairs-equal-target
usecase-pairs-equal-target
(js/expression (pairsEqualTarget (array 1 2 3 4 3 5) 6))
(jsexpr->string '((2 4) (3 3) (1 5))))
(js-program-test
'use-fetch-api-results-errors
usecase-fetch-api
(js/expression (send Promise all (array (loadTitle "/ok") (loadTitle "/fail"))))
"[{\"ok\":true,\"title\":\"Done\"},{\"ok\":false,\"message\":\"network\"}]"
#:preamble fetch-preamble)))
(define engine (find-js-engine))
(run-jsmaker-regression 'jsmaker-usecases tests "/tmp/jsmaker-usecases.js" #:engine engine)