This commit is contained in:
2026-06-08 13:21:57 +02:00
parent 823130e3ac
commit 8bee76328b
23 changed files with 734 additions and 382 deletions
+16 -6
View File
@@ -1,12 +1,22 @@
RACKET ?= racket
RACO ?= raco RACO ?= raco
.PHONY: test setup docs COLLECTION := js-maker
.PHONY: all test docs clean very-clean
all:
$(RACO) make main.rkt testing/jsmaker-regressions.rkt scrbl/js-maker.scrbl scrbl/usecases.scrbl
test: test:
$(RACO) test -p js-maker $(RACKET) testing/jsmaker-regressions.rkt
setup:
$(RACO) setup js-maker
docs: docs:
$(RACO) scribble --html --dest doc scrbl/js-maker.scrbl scrbl/usecases.scrbl $(RACO) scribble --htmls --dest rendered-docs scrbl/js-maker.scrbl scrbl/usecases.scrbl
clean:
find . -type d -name compiled -prune -exec rm -rf {} +
find . -type f -name '*~' -delete
find . -type f -name '*.bak' -delete
rm -rf rendered-docs
+59 -6
View File
@@ -1,13 +1,66 @@
# js-maker # js-maker
js-maker is a deliberately small syntax-driven Racket-to-JavaScript string `js-maker` is a deliberately small Racket macro for generating JavaScript text
maker. The public API is intentionally one macro: from a compact Racket/Scheme-like surface syntax.
The public API is intentionally small:
```racket ```racket
(require js-maker) (require js-maker)
(js (define (square x) (return (* x x))))
(js form ...)
``` ```
js-maker 3 keeps the implementation compact and supports ordinary `let`, There is no public `js1` and no `js/expression`. Expression-oriented examples
`let*`, and tail-recursive named `let` loops while preserving Racket binding should be written as normal JavaScript-producing programs, usually by placing an
semantics. Demos are in `demo/`; regression tests are in `testing/`. explicit `(return ...)` in a generated function.
## Supported core forms
The js-maker 3 restart supports the following core forms:
- `(define (name arg ...) body ...)`
- `(define name expr)`
- `(lambda (arg ...) body ...)` and `(λ (arg ...) body ...)`
- `(if condition then else)`
- `(begin body ...)`
- `(return expr)`
- `(set! target expr)`
- ordinary `let` with parallel binding semantics
- `let*` with sequential binding semantics
- named `let`, compiled to `while (true)` with parallel loop-variable updates
- quote/eval for simple datum insertion
- calls, infix arithmetic/comparison operators, `and`, `or`, `not`
- `(send obj method arg ...)`
- `(new Class arg ...)`
- `(js-dot obj field ...)` / `(dot obj field ...)`
- `(js-ref obj key ...)` for JavaScript bracket/index access
- `(list ...)` and `(cons a b)`
`js-ref` is a DSL form inside `(js ...)`, not a separately exported binding.
For example, `(js-ref xs i)` generates `xs[i]`, `(js-ref obj "name")`
generates `obj["name"]`, and `(set! (js-ref xs i) value)` generates an
indexed assignment.
Named `let` is statement-oriented. A tail call to the loop name continues the
loop. The branch that leaves the loop should use an explicit `(return ...)`.
## Running tests
From the package root:
```sh
raco test -p js-maker
```
If Node is available, several generated JavaScript programs are executed. If no
Node executable is found, those runtime checks are skipped and the string-level
checks still run.
## What was removed from the old test set
The previous larger branch contained tests and demos for a broader language and
runtime shim. Those are not part of the compact js-maker 3 restart. The old
hash, regexp, `with-handlers`, `for/list`, `for/fold`, `while`, `when`, `cond`,
`case`, object/class/destructuring and runtime helper tests were removed or
replaced by tests for the supported core forms above.
+35 -6
View File
@@ -1,9 +1,38 @@
let title = document.getElementById("title"); // exercise01
title.innerHTML = "Hello from js-maker"; function replaceParagraphHtml(html) {
title.addEventListener("click", function (evt) {
{ {
console.log("clicked"); const p3 = document.querySelector("p");
return true; {
let p = p3;
p.innerHTML = html;
return p.innerHTML;
}
}
} }
});
// exercise02
function addSourceLink() {
{
const p8 = document.querySelector("p");
{
let p = p8;
p.insertAdjacentHTML("afterend", "<a href=\"https://forcemipsum.com/\">Source</a>");
return true;
}
}
}
// exercise03
function paragraphText() {
{
const p15 = document.querySelector("p");
{
let p = p15;
return p.textContent;
}
}
}
+43 -10
View File
@@ -2,17 +2,50 @@
(require "../main.rkt") (require "../main.rkt")
(provide generated-js) (provide exercise01
exercise02
exercise03
all-dom-exercises
show-dom-exercises)
(define generated-js ;; These demos intentionally use only the compact js-maker 3 surface language:
;; define, let/let*, return, set!, send, js-dot and ordinary function calls.
;; Exercise 01: replace the paragraph HTML.
(define exercise01
(js (js
(define title (send document getElementById "title")) (define (replaceParagraphHtml html)
(set! (js-dot title innerHTML) "Hello from js-maker") (let ([p (send document querySelector "p")])
(send title addEventListener "click" (set! (js-dot p innerHTML) html)
(lambda (evt) (return (js-dot p innerHTML))))))
(begin
(send console log "clicked") ;; Exercise 02: add a source link after the paragraph tag.
(return #t)))))) (define exercise02
(js
(define (addSourceLink)
(let ([p (send document querySelector "p")])
(send p insertAdjacentHTML
"afterend"
"<a href=\"https://forcemipsum.com/\">Source</a>")
(return #t)))))
;; Exercise 03: read text content from the first paragraph.
(define exercise03
(js
(define (paragraphText)
(let ([p (send document querySelector "p")])
(return (js-dot p textContent))))))
(define all-dom-exercises
`((exercise01 . ,exercise01)
(exercise02 . ,exercise02)
(exercise03 . ,exercise03)))
(define (show-dom-exercises)
(for ([entry (in-list all-dom-exercises)])
(displayln (format "// ~a" (car entry)))
(displayln (cdr entry))
(newline)))
(module+ main (module+ main
(display generated-js)) (show-dom-exercises))
+50 -12
View File
@@ -1,25 +1,63 @@
let answer = 42; // random-number
function square(x) { function randomBetween1And5() {
return x * x; return Math.floor(Math.random() * 5) + 1;
} }
function sum_to(n) {
// unique-values
function uniqueValues(xs) {
return Array.from(new Set(xs));
}
// array-at
function arrayAt(xs, i) {
return xs[i];
}
// sum-to
function sumTo(n) {
{ {
const i7 = 0; const i20 = 0;
const acc8 = 0; const acc21 = 0;
{ {
let i = i7; let i = i20;
let acc = acc8; let acc = acc21;
while (true) { while (true) {
if (i > n) { if (i > n) {
return acc; return acc;
} else { } else {
const i11 = i + 1; const i24 = i + 1;
const acc12 = acc + i; const acc25 = acc + i;
i = i11; i = i24;
acc = acc12; acc = acc25;
continue; continue;
} }
} }
} }
} }
} }
// make-adder
function makeAdder(x) {
return function (y) {
return x + y;
};
}
// set-html
function setHtml(id, html) {
{
const el38 = document.getElementById(id);
{
let el = el38;
el.innerHTML = html;
return el.innerHTML;
}
}
}
+69 -7
View File
@@ -2,18 +2,80 @@
(require "../main.rkt") (require "../main.rkt")
(provide generated-js) (provide usecase-random-number
usecase-unique-values
usecase-array-at
usecase-sum-to
usecase-make-adder
usecase-set-html
all-js-usecases
show-js-usecases
write-js-usecases-file)
(define generated-js ;; Use case 01: generate a random integer between 1 and 5.
(define usecase-random-number
(js (js
(define answer 42) (define (randomBetween1And5)
(define (square x) (return (+ (send Math floor (* (send Math random) 5)) 1)))))
(return (* x x)))
(define (sum-to n) ;; 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: indexed array access with js-ref.
(define usecase-array-at
(js
(define (arrayAt xs i)
(return (js-ref xs i)))))
;; Use case 04: named let as a loop. The branch that leaves the loop uses an
;; explicit return, because js-maker 3 is statement-oriented.
(define usecase-sum-to
(js
(define (sumTo n)
(let loop ([i 0] [acc 0]) (let loop ([i 0] [acc 0])
(if (> i n) (if (> i n)
(return acc) (return acc)
(loop (+ i 1) (+ acc i))))))) (loop (+ i 1) (+ acc i)))))))
;; Use case 05: return a JavaScript function value.
(define usecase-make-adder
(js
(define (makeAdder x)
(return (lambda (y)
(return (+ x y)))))))
;; Use case 06: small DOM setter. It uses send, js-dot and set!.
(define usecase-set-html
(js
(define (setHtml id html)
(let ([el (send document getElementById id)])
(set! (js-dot el innerHTML) html)
(return (js-dot el innerHTML))))))
(define all-js-usecases
`((random-number . ,usecase-random-number)
(unique-values . ,usecase-unique-values)
(array-at . ,usecase-array-at)
(sum-to . ,usecase-sum-to)
(make-adder . ,usecase-make-adder)
(set-html . ,usecase-set-html)))
(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 "demo/js-usecases.generated.js"])
(call-with-output-file path #:exists 'replace
(lambda (out)
(for ([entry (in-list all-js-usecases)])
(displayln (format "// ~a" (car entry)) out)
(displayln (cdr entry) out)
(newline out)))))
(module+ main (module+ main
(display generated-js)) (show-js-usecases))
+22 -7
View File
@@ -1,11 +1,26 @@
#lang racket/base #lang racket/base
(require (prefix-in use: "js-usecases.rkt") (require "../main.rkt")
(prefix-in dom: "dom-exercises.rkt"))
(define examples
`((simple-function . ,(js (define (add1 x) (return (+ x 1)))))
(ordinary-let . ,(js (define (ordinaryLet x)
(let ([x 1] [y x])
(return y)))))
(let-star . ,(js (define (sequentialLet x)
(let* ([x 1] [y x])
(return y)))))
(named-let . ,(js (define (sumTo n)
(let loop ([i 0] [acc 0])
(if (> i n)
(return acc)
(loop (+ i 1) (+ acc i)))))))))
(define (show-examples)
(for ([entry (in-list examples)])
(displayln (format "// ~a" (car entry)))
(displayln (cdr entry))
(newline)))
(module+ main (module+ main
(displayln ";; js-usecases") (show-examples))
(display use:generated-js)
(newline)
(displayln ";; dom-exercises")
(display dom:generated-js))
+11 -8
View File
@@ -2,12 +2,15 @@
(require "../main.rkt") (require "../main.rkt")
;; There is no separate optimizer in js-maker 3. This demo shows the compact ;; js-maker 3 does not have a separate optimizer pass. The notable direct
;; named-let loop output produced directly by the `js` macro. ;; lowering is named let to a while(true) loop with parallel updates.
(module+ main (define optimized-example
(display (js
(js (define (factorial n) (define (sumTo n)
(let loop ([i n] [acc 1]) (let loop ([i 0] [acc 0])
(if (<= i 1) (if (> i n)
(return acc) (return acc)
(loop (- i 1) (* acc i)))))))) (loop (+ i 1) (+ acc i)))))))
(module+ main
(displayln optimized-example))
+4 -7
View File
@@ -5,17 +5,14 @@
(define license 'MIT) (define license 'MIT)
(define pkg-desc "A small syntax-driven Racket-to-JavaScript maker macro.") (define pkg-desc "A small syntax-driven Racket-to-JavaScript maker macro.")
(define pkg-authors '(hnmdijkema)) (define pkg-authors '(hnmdijkema))
(define deps '("base")) (define deps '("base"))
(define build-deps '("scribble-lib" "racket-doc" "rackunit-lib")) (define build-deps '("scribble-lib" "racket-doc"))
(define tags '("javascript" "macro" "racket"))
(define scribblings (define scribblings
'(("scrbl/js-maker.scrbl" () (library)) '(("scrbl/js-maker.scrbl" () (library))
("scrbl/usecases.scrbl" () (library)))) ("scrbl/usecases.scrbl" () (library))))
;; Keep the test entry point explicit. The supporting regression modules are ;; The public package test entry point. Support modules and demos are still
;; required by this runner and compile during package setup. ;; compiled by raco setup, but tests are launched through this maintained suite.
(define test-include-paths '("testing/jsmaker-regressions.rkt")) (define test-include-paths '("testing/jsmaker-regressions.rkt"))
(define test-omit-paths
'("testing/jsmaker-executors.rkt"
"testing/jsmaker-test-framework.rkt"))
+8
View File
@@ -108,6 +108,13 @@
[(_ obj field) (string-append (js1 obj) "." (js-id field))] [(_ obj field) (string-append (js1 obj) "." (js-id field))]
[(_ obj field rest ...) (js-dot* (js-dot* obj field) rest ...)])) [(_ obj field rest ...) (js-dot* (js-dot* obj field) rest ...)]))
(define-syntax js-ref*
(syntax-rules ()
[(_ obj key) (string-append (js1 obj) "[" (js1 key) "]")]
[(_ obj key rest ...)
(string-append (js-ref* obj key)
(string-append "[" (js1 rest) "]") ...)]))
(define-syntax js-send (define-syntax js-send
(syntax-rules () (syntax-rules ()
[(_ obj method) (string-append (js1 obj) "." (js-id method) "()")] [(_ obj method) (string-append (js1 obj) "." (js-id method) "()")]
@@ -295,6 +302,7 @@
[(eq? d 'list) #'(js-array arg ...)] [(eq? d 'list) #'(js-array arg ...)]
[(eq? d 'cons) #'(js-cons arg ...)] [(eq? d 'cons) #'(js-cons arg ...)]
[(or (eq? d 'js-dot) (eq? d 'dot)) #'(js-dot* arg ...)] [(or (eq? d 'js-dot) (eq? d 'dot)) #'(js-dot* arg ...)]
[(eq? d 'js-ref) #'(js-ref* arg ...)]
[(eq? d 'new) #'(js-new arg ...)] [(eq? d 'new) #'(js-new arg ...)]
[(identifier? #'op) #'(js-call op arg ...)] [(identifier? #'op) #'(js-call op arg ...)]
[else (raise-syntax-error 'js1 "unsupported compound expression" stx #'op)]))] [else (raise-syntax-error 'js1 "unsupported compound expression" stx #'op)]))]
+86 -49
View File
@@ -3,82 +3,119 @@
@(require (for-label racket/base js-maker)) @(require (for-label racket/base js-maker))
@title{js-maker} @title{js-maker}
@author{Hans Dijkema} @author+email["Hans Dijkema" "hans@dijkewijk.nl"]
@defmodule[js-maker] @defmodule[js-maker]
@emph{js-maker} is a deliberately small syntax-driven JavaScript string maker. @racketmodname[js-maker] provides a deliberately small macro for generating
It provides one public macro, @racket[js]. The helper machinery used to render JavaScript text from a compact Racket/Scheme-like syntax. The public API is
subexpressions is private to the package. intentionally limited to @racket[js]. The lower-level dispatcher used by the
implementation is internal and is not exported.
@section{Public API} @section{Public API}
@defform[(js form ...)]{ @defform[(js form ...)]{
Produces a JavaScript string for the supplied forms. Each top-level form is Generates a JavaScript program fragment as a string.
rendered as a JavaScript statement. The macro is intended for small generated
snippets, demos, and controlled code generation; it is not a complete Racket to
JavaScript compiler.
Examples: Each @racket[form] is translated and emitted as a JavaScript statement. The
macro is statement-oriented. When a value must leave a generated function, use
an explicit @racket[(return expr)] form.
@racketblock[ @codeblock{
(js (+ 1 2)) (js
(js (define answer 42)) (define (add1 x)
(js (define (square x) (return (+ x 1))))
(return (* x x)))) }
]
} }
@section{Supported core forms} There is no public @racket[js/expression] form in js-maker 3. Expression-style
checks can be written by generating a function and using @racket[(return ...)]
inside that function.
The compact js-maker 3 implementation supports: @section{Supported forms}
The compact implementation supports:
@itemlist[ @itemlist[
@item{@racket[define] for values and functions.} @item{@racket[(define (name arg ...) body ...)]}
@item{@racket[lambda] and @racket[lambda]-style generated JavaScript functions.} @item{@racket[(define name expr)]}
@item{@racket[if], @racket[begin], @racket[set!], and explicit @racket[return].} @item{@racket[(lambda (arg ...) body ...)] and @racket[(λ (arg ...) body ...)]}
@item{Ordinary @racket[let] with parallel binding semantics.} @item{@racket[(if condition then else)]}
@item{@racket[let*] with sequential binding semantics.} @item{@racket[(begin body ...)]}
@item{Named @racket[let] in tail-recursive loop style.} @item{@racket[(return expr)]}
@item{Common infix operators such as @racket[+], @racket[-], @racket[*], @item{@racket[(set! target expr)]}
@racket[/], comparisons, @racket[and], @racket[or], and @racket[not].} @item{ordinary @racket[let], with parallel binding semantics}
@item{@racket[list], @racket[cons], @racket[send], @racket[js-dot], and @item{@racket[let*], with sequential binding semantics}
@racket[new].} @item{named @racket[let], compiled as a @tt{while (true)} loop}
@item{@racket[quote] and @racket[eval] for simple datum insertion}
@item{ordinary calls and the common infix operators @racket[+], @racket[-], @racket[*], @racket[/], @racket[>], @racket[<], @racket[>=], @racket[<=], @racket[==], @racket[===], @racket[!=], @racket[!==]}
@item{@racket[and], @racket[or] and @racket[not]}
@item{@racket[(send obj method arg ...)]}
@item{@racket[(new Class arg ...)]}
@item{@racket[(js-dot obj field ...)] and @racket[(dot obj field ...)]}
@item{@racket[(js-ref obj key ...)], for JavaScript bracket/index access}
@item{@racket[(list ...)] and @racket[(cons a b)]}
] ]
@section{Let semantics} @section{Indexed access}
Ordinary @racket[let] evaluates all right-hand sides before introducing the new @racket[js-ref] is a form understood by the @racket[js] macro. It is not
bindings. js-maker preserves that behavior by emitting temporary JavaScript exported as a separate binding. It generates JavaScript bracket access, which
constants and then opening a nested block for the real @tt{let} bindings. works for arrays and computed object properties.
This avoids JavaScript temporal-dead-zone shadowing when a bound name is also
used by a right-hand side.
@racketblock[ @codeblock{
(js (define (ordinary-let x) (js
(define (arrayAt xs i)
(return (js-ref xs i))))
}
@codeblock{
(js
(define (nameOf obj)
(return (js-ref obj "name"))))
}
The same form can be used as a @racket[set!] target:
@codeblock{
(js
(define (put xs i value)
(set! (js-ref xs i) value)
(return xs)))
}
@section{Ordinary let}
Ordinary @racket[let] keeps Racket's parallel binding semantics. Initializers
are evaluated before the bound names are introduced. The generated JavaScript
therefore uses temporary constants and an inner block, so JavaScript's temporal
dead zone does not accidentally shadow initializer references.
@codeblock{
(js
(define (ordinaryLet x)
(let ([x 1] [y x]) (let ([x 1] [y x])
(return y)))) (return y))))
] }
The generated JavaScript returns the original argument as the value of
@tt{y}, matching Racket's ordinary @racket[let] semantics.
@section{Named let} @section{Named let}
Named @racket[let] is emitted as a @tt{while (true)} loop. A tail call to the Named @racket[let] is compiled to a loop. A tail call to the loop name is
loop name is translated to parallel update assignments followed by translated into parallel assignments to the loop variables followed by
@tt{continue}. The intended style is statement-oriented and uses explicit @tt{continue}. A branch that exits the loop should use @racket[(return ...)].
@racket[return] for terminating branches.
@racketblock[ @codeblock{
(js (define (sum-to n) (js
(define (sumTo n)
(let loop ([i 0] [acc 0]) (let loop ([i 0] [acc 0])
(if (> i n) (if (> i n)
(return acc) (return acc)
(loop (+ i 1) (+ acc i)))))) (loop (+ i 1) (+ acc i))))))
] }
@section{Package layout} @section{Tests and demos}
The package includes small demos under @filepath{demo/} and a regression suite The package contains maintained tests under @filepath{testing/} and small demos
under @filepath{testing/}. The public API remains just @racket[js]. under @filepath{demo/}. The old js-maker 2 tests for runtime shims and broader
language constructs have been removed or replaced where they did not apply to
the compact js-maker 3 surface language.
+50 -26
View File
@@ -2,42 +2,66 @@
@(require (for-label racket/base js-maker)) @(require (for-label racket/base js-maker))
@title{js-maker use cases} @title{js-maker Use Cases}
@author+email["Hans Dijkema" "hans@dijkewijk.nl"]
@section{Generating a small function} @defmodule[js-maker/demo/js-usecases]
@racketblock[ The demos in @filepath{demo/js-usecases.rkt} use only the public
(js (define (square x) @racket[js] macro and the compact js-maker 3 form set. They are intentionally
(return (* x x)))) small: their purpose is to show the supported surface language, not to recreate
] the larger runtime-helper branch.
This produces a JavaScript function declaration. Racket identifiers are mapped @section{Random number}
to JavaScript-friendly names by replacing unsupported characters with
underscores.
@section{Generating a loop} @codeblock{
(js
(define (randomBetween1And5)
(return (+ (send Math floor (* (send Math random) 5)) 1))))
}
@racketblock[ @section{Unique values}
(js (define (sum-to n)
@codeblock{
(js
(define (uniqueValues xs)
(return (send Array from (new Set xs)))))
}
@section{Indexed access}
@codeblock{
(js
(define (arrayAt xs i)
(return (js-ref xs i))))
}
@section{Named let loop}
@codeblock{
(js
(define (sumTo n)
(let loop ([i 0] [acc 0]) (let loop ([i 0] [acc 0])
(if (> i n) (if (> i n)
(return acc) (return acc)
(loop (+ i 1) (+ acc i)))))) (loop (+ i 1) (+ acc i))))))
] }
The named @racket[let] form is useful for simple loops while keeping ordinary @section{Function value}
Racket binding semantics for the initial values and loop updates.
@section{Generating DOM-style calls} @codeblock{
@racketblock[
(js (js
(define title (send document getElementById "title")) (define (makeAdder x)
(set! (js-dot title innerHTML) "Hello") (return (lambda (y)
(send title addEventListener "click" (return (+ x y))))))
(lambda (evt) (return #t)))) }
]
This is string generation only. The generated JavaScript must still be run in a @section{DOM setter}
JavaScript environment that provides the referenced objects, such as
@tt{document}. @codeblock{
(js
(define (setHtml id html)
(let ([el (send document getElementById id)])
(set! (js-dot el innerHTML) html)
(return (js-dot el innerHTML)))))
}
+7 -20
View File
@@ -1,25 +1,12 @@
#lang racket/base #lang racket/base
(require rackunit (require "../demo/dom-exercises.rkt"
"../main.rkt"
"jsmaker-test-framework.rkt") "jsmaker-test-framework.rkt")
(provide dom-tests) (check-contains 'dom-query-selector "document.querySelector(\"p\")" exercise01)
(check-contains 'dom-inner-html "p.innerHTML" exercise01)
(check-contains 'dom-insert-adjacent-html "insertAdjacentHTML" exercise02)
(check-contains 'dom-text-content "textContent" exercise03)
(define dom-snippet (module+ main
(js (test-summary 'jsmaker-dom-exercises))
(define title (send document getElementById "title"))
(set! (js-dot title innerHTML) "Hello")
(send title addEventListener "click" (lambda (evt) (return #t)))))
(define dom-tests
(test-suite
"DOM-like JavaScript generation"
(test-case "send and js-dot generate method calls and property assignment"
(check-js-contains? dom-snippet "document.getElementById(\"title\")")
(check-js-contains? dom-snippet "title.innerHTML = \"Hello\";")
(check-js-contains? dom-snippet "title.addEventListener"))))
(module+ test
(require rackunit/text-ui)
(run-tests dom-tests))
+3 -2
View File
@@ -1,4 +1,5 @@
#lang racket/base #lang racket/base
(require "jsmaker-test-framework.rkt") ;; Kept as a compatibility source file for the package layout. js-maker 3 uses
(provide node-available? run-js/trimmed) ;; the small optional Node runner in jsmaker-test-framework.rkt.
(provide)
+4 -16
View File
@@ -1,18 +1,6 @@
#lang racket/base #lang racket/base
(require rackunit ;; The old js-maker 2 hash tests covered a runtime library that is intentionally
"../main.rkt" ;; not part of the compact js-maker 3 restart. See README.md for the retained
"jsmaker-test-framework.rkt") ;; and removed test categories.
(provide)
(provide object-tests)
(define object-tests
(test-suite
"object construction regression tests"
(test-case "new and method calls"
(check-js-equal? (js (new Date)) "new Date();\n")
(check-js-equal? (js (send console log "ok")) "console.log(\"ok\");\n"))))
(module+ test
(require rackunit/text-ui)
(run-tests object-tests))
+13 -15
View File
@@ -1,21 +1,19 @@
#lang racket/base #lang racket/base
(require rackunit (require "../main.rkt"
"../main.rkt"
"jsmaker-test-framework.rkt") "jsmaker-test-framework.rkt")
(provide list-tests) (define list-program
(string-append
(js (define (makeList) (return (list 1 2 3))))
"\nconsole.log(JSON.stringify(makeList()));\n"))
(run-js-if-available 'list-runtime list-program "[1,2,3]")
(define list-tests (define cons-program
(test-suite (string-append
"list and quoted datum generation" (js (define (prepend xs) (return (cons 1 xs))))
(test-case "list and cons" "\nconsole.log(JSON.stringify(prepend([2,3])));\n"))
(check-js-equal? (js (list 1 2 3)) "[1, 2, 3];\n") (run-js-if-available 'cons-runtime cons-program "[1,2,3]")
(check-js-equal? (js (cons 1 (list 2 3))) "[1].concat([2, 3]);\n"))
(test-case "quoted data"
(check-js-equal? (js (quote alpha)) "\"alpha\";\n")
(check-js-equal? (js (quote (1 2 x))) "[1, 2, \"x\"];\n"))))
(module+ test (module+ main
(require rackunit/text-ui) (test-summary 'jsmaker-list-regression))
(run-tests list-tests))
+41 -30
View File
@@ -1,51 +1,62 @@
#lang racket/base #lang racket/base
(require rackunit (require "../main.rkt"
"../main.rkt"
"jsmaker-test-framework.rkt") "jsmaker-test-framework.rkt")
(provide program-tests)
(define ordinary-let-program (define ordinary-let-program
(string-append (string-append
(js (define (ordinary-let x) (js (define (ordinaryLet x)
(let ([x 1] [y x]) (let ([x 1] [y x])
(return y)))) (return y))))
"console.log(ordinary_let(99));\n")) "\nconsole.log(JSON.stringify(ordinaryLet(99)));\n"))
(run-js-if-available 'ordinary-let-runtime ordinary-let-program "99")
(define sequential-let-program (define let-star-program
(string-append (string-append
(js (define (sequential-let x) (js (define (sequentialLet x)
(let* ([x 1] [y x]) (let* ([x 1] [y x])
(return y)))) (return y))))
"console.log(sequential_let(99));\n")) "\nconsole.log(JSON.stringify(sequentialLet(99)));\n"))
(run-js-if-available 'let-star-runtime let-star-program "1")
(define named-let-program (define named-let-program
(string-append (string-append
(js (define (sum-to n) (js (define (sumTo n)
(let loop ([i 0] [acc 0]) (let loop ([i 0] [acc 0])
(if (> i n) (if (> i n)
(return acc) (return acc)
(loop (+ i 1) (+ acc i)))))) (loop (+ i 1) (+ acc i))))))
"console.log(sum_to(10));\n")) "\nconsole.log(JSON.stringify(sumTo(10)));\n"))
(run-js-if-available 'named-let-runtime named-let-program "55")
(define program-tests (define ref-program
(test-suite (string-append
"generated JavaScript program behavior" (js (define (at xs i)
(test-case "ordinary let uses parallel Racket binding semantics" (return (js-ref xs i))))
(check-js-contains? ordinary-let-program "const") "\nconsole.log(JSON.stringify(at([10,20,30],1)));\n"))
(check-js-contains? ordinary-let-program "let x") (run-js-if-available 'ref-runtime ref-program "20")
(when (node-available?)
(check-equal? (run-js/trimmed ordinary-let-program) "99")))
(test-case "let* uses sequential binding semantics"
(when (node-available?)
(check-equal? (run-js/trimmed sequential-let-program) "1")))
(test-case "named let compiles to a while loop with parallel updates"
(check-js-contains? named-let-program "while (true)")
(check-js-contains? named-let-program "continue;")
(when (node-available?)
(check-equal? (run-js/trimmed named-let-program) "55")))))
(module+ test (define ref-set-program
(require rackunit/text-ui) (string-append
(run-tests program-tests)) (js (define (put xs i value)
(set! (js-ref xs i) value)
(return xs)))
"\nconsole.log(JSON.stringify(put([1,2,3],1,9)));\n"))
(run-js-if-available 'ref-set-runtime ref-set-program "[1,9,3]")
(define ref-string-key-program
(string-append
(js (define (nameOf obj)
(return (js-ref obj "name"))))
"\nconsole.log(JSON.stringify(nameOf({name:\"Ada\"})));\n"))
(run-js-if-available 'ref-string-key-runtime ref-string-key-program "\"Ada\"")
(define lambda-program
(string-append
(js (define (makeAdder x)
(return (lambda (y) (return (+ x y))))))
"\nconsole.log(JSON.stringify(makeAdder(2)(3)));\n"))
(run-js-if-available 'lambda-runtime lambda-program "5")
(module+ main
(test-summary 'jsmaker-program-regression))
+4 -17
View File
@@ -1,19 +1,6 @@
#lang racket/base #lang racket/base
(require rackunit ;; The old regexp tests depended on a JavaScript regexp runtime shim. The new
"../main.rkt" ;; js-maker 3 core does not include that shim. See README.md for the retained
"jsmaker-test-framework.rkt") ;; and removed test categories.
(provide)
(provide regexp-tests)
(define regexp-tests
(test-suite
"string escaping regression tests"
(test-case "strings are JavaScript escaped"
(check-js-equal? (js "a\"b") "\"a\\\"b\";\n")
(check-js-equal? (js "a\\b") "\"a\\\\b\";\n")
(check-js-equal? (js "a\nb") "\"a\\nb\";\n"))))
(module+ test
(require rackunit/text-ui)
(run-tests regexp-tests))
+71 -28
View File
@@ -1,35 +1,78 @@
#lang racket/base #lang racket/base
(require rackunit (require "../main.rkt"
racket/runtime-path
"../main.rkt"
"jsmaker-test-framework.rkt") "jsmaker-test-framework.rkt")
(provide regression-tests) (check-public-api)
(define-runtime-path main-module "../main.rkt") (define simple-function
(js (define (add1 x) (return (+ x 1)))))
(check-contains 'simple-function "function add1(x)" simple-function)
(check-contains 'simple-function-return "return x + 1;" simple-function)
(define regression-tests (define escaped-string
(test-suite (js (define (message) (return "regel 1\nregel 2 \"ok\""))))
"core js macro output" (check-contains 'string-newline "regel 1\\nregel 2" escaped-string)
(test-case "the public API exports js only" (check-contains 'string-quote "\\\"ok\\\"" escaped-string)
(check-exn exn:fail? (lambda () (dynamic-require main-module 'js1)))
(check-exn exn:fail? (lambda () (dynamic-require main-module 'js/expression))))
(test-case "arithmetic and boolean expressions can be emitted as statements"
(check-js-equal? (js (+ 1 2)) "1 + 2;\n")
(check-js-equal? (js (and a b)) "a && b;\n")
(check-js-equal? (js (not ready)) "!(ready);\n"))
(test-case "value and function definitions"
(check-js-equal? (js (define answer 42)) "let answer = 42;\n")
(check-js-contains?
(js (define (square x) (return (* x x))))
"function square(x)"))
(test-case "conditionals and begin blocks"
(define out (js (if (> x 0) (return x) (return 0))))
(check-js-contains? out "if (x > 0)")
(check-js-contains? out "return x;")
(check-js-contains? (js (begin (set! x 1) (return x))) "x = 1;"))))
(module+ test (define list-program
(require rackunit/text-ui) (js (define (values) (return (list 1 "a" #t #f)))))
(run-tests regression-tests)) (check-contains 'list-literal "return [1, \"a\", true, false];" list-program)
(define cons-program
(js (define (prepend xs) (return (cons 1 xs)))))
(check-contains 'cons-generation "[1].concat(xs)" cons-program)
(define send-program
(js (define (unique xs) (return (send Array from (new Set xs))))))
(check-contains 'send-generation "Array.from(new Set(xs))" send-program)
(define dot-set-program
(js (define (setHtml el html) (set! (js-dot el innerHTML) html) (return (js-dot el innerHTML)))))
(check-contains 'dot-set "el.innerHTML = html;" dot-set-program)
(check-contains 'dot-return "return el.innerHTML;" dot-set-program)
(define ref-program
(js (define (at xs i) (return (js-ref xs i)))))
(check-contains 'ref-variable-index "return xs[i];" ref-program)
(define ref-string-key-program
(js (define (nameOf obj) (return (js-ref obj "name")))))
(check-contains 'ref-string-key "return obj[\"name\"];" ref-string-key-program)
(define ref-set-program
(js (define (put xs i value) (set! (js-ref xs i) value) (return xs))))
(check-contains 'ref-set "xs[i] = value;" ref-set-program)
(define ref-nested-program
(js (define (nested matrix r c) (return (js-ref matrix r c)))))
(check-contains 'ref-nested "return matrix[r][c];" ref-nested-program)
(define ordinary-let
(js (define (ordinaryLet x)
(let ([x 1] [y x])
(return y)))))
(check-matches 'ordinary-let-temp #rx"const .* = 1;" ordinary-let)
(check-contains 'ordinary-let-inner "{\nlet x =" ordinary-let)
(check-contains 'ordinary-let-inner-y "\nlet y =" ordinary-let)
(check-contains 'ordinary-let-return "return y;" ordinary-let)
(define let-star
(js (define (sequentialLet x)
(let* ([x 1] [y x])
(return y)))))
(check-contains 'let-star-x "let x = 1;" let-star)
(check-contains 'let-star-y "let y = x;" let-star)
(define named-let
(js (define (sumTo n)
(let loop ([i 0] [acc 0])
(if (> i n)
(return acc)
(loop (+ i 1) (+ acc i)))))))
(check-contains 'named-let-while "while (true)" named-let)
(check-contains 'named-let-continue "continue;" named-let)
(check-matches 'named-let-parallel-update #rx"const .* = i \\+ 1;" named-let)
(module+ main
(test-summary 'jsmaker-regression))
+6 -20
View File
@@ -1,28 +1,14 @@
#lang racket/base #lang racket/base
(require rackunit (require "jsmaker-test-framework.rkt"
rackunit/text-ui
"jsmaker-regression.rkt" "jsmaker-regression.rkt"
"jsmaker-program-regression.rkt" "jsmaker-program-regression.rkt"
"jsmaker-dom-exercises.rkt"
"jsmaker-list-regression.rkt" "jsmaker-list-regression.rkt"
"jsmaker-hash-regression.rkt"
"jsmaker-regexp-regression.rkt" "jsmaker-regexp-regression.rkt"
"jsmaker-usecases.rkt" "jsmaker-dom-exercises.rkt"
"jsmaker-hash-regression.rkt") "jsmaker-usecases.rkt")
(define all-tests
(test-suite
"js-maker 3 regression suite"
regression-tests
program-tests
dom-tests
list-tests
regexp-tests
usecase-tests
object-tests))
(module+ main (module+ main
(void (run-tests all-tests))) (test-summary 'jsmaker-regressions)
(displayln "js-maker regression suite completed."))
(module+ test
(void (run-tests all-tests)))
+106 -48
View File
@@ -1,57 +1,115 @@
#lang racket/base #lang racket/base
(require rackunit (require racket/file
racket/string racket/format
racket/file racket/list
racket/system) racket/port
racket/runtime-path
racket/string)
(provide check-js-equal? (provide check-true
check-js-contains? check-equal
check-js-matches? check-matches
node-available? check-not-matches
run-js/trimmed) check-contains
check-not-contains
check-public-api
run-js-if-available
note-dropped
test-summary)
(define-check (check-js-equal? actual expected) (define checks-run 0)
(check-equal? actual expected)) (define checks-skipped 0)
(define-check (check-js-contains? actual needle) (define (bump!) (set! checks-run (add1 checks-run)))
(check-true (string-contains? actual needle) (define (skip!) (set! checks-skipped (add1 checks-skipped)))
(format "expected generated JavaScript to contain ~s, got:\n~a" needle actual)))
(define-check (check-js-matches? actual pattern) (define (fail name fmt . args)
(check-true (regexp-match? pattern actual) (error name (apply format fmt args)))
(format "expected generated JavaScript to match ~s, got:\n~a" pattern actual)))
(define (node-available?) (define (check-true name value)
(and (find-executable-path "node") #t)) (bump!)
(unless value (fail name "check failed")))
(define (run-js/trimmed program) (define (check-equal name actual expected)
(define node (find-executable-path "node")) (bump!)
(unless node (unless (equal? actual expected)
(error 'run-js/trimmed "node is not available")) (fail name "expected ~s, got ~s" expected actual)))
(define source-path (make-temporary-file "js-maker-test-~a.js"))
(define out-path (make-temporary-file "js-maker-test-out-~a.txt")) (define (check-matches name rx text)
(define err-path (make-temporary-file "js-maker-test-err-~a.txt")) (bump!)
(dynamic-wind (unless (regexp-match? rx text)
void (fail name "expected generated text to match ~s, got:\n~a" rx text)))
(lambda ()
(call-with-output-file source-path #:exists 'truncate (define (check-not-matches name rx text)
(bump!)
(when (regexp-match? rx text)
(fail name "expected generated text not to match ~s, got:\n~a" rx text)))
(define (check-contains name needle text)
(bump!)
(unless (string-contains? text needle)
(fail name "expected generated text to contain ~s, got:\n~a" needle text)))
(define (check-not-contains name needle text)
(bump!)
(when (string-contains? text needle)
(fail name "expected generated text not to contain ~s, got:\n~a" needle text)))
(define-runtime-path main-path "../main.rkt")
(define (all-symbols v)
(cond [(symbol? v) (list v)]
[(pair? v) (append (all-symbols (car v)) (all-symbols (cdr v)))]
[else null]))
(define (check-public-api)
(define mp `(file ,(path->string main-path)))
(dynamic-require mp #f)
(define-values (value-exports syntax-exports) (module->exports mp))
(define exports (remove-duplicates (append (all-symbols value-exports)
(all-symbols syntax-exports))))
(check-true 'public-api-js (memq 'js exports))
(check-true 'public-api-no-js1 (not (memq 'js1 exports)))
(check-true 'public-api-no-js-ref (not (memq 'js-ref exports)))
(check-true 'public-api-no-js/expression (not (memq 'js/expression exports))))
(define (candidate-node)
(define env-node (getenv "JSMAKER_NODE"))
(cond [(and env-node (not (string=? env-node ""))) env-node]
[else (find-executable-path "node")]))
(define (subprocess-output executable file)
(define-values (proc stdout stdin stderr)
(subprocess #f #f #f executable file))
(close-output-port stdin)
(define out (port->string stdout))
(define err (port->string stderr))
(subprocess-wait proc)
(values (subprocess-status proc) out err))
(define (run-js-if-available name program expected-stdout)
(define node (candidate-node))
(cond
[node
(bump!)
(define file (make-temporary-file "jsmaker-~a.js"))
(call-with-output-file file #:exists 'replace
(lambda (out) (display program out))) (lambda (out) (display program out)))
(define exit-code (define-values (status stdout stderr) (subprocess-output node file))
(call-with-output-file out-path #:exists 'truncate (delete-file file)
(lambda (out) (unless (and (zero? status) (string=? (string-trim stdout) expected-stdout))
(call-with-output-file err-path #:exists 'truncate (fail name "JavaScript execution failed; status=~a stdout=~s stderr=~s program:\n~a"
(lambda (err) status stdout stderr program))]
(parameterize ([current-output-port out] [else
[current-error-port err]) (skip!)
(system*/exit-code node source-path))))))) (printf "NOTE: ~a skipped because node was not found.\n" name)]))
(define stdout (file->string out-path))
(define stderr (file->string err-path)) (define (note-dropped topic reason)
(unless (zero? exit-code) (printf "NOTE: dropped old ~a tests: ~a\n" topic reason))
(error 'run-js/trimmed
"node failed with exit code ~a\nstdout:\n~a\nstderr:\n~a\nprogram:\n~a" (define (test-summary who)
exit-code stdout stderr program)) (printf "~a: ~a checks passed" who checks-run)
(string-trim stdout)) (unless (zero? checks-skipped)
(lambda () (printf ", ~a JavaScript execution checks skipped" checks-skipped))
(for ([path (list source-path out-path err-path)]) (newline))
(with-handlers ([exn:fail? void]) (delete-file path))))))
+4 -1
View File
@@ -1,2 +1,5 @@
#lang racket/base #lang racket/base
(require "jsmaker-regressions.rkt")
;; Kept as a compatibility source file for the package layout. Tests are run
;; through testing/jsmaker-regressions.rkt.
(provide)
+8 -27
View File
@@ -1,32 +1,13 @@
#lang racket/base #lang racket/base
(require rackunit (require "../demo/js-usecases.rkt"
"../main.rkt"
"jsmaker-test-framework.rkt") "jsmaker-test-framework.rkt")
(provide usecase-tests) (check-contains 'usecase-random "Math.floor" usecase-random-number)
(check-contains 'usecase-unique "new Set" usecase-unique-values)
(check-contains 'usecase-array-at "return xs[i];" usecase-array-at)
(check-contains 'usecase-named-let "while (true)" usecase-sum-to)
(check-contains 'usecase-dom "getElementById" usecase-set-html)
(define counter-program (module+ main
(string-append (test-summary 'jsmaker-usecases))
(js (define (make-counter start)
(let ([value start])
(return (lambda ()
(begin
(set! value (+ value 1))
(return value)))))))
"const c = make_counter(5);\n"
"console.log(c());\n"
"console.log(c());\n"))
(define usecase-tests
(test-suite
"small use cases"
(test-case "closures and set!"
(check-js-contains? counter-program "function make_counter(start)")
(check-js-contains? counter-program "value = value + 1;")
(when (node-available?)
(check-equal? (run-js/trimmed counter-program) "6\n7")))))
(module+ test
(require rackunit/text-ui)
(run-tests usecase-tests))