oke.
This commit is contained in:
@@ -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:
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
(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
|
(module+ main
|
||||||
(display
|
(displayln optimized-example))
|
||||||
(js (define (factorial n)
|
|
||||||
(let loop ([i n] [acc 1])
|
|
||||||
(if (<= i 1)
|
|
||||||
(return acc)
|
|
||||||
(loop (- i 1) (* acc i))))))))
|
|
||||||
|
|||||||
@@ -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"))
|
|
||||||
|
|||||||
@@ -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)]))]
|
||||||
|
|||||||
+91
-54
@@ -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
|
||||||
(let ([x 1] [y x])
|
(define (arrayAt xs i)
|
||||||
(return y))))
|
(return (js-ref xs i))))
|
||||||
]
|
}
|
||||||
|
|
||||||
The generated JavaScript returns the original argument as the value of
|
@codeblock{
|
||||||
@tt{y}, matching Racket's ordinary @racket[let] semantics.
|
(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}
|
@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
|
||||||
(let loop ([i 0] [acc 0])
|
(define (sumTo n)
|
||||||
(if (> i n)
|
(let loop ([i 0] [acc 0])
|
||||||
(return acc)
|
(if (> i n)
|
||||||
(loop (+ i 1) (+ acc i))))))
|
(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
|
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.
|
||||||
|
|||||||
+57
-33
@@ -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{
|
||||||
|
|
||||||
@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[
|
|
||||||
(js
|
(js
|
||||||
(define title (send document getElementById "title"))
|
(define (randomBetween1And5)
|
||||||
(set! (js-dot title innerHTML) "Hello")
|
(return (+ (send Math floor (* (send Math random) 5)) 1))))
|
||||||
(send title addEventListener "click"
|
}
|
||||||
(lambda (evt) (return #t))))
|
|
||||||
]
|
|
||||||
|
|
||||||
This is string generation only. The generated JavaScript must still be run in a
|
@section{Unique values}
|
||||||
JavaScript environment that provides the referenced objects, such as
|
|
||||||
@tt{document}.
|
@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)))))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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))
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
|
||||||
|
|||||||
@@ -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))
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)))
|
|
||||||
|
|||||||
@@ -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)
|
||||||
(lambda (out) (display program out)))
|
(bump!)
|
||||||
(define exit-code
|
(when (regexp-match? rx text)
|
||||||
(call-with-output-file out-path #:exists 'truncate
|
(fail name "expected generated text not to match ~s, got:\n~a" rx text)))
|
||||||
(lambda (out)
|
|
||||||
(call-with-output-file err-path #:exists 'truncate
|
(define (check-contains name needle text)
|
||||||
(lambda (err)
|
(bump!)
|
||||||
(parameterize ([current-output-port out]
|
(unless (string-contains? text needle)
|
||||||
[current-error-port err])
|
(fail name "expected generated text to contain ~s, got:\n~a" needle text)))
|
||||||
(system*/exit-code node source-path)))))))
|
|
||||||
(define stdout (file->string out-path))
|
(define (check-not-contains name needle text)
|
||||||
(define stderr (file->string err-path))
|
(bump!)
|
||||||
(unless (zero? exit-code)
|
(when (string-contains? text needle)
|
||||||
(error 'run-js/trimmed
|
(fail name "expected generated text not to contain ~s, got:\n~a" needle text)))
|
||||||
"node failed with exit code ~a\nstdout:\n~a\nstderr:\n~a\nprogram:\n~a"
|
|
||||||
exit-code stdout stderr program))
|
(define-runtime-path main-path "../main.rkt")
|
||||||
(string-trim stdout))
|
|
||||||
(lambda ()
|
(define (all-symbols v)
|
||||||
(for ([path (list source-path out-path err-path)])
|
(cond [(symbol? v) (list v)]
|
||||||
(with-handlers ([exn:fail? void]) (delete-file path))))))
|
[(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))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
|
||||||
|
|||||||
Reference in New Issue
Block a user