A mostly AI coded js-maker, supervised by me.
This commit is contained in:
@@ -1,3 +1,153 @@
|
||||
# js-maker
|
||||
|
||||
Converts simple scheme expressions to javascript for use as javascript code.
|
||||
A syntax-driven Racket-to-JavaScript macro experiment.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
jsmaker/
|
||||
main.rkt public macro module
|
||||
info.rkt package metadata and package test entry
|
||||
private/
|
||||
syntax-helpers.rkt compatibility/helper material
|
||||
utils.rkt compatibility/helper material
|
||||
scrbl/
|
||||
jsmaker.scrbl Scribble documentation
|
||||
testing/
|
||||
jsmaker-executors.rkt JavaScript engine discovery/execution
|
||||
jsmaker-test-framework.rkt JS regression framework
|
||||
jsmaker-test-runner.rkt old-name compatibility wrapper
|
||||
jsmaker-regression.rkt core expression tests
|
||||
jsmaker-regexp-regression.rkt regexp tests
|
||||
jsmaker-program-regression.rkt larger program tests
|
||||
jsmaker-regressions.rkt aggregate test entry
|
||||
demo/
|
||||
show-jsmaker-output.rkt
|
||||
show-optimized.rkt
|
||||
```
|
||||
|
||||
## Notes on private helpers
|
||||
|
||||
Static analysis of this package layout shows that `main.rkt`, the test
|
||||
infrastructure, the regression tests, demos and Scribble documentation do not
|
||||
require `private/utils.rkt` or `private/syntax-helpers.rkt`. Those files are
|
||||
retained as compatibility material from the source project and are omitted from
|
||||
compilation and the package test entry point in `info.rkt`.
|
||||
|
||||
The current public module has no dependency on Gregor or the old helper
|
||||
modules. Gregor-style date/time forms are translated syntactically by
|
||||
`main.rkt` into a small JavaScript-side representation.
|
||||
|
||||
## Added language support
|
||||
|
||||
This package includes conservative support for:
|
||||
|
||||
- `(with-handlers ([exn? handler]) body ...)`, translated to JavaScript
|
||||
`try`/`catch`. Only generic `exn?` predicates are accepted.
|
||||
- Gregor-style local names such as `date`, `time`, `moment`, `parse-date`,
|
||||
`parse-time`, `parse-moment`, `date->string`, `time->string`, `->year`,
|
||||
`->month`, `->day`, `->hours`, `->minutes`, `->seconds`, `->js-date`, and
|
||||
`js-date->datetime`. Import prefixes are deliberately not hardcoded; the
|
||||
compiler matches on the local identifier name after any `prefix:` part.
|
||||
|
||||
## Run tests
|
||||
|
||||
From the directory above `jsmaker`:
|
||||
|
||||
```bash
|
||||
raco make jsmaker/main.rkt jsmaker/testing/jsmaker-regressions.rkt \
|
||||
jsmaker/scrbl/jsmaker.scrbl
|
||||
racket jsmaker/testing/jsmaker-regressions.rkt
|
||||
raco test -p jsmaker
|
||||
```
|
||||
|
||||
The test framework looks for JavaScript engines such as `node`, `deno`,
|
||||
`bun`, `qjs`, `d8`, `jsc`, `js`. Chromium is only used when explicitly selected
|
||||
or when `JSMAKER_BROWSER_FALLBACK=1` is set.
|
||||
|
||||
When no JavaScript engine is available, the tests generate the JavaScript test
|
||||
files and print warnings, but do not fail unless `JSMAKER_REQUIRE_ENGINE` or
|
||||
`JSMAKER_REQUIRE_NODE` is set.
|
||||
|
||||
Useful environment variables:
|
||||
|
||||
```bash
|
||||
JSMAKER_ENGINE=auto|node|deno|bun|qjs|d8|jsc|js|chromium
|
||||
JSMAKER_ENGINE_PATH=/path/to/executable
|
||||
JSMAKER_NODE=/path/to/node
|
||||
JSMAKER_REQUIRE_ENGINE=1
|
||||
JSMAKER_ENGINE_TIMEOUT_SECONDS=15
|
||||
JSMAKER_BROWSER_FALLBACK=1
|
||||
```
|
||||
|
||||
## Suggested start prompt for future work
|
||||
|
||||
Use this prompt when asking ChatGPT to make a new Racket module or extend this
|
||||
one:
|
||||
|
||||
```text
|
||||
Werk aan een Racket-module/package in een versievaste buildmap.
|
||||
|
||||
Belangrijk:
|
||||
- Maak altijd een nieuwe submap voor de oplevering, bijvoorbeeld
|
||||
/mnt/data/<project>-build-NNN/<collection-name>.
|
||||
- Werk niet direct in /mnt/data met losse bestanden met dezelfde naam;
|
||||
voorkom versieverwarring door alles in die buildmap te kopiëren/patchen.
|
||||
- Houd de package-structuur stabiel:
|
||||
- main.rkt voor de publieke module;
|
||||
- private/ voor helpers en utils;
|
||||
- testing/ voor test-infrastructuur en regressietests;
|
||||
- demo/ voor demonstratiebestanden;
|
||||
- info.rkt voor package metadata en test entry points.
|
||||
- Pas require-paden aan op die structuur voordat je test.
|
||||
- Test met Racket zelf, bijvoorbeeld:
|
||||
/tmp/racket/bin/raco make <collection>/main.rkt <collection>/testing/<tests>.rkt
|
||||
/tmp/racket/bin/racket <collection>/testing/<tests>.rkt
|
||||
- Als JavaScript nodig is, gebruik een aparte executor/test-framework module.
|
||||
Tests mogen niet falen alleen omdat node/deno/bun/qjs ontbreekt; ze moeten
|
||||
dan skippen met duidelijke warnings, tenzij een REQUIRE-envvar is gezet.
|
||||
- Gebruik geen shell-internet voor dependencies. Als packages nodig zijn, haal
|
||||
ze via de rktsndbx bootstrap/package-index flow op.
|
||||
- Lever na afloop een zip van exact de geteste buildmap op.
|
||||
- Rapporteer kort welke commando's zijn uitgevoerd, wat de testresultaten waren,
|
||||
en welke zip het geteste resultaat bevat.
|
||||
```
|
||||
|
||||
|
||||
## Latest tested fix
|
||||
|
||||
This build includes the `with-handlers` callee-position fix for inline lambda
|
||||
handlers, including rest-argument handlers such as `(lambda args ...)`. It also
|
||||
adds a Racket-like division-by-zero runtime check for `/`, so the generic
|
||||
`exn?` handler subset can catch `(/ 10 0)`.
|
||||
|
||||
## JavaScript use case demos
|
||||
|
||||
The package includes a larger set of JavaScript use case snippets in
|
||||
`demo/js-usecases.rkt`. They are written in the Racket surface syntax accepted
|
||||
by `js` and compiled to JavaScript by the macro. The generated JavaScript is
|
||||
also written to `demo/js-usecases.generated.js`.
|
||||
|
||||
The corresponding regression tests live in `testing/jsmaker-usecases.rkt` and
|
||||
are included by `testing/jsmaker-regressions.rkt`. The test framework now awaits
|
||||
Promise-valued tests, so asynchronous examples such as the Fetch API can be
|
||||
checked with Node as well.
|
||||
|
||||
Covered use cases include random numbers, `Set`, JavaScript falsey values,
|
||||
currying, object destructuring, `setInterval`/`clearInterval`, object property
|
||||
get/set/delete, string concatenation order, `Object.freeze`/`Object.seal`,
|
||||
switch/case, classes with constructor defaults, sorting objects, array deletion
|
||||
techniques, Bubble Sort, recursive Binary Search, `Map` counting, DOM HTML
|
||||
access, anagram checks, pair-sum checks, and Fetch API result/error handling.
|
||||
|
||||
## Use-case documentation
|
||||
|
||||
The file `scrbl/usecases.scrbl` documents the JavaScript use cases from
|
||||
`demo/js-usecases.rkt`. Each use case is shown as Racket/js-maker source next
|
||||
to representative generated JavaScript, followed by the behavior covered by the
|
||||
regression test.
|
||||
|
||||
The use-case tests in `testing/jsmaker-usecases.rkt` intentionally use
|
||||
`js/expression` for the test calls wherever possible. Raw JavaScript is kept
|
||||
only for small test-harness preambles such as fake timers, fake DOM objects, and
|
||||
fake fetch.
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// exercise01
|
||||
{
|
||||
let p = document.querySelector("p");
|
||||
p.innerHTML = ((__rkt_body) => {
|
||||
const __rkt_to_regexp = (__pat, __global) => {
|
||||
const __flags = (__pat instanceof RegExp)
|
||||
? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('')
|
||||
: (__global ? 'g' : '');
|
||||
return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags);
|
||||
};
|
||||
const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x));
|
||||
const __rkt_replacement = (__s) => {
|
||||
if (typeof __s !== 'string') return __s;
|
||||
let __out = '';
|
||||
for (let __i = 0; __i < __s.length; __i++) {
|
||||
const __ch = __s[__i];
|
||||
if (__ch === '$') { __out += '$$'; continue; }
|
||||
if (__ch === '\\' && __i + 1 < __s.length) {
|
||||
const __n = __s[++__i];
|
||||
if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n));
|
||||
else __out += __n;
|
||||
} else __out += __ch;
|
||||
}
|
||||
return __out;
|
||||
};
|
||||
return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement);
|
||||
})((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.innerHTML).replace(__rkt_to_regexp(new RegExp("\\b\\w{9,}\\b"), true), __rkt_replacement(function(word) {
|
||||
return ("<span style=\"background: yellow\">" + word + "</span>");
|
||||
})));
|
||||
}
|
||||
|
||||
// exercise02
|
||||
{
|
||||
let p = document.querySelector("p");
|
||||
p.insertAdjacentHTML("afterend", "<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>");
|
||||
}
|
||||
|
||||
// exercise03
|
||||
{
|
||||
let p = document.querySelector("p");
|
||||
p.innerHTML = ((__rkt_body) => {
|
||||
const __rkt_to_regexp = (__pat, __global) => {
|
||||
const __flags = (__pat instanceof RegExp)
|
||||
? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('')
|
||||
: (__global ? 'g' : '');
|
||||
return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags);
|
||||
};
|
||||
const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x));
|
||||
const __rkt_replacement = (__s) => {
|
||||
if (typeof __s !== 'string') return __s;
|
||||
let __out = '';
|
||||
for (let __i = 0; __i < __s.length; __i++) {
|
||||
const __ch = __s[__i];
|
||||
if (__ch === '$') { __out += '$$'; continue; }
|
||||
if (__ch === '\\' && __i + 1 < __s.length) {
|
||||
const __n = __s[++__i];
|
||||
if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n));
|
||||
else __out += __n;
|
||||
} else __out += __ch;
|
||||
}
|
||||
return __out;
|
||||
};
|
||||
return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement);
|
||||
})((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.textContent).replace(__rkt_to_regexp(new RegExp("\\.\\s*"), true), __rkt_replacement(".<br>")));
|
||||
}
|
||||
|
||||
// exercise04
|
||||
{
|
||||
let heading = document.querySelector("h1");
|
||||
let p = document.querySelector("p");
|
||||
let words = ((__rkt_body) => {
|
||||
const __rkt_to_regexp = (__pat, __global) => {
|
||||
const __flags = (__pat instanceof RegExp)
|
||||
? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('')
|
||||
: (__global ? 'g' : '');
|
||||
return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags);
|
||||
};
|
||||
const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x));
|
||||
const __rkt_replacement = (__s) => {
|
||||
if (typeof __s !== 'string') return __s;
|
||||
let __out = '';
|
||||
for (let __i = 0; __i < __s.length; __i++) {
|
||||
const __ch = __s[__i];
|
||||
if (__ch === '$') { __out += '$$'; continue; }
|
||||
if (__ch === '\\' && __i + 1 < __s.length) {
|
||||
const __n = __s[++__i];
|
||||
if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n));
|
||||
else __out += __n;
|
||||
} else __out += __ch;
|
||||
}
|
||||
return __out;
|
||||
};
|
||||
return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement);
|
||||
})((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.textContent).split(__rkt_to_regexp(new RegExp(" "), true)));
|
||||
let count = (words).length;
|
||||
heading.insertAdjacentHTML("afterend", ("<p>" + String(count) + " words</p>"));
|
||||
}
|
||||
|
||||
// exercise05
|
||||
{
|
||||
let p = document.querySelector("p");
|
||||
let step = ((__rkt_body) => {
|
||||
const __rkt_to_regexp = (__pat, __global) => {
|
||||
const __flags = (__pat instanceof RegExp)
|
||||
? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('')
|
||||
: (__global ? 'g' : '');
|
||||
return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags);
|
||||
};
|
||||
const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x));
|
||||
const __rkt_replacement = (__s) => {
|
||||
if (typeof __s !== 'string') return __s;
|
||||
let __out = '';
|
||||
for (let __i = 0; __i < __s.length; __i++) {
|
||||
const __ch = __s[__i];
|
||||
if (__ch === '$') { __out += '$$'; continue; }
|
||||
if (__ch === '\\' && __i + 1 < __s.length) {
|
||||
const __n = __s[++__i];
|
||||
if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n));
|
||||
else __out += __n;
|
||||
} else __out += __ch;
|
||||
}
|
||||
return __out;
|
||||
};
|
||||
return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement);
|
||||
})((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(p.innerHTML).replace(__rkt_to_regexp(new RegExp("\\?"), true), __rkt_replacement("🤔")));
|
||||
let out = ((__rkt_body) => {
|
||||
const __rkt_to_regexp = (__pat, __global) => {
|
||||
const __flags = (__pat instanceof RegExp)
|
||||
? Array.from(new Set((__pat.flags.replace(/g/g, '') + (__global ? 'g' : '')).split(''))).join('')
|
||||
: (__global ? 'g' : '');
|
||||
return (__pat instanceof RegExp) ? new RegExp(__pat.source, __flags) : new RegExp(String(__pat), __flags);
|
||||
};
|
||||
const __rkt_match_array = (__m) => (__m === null ? false : Array.from(__m, (__x) => __x === undefined ? false : __x));
|
||||
const __rkt_replacement = (__s) => {
|
||||
if (typeof __s !== 'string') return __s;
|
||||
let __out = '';
|
||||
for (let __i = 0; __i < __s.length; __i++) {
|
||||
const __ch = __s[__i];
|
||||
if (__ch === '$') { __out += '$$'; continue; }
|
||||
if (__ch === '\\' && __i + 1 < __s.length) {
|
||||
const __n = __s[++__i];
|
||||
if (__n >= '0' && __n <= '9') __out += (__n === '0' ? '$&' : ('$' + __n));
|
||||
else __out += __n;
|
||||
} else __out += __ch;
|
||||
}
|
||||
return __out;
|
||||
};
|
||||
return __rkt_body(__rkt_to_regexp, __rkt_match_array, __rkt_replacement);
|
||||
})((__rkt_to_regexp, __rkt_match_array, __rkt_replacement) => String(step).replace(__rkt_to_regexp(new RegExp("!"), true), __rkt_replacement("😲")));
|
||||
p.innerHTML = out;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#lang racket/base
|
||||
|
||||
(require "../main.rkt")
|
||||
|
||||
(provide exercise01
|
||||
exercise02
|
||||
exercise03
|
||||
exercise04
|
||||
exercise05
|
||||
all-dom-exercises
|
||||
show-dom-exercises)
|
||||
|
||||
;; JavaScript DOM Exercises 01 Tutorial: https://youtu.be/EHF7xBUAmrQ
|
||||
|
||||
;; Exercise 01
|
||||
;; Highlight all words over 8 characters long in the paragraph text.
|
||||
(define exercise01
|
||||
(js
|
||||
(let* ([p (send document querySelector "p")])
|
||||
(set! p.innerHTML
|
||||
(regexp-replace* #px"\\b\\w{9,}\\b"
|
||||
p.innerHTML
|
||||
(λ (word)
|
||||
(string-append "<span style=\"background: yellow\">"
|
||||
word
|
||||
"</span>")))))))
|
||||
|
||||
;; Exercise 02
|
||||
;; Add a link back to the source of the text after the paragraph tag.
|
||||
(define exercise02
|
||||
(js
|
||||
(let* ([p (send document querySelector "p")])
|
||||
(send p insertAdjacentHTML
|
||||
"afterend"
|
||||
"<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>"))))
|
||||
|
||||
;; Exercise 03
|
||||
;; Split each new sentence on to a separate line in the paragraph text.
|
||||
;; A sentence is assumed to be terminated with a period.
|
||||
(define exercise03
|
||||
(js
|
||||
(let* ([p (send document querySelector "p")])
|
||||
(set! p.innerHTML
|
||||
(regexp-replace* #rx"\\.\\s*" p.textContent ".<br>")))))
|
||||
|
||||
;; Exercise 04
|
||||
;; Count the number of words in the paragraph tag and display the count
|
||||
;; after the heading. Words are separated by one singular whitespace.
|
||||
(define exercise04
|
||||
(js
|
||||
(let* ([heading (send document querySelector "h1")]
|
||||
[p (send document querySelector "p")]
|
||||
[words (regexp-split #rx" " p.textContent)]
|
||||
[count (length words)])
|
||||
(send heading insertAdjacentHTML
|
||||
"afterend"
|
||||
(string-append "<p>" (number->string count) " words</p>")))))
|
||||
|
||||
;; Exercise 05
|
||||
;; Replace question marks with thinking faces and exclamation marks with
|
||||
;; astonished faces.
|
||||
(define exercise05
|
||||
(js
|
||||
(let* ([p (send document querySelector "p")]
|
||||
[step (regexp-replace* #rx"\\?" p.innerHTML "🤔")]
|
||||
[out (regexp-replace* #rx"!" step "😲")])
|
||||
(set! p.innerHTML out))))
|
||||
|
||||
(define all-dom-exercises
|
||||
`((exercise01 . ,exercise01)
|
||||
(exercise02 . ,exercise02)
|
||||
(exercise03 . ,exercise03)
|
||||
(exercise04 . ,exercise04)
|
||||
(exercise05 . ,exercise05)))
|
||||
|
||||
(define (show-dom-exercises)
|
||||
(for ([entry (in-list all-dom-exercises)])
|
||||
(displayln (format "// ~a" (car entry)))
|
||||
(displayln (cdr entry))
|
||||
(newline)))
|
||||
|
||||
(module+ main
|
||||
(show-dom-exercises))
|
||||
@@ -0,0 +1,312 @@
|
||||
// Generated by demo/js-usecases.rkt
|
||||
// Each use case is wrapped in a function so snippets with return are valid.
|
||||
|
||||
// random-number
|
||||
function run_random_number() {
|
||||
function randomBetween1And5() {
|
||||
return (Math.floor((Math.random() * 5)) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// unique-values
|
||||
function run_unique_values() {
|
||||
function uniqueValues(xs) {
|
||||
return Array.from(new Set(xs));
|
||||
}
|
||||
}
|
||||
|
||||
// falsey-values
|
||||
function run_falsey_values() {
|
||||
function falseyValues() {
|
||||
return [false, 0, "", null, undefined, NaN];
|
||||
}
|
||||
}
|
||||
|
||||
// currying
|
||||
function run_currying() {
|
||||
function add(x) {
|
||||
return function(y) {
|
||||
return (x + y);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// object-destructuring
|
||||
function run_object_destructuring() {
|
||||
function describePerson(person) {
|
||||
return (() => {
|
||||
const { name: name, age: age = 0 } = person;
|
||||
return (name + ":" + String(age));
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
// timer-interval
|
||||
function run_timer_interval() {
|
||||
function startTimer() {
|
||||
{
|
||||
let ticks = 0;
|
||||
let intervalId = false;
|
||||
intervalId = setInterval(function() {
|
||||
ticks = (ticks + 1);
|
||||
if ((ticks === 3)) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
return undefined;
|
||||
}, 10);
|
||||
return {id: intervalId, getTicks: function() {
|
||||
return ticks;
|
||||
}};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// object-props
|
||||
function run_object_props() {
|
||||
function objectProps() {
|
||||
{
|
||||
let obj = {a: 1};
|
||||
let a1 = obj.a;
|
||||
let a2 = obj["a"];
|
||||
return (() => {
|
||||
const { a: a3 } = obj;
|
||||
obj.b = 2;
|
||||
obj["c"] = 3;
|
||||
delete obj["a"];
|
||||
return [a1, a2, a3, obj.b, obj["c"], Object.hasOwn(obj, "a")];
|
||||
})();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// string-concat-order
|
||||
function run_string_concat_order() {
|
||||
function concatOrder() {
|
||||
return [(1 + 2 + "3"), ("1" + 2 + 3)];
|
||||
}
|
||||
}
|
||||
|
||||
// freeze-vs-seal
|
||||
function run_freeze_vs_seal() {
|
||||
function freezeVsSeal() {
|
||||
{
|
||||
let frozen = Object.freeze({a: 1});
|
||||
let sealed = Object.seal({a: 1});
|
||||
frozen.a = 9;
|
||||
sealed.a = 9;
|
||||
delete sealed["a"];
|
||||
return [frozen.a, sealed.a, Object.isFrozen(frozen), Object.isSealed(sealed), Object.hasOwn(sealed, "a")];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// switch
|
||||
function run_switch() {
|
||||
function switchExample(n) {
|
||||
return (() => {
|
||||
{
|
||||
const __case_value = n;
|
||||
switch (__case_value) {
|
||||
case 1:
|
||||
return "one";
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
return "two-or-three";
|
||||
break;
|
||||
default:
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
// class-constructor
|
||||
function run_class_constructor() {
|
||||
class Greeter {
|
||||
constructor(name = "world") {
|
||||
this.name = name;
|
||||
}
|
||||
greet() {
|
||||
return ("Hello " + this.name);
|
||||
}
|
||||
}
|
||||
function classExample() {
|
||||
{
|
||||
let a = new Greeter();
|
||||
let b = new Greeter("Ada");
|
||||
return [a.greet(), b.greet()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort-objects-by-property
|
||||
function run_sort_objects_by_property() {
|
||||
function sortByProperty(xs, prop) {
|
||||
return xs.slice().sort(function(a, b) {
|
||||
return (a[prop] - b[prop]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// delete-array-elements
|
||||
function run_delete_array_elements() {
|
||||
function deleteArrayWays(xs) {
|
||||
{
|
||||
let a1 = xs.slice();
|
||||
let a2 = xs.slice();
|
||||
let a3 = xs.slice();
|
||||
let a4 = xs.slice();
|
||||
a1.splice(1, 1);
|
||||
a2 = a2.filter(function(x, i) {
|
||||
return ((i === 1) === false);
|
||||
});
|
||||
a3 = a3.slice(0, 1).concat(a3.slice(2));
|
||||
delete a4[1];
|
||||
return [a1, a2, a3, [Object.hasOwn(a4, "1"), (a4).length]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bubble-sort
|
||||
function run_bubble_sort() {
|
||||
function bubbleSort(xs) {
|
||||
{
|
||||
let a = xs.slice();
|
||||
let n = (a).length;
|
||||
while (n > 1) {
|
||||
{
|
||||
let i = 1;
|
||||
while (i < n) {
|
||||
if ((() => {
|
||||
const __cmp_1 = (a)[(i - 1)];
|
||||
const __cmp_2 = (a)[i];
|
||||
return (__cmp_1 > __cmp_2);
|
||||
})()) {
|
||||
{
|
||||
let tmp = (a)[(i - 1)];
|
||||
a[(i - 1)] = (a)[i];
|
||||
a[i] = tmp;
|
||||
}
|
||||
}
|
||||
i = (i + 1);
|
||||
}
|
||||
}
|
||||
n = (n - 1);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// binary-search
|
||||
function run_binary_search() {
|
||||
function binarySearch(xs, target, low, high) {
|
||||
if (low > high) {
|
||||
return -1;
|
||||
} else {
|
||||
{
|
||||
let mid = Math.floor((() => {
|
||||
const __div_3 = (low + high);
|
||||
const __div_4 = 2;
|
||||
if (__div_4 === 0) throw new Error("division by zero");
|
||||
return (__div_3 / __div_4);
|
||||
})());
|
||||
let value = (xs)[mid];
|
||||
const __cond_value_5 = (value === target);
|
||||
if (__cond_value_5 !== false) {
|
||||
return mid;
|
||||
} else {
|
||||
const __cond_value_6 = (value < target);
|
||||
if (__cond_value_6 !== false) {
|
||||
return binarySearch(xs, target, (mid + 1), high);
|
||||
} else {
|
||||
return binarySearch(xs, target, low, (mid - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// map-count-occurrences
|
||||
function run_map_count_occurrences() {
|
||||
function countOccurrences(xs) {
|
||||
{
|
||||
let counts = new Map();
|
||||
for (const x of xs) {
|
||||
if ((counts.has(x) !== false)) {
|
||||
counts.set(x, (counts.get(x) + 1));
|
||||
} else {
|
||||
counts.set(x, 1);
|
||||
}
|
||||
}
|
||||
return Array.from(counts.entries());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get-html-three-ways
|
||||
function run_get_html_three_ways() {
|
||||
function getHtmlThreeWays() {
|
||||
return [document.body.innerHTML, document.querySelector("body").innerHTML, document.getElementById("root")["innerHTML"]];
|
||||
}
|
||||
}
|
||||
|
||||
// anagram
|
||||
function run_anagram() {
|
||||
function sortChars(s) {
|
||||
return s.split("").sort().join("");
|
||||
}
|
||||
function canArrange(stringA, stringB) {
|
||||
return (() => {
|
||||
const __cmp_8 = sortChars(stringA);
|
||||
const __cmp_9 = sortChars(stringB);
|
||||
return (__cmp_8 === __cmp_9);
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
// pairs-equal-target
|
||||
function run_pairs_equal_target() {
|
||||
function pairsEqualTarget(xs, target) {
|
||||
{
|
||||
let seen = new Set();
|
||||
let used = new Set();
|
||||
let out = [];
|
||||
for (const x of xs) {
|
||||
{
|
||||
let y = (target - x);
|
||||
if (((() => {
|
||||
let __and_value_11 = seen.has(y);
|
||||
if (__and_value_11 === false) return false;
|
||||
__and_value_11 = (used.has(x) === false);
|
||||
if (__and_value_11 === false) return false;
|
||||
return (used.has(y) === false);
|
||||
})() !== false)) {
|
||||
out.push([y, x]);
|
||||
used.add(x);
|
||||
used.add(y);
|
||||
} else {
|
||||
seen.add(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fetch-api
|
||||
function run_fetch_api() {
|
||||
function loadTitle(url) {
|
||||
return fetch(url).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(data) {
|
||||
return {ok: true, title: data.title};
|
||||
}).catch(function(err) {
|
||||
return {ok: false, message: err.message};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
#lang racket/base
|
||||
|
||||
(require racket/list
|
||||
"../main.rkt")
|
||||
|
||||
(provide usecase-random-number
|
||||
usecase-unique-values
|
||||
usecase-falsey-values
|
||||
usecase-currying
|
||||
usecase-object-destructuring
|
||||
usecase-timer-interval
|
||||
usecase-object-props
|
||||
usecase-string-concat-order
|
||||
usecase-freeze-vs-seal
|
||||
usecase-switch
|
||||
usecase-class-constructor
|
||||
usecase-sort-objects-by-property
|
||||
usecase-delete-array-elements
|
||||
usecase-bubble-sort
|
||||
usecase-binary-search
|
||||
usecase-map-count-occurrences
|
||||
usecase-get-html-three-ways
|
||||
usecase-anagram
|
||||
usecase-pairs-equal-target
|
||||
usecase-fetch-api
|
||||
all-js-usecases
|
||||
show-js-usecases
|
||||
write-js-usecases-file)
|
||||
|
||||
;; Use case 01: generate a random integer between 1 and 5.
|
||||
(define usecase-random-number
|
||||
(js
|
||||
(define (randomBetween1And5)
|
||||
(return (+ (send Math floor (* (send Math random) 5)) 1)))))
|
||||
|
||||
;; Use case 02: get unique values from an array with duplicates using Set.
|
||||
(define usecase-unique-values
|
||||
(js
|
||||
(define (uniqueValues xs)
|
||||
(return (send Array from (new Set xs))))))
|
||||
|
||||
;; Use case 03: the six JavaScript falsey values.
|
||||
(define usecase-falsey-values
|
||||
(js
|
||||
(define (falseyValues)
|
||||
(return (array #f 0 "" js-null js-undefined js-NaN)))))
|
||||
|
||||
;; Use case 04: currying, simple example.
|
||||
(define usecase-currying
|
||||
(js
|
||||
(define (add x)
|
||||
(return (lambda (y)
|
||||
(return (+ x y)))))))
|
||||
|
||||
;; Use case 05: object destructuring.
|
||||
(define usecase-object-destructuring
|
||||
(js
|
||||
(define (describePerson person)
|
||||
(let-object ([name 'name]
|
||||
[age 'age 0])
|
||||
person
|
||||
(return (string-append name ":" (number->string age)))))))
|
||||
|
||||
;; Use case 06: get out of a timer interval with setInterval/clearInterval.
|
||||
(define usecase-timer-interval
|
||||
(js
|
||||
(define (startTimer)
|
||||
(let* ([ticks 0]
|
||||
[intervalId #f])
|
||||
(set! intervalId
|
||||
(setInterval (lambda ()
|
||||
(set! ticks (+ ticks 1))
|
||||
(when (= ticks 3)
|
||||
(clearInterval intervalId)))
|
||||
10))
|
||||
(return (object 'id intervalId
|
||||
'getTicks (lambda () (return ticks))))))))
|
||||
|
||||
;; Use case 07: get/set/delete object properties. The value of a is read via
|
||||
;; dot access, bracket access, and destructuring.
|
||||
(define usecase-object-props
|
||||
(js
|
||||
(define (objectProps)
|
||||
(let* ([obj (object 'a 1)]
|
||||
[a1 obj.a]
|
||||
[a2 (js-ref obj "a")])
|
||||
(let-object ([a3 'a]) obj
|
||||
(set! obj.b 2)
|
||||
(set-prop! obj "c" 3)
|
||||
(delete-prop! obj "a")
|
||||
(return (array a1 a2 a3 obj.b (js-ref obj "c")
|
||||
(send Object hasOwn obj "a"))))))))
|
||||
|
||||
;; Use case 08: string concatenation; order matters with JavaScript +.
|
||||
(define usecase-string-concat-order
|
||||
(js
|
||||
(define (concatOrder)
|
||||
(return (array (+ 1 2 "3")
|
||||
(+ "1" 2 3))))))
|
||||
|
||||
;; Use case 09: Object.freeze() vs Object.seal().
|
||||
(define usecase-freeze-vs-seal
|
||||
(js
|
||||
(define (freezeVsSeal)
|
||||
(let* ([frozen (send Object freeze (object 'a 1))]
|
||||
[sealed (send Object seal (object 'a 1))])
|
||||
(set! frozen.a 9)
|
||||
(set! sealed.a 9)
|
||||
(delete-prop! sealed "a")
|
||||
(return (array frozen.a
|
||||
sealed.a
|
||||
(send Object isFrozen frozen)
|
||||
(send Object isSealed sealed)
|
||||
(send Object hasOwn sealed "a")))))))
|
||||
|
||||
;; Use case 10: switch example. The Racket surface form is case.
|
||||
(define usecase-switch
|
||||
(js
|
||||
(define (switchExample n)
|
||||
(case n
|
||||
[(1) (return "one")]
|
||||
[(2 3) (return "two-or-three")]
|
||||
[else (return "other")]))))
|
||||
|
||||
;; Use case 11: class constructor with a default value.
|
||||
(define usecase-class-constructor
|
||||
(js
|
||||
(define-class Greeter
|
||||
(constructor ([name "world"])
|
||||
(set! this.name name))
|
||||
(method greet ()
|
||||
(return (string-append "Hello " this.name))))
|
||||
|
||||
(define (classExample)
|
||||
(let* ([a (new Greeter)]
|
||||
[b (new Greeter "Ada")])
|
||||
(return (array (send a greet) (send b greet)))))))
|
||||
|
||||
;; Use case 12: sort an array of objects by a given property.
|
||||
(define usecase-sort-objects-by-property
|
||||
(js
|
||||
(define (sortByProperty xs prop)
|
||||
(return (send (send xs slice)
|
||||
sort
|
||||
(lambda (a b)
|
||||
(return (- (js-ref a prop) (js-ref b prop)))))))))
|
||||
|
||||
;; Use case 13: four ways to delete/remove an element from an array.
|
||||
(define usecase-delete-array-elements
|
||||
(js
|
||||
(define (deleteArrayWays xs)
|
||||
(let* ([a1 (send xs slice)]
|
||||
[a2 (send xs slice)]
|
||||
[a3 (send xs slice)]
|
||||
[a4 (send xs slice)])
|
||||
;; 1. Mutating removal with splice.
|
||||
(send a1 splice 1 1)
|
||||
;; 2. Functional removal with filter.
|
||||
(set! a2 (send a2 filter (lambda (x i) (return (not (= i 1))))))
|
||||
;; 3. Rebuild with slice + concat.
|
||||
(set! a3 (send (send a3 slice 0 1) concat (send a3 slice 2)))
|
||||
;; 4. delete leaves a hole and preserves length.
|
||||
(delete-prop! a4 1)
|
||||
(return (array a1 a2 a3 (array (send Object hasOwn a4 "1") (length a4))))))))
|
||||
|
||||
;; Use case 14: Bubble Sort.
|
||||
(define usecase-bubble-sort
|
||||
(js
|
||||
(define (bubbleSort xs)
|
||||
(let* ([a (send xs slice)]
|
||||
[n (length a)])
|
||||
(while (> n 1)
|
||||
(let* ([i 1])
|
||||
(while (< i n)
|
||||
(when (> (list-ref a (- i 1)) (list-ref a i))
|
||||
(let* ([tmp (list-ref a (- i 1))])
|
||||
(vector-set! a (- i 1) (list-ref a i))
|
||||
(vector-set! a i tmp)))
|
||||
(set! i (+ i 1))))
|
||||
(set! n (- n 1)))
|
||||
(return a)))))
|
||||
|
||||
;; Use case 15: Binary Search using recursion.
|
||||
(define usecase-binary-search
|
||||
(js
|
||||
(define (binarySearch xs target low high)
|
||||
(if (> low high)
|
||||
(return -1)
|
||||
(let* ([mid (send Math floor (/ (+ low high) 2))]
|
||||
[value (list-ref xs mid)])
|
||||
(cond
|
||||
[(= value target) (return mid)]
|
||||
[(< value target) (return (binarySearch xs target (+ mid 1) high))]
|
||||
[else (return (binarySearch xs target low (- mid 1)))]))))))
|
||||
|
||||
;; Use case 16: use Map to count how often each element occurs in an array.
|
||||
(define usecase-map-count-occurrences
|
||||
(js
|
||||
(define (countOccurrences xs)
|
||||
(let* ([counts (new Map)])
|
||||
(for ([x (in-list xs)])
|
||||
(if (send counts has x)
|
||||
(send counts set x (+ (send counts get x) 1))
|
||||
(send counts set x 1)))
|
||||
(return (send Array from (send counts entries)))))))
|
||||
|
||||
;; Use case 17: get HTML in three different ways via the DOM.
|
||||
(define usecase-get-html-three-ways
|
||||
(js
|
||||
(define (getHtmlThreeWays)
|
||||
(return (array document.body.innerHTML
|
||||
(js-dot (send document querySelector "body") innerHTML)
|
||||
(js-ref (send document getElementById "root") "innerHTML"))))))
|
||||
|
||||
;; Use case 18: determine if stringA can be arranged into stringB.
|
||||
(define usecase-anagram
|
||||
(js
|
||||
(define (sortChars s)
|
||||
(return (send (send (send s split "") sort) join "")))
|
||||
|
||||
(define (canArrange stringA stringB)
|
||||
(return (string=? (sortChars stringA) (sortChars stringB))))))
|
||||
|
||||
;; Use case 19: determine what pairs in an array equal a given value, with no
|
||||
;; repeated numbers in the result pairs.
|
||||
(define usecase-pairs-equal-target
|
||||
(js
|
||||
(define (pairsEqualTarget xs target)
|
||||
(let* ([seen (new Set)]
|
||||
[used (new Set)]
|
||||
[out (array)])
|
||||
(for ([x (in-list xs)])
|
||||
(let* ([y (- target x)])
|
||||
(if (and (send seen has y)
|
||||
(not (send used has x))
|
||||
(not (send used has y)))
|
||||
(begin
|
||||
(send out push (array y x))
|
||||
(send used add x)
|
||||
(send used add y))
|
||||
(send seen add x))))
|
||||
(return out)))))
|
||||
|
||||
;; Use case 20: fetch API, handling results and errors. The function returns a
|
||||
;; Promise, which the test framework awaits.
|
||||
(define usecase-fetch-api
|
||||
(js
|
||||
(define (loadTitle url)
|
||||
(return
|
||||
(send
|
||||
(send
|
||||
(send (fetch url)
|
||||
then
|
||||
(lambda (response)
|
||||
(return (send response json))))
|
||||
then
|
||||
(lambda (data)
|
||||
(return (object 'ok #t 'title data.title))))
|
||||
catch
|
||||
(lambda (err)
|
||||
(return (object 'ok #f 'message err.message))))))))
|
||||
|
||||
(define all-js-usecases
|
||||
`((random-number . ,usecase-random-number)
|
||||
(unique-values . ,usecase-unique-values)
|
||||
(falsey-values . ,usecase-falsey-values)
|
||||
(currying . ,usecase-currying)
|
||||
(object-destructuring . ,usecase-object-destructuring)
|
||||
(timer-interval . ,usecase-timer-interval)
|
||||
(object-props . ,usecase-object-props)
|
||||
(string-concat-order . ,usecase-string-concat-order)
|
||||
(freeze-vs-seal . ,usecase-freeze-vs-seal)
|
||||
(switch . ,usecase-switch)
|
||||
(class-constructor . ,usecase-class-constructor)
|
||||
(sort-objects-by-property . ,usecase-sort-objects-by-property)
|
||||
(delete-array-elements . ,usecase-delete-array-elements)
|
||||
(bubble-sort . ,usecase-bubble-sort)
|
||||
(binary-search . ,usecase-binary-search)
|
||||
(map-count-occurrences . ,usecase-map-count-occurrences)
|
||||
(get-html-three-ways . ,usecase-get-html-three-ways)
|
||||
(anagram . ,usecase-anagram)
|
||||
(pairs-equal-target . ,usecase-pairs-equal-target)
|
||||
(fetch-api . ,usecase-fetch-api)))
|
||||
|
||||
(define (show-js-usecases)
|
||||
(for ([entry (in-list all-js-usecases)])
|
||||
(displayln (format "// ~a" (car entry)))
|
||||
(displayln (cdr entry))
|
||||
(newline)))
|
||||
|
||||
(define (write-js-usecases-file path)
|
||||
(call-with-output-file path
|
||||
#:exists 'replace
|
||||
(lambda (out)
|
||||
(displayln "// Generated by demo/js-usecases.rkt" out)
|
||||
(displayln "// Each use case is wrapped in a function so snippets with return are valid." out)
|
||||
(for ([entry (in-list all-js-usecases)])
|
||||
(fprintf out "\n// ~a\n" (car entry))
|
||||
(fprintf out "function run_~a() {\n~a\n}\n"
|
||||
(regexp-replace* #rx"[^A-Za-z0-9_$]" (symbol->string (car entry)) "_")
|
||||
(cdr entry))))))
|
||||
|
||||
(module+ main
|
||||
(show-js-usecases))
|
||||
@@ -0,0 +1,32 @@
|
||||
#lang racket/base
|
||||
(require "../main.rkt")
|
||||
|
||||
(define examples
|
||||
(list
|
||||
(cons 'expression
|
||||
(js/expression
|
||||
(let loop ([i 0] [acc 0])
|
||||
(if (< i 5)
|
||||
(loop (+ i 1) (+ acc i))
|
||||
acc))))
|
||||
(cons 'program-t1
|
||||
(js
|
||||
(set! window.myfunc
|
||||
(λ (x)
|
||||
(let* ((el (send document getElementById 'hi))
|
||||
(y (* x x)))
|
||||
(send el setAttribute "x" (+ y "")))
|
||||
(send console log "dit set attribute x on element hi")))))
|
||||
(cons 'program-t2
|
||||
(js
|
||||
(define (f x)
|
||||
(if (and (> x 10) (< x 15))
|
||||
(begin (console.log x)
|
||||
(return x))
|
||||
(return (* x x))))))
|
||||
(cons 'regexp
|
||||
(js/expression
|
||||
(regexp-match #px"([a-z]+)-([0-9]+)" "abc-123")))))
|
||||
|
||||
(for ([e (in-list examples)])
|
||||
(printf "===== ~a =====\n~a\n\n" (car e) (cdr e)))
|
||||
@@ -0,0 +1,16 @@
|
||||
#lang racket/base
|
||||
(require "../main.rkt")
|
||||
(displayln "--- and ---")
|
||||
(displayln (js/expression (and (> x 10) (< x 15))))
|
||||
(displayln "--- t2 ---")
|
||||
(displayln (js (define (f x)
|
||||
(if (and (> x 10) (< x 15))
|
||||
(begin (console.log x)
|
||||
(return x))
|
||||
(return (* x x))))))
|
||||
(displayln "--- let* ---")
|
||||
(displayln (js (let* ((x 10)
|
||||
(y (+ x x)))
|
||||
(return y))))
|
||||
(displayln "--- let* tdz ---")
|
||||
(displayln (js/expression (let ([x 4]) (let* ([x x] [y x]) (+ x y)))))
|
||||
@@ -0,0 +1,41 @@
|
||||
#lang info
|
||||
|
||||
(define collection "jsmaker")
|
||||
(define version "0.2")
|
||||
(define pkg-desc "Syntax-driven Racket-to-JavaScript maker macro with regression tests.")
|
||||
(define pkg-authors '(hans-dijkema))
|
||||
(define deps '("base"))
|
||||
(define build-deps '("scribble-lib" "racket-doc"))
|
||||
|
||||
(define scribblings
|
||||
'(("scrbl/jsmaker.scrbl" () (library))
|
||||
("scrbl/usecases.scrbl" () (library))))
|
||||
|
||||
;; Running the package test suite should invoke exactly the maintained
|
||||
;; regression entry point. The regression framework itself skips JavaScript
|
||||
;; execution with warnings when no JavaScript engine is available, unless
|
||||
;; JSMAKER_REQUIRE_ENGINE or JSMAKER_REQUIRE_NODE is set.
|
||||
(define test-include-paths '("testing/jsmaker-regressions.rkt"))
|
||||
|
||||
;; These files are supporting/reference files in this package layout and are
|
||||
;; not part of the package test entry point.
|
||||
(define test-omit-paths
|
||||
'("private/utils.rkt"
|
||||
"private/syntax-helpers.rkt"
|
||||
"demo/show-jsmaker-output.rkt"
|
||||
"demo/show-optimized.rkt"
|
||||
"testing/jsmaker-executors.rkt"
|
||||
"testing/jsmaker-test-framework.rkt"
|
||||
"testing/jsmaker-test-runner.rkt"
|
||||
"testing/jsmaker-regression.rkt"
|
||||
"testing/jsmaker-regexp-regression.rkt"
|
||||
"testing/jsmaker-program-regression.rkt"
|
||||
"testing/jsmaker-dom-exercises.rkt"
|
||||
"testing/jsmaker-usecases.rkt"))
|
||||
|
||||
;; The private files are compatibility/support material and have project-local
|
||||
;; dependencies in downstream copies. The public module and tests do not
|
||||
;; depend on them.
|
||||
(define compile-omit-paths
|
||||
'("private/utils.rkt"
|
||||
"private/syntax-helpers.rkt"))
|
||||
@@ -0,0 +1,42 @@
|
||||
#lang racket/base
|
||||
|
||||
(require racket/string
|
||||
"utils.rkt"
|
||||
)
|
||||
(provide symbol??
|
||||
symstr
|
||||
symstr-eval
|
||||
is-if?)
|
||||
|
||||
|
||||
(define (symbol?? a)
|
||||
(let ((r (symbol? a)))
|
||||
r))
|
||||
|
||||
(define (symstr x)
|
||||
(cond
|
||||
((list? x)
|
||||
(string-append "[ "
|
||||
(string-join (map symstr-eval x) ", ")
|
||||
" ]"))
|
||||
((vector? x)
|
||||
(symstr (vector->list x)))
|
||||
(else
|
||||
(let ((r (format "~a" x)))
|
||||
(let ((r* (if (string-prefix? r "(quote")
|
||||
(let ((s (substring r 7)))
|
||||
(substring s 0 (- (string-length s) 1)))
|
||||
r)))
|
||||
r*)))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
(define (symstr-eval x)
|
||||
(cond
|
||||
((string? x) (format "\"~a\"" (esc-double-quote x)))
|
||||
(else (symstr x))))
|
||||
|
||||
(define (is-if? x)
|
||||
(displayln x)
|
||||
(eq? x 'if))
|
||||
@@ -0,0 +1,209 @@
|
||||
#lang racket/base
|
||||
|
||||
(require racket/string
|
||||
racket/port
|
||||
racket/contract
|
||||
json
|
||||
(prefix-in g: gregor)
|
||||
(prefix-in g: gregor/time)
|
||||
gregor-utils
|
||||
racket-sprintf
|
||||
simple-log
|
||||
)
|
||||
|
||||
(provide while
|
||||
until
|
||||
get-lib-path
|
||||
do-for
|
||||
esc-quote
|
||||
esc-double-quote
|
||||
fromJson
|
||||
mk-js-array
|
||||
js-code
|
||||
kv-1
|
||||
kv-2
|
||||
make-kv
|
||||
kv?
|
||||
list-of-kv?
|
||||
list-of-symbol?
|
||||
list-of?
|
||||
string->time
|
||||
time->string
|
||||
string->date
|
||||
date->string
|
||||
string->datetime
|
||||
datetime->string
|
||||
|
||||
dbg-webview
|
||||
err-webview
|
||||
info-webview
|
||||
warn-webview
|
||||
fatal-webview
|
||||
sync-log-webview
|
||||
|
||||
)
|
||||
|
||||
|
||||
(sl-def-log webview)
|
||||
|
||||
(define-syntax while
|
||||
(syntax-rules ()
|
||||
((_ cond body ...)
|
||||
(letrec ((while-f (lambda (last-result)
|
||||
(if cond
|
||||
(let ((last-result (begin
|
||||
body
|
||||
...)))
|
||||
(while-f last-result))
|
||||
last-result))))
|
||||
(while-f #f))
|
||||
)
|
||||
))
|
||||
|
||||
(define-syntax until
|
||||
(syntax-rules ()
|
||||
((_ cond body ...)
|
||||
(letrec ((until-f (lambda (last-result)
|
||||
(if cond
|
||||
last-result
|
||||
(let ((last-reult (begin
|
||||
body
|
||||
...)))
|
||||
(until-f last-result))))))
|
||||
(until-f #f)))))
|
||||
|
||||
(define-syntax do-for
|
||||
(syntax-rules ()
|
||||
((_ (init cond next) body ...)
|
||||
(begin
|
||||
init
|
||||
(letrec ((do-for-f (lamba ()
|
||||
(if cond
|
||||
(begin
|
||||
(begin
|
||||
body
|
||||
...)
|
||||
next
|
||||
(do-for-f))))))
|
||||
(do-for-f))))))
|
||||
|
||||
(define (get-lib-path lib)
|
||||
(let ((platform (system-type)))
|
||||
(cond
|
||||
[(eq? platform 'windows)
|
||||
(let ((try1 (build-path (current-directory) ".." "lib" "dll" lib))
|
||||
(try2 (build-path (current-directory) "lib" "dll" lib)))
|
||||
(if (file-exists? try1)
|
||||
try1
|
||||
try2)
|
||||
)]
|
||||
[else
|
||||
(error (format "Install the shared library: ~a" lib))]
|
||||
)))
|
||||
|
||||
(define (esc-quote str)
|
||||
(string-replace (string-replace str "\\" "\\\\") "'" "\\'"))
|
||||
|
||||
(define (esc-double-quote str)
|
||||
(string-replace (string-replace str "\\" "\\\\") "\"" "\\\""))
|
||||
|
||||
(define (fromJson str)
|
||||
(with-input-from-string str read-json))
|
||||
|
||||
(define (mk-js-array l)
|
||||
(if (list-of-kv? l)
|
||||
(string-append "[ " (string-join (map (λ (e) (mk-js-array e)) l) ", ") " ]")
|
||||
(if (list? l)
|
||||
(string-append "[ " (string-join (map (λ (e) (format "'~a'"
|
||||
(esc-quote (format "~a" e)))) l) ", ") " ]")
|
||||
(if (pair? l)
|
||||
(format "[ '~a', '~a' ]" (car l) (cdr l))
|
||||
(format "[ '~a' ]" (esc-quote (format "~a" l)))))))
|
||||
|
||||
(define (js-code . a)
|
||||
(define (code* l)
|
||||
(if (null? l)
|
||||
""
|
||||
(string-append (car l) "\n" (code* (cdr l)))
|
||||
)
|
||||
)
|
||||
(code* a))
|
||||
|
||||
(define (kv? e)
|
||||
(or
|
||||
(and (list? e) (= (length e) 2) (symbol? (car e)))
|
||||
(and (pair? e) (symbol? (car e)))))
|
||||
|
||||
(define/contract (kv-1 e)
|
||||
(-> kv? symbol?)
|
||||
(car e))
|
||||
|
||||
(define/contract (kv-2 e)
|
||||
(-> kv? any/c)
|
||||
(if (list? e)
|
||||
(cadr e)
|
||||
(cdr e)))
|
||||
|
||||
(define/contract (make-kv k v)
|
||||
(-> symbol? any/c kv?)
|
||||
(if (list? v)
|
||||
(list k v)
|
||||
(cons k v)))
|
||||
|
||||
(define (list-of? pred? l)
|
||||
(define (all-pred? l)
|
||||
(if (null? l)
|
||||
#t
|
||||
(if (pred? (car l))
|
||||
(all-pred? (cdr l))
|
||||
#f)))
|
||||
(if (list? l)
|
||||
(all-pred? l)
|
||||
#f))
|
||||
|
||||
(define (list-of-kv? l)
|
||||
(list-of? kv? l))
|
||||
|
||||
(define (list-of-symbol? l)
|
||||
(list-of? symbol? l))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Date / Time conversion
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(define (string->time s)
|
||||
(with-handlers ([exn:fail?
|
||||
(λ (e) (g:parse-time s "HH:mm"))])
|
||||
(g:parse-time s "HH:mm:ss")))
|
||||
|
||||
(define (time->string t)
|
||||
(unless (or (g:time? t) (g:datetime? t) (g:moment? t))
|
||||
(error "set! - gregor time?, moment? or datetime? expected"))
|
||||
(sprintf "%02d:%02d:%02d" (g:->hours t) (g:->minutes t) (g:->seconds t)))
|
||||
|
||||
(define (string->datetime s)
|
||||
(with-handlers ([exn:fail?
|
||||
(λ (e) (g:parse-moment s "yyyy-MM-dd'T'HH:mm:ss"))])
|
||||
(g:parse-moment s "yyyy-MM-dd'T'HH:mm")))
|
||||
|
||||
(define (datetime->string dt)
|
||||
(when (racket-date? dt)
|
||||
(datetime->string date->moment dt))
|
||||
(unless (or (g:datetime? dt) (g:moment? dt) (g:date? dt) (g:time? dt))
|
||||
(error "set! - gregor time? , date?, datetime? or moment? expected"))
|
||||
(sprintf "%04d:%02d:%02dT%02d:%02d:%02d"
|
||||
(g:->year dt) (g:->month dt) (g:->day dt)
|
||||
(g:->hours dt) (g:->minutes dt) (g:->seconds dt))
|
||||
)
|
||||
|
||||
(define (string->date d)
|
||||
(g:parse-date d "yyyy-MM-dd"))
|
||||
|
||||
(define (date->string d)
|
||||
(when (racket-date? d)
|
||||
(date->string (date->moment d)))
|
||||
(unless (or (g:date? d) (g:moment? d) (g:datetime? d))
|
||||
(error "set! - gregor date expected"))
|
||||
(sprintf "%04d-%02d-%02d" (g:->year d) (g:->month d) (g:->day d)))
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
*.html
|
||||
*.js
|
||||
*.css
|
||||
*.bak
|
||||
*.scrbl~
|
||||
@@ -0,0 +1,245 @@
|
||||
#lang scribble/manual
|
||||
|
||||
@(require (for-label racket/base
|
||||
racket/list
|
||||
racket/string
|
||||
"../main.rkt"))
|
||||
|
||||
@title{jsmaker}
|
||||
@author+email["Hans Dijkema" ""]
|
||||
|
||||
@defmodule[jsmaker]
|
||||
|
||||
The @racketmodname[jsmaker] collection provides two syntax forms that
|
||||
translate a practical subset of Racket expressions to JavaScript source code.
|
||||
The translation is syntax-driven: the Racket expression is not evaluated, but
|
||||
is inspected by the macro and emitted as a JavaScript string.
|
||||
|
||||
The goal is not to implement a complete Racket compiler. The goal is a useful
|
||||
and predictable source generator for small pieces of JavaScript, including
|
||||
callbacks, browser-facing functions, simple data processing, regular
|
||||
expressions, and some date/time helper forms.
|
||||
|
||||
@section{Public API}
|
||||
|
||||
@defform[(js form ...)]{
|
||||
Translates one or more Racket forms to JavaScript statement code. The result
|
||||
is a string.
|
||||
|
||||
@racketblock[
|
||||
(js
|
||||
(define (f x)
|
||||
(if (and (> x 10) (< x 15))
|
||||
(begin
|
||||
(console.log x)
|
||||
(return x))
|
||||
(return (* x x)))))
|
||||
]
|
||||
|
||||
The generated JavaScript is statement-oriented:
|
||||
|
||||
@codeblock{
|
||||
function f(x) {
|
||||
if ((x > 10) && (x < 15)) {
|
||||
console.log(x);
|
||||
return x;
|
||||
} else {
|
||||
return (x * x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Inside function bodies, the final expression is returned automatically unless
|
||||
an explicit @racket[return] form is used.
|
||||
}
|
||||
|
||||
@defform[(js/expression form)]{
|
||||
Translates a single Racket expression to a JavaScript expression string.
|
||||
|
||||
@racketblock[
|
||||
(js/expression
|
||||
(let loop ([i 0] [acc 0])
|
||||
(if (< i 5)
|
||||
(loop (+ i 1) (+ acc i))
|
||||
acc)))
|
||||
]
|
||||
|
||||
Tail-recursive named @racket[let] loops are lowered to JavaScript
|
||||
@tt{while (true)} loops when the recursive call is in tail position.
|
||||
}
|
||||
|
||||
@section{Supported expression subset}
|
||||
|
||||
The supported subset includes literals, identifiers, function calls,
|
||||
@racket[lambda], @racket[λ], @racket[define], @racket[set!], @racket[if],
|
||||
@racket[begin], @racket[begin0], @racket[cond], @racket[case], @racket[let],
|
||||
@racket[let*], @racket[letrec], @racket[let-values], @racket[let*-values],
|
||||
named @racket[let], @racket[when], @racket[unless], @racket[while],
|
||||
@racket[for], @racket[for/list], @racket[for/vector], and
|
||||
@racket[for/fold].
|
||||
|
||||
The common arithmetic, comparison, list/vector/string, hash, and higher-order
|
||||
forms used in the regression suite are also supported. Examples include
|
||||
@racket[+], @racket[-], @racket[*], @racket[/], @racket[quotient],
|
||||
@racket[remainder], @racket[<], @racket[<=], @racket[>], @racket[>=],
|
||||
@racket[=], @racket[equal?], @racket[and], @racket[or], @racket[not],
|
||||
@racket[list], @racket[vector], @racket[cons], @racket[append],
|
||||
@racket[map], @racket[filter], @racket[foldl], @racket[foldr],
|
||||
@racket[substring], @racket[string-append], @racket[hash], and
|
||||
@racket[hash-ref].
|
||||
|
||||
@section{Truthiness and boolean simplification}
|
||||
|
||||
Racket treats only @racket[#f] as false. JavaScript treats many values as
|
||||
false. The generator therefore preserves Racket truthiness where needed.
|
||||
When a form is known to produce a JavaScript boolean, the generator emits
|
||||
simpler JavaScript. For example:
|
||||
|
||||
@racketblock[
|
||||
(js/expression (and (> x 10) (< x 15)))
|
||||
]
|
||||
|
||||
emits:
|
||||
|
||||
@codeblock{
|
||||
((x > 10) && (x < 15))
|
||||
}
|
||||
|
||||
A chained comparison such as @racket[(< x y 10)] is emitted directly when all
|
||||
reused operands are simple:
|
||||
|
||||
@codeblock{
|
||||
((x < y) && (y < 10))
|
||||
}
|
||||
|
||||
When an intermediate expression might have side effects, the generator uses
|
||||
temporaries so the expression is evaluated once and in order.
|
||||
|
||||
@section{Regular expressions}
|
||||
|
||||
Racket @tt{#rx} and common @tt{#px} patterns are translated to
|
||||
JavaScript @tt{RegExp} values where the syntax is compatible. The generator
|
||||
supports @racket[regexp?], @racket[pregexp?], @racket[regexp-match],
|
||||
@racket[regexp-match?], @racket[regexp-match*],
|
||||
@racket[regexp-match-positions], @racket[regexp-split],
|
||||
@racket[regexp-replace], @racket[regexp-replace*], and
|
||||
@racket[regexp-quote].
|
||||
|
||||
Match results are normalized to Racket-like values: a failed match becomes
|
||||
@tt{false}, successful matches become JavaScript arrays, and an unmatched
|
||||
optional capture becomes @tt{false}.
|
||||
|
||||
The regexp support is deliberately conservative. Known incompatible constructs
|
||||
such as inline option groups and atomic groups are rejected instead of being
|
||||
silently miscompiled.
|
||||
|
||||
@section{Exceptions}
|
||||
|
||||
A small @racket[with-handlers] subset is supported:
|
||||
|
||||
@racketblock[
|
||||
(js/expression
|
||||
(with-handlers ([exn? (lambda (e)
|
||||
(string-append "caught:" (exn-message e)))])
|
||||
(error "boom")))
|
||||
]
|
||||
|
||||
The generated JavaScript uses @tt{try}/@tt{catch}. Only a generic
|
||||
@racket[exn?] predicate is supported. Handler procedures are emitted as
|
||||
function expressions in callee position, so rest-argument handlers such as
|
||||
@racket[(lambda args ...)] are valid JavaScript. Division by zero in
|
||||
@racket[/] is checked at run time and throws a JavaScript @tt{Error}, which
|
||||
this subset can catch with @racket[with-handlers]. The generator does not
|
||||
model Racket's exception hierarchy, continuable exceptions, exception marks,
|
||||
or the full exact/inexact numeric distinction.
|
||||
|
||||
@section{Gregor-style date and time helpers}
|
||||
|
||||
The generator includes a small JavaScript-side representation for a subset of
|
||||
Gregor-style date/time operations. Prefixes are not hardcoded. A call such as
|
||||
@racket[(g:date 2026 5 25)], @racket[(gregor:date 2026 5 25)], or
|
||||
@racket[(date 2026 5 25)] is matched by the local name @racket[date].
|
||||
|
||||
Supported local names include @racket[date], @racket[time], @racket[datetime],
|
||||
@racket[moment], @racket[parse-date], @racket[parse-time],
|
||||
@racket[parse-datetime], @racket[parse-moment], @racket[string->date],
|
||||
@racket[string->time], @racket[string->datetime], @racket[date->string],
|
||||
@racket[time->string], @racket[datetime->string], @racket[moment->string],
|
||||
@racket[date?], @racket[time?], @racket[datetime?], @racket[moment?],
|
||||
@racket[->year], @racket[->month], @racket[->day], @racket[->hours],
|
||||
@racket[->minutes], @racket[->seconds], @racket[->js-date], and
|
||||
@racket[js-date->datetime].
|
||||
|
||||
Plain dates and times are represented as tagged JavaScript objects rather than
|
||||
native @tt{Date} objects, avoiding accidental timezone shifts. Use
|
||||
@racket[->js-date] when a native JavaScript @tt{Date} is desired.
|
||||
|
||||
@section{JavaScript interop forms}
|
||||
|
||||
Several forms are emitted as direct JavaScript interop:
|
||||
|
||||
@itemlist[#:style 'compact
|
||||
@item{@racket[(send obj method arg ...)] emits @tt{obj.method(arg, ...)}.}
|
||||
@item{@racket[(new cls arg ...)] emits @tt{new cls(arg, ...)}.}
|
||||
@item{@racket[(js-ref obj key)] emits @tt{obj[key]}.}
|
||||
@item{@racket[(js-dot obj field)] emits @tt{obj.field}.}
|
||||
@item{@racket[(object key value ...)] emits a JavaScript object literal.}
|
||||
@item{@racket[(array value ...)] emits a JavaScript array literal.}]
|
||||
|
||||
|
||||
@section{JavaScript use case demos}
|
||||
|
||||
The @filepath{demo/js-usecases.rkt} file contains a set of practical JavaScript
|
||||
examples written in the Racket surface syntax accepted by @racket[js]. The
|
||||
module also writes @filepath{demo/js-usecases.generated.js}, which contains the
|
||||
generated JavaScript snippets wrapped as callable demo functions.
|
||||
|
||||
The corresponding tests are in @filepath{testing/jsmaker-usecases.rkt} and are
|
||||
included by @filepath{testing/jsmaker-regressions.rkt}. These tests execute the
|
||||
generated JavaScript with the configured JavaScript executor. Promise-valued
|
||||
results are awaited by the test framework, which allows examples such as Fetch
|
||||
API success/error handling to be checked directly.
|
||||
|
||||
The use case set covers random numbers, @tt{Set}, JavaScript falsey values,
|
||||
currying, object destructuring, intervals, object property get/set/delete,
|
||||
string concatenation order, @tt{Object.freeze} and @tt{Object.seal}, switch/case,
|
||||
classes with constructor defaults, sorting objects, array deletion techniques,
|
||||
Bubble Sort, recursive Binary Search, @tt{Map} counting, DOM HTML access,
|
||||
anagram checks, pair-sum checks, and Fetch API result/error handling.
|
||||
|
||||
@section{Testing}
|
||||
|
||||
The regression suite lives in @filepath{testing/}. The main entry point is
|
||||
@filepath{testing/jsmaker-regressions.rkt}. The test framework searches for a
|
||||
JavaScript engine such as Node, Deno, Bun, QuickJS, or another supported
|
||||
executor. If no engine is available, the JavaScript test files are generated
|
||||
and execution is skipped with warnings. This skip is intentional so package
|
||||
tests do not fail on systems without a JavaScript runtime.
|
||||
|
||||
The usual command is:
|
||||
|
||||
@codeblock{
|
||||
raco test jsmaker/testing/jsmaker-regressions.rkt
|
||||
}
|
||||
|
||||
@section{Private compatibility files}
|
||||
|
||||
The @filepath{private/} directory contains legacy helper material from the
|
||||
source project. The current public @racketmodname[jsmaker] module, demos, and
|
||||
regression tests do not require these helper files. They are kept in the
|
||||
package layout for compatibility and are omitted from compilation and the test
|
||||
entry point in @filepath{info.rkt}.
|
||||
|
||||
@section{Limitations}
|
||||
|
||||
This package is not a full Racket compiler. It does not expand arbitrary
|
||||
Racket macros, implement modules, contracts, classes, continuations, parameters,
|
||||
or the full numeric tower. Unsupported forms should fail explicitly rather than
|
||||
silently generate JavaScript with different semantics.
|
||||
|
||||
@section{Use-case documentation}
|
||||
|
||||
The practical JavaScript examples are documented separately in
|
||||
@other-doc['(lib "jsmaker/scrbl/usecases.scrbl")]. That document shows each
|
||||
use case as Racket/js-maker source next to representative generated
|
||||
JavaScript, and lists the behavior covered by the regression test.
|
||||
@@ -0,0 +1,584 @@
|
||||
#lang scribble/manual
|
||||
|
||||
@(require scribble/core
|
||||
(for-label racket/base
|
||||
"../main.rkt"))
|
||||
|
||||
@(define (side-by-side racket-source js-source)
|
||||
(tabular #:style 'boxed #:sep (hspace 2)
|
||||
(list (list (bold "Racket / js-maker") (bold "Generated JavaScript"))
|
||||
(list (verbatim racket-source) (verbatim js-source)))))
|
||||
|
||||
@(define (tested s)
|
||||
(nested #:style 'inset (bold "Tested behavior: ") s))
|
||||
|
||||
@title{jsmaker JavaScript Use Cases}
|
||||
@author+email["Hans Dijkema" ""]
|
||||
|
||||
@defmodule[jsmaker/demo/js-usecases]
|
||||
|
||||
This document describes the practical JavaScript use cases in
|
||||
@filepath{demo/js-usecases.rkt}. Each implementation is written as a Racket
|
||||
snippet using @racket[js] and is tested by compiling it to JavaScript and
|
||||
executing that JavaScript with the configured test executor. The corresponding
|
||||
tests are in @filepath{testing/jsmaker-usecases.rkt}.
|
||||
|
||||
The tests intentionally use @racket[js/expression] for their calls wherever
|
||||
possible. Raw JavaScript remains only in small harness preambles, such as fake
|
||||
@tt{setInterval}, fake DOM objects, and fake @tt{fetch}.
|
||||
|
||||
@section{Running the examples}
|
||||
|
||||
Generate the JavaScript examples with:
|
||||
|
||||
@codeblock{
|
||||
racket demo/js-usecases.rkt
|
||||
}
|
||||
|
||||
Run the use-case regression tests with:
|
||||
|
||||
@codeblock{
|
||||
racket testing/jsmaker-usecases.rkt
|
||||
}
|
||||
|
||||
@section{Use cases}
|
||||
|
||||
@subsection{1. Random number between 1 and 5}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (randomBetween1And5)
|
||||
(return (+ (send Math floor (* (send Math random) 5)) 1))))
|
||||
RKT
|
||||
#<<JS
|
||||
function randomBetween1And5() {
|
||||
return (Math.floor((Math.random() * 5)) + 1);
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "With Math.random fixed at 0.80, the generated function returns 5.")
|
||||
|
||||
@subsection{2. Unique values with Set}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (uniqueValues xs)
|
||||
(return (send Array from (new Set xs)))))
|
||||
RKT
|
||||
#<<JS
|
||||
function uniqueValues(xs) {
|
||||
return Array.from(new Set(xs));
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "The array [1, 2, 2, 3, 1] becomes [1, 2, 3].")
|
||||
|
||||
@subsection{3. Six falsey JavaScript values}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (falseyValues)
|
||||
(return (array #f 0 "" js-null js-undefined js-NaN))))
|
||||
RKT
|
||||
#<<JS
|
||||
function falseyValues() {
|
||||
return [false, 0, "", null, undefined, NaN];
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "Mapping JavaScript Boolean over the returned values yields six false values.")
|
||||
|
||||
@subsection{4. Currying}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (add x)
|
||||
(return (lambda (y)
|
||||
(return (+ x y))))))
|
||||
RKT
|
||||
#<<JS
|
||||
function add(x) {
|
||||
return function(y) {
|
||||
return (x + y);
|
||||
};
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "The generated call add(2)(3) returns 5; the test call itself is produced with js/expression.")
|
||||
|
||||
@subsection{5. Object destructuring}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (describePerson person)
|
||||
(let-object ([name 'name]
|
||||
[age 'age 0])
|
||||
person
|
||||
(return (string-append name ":" (number->string age))))))
|
||||
RKT
|
||||
#<<JS
|
||||
function describePerson(person) {
|
||||
return (() => {
|
||||
const { name: name, age: age = 0 } = person;
|
||||
return (name + ":" + String(age));
|
||||
})();
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "The object { name: \"Ada\", age: 37 } is rendered as \"Ada:37\".")
|
||||
|
||||
@subsection{6. Escaping a timer interval}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (startTimer)
|
||||
(let* ([ticks 0]
|
||||
[intervalId #f])
|
||||
(set! intervalId
|
||||
(setInterval (lambda ()
|
||||
(set! ticks (+ ticks 1))
|
||||
(when (= ticks 3)
|
||||
(clearInterval intervalId)))
|
||||
10))
|
||||
(return (object 'id intervalId
|
||||
'getTicks (lambda () (return ticks)))))))
|
||||
RKT
|
||||
#<<JS
|
||||
function startTimer() {
|
||||
{
|
||||
let ticks = 0;
|
||||
let intervalId = false;
|
||||
intervalId = setInterval(function() {
|
||||
ticks = (ticks + 1);
|
||||
if ((ticks === 3)) clearInterval(intervalId);
|
||||
return undefined;
|
||||
}, 10);
|
||||
return {id: intervalId, getTicks: function() { return ticks; }};
|
||||
}
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "A fake timer runs the callback five times, but clearInterval stops it at tick 3.")
|
||||
|
||||
@subsection{7. Get, set, and delete object properties}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (objectProps)
|
||||
(let* ([obj (object 'a 1)]
|
||||
[a1 obj.a]
|
||||
[a2 (js-ref obj "a")])
|
||||
(let-object ([a3 'a]) obj
|
||||
(set! obj.b 2)
|
||||
(set-prop! obj "c" 3)
|
||||
(delete-prop! obj "a")
|
||||
(return (array a1 a2 a3 obj.b (js-ref obj "c")
|
||||
(send Object hasOwn obj "a")))))))
|
||||
RKT
|
||||
#<<JS
|
||||
function objectProps() {
|
||||
let obj = {a: 1};
|
||||
let a1 = obj.a;
|
||||
let a2 = obj["a"];
|
||||
return (() => {
|
||||
const { a: a3 } = obj;
|
||||
obj.b = 2;
|
||||
obj["c"] = 3;
|
||||
delete obj["a"];
|
||||
return [a1, a2, a3, obj.b, obj["c"], Object.hasOwn(obj, "a")];
|
||||
})();
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "The three reads produce 1, the two writes produce 2 and 3, and the deleted property is absent.")
|
||||
|
||||
@subsection{8. String concatenation order}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (concatOrder)
|
||||
(return (array (+ 1 2 "3")
|
||||
(+ "1" 2 3)))))
|
||||
RKT
|
||||
#<<JS
|
||||
function concatOrder() {
|
||||
return [(1 + 2 + "3"), ("1" + 2 + 3)];
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "The generated function returns [\"33\", \"123\"].")
|
||||
|
||||
@subsection{9. Object.freeze versus Object.seal}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (freezeVsSeal)
|
||||
(let* ([frozen (send Object freeze (object 'a 1))]
|
||||
[sealed (send Object seal (object 'a 1))])
|
||||
(set! frozen.a 9)
|
||||
(set! sealed.a 9)
|
||||
(delete-prop! sealed "a")
|
||||
(return (array frozen.a sealed.a
|
||||
(send Object isFrozen frozen)
|
||||
(send Object isSealed sealed)
|
||||
(send Object hasOwn sealed "a"))))))
|
||||
RKT
|
||||
#<<JS
|
||||
function freezeVsSeal() {
|
||||
let frozen = Object.freeze({a: 1});
|
||||
let sealed = Object.seal({a: 1});
|
||||
frozen.a = 9;
|
||||
sealed.a = 9;
|
||||
delete sealed["a"];
|
||||
return [frozen.a, sealed.a, Object.isFrozen(frozen),
|
||||
Object.isSealed(sealed), Object.hasOwn(sealed, "a")];
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "The frozen value stays 1; the sealed value changes to 9 but remains present.")
|
||||
|
||||
@subsection{10. Switch example}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (switchExample n)
|
||||
(case n
|
||||
[(1) (return "one")]
|
||||
[(2 3) (return "two-or-three")]
|
||||
[else (return "other")]))))
|
||||
RKT
|
||||
#<<JS
|
||||
function switchExample(n) {
|
||||
switch (n) {
|
||||
case 1: return "one";
|
||||
case 2:
|
||||
case 3: return "two-or-three";
|
||||
default: return "other";
|
||||
}
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "The generated function maps 1, 2, and 9 to the three expected branches.")
|
||||
|
||||
@subsection{11. Class constructor with a default value}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define-class Greeter
|
||||
(constructor ([name "world"])
|
||||
(set! this.name name))
|
||||
(method greet ()
|
||||
(return (string-append "Hello " this.name))))
|
||||
|
||||
(define (classExample)
|
||||
(let* ([a (new Greeter)]
|
||||
[b (new Greeter "Ada")])
|
||||
(return (array (send a greet) (send b greet))))))
|
||||
RKT
|
||||
#<<JS
|
||||
class Greeter {
|
||||
constructor(name = "world") {
|
||||
this.name = name;
|
||||
}
|
||||
greet() {
|
||||
return ("Hello " + this.name);
|
||||
}
|
||||
}
|
||||
function classExample() {
|
||||
let a = new Greeter();
|
||||
let b = new Greeter("Ada");
|
||||
return [a.greet(), b.greet()];
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "The default constructor value and explicit constructor argument both work.")
|
||||
|
||||
@subsection{12. Sort objects by a property}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (sortByProperty xs prop)
|
||||
(return (send (send xs slice)
|
||||
sort
|
||||
(lambda (a b)
|
||||
(return (- (js-ref a prop) (js-ref b prop))))))))
|
||||
RKT
|
||||
#<<JS
|
||||
function sortByProperty(xs, prop) {
|
||||
return xs.slice().sort(function(a, b) {
|
||||
return (a[prop] - b[prop]);
|
||||
});
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "Sorting age objects by age yields [20, 25, 30].")
|
||||
|
||||
@subsection{13. Four ways to delete or remove an array element}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (deleteArrayWays xs)
|
||||
(let* ([a1 (send xs slice)]
|
||||
[a2 (send xs slice)]
|
||||
[a3 (send xs slice)]
|
||||
[a4 (send xs slice)])
|
||||
(send a1 splice 1 1)
|
||||
(set! a2 (send a2 filter (lambda (x i) (return (not (= i 1))))))
|
||||
(set! a3 (send (send a3 slice 0 1) concat (send a3 slice 2)))
|
||||
(delete-prop! a4 1)
|
||||
(return (array a1 a2 a3
|
||||
(array (send Object hasOwn a4 "1") (length a4)))))))
|
||||
RKT
|
||||
#<<JS
|
||||
function deleteArrayWays(xs) {
|
||||
let a1 = xs.slice(), a2 = xs.slice(), a3 = xs.slice(), a4 = xs.slice();
|
||||
a1.splice(1, 1);
|
||||
a2 = a2.filter(function(x, i) { return !(i === 1); });
|
||||
a3 = a3.slice(0, 1).concat(a3.slice(2));
|
||||
delete a4[1];
|
||||
return [a1, a2, a3, [Object.hasOwn(a4, "1"), a4.length]];
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "splice, filter, and slice/concat remove the item; delete leaves a hole and preserves length.")
|
||||
|
||||
@subsection{14. Bubble sort}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (bubbleSort xs)
|
||||
(let* ([a (send xs slice)]
|
||||
[n (length a)])
|
||||
(while (> n 1)
|
||||
(let* ([i 1])
|
||||
(while (< i n)
|
||||
(when (> (list-ref a (- i 1)) (list-ref a i))
|
||||
(let* ([tmp (list-ref a (- i 1))])
|
||||
(vector-set! a (- i 1) (list-ref a i))
|
||||
(vector-set! a i tmp)))
|
||||
(set! i (+ i 1))))
|
||||
(set! n (- n 1)))
|
||||
(return a))))
|
||||
RKT
|
||||
#<<JS
|
||||
function bubbleSort(xs) {
|
||||
let a = xs.slice();
|
||||
let n = a.length;
|
||||
while ((n > 1)) {
|
||||
let i = 1;
|
||||
while ((i < n)) {
|
||||
if ((a[(i - 1)] > a[i])) {
|
||||
let tmp = a[(i - 1)];
|
||||
a[(i - 1)] = a[i];
|
||||
a[i] = tmp;
|
||||
}
|
||||
i = (i + 1);
|
||||
}
|
||||
n = (n - 1);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "Sorting [5, 1, 4, 2, 8] yields [1, 2, 4, 5, 8].")
|
||||
|
||||
@subsection{15. Recursive binary search}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (binarySearch xs target low high)
|
||||
(if (> low high)
|
||||
(return -1)
|
||||
(let* ([mid (send Math floor (/ (+ low high) 2))]
|
||||
[value (list-ref xs mid)])
|
||||
(cond
|
||||
[(= value target) (return mid)]
|
||||
[(< value target) (return (binarySearch xs target (+ mid 1) high))]
|
||||
[else (return (binarySearch xs target low (- mid 1)))])))))
|
||||
RKT
|
||||
#<<JS
|
||||
function binarySearch(xs, target, low, high) {
|
||||
if ((low > high)) return -1;
|
||||
let mid = Math.floor(((low + high) / 2));
|
||||
let value = xs[mid];
|
||||
if ((value === target)) return mid;
|
||||
if ((value < target)) return binarySearch(xs, target, (mid + 1), high);
|
||||
return binarySearch(xs, target, low, (mid - 1));
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "Searching 7 returns index 3; searching 4 returns -1.")
|
||||
|
||||
@subsection{16. Count occurrences with Map}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (countOccurrences xs)
|
||||
(let* ([counts (new Map)])
|
||||
(for ([x (in-list xs)])
|
||||
(if (send counts has x)
|
||||
(send counts set x (+ (send counts get x) 1))
|
||||
(send counts set x 1)))
|
||||
(return (send Array from (send counts entries))))))
|
||||
RKT
|
||||
#<<JS
|
||||
function countOccurrences(xs) {
|
||||
let counts = new Map();
|
||||
for (const x of xs) {
|
||||
if (counts.has(x)) counts.set(x, (counts.get(x) + 1));
|
||||
else counts.set(x, 1);
|
||||
}
|
||||
return Array.from(counts.entries());
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "The array [\"a\", \"b\", \"a\", \"c\", \"b\", \"a\"] yields [[\"a\",3],[\"b\",2],[\"c\",1]].")
|
||||
|
||||
@subsection{17. Get HTML in three ways}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (getHtmlThreeWays)
|
||||
(return (array document.body.innerHTML
|
||||
(js-dot (send document querySelector "body") innerHTML)
|
||||
(js-ref (send document getElementById "root") "innerHTML")))))
|
||||
RKT
|
||||
#<<JS
|
||||
function getHtmlThreeWays() {
|
||||
return [document.body.innerHTML,
|
||||
document.querySelector("body").innerHTML,
|
||||
document.getElementById("root")["innerHTML"]];
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "A fake DOM returns the same HTML string through all three access forms.")
|
||||
|
||||
@subsection{18. Anagram / rearrangement check}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (sortChars s)
|
||||
(return (send (send (send s split "") sort) join "")))
|
||||
|
||||
(define (canArrange stringA stringB)
|
||||
(return (string=? (sortChars stringA) (sortChars stringB)))))
|
||||
RKT
|
||||
#<<JS
|
||||
function sortChars(s) {
|
||||
return s.split("").sort().join("");
|
||||
}
|
||||
function canArrange(stringA, stringB) {
|
||||
return (sortChars(stringA) === sortChars(stringB));
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "listen/silent returns true; abc/abd returns false.")
|
||||
|
||||
@subsection{19. Pairs equal to a target, without repeats}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (pairsEqualTarget xs target)
|
||||
(let* ([seen (new Set)]
|
||||
[used (new Set)]
|
||||
[out (array)])
|
||||
(for ([x (in-list xs)])
|
||||
(let* ([y (- target x)])
|
||||
(if (and (send seen has y)
|
||||
(not (send used has x))
|
||||
(not (send used has y)))
|
||||
(begin
|
||||
(send out push (array y x))
|
||||
(send used add x)
|
||||
(send used add y))
|
||||
(send seen add x))))
|
||||
(return out))))
|
||||
RKT
|
||||
#<<JS
|
||||
function pairsEqualTarget(xs, target) {
|
||||
let seen = new Set();
|
||||
let used = new Set();
|
||||
let out = [];
|
||||
for (const x of xs) {
|
||||
let y = (target - x);
|
||||
if (seen.has(y) && !(used.has(x)) && !(used.has(y))) {
|
||||
out.push([y, x]); used.add(x); used.add(y);
|
||||
} else {
|
||||
seen.add(x);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "For [1, 2, 3, 4, 3, 5] and target 6 the returned pairs are [[2,4],[3,3],[1,5]].")
|
||||
|
||||
@subsection{20. Fetch API with result and error handling}
|
||||
|
||||
@(side-by-side
|
||||
#<<RKT
|
||||
(js
|
||||
(define (loadTitle url)
|
||||
(return
|
||||
(send
|
||||
(send
|
||||
(send (fetch url)
|
||||
then
|
||||
(lambda (response)
|
||||
(return (send response json))))
|
||||
then
|
||||
(lambda (data)
|
||||
(return (object 'ok #t 'title data.title))))
|
||||
catch
|
||||
(lambda (err)
|
||||
(return (object 'ok #f 'message err.message)))))))
|
||||
RKT
|
||||
#<<JS
|
||||
function loadTitle(url) {
|
||||
return fetch(url)
|
||||
.then(function(response) { return response.json(); })
|
||||
.then(function(data) { return {ok: true, title: data.title}; })
|
||||
.catch(function(err) { return {ok: false, message: err.message}; });
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(tested "A fake fetch resolves /ok to {ok:true,title:\"Done\"} and rejects /fail to {ok:false,message:\"network\"}.")
|
||||
@@ -0,0 +1,68 @@
|
||||
#lang racket/base
|
||||
|
||||
(require json
|
||||
"jsmaker-test-framework.rkt"
|
||||
"jsmaker-executors.rkt"
|
||||
"../demo/dom-exercises.rkt")
|
||||
|
||||
(define (dom-preamble paragraph-text)
|
||||
(format #<<JS
|
||||
const state = { afterParagraph: [], afterHeading: [] };
|
||||
const paragraph = {
|
||||
innerHTML: ~a,
|
||||
get textContent() { return String(this.innerHTML).replace(/<[^>]*>/g, ''); },
|
||||
set textContent(v) { this.innerHTML = String(v); },
|
||||
insertAdjacentHTML: (position, html) => { state.afterParagraph.push([position, html]); }
|
||||
};
|
||||
const heading = {
|
||||
insertAdjacentHTML: (position, html) => { state.afterHeading.push([position, html]); }
|
||||
};
|
||||
const document = {
|
||||
querySelector: (selector) => {
|
||||
if (selector === 'p') return paragraph;
|
||||
if (selector === 'h1') return heading;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
JS
|
||||
(jsexpr->string paragraph-text)))
|
||||
|
||||
(define tests
|
||||
(list
|
||||
(js-program-test
|
||||
'dom-ex01-highlight-long-words
|
||||
exercise01
|
||||
"paragraph.innerHTML"
|
||||
(jsexpr->string "Short <span style=\"background: yellow\">extraordinary</span> words remain.")
|
||||
#:preamble (dom-preamble "Short extraordinary words remain."))
|
||||
|
||||
(js-program-test
|
||||
'dom-ex02-add-source-link
|
||||
exercise02
|
||||
"state.afterParagraph"
|
||||
(jsexpr->string '(("afterend" "<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>")))
|
||||
#:preamble (dom-preamble "Force ipsum text."))
|
||||
|
||||
(js-program-test
|
||||
'dom-ex03-split-sentences
|
||||
exercise03
|
||||
"paragraph.innerHTML"
|
||||
(jsexpr->string "First sentence.<br>Second sentence.<br>Third.<br>")
|
||||
#:preamble (dom-preamble "First sentence. Second sentence. Third."))
|
||||
|
||||
(js-program-test
|
||||
'dom-ex04-count-words-after-heading
|
||||
exercise04
|
||||
"state.afterHeading"
|
||||
(jsexpr->string '(("afterend" "<p>4 words</p>")))
|
||||
#:preamble (dom-preamble "These are four words"))
|
||||
|
||||
(js-program-test
|
||||
'dom-ex05-replace-punctuation-faces
|
||||
exercise05
|
||||
"paragraph.innerHTML"
|
||||
(jsexpr->string "Really🤔 Yes😲 No🤔")
|
||||
#:preamble (dom-preamble "Really? Yes! No?"))))
|
||||
|
||||
(define engine (find-js-engine))
|
||||
(run-jsmaker-regression 'jsmaker-dom-exercises tests "/tmp/jsmaker-dom-exercises.js" #:engine engine)
|
||||
@@ -0,0 +1,164 @@
|
||||
#lang racket/base
|
||||
|
||||
(require racket/file
|
||||
racket/list
|
||||
racket/match
|
||||
racket/port
|
||||
racket/string)
|
||||
|
||||
(provide js-engine?
|
||||
js-engine-name
|
||||
js-engine-path
|
||||
js-engine-kind
|
||||
js-engine-version
|
||||
js-run-result?
|
||||
js-run-result-engine
|
||||
js-run-result-status
|
||||
js-run-result-stdout
|
||||
js-run-result-stderr
|
||||
js-run-result-success?
|
||||
known-js-engine-names
|
||||
find-js-engine
|
||||
run-js-file)
|
||||
|
||||
(struct js-engine (name path kind version-args) #:transparent)
|
||||
(struct js-run-result (engine status stdout stderr) #:transparent)
|
||||
|
||||
(define (path-string/non-empty? v)
|
||||
(and (string? v) (not (string=? v ""))))
|
||||
|
||||
(define (executable-file? p)
|
||||
(and p (file-exists? p)))
|
||||
|
||||
(define (getenv/path name)
|
||||
(define v (getenv name))
|
||||
(and (path-string/non-empty? v) (string->path v)))
|
||||
|
||||
(define engine-specs
|
||||
;; name aliases kind version-args extra-paths
|
||||
'((node ("node" "nodejs") plain ("--version")
|
||||
("/opt/nvm/versions/node/v22.16.0/bin/node" "/usr/local/bin/node" "/usr/bin/node"))
|
||||
(deno ("deno") plain ("--version") ())
|
||||
(bun ("bun") plain ("--version") ())
|
||||
(qjs ("qjs" "quickjs") plain ("-v") ())
|
||||
(d8 ("d8") plain ("--version") ())
|
||||
(jsc ("jsc") plain ("--version") ())
|
||||
(js ("js") plain ("--version") ())
|
||||
(chromium ("chromium" "chromium-browser" "google-chrome" "google-chrome-stable") chromium ("--version")
|
||||
("/usr/bin/chromium" "/usr/bin/chromium-browser" "/usr/bin/google-chrome"))))
|
||||
|
||||
(define (known-js-engine-names)
|
||||
(map car engine-specs))
|
||||
|
||||
(define (lookup-engine-spec name)
|
||||
(define wanted (string-downcase (format "~a" name)))
|
||||
(for/first ([spec (in-list engine-specs)]
|
||||
#:when (or (string=? wanted (symbol->string (car spec)))
|
||||
(member wanted (cadr spec))))
|
||||
spec))
|
||||
|
||||
(define (candidate-paths spec)
|
||||
(match spec
|
||||
[(list name aliases kind version-args extra-paths)
|
||||
(append
|
||||
(if (eq? name 'node)
|
||||
(filter values (list (getenv/path "JSMAKER_NODE")))
|
||||
'())
|
||||
(filter values (list (getenv/path "JSMAKER_ENGINE_PATH")))
|
||||
(filter values (map find-executable-path aliases))
|
||||
(map string->path extra-paths))]))
|
||||
|
||||
(define (make-engine-from-spec spec)
|
||||
(match spec
|
||||
[(list name aliases kind version-args extra-paths)
|
||||
(for/first ([p (in-list (candidate-paths spec))]
|
||||
#:when (executable-file? p))
|
||||
(js-engine name p kind version-args))]))
|
||||
|
||||
(define (find-js-engine [requested (or (getenv "JSMAKER_ENGINE") "auto")])
|
||||
(define req (string-downcase requested))
|
||||
(cond
|
||||
[(or (string=? req "") (string=? req "auto"))
|
||||
(define browser-fallback? (getenv "JSMAKER_BROWSER_FALLBACK"))
|
||||
(or (for*/first ([spec (in-list engine-specs)]
|
||||
#:unless (and (not browser-fallback?) (eq? (car spec) 'chromium))
|
||||
[engine (in-value (make-engine-from-spec spec))]
|
||||
#:when engine)
|
||||
engine)
|
||||
#f)]
|
||||
[else
|
||||
(define spec (lookup-engine-spec req))
|
||||
(and spec (make-engine-from-spec spec))]))
|
||||
|
||||
(define (capture-timeout-seconds)
|
||||
(define v (getenv "JSMAKER_ENGINE_TIMEOUT_SECONDS"))
|
||||
(cond
|
||||
[(and (path-string/non-empty? v) (string->number v)) => values]
|
||||
[else 15]))
|
||||
|
||||
(define (capture path args)
|
||||
(with-handlers ([exn:fail? (lambda (e) (values 127 "" (exn-message e)))])
|
||||
(define-values (proc stdout stdin stderr)
|
||||
(apply subprocess #f #f #f path args))
|
||||
(define waiter (thread (lambda () (subprocess-wait proc))))
|
||||
(define finished? (sync/timeout (capture-timeout-seconds) waiter))
|
||||
(unless finished?
|
||||
(subprocess-kill proc #t)
|
||||
(thread-wait waiter))
|
||||
(values (if finished? (subprocess-status proc) 124)
|
||||
(port->string stdout)
|
||||
(string-append (port->string stderr)
|
||||
(if finished? "" "\nJavaScript engine timed out and was killed.\n")))))
|
||||
|
||||
(define (js-engine-version engine)
|
||||
(define-values (status stdout stderr)
|
||||
(capture (js-engine-path engine) (js-engine-version-args engine)))
|
||||
(define s (string-trim (if (string=? stdout "") stderr stdout)))
|
||||
(and (zero? status) (not (string=? s "")) s))
|
||||
|
||||
(define (plain-run-args engine js-path)
|
||||
(case (js-engine-name engine)
|
||||
[(deno) (list "run" "--quiet" js-path)]
|
||||
[else (list js-path)]))
|
||||
|
||||
(define (copy-js-as-browser-html js-path)
|
||||
(define html-path (build-path (find-system-path 'temp-dir)
|
||||
(format "jsmaker-browser-~a.html" (current-inexact-milliseconds))))
|
||||
(define js (file->string js-path))
|
||||
(call-with-output-file html-path
|
||||
#:exists 'replace
|
||||
(lambda (out)
|
||||
(displayln "<!doctype html><meta charset=\"utf-8\"><pre id=\"out\"></pre>" out)
|
||||
(displayln "<script>" out)
|
||||
(displayln "const __out = document.getElementById('out');" out)
|
||||
(displayln "const print = (...xs) => { __out.append(xs.join(' ') + '\\n'); };" out)
|
||||
(displayln "console.log = (...xs) => print(...xs);" out)
|
||||
(displayln "console.error = (...xs) => print(...xs);" out)
|
||||
(displayln "try {" out)
|
||||
(display js out)
|
||||
(displayln "\ndocument.documentElement.setAttribute('data-jsmaker-status', 'ok');" out)
|
||||
(displayln "} catch (e) {" out)
|
||||
(displayln " print('ERROR\\t' + (e && e.stack ? e.stack : String(e)));" out)
|
||||
(displayln " document.documentElement.setAttribute('data-jsmaker-status', 'fail');" out)
|
||||
(displayln "}" out)
|
||||
(displayln "</script>" out)))
|
||||
html-path)
|
||||
|
||||
(define (chromium-run engine js-path)
|
||||
(define html-path (copy-js-as-browser-html js-path))
|
||||
(define url (string-append "file://" (path->string html-path)))
|
||||
(define args (list "--headless" "--disable-gpu" "--no-sandbox" "--dump-dom" url))
|
||||
(define-values (status stdout stderr) (capture (js-engine-path engine) args))
|
||||
(define browser-fail? (regexp-match? #rx"data-jsmaker-status=\"fail\"" stdout))
|
||||
(js-run-result engine (if browser-fail? 1 status) stdout stderr))
|
||||
|
||||
(define (run-js-file engine js-path)
|
||||
(case (js-engine-kind engine)
|
||||
[(chromium) (chromium-run engine js-path)]
|
||||
[else
|
||||
(define-values (status stdout stderr)
|
||||
(capture (js-engine-path engine) (plain-run-args engine js-path)))
|
||||
(js-run-result engine status stdout stderr)]))
|
||||
|
||||
(define (js-run-result-success? result)
|
||||
(zero? (js-run-result-status result)))
|
||||
@@ -0,0 +1,118 @@
|
||||
#lang racket/base
|
||||
|
||||
(require "../main.rkt"
|
||||
"jsmaker-executors.rkt"
|
||||
"jsmaker-test-framework.rkt")
|
||||
|
||||
(define dom-preamble
|
||||
(string-append
|
||||
"const window = {};\n"
|
||||
"const logs = [];\n"
|
||||
"const attrs = {};\n"
|
||||
"const console = { log: (...xs) => logs.push(xs.length === 1 ? xs[0] : xs) };\n"
|
||||
"const document = {\n"
|
||||
" getElementById: (id) => id === 'hi'\n"
|
||||
" ? { setAttribute: (k, v) => { attrs[k] = v; }, getAttribute: (k) => attrs[k] || '' }\n"
|
||||
" : false\n"
|
||||
"};"))
|
||||
|
||||
(define t1-program
|
||||
(js (set! window.myfunc (λ (x)
|
||||
(let* ((el (send document getElementById 'hi))
|
||||
(old (send el getAttribute "x"))
|
||||
(y (+ old (* x x))))
|
||||
(send el setAttribute "x" (+ y "")))
|
||||
(send console log "dit set attribute x on element hi")))))
|
||||
|
||||
(define t2-program
|
||||
(js (define (f x)
|
||||
(if (and (> x 10) (< x 15))
|
||||
(begin (console.log x)
|
||||
(return x))
|
||||
(return (* x x))))))
|
||||
|
||||
(define t3-program
|
||||
(js (define (f x y z)
|
||||
(send console log (cons x (cons y (list z))))
|
||||
(let* ((l (cons x (cons y (list z)))))
|
||||
(return (send l map (λ (a) (return (+ a 10)))))))))
|
||||
|
||||
(define tail-loop-expression
|
||||
(js/expression (let loop ([i 0] [s 0])
|
||||
(if (< i 5)
|
||||
(loop (+ i 1) (+ s i))
|
||||
s))))
|
||||
|
||||
(unless (regexp-match? #rx"while \\(true\\)" tail-loop-expression)
|
||||
(error 'jsmaker-program-regression
|
||||
"tail-recursive named let was not lowered to while(true): ~a"
|
||||
tail-loop-expression))
|
||||
|
||||
(unless (regexp-match? #rx"\n " t1-program)
|
||||
(error 'jsmaker-program-regression
|
||||
"generated statement code does not appear to contain indentation: ~a"
|
||||
t1-program))
|
||||
|
||||
(unless (regexp-match? #rx"if \\(\\(x > 10\\) && \\(x < 15\\)\\)" t2-program)
|
||||
(error 'jsmaker-program-regression
|
||||
"boolean and should compile directly in if test: ~a"
|
||||
t2-program))
|
||||
|
||||
(define program-tests
|
||||
(list
|
||||
(js-program-test
|
||||
'dom-style-set-attribute
|
||||
t1-program
|
||||
"(() => { attrs.x = 'old:'; window.myfunc(4); return [attrs.x, logs]; })()"
|
||||
"[\"old:16\",[\"dit set attribute x on element hi\"]]"
|
||||
#:preamble dom-preamble)
|
||||
|
||||
(js-program-test
|
||||
'define-if-return-console
|
||||
t2-program
|
||||
"(() => { const a = f(12); const b = f(5); return [a, b, logs]; })()"
|
||||
"[12,25,[12]]"
|
||||
#:preamble "const logs = []; const console = { log: (...xs) => logs.push(xs.length === 1 ? xs[0] : xs) };" )
|
||||
|
||||
(js-program-test
|
||||
'define-send-map-return
|
||||
t3-program
|
||||
"(() => { const r = f(1, 2, 3); return [r, logs]; })()"
|
||||
"[[11,12,13],[[1,2,3]]]"
|
||||
#:preamble "const logs = []; const console = { log: (...xs) => logs.push(xs.length === 1 ? xs[0] : xs) };" )
|
||||
|
||||
(js-expression-test 'named-let-tail-big
|
||||
(js/expression (let loop ([i 0] [s 0])
|
||||
(if (< i 100000)
|
||||
(loop (+ i 1) (+ s 1))
|
||||
s)))
|
||||
"100000")
|
||||
|
||||
(js-expression-test 'named-let-tail-accumulator tail-loop-expression "10")
|
||||
|
||||
(js-expression-test 'explicit-while-form
|
||||
(js/expression (let ([i 0] [s 0])
|
||||
(while (< i 4)
|
||||
(set! s (+ s i))
|
||||
(set! i (+ i 1)))
|
||||
s))
|
||||
"6")
|
||||
|
||||
(js-expression-test 'bare-return
|
||||
(js/expression ((lambda (x) (if x (return) (return 7))) #t))
|
||||
"undefined")
|
||||
|
||||
(js-expression-test 'implicit-last-expression-return
|
||||
(js/expression ((lambda (x) (+ x 10)) 5))
|
||||
"15")
|
||||
|
||||
(js-program-test
|
||||
'with-handlers-rest-lambda-statement
|
||||
(js (with-handlers ([exn? (λ args (displayln (length args)))])
|
||||
(/ 10 0)))
|
||||
"logs"
|
||||
"[1]"
|
||||
#:preamble "const logs = []; const console = { log: (...xs) => logs.push(xs.length === 1 ? xs[0] : xs) };")))
|
||||
|
||||
(define engine (find-js-engine))
|
||||
(run-jsmaker-regression 'jsmaker-program-regression program-tests "/tmp/jsmaker-program-regression.js" #:engine engine)
|
||||
@@ -0,0 +1,24 @@
|
||||
#lang racket/base
|
||||
|
||||
(require "../main.rkt"
|
||||
"jsmaker-executors.rkt"
|
||||
"jsmaker-test-framework.rkt")
|
||||
|
||||
(define tests
|
||||
(list
|
||||
(js-expression-test 'regexp-literal-test (js/expression (regexp-match? #rx"a+" "baac")) "true")
|
||||
(js-expression-test 'regexp-match-basic (js/expression (regexp-match #rx"a+" "baac")) "[\"aa\"]")
|
||||
(js-expression-test 'regexp-match-captures (js/expression (regexp-match #rx"(a)(b)?" "a")) "[\"a\",\"a\",false]")
|
||||
(js-expression-test 'pregexp-match-digits (js/expression (regexp-match #px"([a-z]+)-([0-9]+)" "abc-123")) "[\"abc-123\",\"abc\",\"123\"]")
|
||||
(js-expression-test 'regexp-match-star (js/expression (regexp-match* #rx"a+" "baacaa")) "[\"aa\",\"aa\"]")
|
||||
(js-expression-test 'regexp-split (js/expression (regexp-split #rx"," "a,b,,c")) "[\"a\",\"b\",\"\",\"c\"]")
|
||||
(js-expression-test 'regexp-replace (js/expression (regexp-replace #rx"a" "banana" "X")) "\"bXnana\"")
|
||||
(js-expression-test 'regexp-replace-star (js/expression (regexp-replace* #rx"a" "banana" "X")) "\"bXnXnX\"")
|
||||
(js-expression-test 'regexp-replace-backref (js/expression (regexp-replace #rx"([a-z]+)-([0-9]+)" "abc-123" "\\2/\\1")) "\"123/abc\"")
|
||||
(js-expression-test 'regexp-replace-full (js/expression (regexp-replace #rx"a+" "baac" "[\\0]")) "\"b[aa]c\"")
|
||||
(js-expression-test 'regexp-pattern-string (js/expression (regexp-match "a+" "baac")) "[\"aa\"]")
|
||||
(js-expression-test 'regexp-quote (js/expression (regexp-quote "a+b*c?")) "\"a\\\\+b\\\\*c\\\\?\"")
|
||||
(js-expression-test 'regexp-match-positions (js/expression (regexp-match-positions #rx"(a)(b)?" "xa")) "[[1,2],[1,2],false]")))
|
||||
|
||||
(define engine (find-js-engine))
|
||||
(run-jsmaker-regression 'jsmaker-regexp-regression tests "/tmp/jsmaker-regexp-regression.js" #:engine engine)
|
||||
@@ -0,0 +1,110 @@
|
||||
#lang racket/base
|
||||
|
||||
(require racket/string
|
||||
"../main.rkt"
|
||||
"jsmaker-executors.rkt"
|
||||
"jsmaker-test-framework.rkt")
|
||||
|
||||
(define direct-and (js/expression (and (> x 10) (< x 15))))
|
||||
(unless (string=? direct-and "((x > 10) && (x < 15))")
|
||||
(error 'jsmaker-regression "expected direct && generation, got: ~a" direct-and))
|
||||
|
||||
(define direct-or (js/expression (or (< x 10) (> x 20))))
|
||||
(unless (string=? direct-or "((x < 10) || (x > 20))")
|
||||
(error 'jsmaker-regression "expected direct || generation, got: ~a" direct-or))
|
||||
|
||||
(define direct-chain (js/expression (< x y 10)))
|
||||
(unless (string=? direct-chain "((x < y) && (y < 10))")
|
||||
(error 'jsmaker-regression "expected direct pairwise comparison, got: ~a" direct-chain))
|
||||
|
||||
(define effectful-chain (js/expression (< x (f y) 10)))
|
||||
(unless (regexp-match? #rx"__cmp" effectful-chain)
|
||||
(error 'jsmaker-regression "effectful chained comparison should use temporaries, got: ~a" effectful-chain))
|
||||
|
||||
(define simple-let-star
|
||||
(js (let* ((x 10)
|
||||
(y (+ x x)))
|
||||
(return y))))
|
||||
|
||||
(define with-handlers-rest-lambda-program
|
||||
(js (with-handlers ([exn? (λ args (displayln args))])
|
||||
(/ 10 0))))
|
||||
(unless (regexp-match? #rx"\\(function\\(\\.\\.\\.args\\)" with-handlers-rest-lambda-program)
|
||||
(error 'jsmaker-regression
|
||||
"with-handlers rest-lambda handler should be parenthesized in callee position, got: ~a"
|
||||
with-handlers-rest-lambda-program))
|
||||
(unless (not (regexp-match? #rx"catch[^{]*\\{\\n function\\(" with-handlers-rest-lambda-program))
|
||||
(error 'jsmaker-regression
|
||||
"with-handlers emitted a function declaration in statement position, got: ~a"
|
||||
with-handlers-rest-lambda-program))
|
||||
(unless (regexp-match? #rx"let x = 10;" simple-let-star)
|
||||
(error 'jsmaker-regression "simple let* should emit direct let for x, got: ~a" simple-let-star))
|
||||
(unless (regexp-match? #rx"let y = .*x.*x.*;" simple-let-star)
|
||||
(error 'jsmaker-regression "simple let* should emit direct dependent let for y, got: ~a" simple-let-star))
|
||||
(unless (not (regexp-match? #rx"__let_star_value" simple-let-star))
|
||||
(error 'jsmaker-regression "simple let* should not use tempvars, got: ~a" simple-let-star))
|
||||
|
||||
(define tests
|
||||
(list
|
||||
(js-expression-test 'if-zero (js/expression (if 0 1 2)) "1")
|
||||
(js-expression-test 'if-false (js/expression (if #f 1 2)) "2")
|
||||
(js-expression-test 'and-false (js/expression (and #f 5)) "false")
|
||||
(js-expression-test 'and-zero (js/expression (and 0 5)) "5")
|
||||
(js-expression-test 'or-empty-string (js/expression (or "" 5)) "\"\"")
|
||||
(js-expression-test 'direct-and-boolean (format "((x) => ~a)(12)" direct-and) "true")
|
||||
(js-expression-test 'direct-or-boolean (format "((x) => ~a)(5)" direct-or) "true")
|
||||
(js-expression-test 'chain-lt (js/expression (< 1 2 3)) "true")
|
||||
(js-expression-test 'chain-lt-false (js/expression (< 1 3 2)) "false")
|
||||
(js-expression-test 'let-expr (js/expression (let ([x 2] [y 3]) (+ x y))) "5")
|
||||
(js-expression-test 'let-star-sequential-binding (js/expression (let* ([x 10] [y (+ x x)]) y)) "20")
|
||||
(js-expression-test 'let-star-dependent-shadowing (js/expression (let ([x 4]) (let* ([x x] [y x]) (+ x y)))) "8")
|
||||
(js-expression-test 'named-let (js/expression (let loop ([i 0] [s 0]) (if (< i 5) (loop (+ i 1) (+ s i)) s))) "10")
|
||||
(js-expression-test 'for-list (js/expression (for/list ([x (in-range 5)] #:when (odd? x)) (* x x))) "[1,9]")
|
||||
(js-expression-test 'for-fold (js/expression (for/fold ([s 0]) ([x (in-list (list 1 2 3))]) (+ x s))) "6")
|
||||
(js-expression-test 'map-filter (js/expression (filter (lambda (x) (> x 2)) (map (lambda (x) (+ x 1)) (list 1 2 3)))) "[3,4]")
|
||||
(js-expression-test 'hash-ref (js/expression (hash-ref (hash 'a 1 'b 2) 'b)) "2")
|
||||
(js-expression-test 'substring (js/expression (substring "abcdef" 1 4)) "\"bcd\"")
|
||||
(js-expression-test 'equal-list (js/expression (equal? (list 1 2) (list 1 2))) "true")
|
||||
(js-expression-test 'cond-test-only (js/expression (cond [0] [else 2])) "0")
|
||||
(js-expression-test 'cond-arrow (js/expression (cond [(+ 1 2) => (lambda (x) (+ x 10))] [else 0])) "13")
|
||||
(js-expression-test 'cond-false-arrow (js/expression (cond [#f => (lambda (x) (+ x 10))] [else 7])) "7")
|
||||
(js-expression-test 'rest-lambda (js/expression ((lambda xs (length xs)) 1 2 3)) "3")
|
||||
(js-expression-test 'dotted-lambda (js/expression ((lambda (a . xs) (+ a (length xs))) 10 20 30)) "12")
|
||||
(js-expression-test 'let-values-one (js/expression (let-values ([(x) 5]) (+ x 1))) "6")
|
||||
(js-expression-test 'let-values-many (js/expression (let-values ([(x y) (values 2 3)]) (+ x y))) "5")
|
||||
(js-expression-test 'let-tdz (js/expression (let ([x 4]) (let ([x x]) (+ x 1)))) "5")
|
||||
(js-expression-test 'let-star-tdz (js/expression (let ([x 4]) (let* ([x x] [y x]) (+ x y)))) "8")
|
||||
(js-expression-test 'division-normal
|
||||
(js/expression (/ 20 2 2))
|
||||
"5")
|
||||
(js-expression-test 'with-handlers-division-by-zero
|
||||
(js/expression (with-handlers ([exn? (lambda args (string-append "caught:" (exn-message (car args))))])
|
||||
(/ 10 0)))
|
||||
"\"caught:division by zero\"")
|
||||
(js-expression-test 'with-handlers-generic-exn
|
||||
(js/expression (with-handlers ([exn? (lambda (e) (string-append "caught:" (exn-message e)))])
|
||||
(error "boom")))
|
||||
"\"caught:boom\"")
|
||||
(js-expression-test 'with-handlers-no-error
|
||||
(js/expression (with-handlers ([exn? (lambda (e) 99)])
|
||||
(+ 20 22)))
|
||||
"42")
|
||||
(js-expression-test 'gregor-prefix-date-string
|
||||
(js/expression (date->string (foo:date 2026 5 25)))
|
||||
"\"2026-05-25\"")
|
||||
(js-expression-test 'gregor-prefix-time-string
|
||||
(js/expression (time->string (bar:time 8 9 10)))
|
||||
"\"08:09:10\"")
|
||||
(js-expression-test 'gregor-prefix-moment-fields
|
||||
(js/expression (list (baz:->year (baz:parse-moment "2026-05-25T08:09:10" "yyyy-MM-dd'T'HH:mm:ss"))
|
||||
(baz:->month (baz:parse-moment "2026-05-25T08:09:10"))
|
||||
(baz:->day (baz:parse-moment "2026-05-25T08:09:10"))))
|
||||
"[2026,5,25]")
|
||||
(js-expression-test 'gregor-js-date-conversion
|
||||
(js/expression (list (q:->year (js-date->datetime (q:->js-date (q:date 2026 5 25))))
|
||||
(q:date? (q:date 2026 5 25))
|
||||
(q:moment? (q:moment 2026 5 25 8 9 10))))
|
||||
"[2026,true,true]")))
|
||||
|
||||
(define engine (find-js-engine))
|
||||
(run-jsmaker-regression 'jsmaker-core-regression tests "/tmp/jsmaker-core-regression.js" #:engine engine)
|
||||
@@ -0,0 +1,7 @@
|
||||
#lang racket/base
|
||||
|
||||
(require "jsmaker-regression.rkt"
|
||||
"jsmaker-regexp-regression.rkt"
|
||||
"jsmaker-program-regression.rkt"
|
||||
"jsmaker-dom-exercises.rkt"
|
||||
"jsmaker-usecases.rkt")
|
||||
@@ -0,0 +1,88 @@
|
||||
#lang racket/base
|
||||
|
||||
(require racket/format
|
||||
racket/string
|
||||
"jsmaker-executors.rkt")
|
||||
|
||||
(provide js-expression-test
|
||||
js-program-test
|
||||
write-js-test-file
|
||||
run-jsmaker-regression
|
||||
warning-line)
|
||||
|
||||
(define (warning-line who fmt . args)
|
||||
(displayln (string-append "WARNING: " (apply format fmt args))
|
||||
(current-error-port)))
|
||||
|
||||
(define (js-expression-test name expr expected)
|
||||
(list name expr expected))
|
||||
|
||||
(define (js-program-test name program check-expr expected #:preamble [preamble ""])
|
||||
(list name
|
||||
(format "(() => {~a~a~areturn (~a); })()"
|
||||
(if (string=? preamble "") "" (string-append "\n" preamble "\n"))
|
||||
program
|
||||
(if (regexp-match? #rx"\n$" program) "" "\n")
|
||||
check-expr)
|
||||
expected))
|
||||
|
||||
(define (write-js-test-file js-path tests)
|
||||
(call-with-output-file js-path
|
||||
#:exists 'replace
|
||||
(lambda (out)
|
||||
(displayln "const tests = [];" out)
|
||||
(displayln "const __normalize = (v) => v === undefined ? 'undefined' : JSON.stringify(v);" out)
|
||||
(displayln "const __print = (...xs) => {" out)
|
||||
(displayln " if (typeof console !== 'undefined' && console.log) console.log(...xs);" out)
|
||||
(displayln " else if (typeof print === 'function') print(xs.join(' '));" out)
|
||||
(displayln "};" out)
|
||||
(for ([t tests])
|
||||
(define name (symbol->string (car t)))
|
||||
(define expr (cadr t))
|
||||
(define expected (caddr t))
|
||||
(fprintf out "tests.push([~s, () => (~a), ~s]);\n" name expr expected))
|
||||
(displayln "(async () => {" out)
|
||||
(displayln " let failed = 0;" out)
|
||||
(displayln " for (const [name, thunk, expected] of tests) {" out)
|
||||
(displayln " let value;" out)
|
||||
(displayln " try { value = await Promise.resolve(thunk()); }" out)
|
||||
(displayln " catch (e) { value = { __thrown: String(e && e.message !== undefined ? e.message : e) }; }" out)
|
||||
(displayln " const actual = __normalize(value);" out)
|
||||
(displayln " const ok = actual === expected;" out)
|
||||
(displayln " __print((ok ? 'ok' : 'FAIL') + '\\t' + name + '\\t' + actual);" out)
|
||||
(displayln " if (!ok) failed++;" out)
|
||||
(displayln " }" out)
|
||||
(displayln " if (failed !== 0) throw new Error(failed + ' jsmaker test(s) failed');" out)
|
||||
(displayln "})().catch((e) => {" out)
|
||||
(displayln " __print(e && e.stack ? e.stack : String(e));" out)
|
||||
(displayln " if (typeof process !== 'undefined' && process.exit) process.exit(1);" out)
|
||||
(displayln " throw e;" out)
|
||||
(displayln "});" out))))
|
||||
|
||||
(define (describe-engine engine)
|
||||
(define version (or (js-engine-version engine) "unknown version"))
|
||||
(format "~a at ~a (~a)"
|
||||
(js-engine-name engine)
|
||||
(path->string (js-engine-path engine))
|
||||
version))
|
||||
|
||||
(define (run-jsmaker-regression who tests js-path #:engine [engine (find-js-engine)])
|
||||
(write-js-test-file js-path tests)
|
||||
(cond
|
||||
[engine
|
||||
(printf "~a: using JavaScript engine ~a\n" who (describe-engine engine))
|
||||
(define result (run-js-file engine js-path))
|
||||
(display (js-run-result-stdout result))
|
||||
(unless (string=? (js-run-result-stderr result) "")
|
||||
(display (js-run-result-stderr result) (current-error-port)))
|
||||
(unless (js-run-result-success? result)
|
||||
(error who "JavaScript regression failed with ~a; see ~a"
|
||||
(js-engine-name engine) js-path))]
|
||||
[else
|
||||
(warning-line who "No JavaScript engine was found; regression tests were generated but not executed.")
|
||||
(warning-line who "Generated JavaScript test file: ~a" js-path)
|
||||
(warning-line who "Tried engines: ~a" (string-join (map symbol->string (known-js-engine-names)) ", "))
|
||||
(warning-line who "Set JSMAKER_ENGINE to auto/node/deno/bun/qjs/d8/jsc/js/chromium or set JSMAKER_ENGINE_PATH.")
|
||||
(warning-line who "For backwards compatibility, JSMAKER_NODE can point to a Node executable.")
|
||||
(when (or (getenv "JSMAKER_REQUIRE_ENGINE") (getenv "JSMAKER_REQUIRE_NODE"))
|
||||
(error who "JavaScript engine required by environment setting"))]))
|
||||
@@ -0,0 +1,12 @@
|
||||
#lang racket/base
|
||||
|
||||
(require "jsmaker-test-framework.rkt")
|
||||
|
||||
(provide run-jsmaker-node-regression)
|
||||
|
||||
;; Compatibility wrapper for the older single-engine test runner name.
|
||||
;; It now delegates to the generic framework/executor layer. When no engine
|
||||
;; is available, the framework generates the JavaScript test file and reports a
|
||||
;; skip/warning unless JSMAKER_REQUIRE_ENGINE or JSMAKER_REQUIRE_NODE is set.
|
||||
(define (run-jsmaker-node-regression who tests js-path)
|
||||
(run-jsmaker-regression who tests js-path))
|
||||
@@ -0,0 +1,191 @@
|
||||
#lang racket/base
|
||||
|
||||
(require json
|
||||
"../main.rkt"
|
||||
"jsmaker-test-framework.rkt"
|
||||
"jsmaker-executors.rkt"
|
||||
"../demo/js-usecases.rkt")
|
||||
|
||||
(define timer-preamble
|
||||
#<<JS
|
||||
const intervals = {};
|
||||
let nextIntervalId = 1;
|
||||
function setInterval(cb, ms) {
|
||||
const id = nextIntervalId++;
|
||||
intervals[id] = { cb, ms, active: true };
|
||||
return id;
|
||||
}
|
||||
function clearInterval(id) {
|
||||
if (intervals[id]) intervals[id].active = false;
|
||||
}
|
||||
function runInterval(id, times) {
|
||||
for (let i = 0; i < times; i++) {
|
||||
if (!intervals[id] || !intervals[id].active) break;
|
||||
intervals[id].cb();
|
||||
}
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
(define dom-preamble
|
||||
#<<JS
|
||||
const body = { innerHTML: "<p>A</p>" };
|
||||
const root = { innerHTML: "<p>A</p>" };
|
||||
const document = {
|
||||
body,
|
||||
querySelector: (selector) => selector === "body" ? body : false,
|
||||
getElementById: (id) => id === "root" ? root : false
|
||||
};
|
||||
JS
|
||||
)
|
||||
|
||||
(define fetch-preamble
|
||||
#<<JS
|
||||
function fetch(url) {
|
||||
if (url === "/ok") {
|
||||
return Promise.resolve({
|
||||
json: () => Promise.resolve({ title: "Done" })
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error("network"));
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
(define tests
|
||||
(list
|
||||
(js-program-test
|
||||
'use-random-number-1-to-5
|
||||
usecase-random-number
|
||||
(js/expression (randomBetween1And5))
|
||||
(jsexpr->string 5)
|
||||
#:preamble "Math.random = () => 0.80;")
|
||||
|
||||
(js-program-test
|
||||
'use-unique-values-set
|
||||
usecase-unique-values
|
||||
(js/expression (uniqueValues (array 1 2 2 3 1)))
|
||||
(jsexpr->string '(1 2 3)))
|
||||
|
||||
(js-program-test
|
||||
'use-six-falsey-values
|
||||
usecase-falsey-values
|
||||
(js/expression (send (falseyValues) map (lambda (x) (return (Boolean x)))))
|
||||
(jsexpr->string '(#f #f #f #f #f #f)))
|
||||
|
||||
(js-program-test
|
||||
'use-currying-simple
|
||||
usecase-currying
|
||||
(js/expression ((add 2) 3))
|
||||
(jsexpr->string 5))
|
||||
|
||||
(js-program-test
|
||||
'use-object-destructuring
|
||||
usecase-object-destructuring
|
||||
(js/expression (describePerson (object 'name "Ada" 'age 37)))
|
||||
(jsexpr->string "Ada:37"))
|
||||
|
||||
(js-program-test
|
||||
'use-timer-clear-interval
|
||||
usecase-timer-interval
|
||||
(js/expression
|
||||
(let* ([t (startTimer)])
|
||||
(runInterval t.id 5)
|
||||
(array t.id (send t getTicks) (js-dot (js-ref intervals t.id) active))))
|
||||
(jsexpr->string '(1 3 #f))
|
||||
#:preamble timer-preamble)
|
||||
|
||||
(js-program-test
|
||||
'use-object-get-set-delete-prop
|
||||
usecase-object-props
|
||||
(js/expression (objectProps))
|
||||
(jsexpr->string '(1 1 1 2 3 #f)))
|
||||
|
||||
(js-program-test
|
||||
'use-string-concat-order
|
||||
usecase-string-concat-order
|
||||
(js/expression (concatOrder))
|
||||
(jsexpr->string '("33" "123")))
|
||||
|
||||
(js-program-test
|
||||
'use-freeze-vs-seal
|
||||
usecase-freeze-vs-seal
|
||||
(js/expression (freezeVsSeal))
|
||||
(jsexpr->string '(1 9 #t #t #t)))
|
||||
|
||||
(js-program-test
|
||||
'use-switch-example
|
||||
usecase-switch
|
||||
(js/expression (array (switchExample 1) (switchExample 2) (switchExample 9)))
|
||||
(jsexpr->string '("one" "two-or-three" "other")))
|
||||
|
||||
(js-program-test
|
||||
'use-class-constructor-default
|
||||
usecase-class-constructor
|
||||
(js/expression (classExample))
|
||||
(jsexpr->string '("Hello world" "Hello Ada")))
|
||||
|
||||
(js-program-test
|
||||
'use-sort-objects-by-property
|
||||
usecase-sort-objects-by-property
|
||||
(js/expression
|
||||
(send (sortByProperty (array (object 'age 30) (object 'age 20) (object 'age 25)) "age")
|
||||
map
|
||||
(lambda (x) (return x.age))))
|
||||
(jsexpr->string '(20 25 30)))
|
||||
|
||||
(js-program-test
|
||||
'use-delete-array-elements-four-ways
|
||||
usecase-delete-array-elements
|
||||
(js/expression (deleteArrayWays (array "a" "b" "c")))
|
||||
(jsexpr->string '(("a" "c") ("a" "c") ("a" "c") (#f 3))))
|
||||
|
||||
(js-program-test
|
||||
'use-bubble-sort
|
||||
usecase-bubble-sort
|
||||
(js/expression (bubbleSort (array 5 1 4 2 8)))
|
||||
(jsexpr->string '(1 2 4 5 8)))
|
||||
|
||||
(js-program-test
|
||||
'use-binary-search-recursive
|
||||
usecase-binary-search
|
||||
(js/expression
|
||||
(array (binarySearch (array 1 3 5 7 9) 7 0 4)
|
||||
(binarySearch (array 1 3 5 7 9) 4 0 4)))
|
||||
(jsexpr->string '(3 -1)))
|
||||
|
||||
(js-program-test
|
||||
'use-map-count-occurrences
|
||||
usecase-map-count-occurrences
|
||||
(js/expression (countOccurrences (array "a" "b" "a" "c" "b" "a")))
|
||||
(jsexpr->string '(("a" 3) ("b" 2) ("c" 1))))
|
||||
|
||||
(js-program-test
|
||||
'use-get-html-three-ways
|
||||
usecase-get-html-three-ways
|
||||
(js/expression (getHtmlThreeWays))
|
||||
(jsexpr->string '("<p>A</p>" "<p>A</p>" "<p>A</p>"))
|
||||
#:preamble dom-preamble)
|
||||
|
||||
(js-program-test
|
||||
'use-anagram
|
||||
usecase-anagram
|
||||
(js/expression (array (canArrange "listen" "silent")
|
||||
(canArrange "abc" "abd")))
|
||||
(jsexpr->string '(#t #f)))
|
||||
|
||||
(js-program-test
|
||||
'use-pairs-equal-target
|
||||
usecase-pairs-equal-target
|
||||
(js/expression (pairsEqualTarget (array 1 2 3 4 3 5) 6))
|
||||
(jsexpr->string '((2 4) (3 3) (1 5))))
|
||||
|
||||
(js-program-test
|
||||
'use-fetch-api-results-errors
|
||||
usecase-fetch-api
|
||||
(js/expression (send Promise all (array (loadTitle "/ok") (loadTitle "/fail"))))
|
||||
"[{\"ok\":true,\"title\":\"Done\"},{\"ok\":false,\"message\":\"network\"}]"
|
||||
#:preamble fetch-preamble)))
|
||||
|
||||
(define engine (find-js-engine))
|
||||
(run-jsmaker-regression 'jsmaker-usecases tests "/tmp/jsmaker-usecases.js" #:engine engine)
|
||||
Reference in New Issue
Block a user