A mostly AI coded js-maker, supervised by me.

This commit is contained in:
2026-05-26 09:42:35 +02:00
parent 8e8afc321b
commit 2cf831c180
23 changed files with 4143 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
*.html
*.js
*.css
*.bak
*.scrbl~
+245
View File
@@ -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.
+584
View File
@@ -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\"}.")