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))