diff --git a/Makefile b/Makefile
index e748317..ab14ad3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,12 +1,22 @@
-RACO ?= raco
+RACKET ?= racket
+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:
- $(RACO) test -p js-maker
-
-setup:
- $(RACO) setup js-maker
+ $(RACKET) testing/jsmaker-regressions.rkt
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
+
diff --git a/README.md b/README.md
index 36961d8..e35cd87 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,66 @@
# js-maker
-js-maker is a deliberately small syntax-driven Racket-to-JavaScript string
-maker. The public API is intentionally one macro:
+`js-maker` is a deliberately small Racket macro for generating JavaScript text
+from a compact Racket/Scheme-like surface syntax.
+
+The public API is intentionally small:
```racket
(require js-maker)
-(js (define (square x) (return (* x x))))
+
+(js form ...)
```
-js-maker 3 keeps the implementation compact and supports ordinary `let`,
-`let*`, and tail-recursive named `let` loops while preserving Racket binding
-semantics. Demos are in `demo/`; regression tests are in `testing/`.
+There is no public `js1` and no `js/expression`. Expression-oriented examples
+should be written as normal JavaScript-producing programs, usually by placing an
+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.
diff --git a/demo/dom-exercises.generated.js b/demo/dom-exercises.generated.js
index 06b9562..b765165 100644
--- a/demo/dom-exercises.generated.js
+++ b/demo/dom-exercises.generated.js
@@ -1,9 +1,38 @@
-let title = document.getElementById("title");
-title.innerHTML = "Hello from js-maker";
-title.addEventListener("click", function (evt) {
+// exercise01
+function replaceParagraphHtml(html) {
{
-console.log("clicked");
-return true;
+const p3 = document.querySelector("p");
+{
+let p = p3;
+p.innerHTML = html;
+return p.innerHTML;
+}
+}
}
-});
+
+// exercise02
+function addSourceLink() {
+{
+const p8 = document.querySelector("p");
+{
+let p = p8;
+p.insertAdjacentHTML("afterend", "Source");
+return true;
+}
+}
+}
+
+
+// exercise03
+function paragraphText() {
+{
+const p15 = document.querySelector("p");
+{
+let p = p15;
+return p.textContent;
+}
+}
+}
+
+
diff --git a/demo/dom-exercises.rkt b/demo/dom-exercises.rkt
index 78e5510..3d675e2 100644
--- a/demo/dom-exercises.rkt
+++ b/demo/dom-exercises.rkt
@@ -2,17 +2,50 @@
(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
- (define title (send document getElementById "title"))
- (set! (js-dot title innerHTML) "Hello from js-maker")
- (send title addEventListener "click"
- (lambda (evt)
- (begin
- (send console log "clicked")
- (return #t))))))
+ (define (replaceParagraphHtml html)
+ (let ([p (send document querySelector "p")])
+ (set! (js-dot p innerHTML) html)
+ (return (js-dot p innerHTML))))))
+
+;; Exercise 02: add a source link after the paragraph tag.
+(define exercise02
+ (js
+ (define (addSourceLink)
+ (let ([p (send document querySelector "p")])
+ (send p insertAdjacentHTML
+ "afterend"
+ "Source")
+ (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
- (display generated-js))
+ (show-dom-exercises))
diff --git a/demo/js-usecases.generated.js b/demo/js-usecases.generated.js
index c0efa8b..e0c85e3 100644
--- a/demo/js-usecases.generated.js
+++ b/demo/js-usecases.generated.js
@@ -1,25 +1,63 @@
-let answer = 42;
-function square(x) {
-return x * x;
+// random-number
+function randomBetween1And5() {
+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 acc8 = 0;
+const i20 = 0;
+const acc21 = 0;
{
-let i = i7;
-let acc = acc8;
+let i = i20;
+let acc = acc21;
while (true) {
if (i > n) {
return acc;
} else {
-const i11 = i + 1;
-const acc12 = acc + i;
-i = i11;
-acc = acc12;
+const i24 = i + 1;
+const acc25 = acc + i;
+i = i24;
+acc = acc25;
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;
+}
+}
+}
+
+
diff --git a/demo/js-usecases.rkt b/demo/js-usecases.rkt
index b017a50..5f47a66 100644
--- a/demo/js-usecases.rkt
+++ b/demo/js-usecases.rkt
@@ -2,18 +2,80 @@
(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
- (define answer 42)
- (define (square x)
- (return (* x x)))
- (define (sum-to n)
+ (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: 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])
(if (> i n)
(return acc)
(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
- (display generated-js))
+ (show-js-usecases))
diff --git a/demo/show-jsmaker-output.rkt b/demo/show-jsmaker-output.rkt
index 220bf0d..2fb6e13 100644
--- a/demo/show-jsmaker-output.rkt
+++ b/demo/show-jsmaker-output.rkt
@@ -1,11 +1,26 @@
#lang racket/base
-(require (prefix-in use: "js-usecases.rkt")
- (prefix-in dom: "dom-exercises.rkt"))
+(require "../main.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
- (displayln ";; js-usecases")
- (display use:generated-js)
- (newline)
- (displayln ";; dom-exercises")
- (display dom:generated-js))
+ (show-examples))
diff --git a/demo/show-optimized.rkt b/demo/show-optimized.rkt
index 78a0dbf..25e8b1f 100644
--- a/demo/show-optimized.rkt
+++ b/demo/show-optimized.rkt
@@ -2,12 +2,15 @@
(require "../main.rkt")
-;; There is no separate optimizer in js-maker 3. This demo shows the compact
-;; named-let loop output produced directly by the `js` macro.
+;; js-maker 3 does not have a separate optimizer pass. The notable direct
+;; lowering is named let to a while(true) loop with parallel updates.
+(define optimized-example
+ (js
+ (define (sumTo n)
+ (let loop ([i 0] [acc 0])
+ (if (> i n)
+ (return acc)
+ (loop (+ i 1) (+ acc i)))))))
+
(module+ main
- (display
- (js (define (factorial n)
- (let loop ([i n] [acc 1])
- (if (<= i 1)
- (return acc)
- (loop (- i 1) (* acc i))))))))
+ (displayln optimized-example))
diff --git a/info.rkt b/info.rkt
index 4962e84..0431883 100644
--- a/info.rkt
+++ b/info.rkt
@@ -5,17 +5,14 @@
(define license 'MIT)
(define pkg-desc "A small syntax-driven Racket-to-JavaScript maker macro.")
(define pkg-authors '(hnmdijkema))
+
(define deps '("base"))
-(define build-deps '("scribble-lib" "racket-doc" "rackunit-lib"))
-(define tags '("javascript" "macro" "racket"))
+(define build-deps '("scribble-lib" "racket-doc"))
(define scribblings
'(("scrbl/js-maker.scrbl" () (library))
("scrbl/usecases.scrbl" () (library))))
-;; Keep the test entry point explicit. The supporting regression modules are
-;; required by this runner and compile during package setup.
+;; The public package test entry point. Support modules and demos are still
+;; compiled by raco setup, but tests are launched through this maintained suite.
(define test-include-paths '("testing/jsmaker-regressions.rkt"))
-(define test-omit-paths
- '("testing/jsmaker-executors.rkt"
- "testing/jsmaker-test-framework.rkt"))
diff --git a/main.rkt b/main.rkt
index 205f291..91e335e 100644
--- a/main.rkt
+++ b/main.rkt
@@ -108,6 +108,13 @@
[(_ obj field) (string-append (js1 obj) "." (js-id field))]
[(_ 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
(syntax-rules ()
[(_ obj method) (string-append (js1 obj) "." (js-id method) "()")]
@@ -295,6 +302,7 @@
[(eq? d 'list) #'(js-array arg ...)]
[(eq? d 'cons) #'(js-cons 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 ...)]
[(identifier? #'op) #'(js-call op arg ...)]
[else (raise-syntax-error 'js1 "unsupported compound expression" stx #'op)]))]
diff --git a/scrbl/js-maker.scrbl b/scrbl/js-maker.scrbl
index 3c942ea..661f861 100644
--- a/scrbl/js-maker.scrbl
+++ b/scrbl/js-maker.scrbl
@@ -3,82 +3,119 @@
@(require (for-label racket/base js-maker))
@title{js-maker}
-@author{Hans Dijkema}
+@author+email["Hans Dijkema" "hans@dijkewijk.nl"]
@defmodule[js-maker]
-@emph{js-maker} is a deliberately small syntax-driven JavaScript string maker.
-It provides one public macro, @racket[js]. The helper machinery used to render
-subexpressions is private to the package.
+@racketmodname[js-maker] provides a deliberately small macro for generating
+JavaScript text from a compact Racket/Scheme-like syntax. The public API is
+intentionally limited to @racket[js]. The lower-level dispatcher used by the
+implementation is internal and is not exported.
@section{Public API}
@defform[(js form ...)]{
-Produces a JavaScript string for the supplied forms. Each top-level form is
-rendered as a JavaScript statement. The macro is intended for small generated
-snippets, demos, and controlled code generation; it is not a complete Racket to
-JavaScript compiler.
+Generates a JavaScript program fragment as a string.
-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[
-(js (+ 1 2))
-(js (define answer 42))
-(js (define (square x)
- (return (* x x))))
-]
+@codeblock{
+(js
+ (define (add1 x)
+ (return (+ x 1))))
+}
}
-@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[
- @item{@racket[define] for values and functions.}
- @item{@racket[lambda] and @racket[lambda]-style generated JavaScript functions.}
- @item{@racket[if], @racket[begin], @racket[set!], and explicit @racket[return].}
- @item{Ordinary @racket[let] with parallel binding semantics.}
- @item{@racket[let*] with sequential binding semantics.}
- @item{Named @racket[let] in tail-recursive loop style.}
- @item{Common infix operators such as @racket[+], @racket[-], @racket[*],
- @racket[/], comparisons, @racket[and], @racket[or], and @racket[not].}
- @item{@racket[list], @racket[cons], @racket[send], @racket[js-dot], and
- @racket[new].}
+ @item{@racket[(define (name arg ...) body ...)]}
+ @item{@racket[(define name expr)]}
+ @item{@racket[(lambda (arg ...) body ...)] and @racket[(λ (arg ...) body ...)]}
+ @item{@racket[(if condition then else)]}
+ @item{@racket[(begin body ...)]}
+ @item{@racket[(return expr)]}
+ @item{@racket[(set! target expr)]}
+ @item{ordinary @racket[let], with parallel binding semantics}
+ @item{@racket[let*], with sequential binding semantics}
+ @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
-bindings. js-maker preserves that behavior by emitting temporary JavaScript
-constants and then opening a nested block for the real @tt{let} bindings.
-This avoids JavaScript temporal-dead-zone shadowing when a bound name is also
-used by a right-hand side.
+@racket[js-ref] is a form understood by the @racket[js] macro. It is not
+exported as a separate binding. It generates JavaScript bracket access, which
+works for arrays and computed object properties.
-@racketblock[
-(js (define (ordinary-let x)
- (let ([x 1] [y x])
- (return y))))
-]
+@codeblock{
+(js
+ (define (arrayAt xs i)
+ (return (js-ref xs i))))
+}
-The generated JavaScript returns the original argument as the value of
-@tt{y}, matching Racket's ordinary @racket[let] semantics.
+@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])
+ (return y))))
+}
@section{Named let}
-Named @racket[let] is emitted as a @tt{while (true)} loop. A tail call to the
-loop name is translated to parallel update assignments followed by
-@tt{continue}. The intended style is statement-oriented and uses explicit
-@racket[return] for terminating branches.
+Named @racket[let] is compiled to a loop. A tail call to the loop name is
+translated into parallel assignments to the loop variables followed by
+@tt{continue}. A branch that exits the loop should use @racket[(return ...)].
-@racketblock[
-(js (define (sum-to n)
- (let loop ([i 0] [acc 0])
- (if (> i n)
- (return acc)
- (loop (+ i 1) (+ acc i))))))
-]
+@codeblock{
+(js
+ (define (sumTo n)
+ (let loop ([i 0] [acc 0])
+ (if (> i n)
+ (return acc)
+ (loop (+ i 1) (+ acc i))))))
+}
-@section{Package layout}
+@section{Tests and demos}
-The package includes small demos under @filepath{demo/} and a regression suite
-under @filepath{testing/}. The public API remains just @racket[js].
+The package contains maintained tests under @filepath{testing/} and small demos
+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.
diff --git a/scrbl/usecases.scrbl b/scrbl/usecases.scrbl
index d42b261..403597e 100644
--- a/scrbl/usecases.scrbl
+++ b/scrbl/usecases.scrbl
@@ -2,42 +2,66 @@
@(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[
-(js (define (square x)
- (return (* x x))))
-]
+The demos in @filepath{demo/js-usecases.rkt} use only the public
+@racket[js] macro and the compact js-maker 3 form set. They are intentionally
+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
-to JavaScript-friendly names by replacing unsupported characters with
-underscores.
+@section{Random number}
-@section{Generating a loop}
-
-@racketblock[
-(js (define (sum-to n)
- (let loop ([i 0] [acc 0])
- (if (> i n)
- (return acc)
- (loop (+ i 1) (+ acc i))))))
-]
-
-The named @racket[let] form is useful for simple loops while keeping ordinary
-Racket binding semantics for the initial values and loop updates.
-
-@section{Generating DOM-style calls}
-
-@racketblock[
+@codeblock{
(js
- (define title (send document getElementById "title"))
- (set! (js-dot title innerHTML) "Hello")
- (send title addEventListener "click"
- (lambda (evt) (return #t))))
-]
+ (define (randomBetween1And5)
+ (return (+ (send Math floor (* (send Math random) 5)) 1))))
+}
-This is string generation only. The generated JavaScript must still be run in a
-JavaScript environment that provides the referenced objects, such as
-@tt{document}.
+@section{Unique values}
+
+@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])
+ (if (> i n)
+ (return acc)
+ (loop (+ i 1) (+ acc i))))))
+}
+
+@section{Function value}
+
+@codeblock{
+(js
+ (define (makeAdder x)
+ (return (lambda (y)
+ (return (+ x y))))))
+}
+
+@section{DOM setter}
+
+@codeblock{
+(js
+ (define (setHtml id html)
+ (let ([el (send document getElementById id)])
+ (set! (js-dot el innerHTML) html)
+ (return (js-dot el innerHTML)))))
+}
diff --git a/testing/jsmaker-dom-exercises.rkt b/testing/jsmaker-dom-exercises.rkt
index ca89933..80a030c 100644
--- a/testing/jsmaker-dom-exercises.rkt
+++ b/testing/jsmaker-dom-exercises.rkt
@@ -1,25 +1,12 @@
#lang racket/base
-(require rackunit
- "../main.rkt"
+(require "../demo/dom-exercises.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
- (js
- (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))
+(module+ main
+ (test-summary 'jsmaker-dom-exercises))
diff --git a/testing/jsmaker-executors.rkt b/testing/jsmaker-executors.rkt
index 210869f..f7c5afd 100644
--- a/testing/jsmaker-executors.rkt
+++ b/testing/jsmaker-executors.rkt
@@ -1,4 +1,5 @@
#lang racket/base
-(require "jsmaker-test-framework.rkt")
-(provide node-available? run-js/trimmed)
+;; Kept as a compatibility source file for the package layout. js-maker 3 uses
+;; the small optional Node runner in jsmaker-test-framework.rkt.
+(provide)
diff --git a/testing/jsmaker-hash-regression.rkt b/testing/jsmaker-hash-regression.rkt
index effe67d..d50a142 100644
--- a/testing/jsmaker-hash-regression.rkt
+++ b/testing/jsmaker-hash-regression.rkt
@@ -1,18 +1,6 @@
#lang racket/base
-(require rackunit
- "../main.rkt"
- "jsmaker-test-framework.rkt")
-
-(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))
+;; The old js-maker 2 hash tests covered a runtime library that is intentionally
+;; not part of the compact js-maker 3 restart. See README.md for the retained
+;; and removed test categories.
+(provide)
diff --git a/testing/jsmaker-list-regression.rkt b/testing/jsmaker-list-regression.rkt
index 1d66aa5..fa780c6 100644
--- a/testing/jsmaker-list-regression.rkt
+++ b/testing/jsmaker-list-regression.rkt
@@ -1,21 +1,19 @@
#lang racket/base
-(require rackunit
- "../main.rkt"
+(require "../main.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
- (test-suite
- "list and quoted datum generation"
- (test-case "list and cons"
- (check-js-equal? (js (list 1 2 3)) "[1, 2, 3];\n")
- (check-js-equal? (js (cons 1 (list 2 3))) "[1].concat([2, 3]);\n"))
- (test-case "quoted data"
- (check-js-equal? (js (quote alpha)) "\"alpha\";\n")
- (check-js-equal? (js (quote (1 2 x))) "[1, 2, \"x\"];\n"))))
+(define cons-program
+ (string-append
+ (js (define (prepend xs) (return (cons 1 xs))))
+ "\nconsole.log(JSON.stringify(prepend([2,3])));\n"))
+(run-js-if-available 'cons-runtime cons-program "[1,2,3]")
-(module+ test
- (require rackunit/text-ui)
- (run-tests list-tests))
+(module+ main
+ (test-summary 'jsmaker-list-regression))
diff --git a/testing/jsmaker-program-regression.rkt b/testing/jsmaker-program-regression.rkt
index e913682..105a5e3 100644
--- a/testing/jsmaker-program-regression.rkt
+++ b/testing/jsmaker-program-regression.rkt
@@ -1,51 +1,62 @@
#lang racket/base
-(require rackunit
- "../main.rkt"
+(require "../main.rkt"
"jsmaker-test-framework.rkt")
-(provide program-tests)
-
(define ordinary-let-program
(string-append
- (js (define (ordinary-let x)
+ (js (define (ordinaryLet x)
(let ([x 1] [y x])
(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
- (js (define (sequential-let x)
+ (js (define (sequentialLet x)
(let* ([x 1] [y x])
(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
(string-append
- (js (define (sum-to n)
+ (js (define (sumTo n)
(let loop ([i 0] [acc 0])
(if (> i n)
(return acc)
(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
- (test-suite
- "generated JavaScript program behavior"
- (test-case "ordinary let uses parallel Racket binding semantics"
- (check-js-contains? ordinary-let-program "const")
- (check-js-contains? ordinary-let-program "let x")
- (when (node-available?)
- (check-equal? (run-js/trimmed ordinary-let-program) "99")))
- (test-case "let* uses sequential binding semantics"
- (when (node-available?)
- (check-equal? (run-js/trimmed sequential-let-program) "1")))
- (test-case "named let compiles to a while loop with parallel updates"
- (check-js-contains? named-let-program "while (true)")
- (check-js-contains? named-let-program "continue;")
- (when (node-available?)
- (check-equal? (run-js/trimmed named-let-program) "55")))))
+(define ref-program
+ (string-append
+ (js (define (at xs i)
+ (return (js-ref xs i))))
+ "\nconsole.log(JSON.stringify(at([10,20,30],1)));\n"))
+(run-js-if-available 'ref-runtime ref-program "20")
-(module+ test
- (require rackunit/text-ui)
- (run-tests program-tests))
+(define ref-set-program
+ (string-append
+ (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))
diff --git a/testing/jsmaker-regexp-regression.rkt b/testing/jsmaker-regexp-regression.rkt
index c085d21..0d1380d 100644
--- a/testing/jsmaker-regexp-regression.rkt
+++ b/testing/jsmaker-regexp-regression.rkt
@@ -1,19 +1,6 @@
#lang racket/base
-(require rackunit
- "../main.rkt"
- "jsmaker-test-framework.rkt")
-
-(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))
+;; The old regexp tests depended on a JavaScript regexp runtime shim. The new
+;; js-maker 3 core does not include that shim. See README.md for the retained
+;; and removed test categories.
+(provide)
diff --git a/testing/jsmaker-regression.rkt b/testing/jsmaker-regression.rkt
index 1a2c2f7..e8bf868 100644
--- a/testing/jsmaker-regression.rkt
+++ b/testing/jsmaker-regression.rkt
@@ -1,35 +1,78 @@
#lang racket/base
-(require rackunit
- racket/runtime-path
- "../main.rkt"
+(require "../main.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
- (test-suite
- "core js macro output"
- (test-case "the public API exports js only"
- (check-exn exn:fail? (lambda () (dynamic-require main-module 'js1)))
- (check-exn exn:fail? (lambda () (dynamic-require main-module 'js/expression))))
- (test-case "arithmetic and boolean expressions can be emitted as statements"
- (check-js-equal? (js (+ 1 2)) "1 + 2;\n")
- (check-js-equal? (js (and a b)) "a && b;\n")
- (check-js-equal? (js (not ready)) "!(ready);\n"))
- (test-case "value and function definitions"
- (check-js-equal? (js (define answer 42)) "let answer = 42;\n")
- (check-js-contains?
- (js (define (square x) (return (* x x))))
- "function square(x)"))
- (test-case "conditionals and begin blocks"
- (define out (js (if (> x 0) (return x) (return 0))))
- (check-js-contains? out "if (x > 0)")
- (check-js-contains? out "return x;")
- (check-js-contains? (js (begin (set! x 1) (return x))) "x = 1;"))))
+(define escaped-string
+ (js (define (message) (return "regel 1\nregel 2 \"ok\""))))
+(check-contains 'string-newline "regel 1\\nregel 2" escaped-string)
+(check-contains 'string-quote "\\\"ok\\\"" escaped-string)
-(module+ test
- (require rackunit/text-ui)
- (run-tests regression-tests))
+(define list-program
+ (js (define (values) (return (list 1 "a" #t #f)))))
+(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))
diff --git a/testing/jsmaker-regressions.rkt b/testing/jsmaker-regressions.rkt
index e07022d..9791902 100644
--- a/testing/jsmaker-regressions.rkt
+++ b/testing/jsmaker-regressions.rkt
@@ -1,28 +1,14 @@
#lang racket/base
-(require rackunit
- rackunit/text-ui
+(require "jsmaker-test-framework.rkt"
"jsmaker-regression.rkt"
"jsmaker-program-regression.rkt"
- "jsmaker-dom-exercises.rkt"
"jsmaker-list-regression.rkt"
+ "jsmaker-hash-regression.rkt"
"jsmaker-regexp-regression.rkt"
- "jsmaker-usecases.rkt"
- "jsmaker-hash-regression.rkt")
-
-(define all-tests
- (test-suite
- "js-maker 3 regression suite"
- regression-tests
- program-tests
- dom-tests
- list-tests
- regexp-tests
- usecase-tests
- object-tests))
+ "jsmaker-dom-exercises.rkt"
+ "jsmaker-usecases.rkt")
(module+ main
- (void (run-tests all-tests)))
-
-(module+ test
- (void (run-tests all-tests)))
+ (test-summary 'jsmaker-regressions)
+ (displayln "js-maker regression suite completed."))
diff --git a/testing/jsmaker-test-framework.rkt b/testing/jsmaker-test-framework.rkt
index 15854b6..3ed3dc1 100644
--- a/testing/jsmaker-test-framework.rkt
+++ b/testing/jsmaker-test-framework.rkt
@@ -1,57 +1,115 @@
#lang racket/base
-(require rackunit
- racket/string
- racket/file
- racket/system)
+(require racket/file
+ racket/format
+ racket/list
+ racket/port
+ racket/runtime-path
+ racket/string)
-(provide check-js-equal?
- check-js-contains?
- check-js-matches?
- node-available?
- run-js/trimmed)
+(provide check-true
+ check-equal
+ check-matches
+ check-not-matches
+ check-contains
+ check-not-contains
+ check-public-api
+ run-js-if-available
+ note-dropped
+ test-summary)
-(define-check (check-js-equal? actual expected)
- (check-equal? actual expected))
+(define checks-run 0)
+(define checks-skipped 0)
-(define-check (check-js-contains? actual needle)
- (check-true (string-contains? actual needle)
- (format "expected generated JavaScript to contain ~s, got:\n~a" needle actual)))
+(define (bump!) (set! checks-run (add1 checks-run)))
+(define (skip!) (set! checks-skipped (add1 checks-skipped)))
-(define-check (check-js-matches? actual pattern)
- (check-true (regexp-match? pattern actual)
- (format "expected generated JavaScript to match ~s, got:\n~a" pattern actual)))
+(define (fail name fmt . args)
+ (error name (apply format fmt args)))
-(define (node-available?)
- (and (find-executable-path "node") #t))
+(define (check-true name value)
+ (bump!)
+ (unless value (fail name "check failed")))
-(define (run-js/trimmed program)
- (define node (find-executable-path "node"))
- (unless node
- (error 'run-js/trimmed "node is not available"))
- (define source-path (make-temporary-file "js-maker-test-~a.js"))
- (define out-path (make-temporary-file "js-maker-test-out-~a.txt"))
- (define err-path (make-temporary-file "js-maker-test-err-~a.txt"))
- (dynamic-wind
- void
- (lambda ()
- (call-with-output-file source-path #:exists 'truncate
- (lambda (out) (display program out)))
- (define exit-code
- (call-with-output-file out-path #:exists 'truncate
- (lambda (out)
- (call-with-output-file err-path #:exists 'truncate
- (lambda (err)
- (parameterize ([current-output-port out]
- [current-error-port err])
- (system*/exit-code node source-path)))))))
- (define stdout (file->string out-path))
- (define stderr (file->string err-path))
- (unless (zero? exit-code)
- (error 'run-js/trimmed
- "node failed with exit code ~a\nstdout:\n~a\nstderr:\n~a\nprogram:\n~a"
- exit-code stdout stderr program))
- (string-trim stdout))
- (lambda ()
- (for ([path (list source-path out-path err-path)])
- (with-handlers ([exn:fail? void]) (delete-file path))))))
+(define (check-equal name actual expected)
+ (bump!)
+ (unless (equal? actual expected)
+ (fail name "expected ~s, got ~s" expected actual)))
+
+(define (check-matches name rx text)
+ (bump!)
+ (unless (regexp-match? rx text)
+ (fail name "expected generated text to match ~s, got:\n~a" rx text)))
+
+(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)))
+ (define-values (status stdout stderr) (subprocess-output node file))
+ (delete-file file)
+ (unless (and (zero? status) (string=? (string-trim stdout) expected-stdout))
+ (fail name "JavaScript execution failed; status=~a stdout=~s stderr=~s program:\n~a"
+ status stdout stderr program))]
+ [else
+ (skip!)
+ (printf "NOTE: ~a skipped because node was not found.\n" name)]))
+
+(define (note-dropped topic reason)
+ (printf "NOTE: dropped old ~a tests: ~a\n" topic reason))
+
+(define (test-summary who)
+ (printf "~a: ~a checks passed" who checks-run)
+ (unless (zero? checks-skipped)
+ (printf ", ~a JavaScript execution checks skipped" checks-skipped))
+ (newline))
diff --git a/testing/jsmaker-test-runner.rkt b/testing/jsmaker-test-runner.rkt
index 16a7b6c..1d7e02e 100644
--- a/testing/jsmaker-test-runner.rkt
+++ b/testing/jsmaker-test-runner.rkt
@@ -1,2 +1,5 @@
#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)
diff --git a/testing/jsmaker-usecases.rkt b/testing/jsmaker-usecases.rkt
index 9bde3ae..1443fb1 100644
--- a/testing/jsmaker-usecases.rkt
+++ b/testing/jsmaker-usecases.rkt
@@ -1,32 +1,13 @@
#lang racket/base
-(require rackunit
- "../main.rkt"
+(require "../demo/js-usecases.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
- (string-append
- (js (define (make-counter start)
- (let ([value start])
- (return (lambda ()
- (begin
- (set! value (+ value 1))
- (return value)))))))
- "const c = make_counter(5);\n"
- "console.log(c());\n"
- "console.log(c());\n"))
-
-(define 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))
+(module+ main
+ (test-summary 'jsmaker-usecases))