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 "_"))
(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)
(match target
[(? symbol?) (id->js target)]
@@ -398,6 +407,14 @@
(format "if (~a) ~a\nreturn undefined;" (compile-expr c) (block (compile-body body)))]
[(list 'unless c 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))]))
(define (compile-stmt d)
@@ -414,6 +431,8 @@
(format "let ~a = ~a;" (id->js id) (compile-expr rhs))]
[(list 'define-values ids 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)
(format "~a = ~a;" (compile-assignment-target target) (compile-expr rhs))]
[(list 'return)
@@ -1383,6 +1402,8 @@ if (~a !== false) return ~a;" tmp (compile-expr arg) tmp tmp)))
(parens (compile-delete-target obj key))]
[(list 'set-prop! obj key 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)
(format "(~a = ~a)" (compile-assignment-target target) (compile-expr rhs))]
[(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)]
[_ (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)
(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)
(syntax-case stx ()
+39 -3
View File
@@ -16,7 +16,7 @@
(list (verbatim rkt) (verbatim js)))))
@title{js-maker: a Syntax-Driven Racket-to-JavaScript Generator}
@author+email["Hans Dijkema" "hans@dijkewijk.nl"]
@author+email["Hans Dijkema" ""]
@defmodule[jsmaker]
@@ -58,7 +58,32 @@ function square(x) {
Inside function bodies, the last expression is returned automatically unless it
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)]{
@@ -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[(new cls arg ...)] emits @tt{new cls(arg, ...)}.}
@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[(delete-prop! obj key)] and @racket[(js-delete obj key)] emit
JavaScript @tt{delete}.}
@@ -719,6 +750,7 @@ The package uses this layout:
js-maker/
main.rkt
info.rkt
private/
testing/
demo/
scrbl/
@@ -727,6 +759,10 @@ js-maker/
@filepath{main.rkt} is the public module. @filepath{testing/} contains the
executor and regression framework. @filepath{demo/} contains generated-code
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}
+26 -25
View File
@@ -27,42 +27,43 @@ const document = {
JS
(jsexpr->string paragraph-text)))
(define (side-effect-expr preamble program check-expr)
(format "(() => {~a
(function() {
~a
})();
return (~a);
})()"
preamble
program
check-expr))
(define tests
(list
(js-program-test
(js-expression-test
'dom-ex01-highlight-long-words
exercise01
"paragraph.innerHTML"
(jsexpr->string "Short <span style=\"background: yellow\">extraordinary</span> words remain.")
#:preamble (dom-preamble "Short extraordinary words remain."))
(side-effect-expr (dom-preamble "Short extraordinary words remain.") exercise01 "paragraph.innerHTML")
(jsexpr->string "Short <span style=\"background: yellow\">extraordinary</span> words remain."))
(js-program-test
(js-expression-test
'dom-ex02-add-source-link
exercise02
"state.afterParagraph"
(jsexpr->string '(("afterend" "<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>")))
#:preamble (dom-preamble "Force ipsum text."))
(side-effect-expr (dom-preamble "Force ipsum text.") exercise02 "state.afterParagraph")
(jsexpr->string '(("afterend" "<a href=\"https://forcemipsum.com/\">Source: ForceM Ipsum</a>"))))
(js-program-test
(js-expression-test
'dom-ex03-split-sentences
exercise03
"paragraph.innerHTML"
(jsexpr->string "First sentence.<br>Second sentence.<br>Third.<br>")
#:preamble (dom-preamble "First sentence. Second sentence. Third."))
(side-effect-expr (dom-preamble "First sentence. Second sentence. Third.") exercise03 "paragraph.innerHTML")
(jsexpr->string "First sentence.<br>Second sentence.<br>Third.<br>"))
(js-program-test
(js-expression-test
'dom-ex04-count-words-after-heading
exercise04
"state.afterHeading"
(jsexpr->string '(("afterend" "<p>4 words</p>")))
#:preamble (dom-preamble "These are four words"))
(side-effect-expr (dom-preamble "These are four words") exercise04 "state.afterHeading")
(jsexpr->string '(("afterend" "<p>4 words</p>"))))
(js-program-test
(js-expression-test
'dom-ex05-replace-punctuation-faces
exercise05
"paragraph.innerHTML"
(jsexpr->string "Really🤔 Yes😲 No🤔")
#:preamble (dom-preamble "Really? Yes! No?"))))
(side-effect-expr (dom-preamble "Really? Yes! No?") exercise05 "paragraph.innerHTML")
(jsexpr->string "Really🤔 Yes😲 No🤔"))))
(define engine (find-js-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 '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 '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 (eval (+ 1 2)))
"3")