From 9026d2cbdff9e0ce52cc2cba7afcc23c171b7f9e Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Wed, 27 May 2026 17:00:11 +0200 Subject: [PATCH] tail returns and complex stuff --- main.rkt | 41 ++++++++++++++++++++++++- scrbl/js-maker.scrbl | 42 +++++++++++++++++++++++-- testing/jsmaker-dom-exercises.rkt | 51 ++++++++++++++++--------------- testing/jsmaker-regression.rkt | 6 ---- 4 files changed, 105 insertions(+), 35 deletions(-) diff --git a/main.rkt b/main.rkt index 7005df3..6bf1a6c 100644 --- a/main.rkt +++ b/main.rkt @@ -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 () diff --git a/scrbl/js-maker.scrbl b/scrbl/js-maker.scrbl index 9f607ed..2b4ff3e 100644 --- a/scrbl/js-maker.scrbl +++ b/scrbl/js-maker.scrbl @@ -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 "

Hi

"]) + (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 = "

Hi

"; + 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} diff --git a/testing/jsmaker-dom-exercises.rkt b/testing/jsmaker-dom-exercises.rkt index c0fb1b7..9c1b9f8 100644 --- a/testing/jsmaker-dom-exercises.rkt +++ b/testing/jsmaker-dom-exercises.rkt @@ -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 extraordinary words remain.") - #:preamble (dom-preamble "Short extraordinary words remain.")) + (side-effect-expr (dom-preamble "Short extraordinary words remain.") exercise01 "paragraph.innerHTML") + (jsexpr->string "Short extraordinary words remain.")) - (js-program-test + (js-expression-test 'dom-ex02-add-source-link - exercise02 - "state.afterParagraph" - (jsexpr->string '(("afterend" "Source: ForceM Ipsum"))) - #:preamble (dom-preamble "Force ipsum text.")) + (side-effect-expr (dom-preamble "Force ipsum text.") exercise02 "state.afterParagraph") + (jsexpr->string '(("afterend" "Source: ForceM Ipsum")))) - (js-program-test + (js-expression-test 'dom-ex03-split-sentences - exercise03 - "paragraph.innerHTML" - (jsexpr->string "First sentence.
Second sentence.
Third.
") - #: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.
Second sentence.
Third.
")) - (js-program-test + (js-expression-test 'dom-ex04-count-words-after-heading - exercise04 - "state.afterHeading" - (jsexpr->string '(("afterend" "

4 words

"))) - #:preamble (dom-preamble "These are four words")) + (side-effect-expr (dom-preamble "These are four words") exercise04 "state.afterHeading") + (jsexpr->string '(("afterend" "

4 words

")))) - (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) diff --git a/testing/jsmaker-regression.rkt b/testing/jsmaker-regression.rkt index f9c5a51..79c6331 100644 --- a/testing/jsmaker-regression.rkt +++ b/testing/jsmaker-regression.rkt @@ -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")