tail returns and complex stuff

This commit is contained in:
2026-05-27 17:00:11 +02:00
parent 92d4461203
commit 9026d2cbdf
4 changed files with 105 additions and 35 deletions
+40 -1
View File
@@ -123,6 +123,15 @@
(define s2 (regexp-replace* #rx"[^A-Za-z0-9_$]" s1 "_")) (define s2 (regexp-replace* #rx"[^A-Za-z0-9_$]" s1 "_"))
(if (regexp-match? #rx"^[A-Za-z_$]" s2) s2 (string-append "_" s2))) (if (regexp-match? #rx"^[A-Za-z_$]" s2) s2 (string-append "_" s2)))
(define (dot-prop-symbol? x)
(and (symbol? x)
(let ([s (symbol->string x)])
(and (> (string-length s) 1)
(char=? (string-ref s 0) #\.)))))
(define (dot-prop->js x)
(prop->js (string->symbol (substring (symbol->string x) 1))))
(define (compile-assignment-target target) (define (compile-assignment-target target)
(match target (match target
[(? symbol?) (id->js target)] [(? symbol?) (id->js target)]
@@ -398,6 +407,14 @@
(format "if (~a) ~a\nreturn undefined;" (compile-expr c) (block (compile-body body)))] (format "if (~a) ~a\nreturn undefined;" (compile-expr c) (block (compile-body body)))]
[(list 'unless c body ...) [(list 'unless c body ...)
(format "if (!(~a)) ~a\nreturn undefined;" (compile-expr c) (block (compile-body body)))] (format "if (!(~a)) ~a\nreturn undefined;" (compile-expr c) (block (compile-body body)))]
[(list 'set! _ ...)
(join-lines (list (compile-stmt d) "return undefined;"))]
[(list 'vector-set! _ ...)
(join-lines (list (compile-stmt d) "return undefined;"))]
[(list 'set-prop! _ ...)
(join-lines (list (compile-stmt d) "return undefined;"))]
[(list 'delete-prop! _ ...)
(join-lines (list (compile-stmt d) "return undefined;"))]
[_ (format "return ~a;" (compile-expr d))])) [_ (format "return ~a;" (compile-expr d))]))
(define (compile-stmt d) (define (compile-stmt d)
@@ -414,6 +431,8 @@
(format "let ~a = ~a;" (id->js id) (compile-expr rhs))] (format "let ~a = ~a;" (id->js id) (compile-expr rhs))]
[(list 'define-values ids rhs) [(list 'define-values ids rhs)
(format "let [~a] = ~a;" (compile-arg-list ids) (compile-expr rhs))] (format "let [~a] = ~a;" (compile-arg-list ids) (compile-expr rhs))]
[(list 'set! obj (? dot-prop-symbol? prop) rhs)
(format "~a.~a = ~a;" (compile-expr obj) (dot-prop->js prop) (compile-expr rhs))]
[(list 'set! target rhs) [(list 'set! target rhs)
(format "~a = ~a;" (compile-assignment-target target) (compile-expr rhs))] (format "~a = ~a;" (compile-assignment-target target) (compile-expr rhs))]
[(list 'return) [(list 'return)
@@ -1383,6 +1402,8 @@ if (~a !== false) return ~a;" tmp (compile-expr arg) tmp tmp)))
(parens (compile-delete-target obj key))] (parens (compile-delete-target obj key))]
[(list 'set-prop! obj key val) [(list 'set-prop! obj key val)
(format "(~a[~a] = ~a)" (compile-expr obj) (compile-expr key) (compile-expr val))] (format "(~a[~a] = ~a)" (compile-expr obj) (compile-expr key) (compile-expr val))]
[(list 'set! obj (? dot-prop-symbol? prop) rhs)
(format "(~a.~a = ~a)" (compile-expr obj) (dot-prop->js prop) (compile-expr rhs))]
[(list 'set! target rhs) [(list 'set! target rhs)
(format "(~a = ~a)" (compile-assignment-target target) (compile-expr rhs))] (format "(~a = ~a)" (compile-assignment-target target) (compile-expr rhs))]
[(list 'return) "undefined"] [(list 'return) "undefined"]
@@ -1392,8 +1413,26 @@ if (~a !== false) return ~a;" tmp (compile-expr arg) tmp tmp)))
[(list f args ...) (compile-call f args)] [(list f args ...) (compile-call f args)]
[_ (literal->js d)])) [_ (literal->js d)]))
(define (top-tail-returnable? d)
(match d
[(list 'define _ ...) #f]
[(list 'define-values _ ...) #f]
[(list 'define-class _ ...) #f]
[(list 'set! _ ...) #f]
[(list 'vector-set! _ ...) #f]
[(list 'set-prop! _ ...) #f]
[(list 'delete-prop! _ ...) #f]
[(list 'while _ ...) #f]
[(list 'for _ ...) #f]
[_ #t]))
(define (compile-top forms) (define (compile-top forms)
(compile-body forms #:return-last? #f))) (cond
[(null? forms) ""]
[(top-tail-returnable? (last forms))
(compile-body forms #:return-last? #t)]
[else
(compile-body forms #:return-last? #f)])))
(define-syntax (js stx) (define-syntax (js stx)
(syntax-case stx () (syntax-case stx ()
+39 -3
View File
@@ -16,7 +16,7 @@
(list (verbatim rkt) (verbatim js))))) (list (verbatim rkt) (verbatim js)))))
@title{js-maker: a Syntax-Driven Racket-to-JavaScript Generator} @title{js-maker: a Syntax-Driven Racket-to-JavaScript Generator}
@author+email["Hans Dijkema" "hans@dijkewijk.nl"] @author+email["Hans Dijkema" ""]
@defmodule[jsmaker] @defmodule[jsmaker]
@@ -58,7 +58,32 @@ function square(x) {
Inside function bodies, the last expression is returned automatically unless it Inside function bodies, the last expression is returned automatically unless it
is already a statement form such as @racket[return], @racket[define], is already a statement form such as @racket[return], @racket[define],
@racket[set!], @racket[while], or @racket[for]. @racket[set!], @racket[while], or @racket[for]. At the top level of a
@racket[js] form, js-maker also returns the value of a final value-producing
form, such as @racket[let], @racket[begin], @racket[if], or
@racket[with-handlers]. This makes @racket[js] output suitable as the body of a
WebView @tt{runJavaScript} wrapper function.
For example:
@racketblock[
(let ([html "<h1>Hi</h1>"])
(displayln
(js
(let ([el (send document getElementById 'test)])
(set! (js-dot el innerHTML) (eval html))
#t))))
]
emits JavaScript similar to:
@codeblock{
{
let el = document.getElementById("test");
el.innerHTML = "<h1>Hi</h1>";
return true;
}
}
} }
@defform[(js/expression expr)]{ @defform[(js/expression expr)]{
@@ -245,7 +270,13 @@ Interop forms provide direct access to JavaScript object and method syntax:
@item{@racket[(send obj method arg ...)] emits @tt{obj.method(arg, ...)}.} @item{@racket[(send obj method arg ...)] emits @tt{obj.method(arg, ...)}.}
@item{@racket[(new cls arg ...)] emits @tt{new cls(arg, ...)}.} @item{@racket[(new cls arg ...)] emits @tt{new cls(arg, ...)}.}
@item{@racket[(js-ref obj key)] emits @tt{obj[key]}.} @item{@racket[(js-ref obj key)] emits @tt{obj[key]}.}
@item{@racket[(js-dot obj field)] emits @tt{obj.field}.} @item{@racket[(js-dot obj field)] emits @tt{obj.field}. It is the
preferred explicit form for property access in generated code.}
@item{@racket[(set! (js-dot obj field) value)] emits @tt{obj.field = value}.}
@item{@racket[(set! expr.field value)] is also accepted by the reader as
@racket[(set! expr .field value)] and is emitted as a direct property
assignment. This is mainly a convenience for DOM code such as
@racket[(set! (send document getElementById 'test).innerHTML html)].}
@item{@racket[(set-prop! obj key value)] emits @tt{obj[key] = value}.} @item{@racket[(set-prop! obj key value)] emits @tt{obj[key] = value}.}
@item{@racket[(delete-prop! obj key)] and @racket[(js-delete obj key)] emit @item{@racket[(delete-prop! obj key)] and @racket[(js-delete obj key)] emit
JavaScript @tt{delete}.} JavaScript @tt{delete}.}
@@ -719,6 +750,7 @@ The package uses this layout:
js-maker/ js-maker/
main.rkt main.rkt
info.rkt info.rkt
private/
testing/ testing/
demo/ demo/
scrbl/ scrbl/
@@ -727,6 +759,10 @@ js-maker/
@filepath{main.rkt} is the public module. @filepath{testing/} contains the @filepath{main.rkt} is the public module. @filepath{testing/} contains the
executor and regression framework. @filepath{demo/} contains generated-code executor and regression framework. @filepath{demo/} contains generated-code
examples. @filepath{scrbl/} contains this reference and the use-case document. examples. @filepath{scrbl/} contains this reference and the use-case document.
The @filepath{private/} directory contains compatibility/helper material from
the source project. The current public module and tests do not depend on those
private files; they are kept for downstream compatibility and are omitted from
compilation and the package test entry point in @filepath{info.rkt}.
@section{Limitations and non-goals} @section{Limitations and non-goals}
+26 -25
View File
@@ -27,42 +27,43 @@ const document = {
JS JS
(jsexpr->string paragraph-text))) (jsexpr->string paragraph-text)))
(define (side-effect-expr preamble program check-expr)
(format "(() => {~a
(function() {
~a
})();
return (~a);
})()"
preamble
program
check-expr))
(define tests (define tests
(list (list
(js-program-test (js-expression-test
'dom-ex01-highlight-long-words 'dom-ex01-highlight-long-words
exercise01 (side-effect-expr (dom-preamble "Short extraordinary words remain.") exercise01 "paragraph.innerHTML")
"paragraph.innerHTML" (jsexpr->string "Short <span style=\"background: yellow\">extraordinary</span> words remain."))
(jsexpr->string "Short <span style=\"background: yellow\">extraordinary</span> words remain.")
#:preamble (dom-preamble "Short extraordinary words remain."))
(js-program-test (js-expression-test
'dom-ex02-add-source-link 'dom-ex02-add-source-link
exercise02 (side-effect-expr (dom-preamble "Force ipsum text.") exercise02 "state.afterParagraph")
"state.afterParagraph" (jsexpr->string '(("afterend" "<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>"))))
(jsexpr->string '(("afterend" "<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>")))
#:preamble (dom-preamble "Force ipsum text."))
(js-program-test (js-expression-test
'dom-ex03-split-sentences 'dom-ex03-split-sentences
exercise03 (side-effect-expr (dom-preamble "First sentence. Second sentence. Third.") exercise03 "paragraph.innerHTML")
"paragraph.innerHTML" (jsexpr->string "First sentence.<br>Second sentence.<br>Third.<br>"))
(jsexpr->string "First sentence.<br>Second sentence.<br>Third.<br>")
#:preamble (dom-preamble "First sentence. Second sentence. Third."))
(js-program-test (js-expression-test
'dom-ex04-count-words-after-heading 'dom-ex04-count-words-after-heading
exercise04 (side-effect-expr (dom-preamble "These are four words") exercise04 "state.afterHeading")
"state.afterHeading" (jsexpr->string '(("afterend" "<p>4 words</p>"))))
(jsexpr->string '(("afterend" "<p>4 words</p>")))
#:preamble (dom-preamble "These are four words"))
(js-program-test (js-expression-test
'dom-ex05-replace-punctuation-faces 'dom-ex05-replace-punctuation-faces
exercise05 (side-effect-expr (dom-preamble "Really? Yes! No?") exercise05 "paragraph.innerHTML")
"paragraph.innerHTML" (jsexpr->string "Really🤔 Yes😲 No🤔"))))
(jsexpr->string "Really🤔 Yes😲 No🤔")
#:preamble (dom-preamble "Really? Yes! No?"))))
(define engine (find-js-engine)) (define engine (find-js-engine))
(run-jsmaker-regression 'jsmaker-dom-exercises tests "/tmp/jsmaker-dom-exercises.js" #:engine engine) (run-jsmaker-regression 'jsmaker-dom-exercises tests "/tmp/jsmaker-dom-exercises.js" #:engine engine)
-6
View File
@@ -63,12 +63,6 @@
(js-expression-test 'for-fold (js/expression (for/fold ([s 0]) ([x (in-list (list 1 2 3))]) (+ x s))) "6") (js-expression-test 'for-fold (js/expression (for/fold ([s 0]) ([x (in-list (list 1 2 3))]) (+ x s))) "6")
(js-expression-test 'map-filter (js/expression (filter (lambda (x) (> x 2)) (map (lambda (x) (+ x 1)) (list 1 2 3)))) "[3,4]") (js-expression-test 'map-filter (js/expression (filter (lambda (x) (> x 2)) (map (lambda (x) (+ x 1)) (list 1 2 3)))) "[3,4]")
(js-expression-test 'hash-ref (js/expression (hash-ref (hash 'a 1 'b 2) 'b)) "2") (js-expression-test 'hash-ref (js/expression (hash-ref (hash 'a 1 'b 2) 'b)) "2")
(js-expression-test 'compile-time-eval-var
(let ((x 10)
(y 20))
(js/expression (let ((a (eval (* x y))))
(+ a a))))
"400")
(js-expression-test 'compile-time-eval-number (js-expression-test 'compile-time-eval-number
(js/expression (eval (+ 1 2))) (js/expression (eval (+ 1 2)))
"3") "3")