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