eval refitted
This commit is contained in:
@@ -1,12 +1,62 @@
|
||||
#lang racket/base
|
||||
|
||||
(require (for-syntax racket/base
|
||||
(require racket/string
|
||||
(for-syntax racket/base
|
||||
racket/list
|
||||
racket/match
|
||||
racket/string))
|
||||
|
||||
(provide js js/expression)
|
||||
|
||||
;; Convert a Racket value produced by (eval <racket-expr>) inside a js form to
|
||||
;; a JavaScript literal string. This is a Racket -> JavaScript interpolation
|
||||
;; boundary, not JavaScript eval.
|
||||
(define (jsmaker-runtime-escape-js-string s)
|
||||
(define out (open-output-string))
|
||||
(for ([ch (in-string s)])
|
||||
(case ch
|
||||
[(#\\) (display "\\\\" out)]
|
||||
[(#\") (display "\\\"" out)]
|
||||
[(#\newline) (display "\\n" out)]
|
||||
[(#\return) (display "\\r" out)]
|
||||
[(#\tab) (display "\\t" out)]
|
||||
[else (display ch out)]))
|
||||
(get-output-string out))
|
||||
|
||||
(define (jsmaker-runtime-js-string s)
|
||||
(format "\"~a\"" (jsmaker-runtime-escape-js-string s)))
|
||||
|
||||
(define (jsmaker-runtime-key->js k)
|
||||
(jsmaker-runtime-js-string
|
||||
(cond [(symbol? k) (symbol->string k)]
|
||||
[(keyword? k) (keyword->string k)]
|
||||
[(string? k) k]
|
||||
[else (format "~a" k)])))
|
||||
|
||||
(define (jsmaker-runtime-value->js v)
|
||||
(cond
|
||||
[(void? v) "undefined"]
|
||||
[(eq? v #t) "true"]
|
||||
[(eq? v #f) "false"]
|
||||
[(number? v) (number->string v)]
|
||||
[(string? v) (jsmaker-runtime-js-string v)]
|
||||
[(char? v) (jsmaker-runtime-js-string (string v))]
|
||||
[(symbol? v) (jsmaker-runtime-js-string (symbol->string v))]
|
||||
[(keyword? v) (jsmaker-runtime-js-string (keyword->string v))]
|
||||
[(null? v) "[]"]
|
||||
[(pair? v)
|
||||
(format "[~a]" (string-join (map jsmaker-runtime-value->js v) ", "))]
|
||||
[(vector? v)
|
||||
(format "[~a]" (string-join (map jsmaker-runtime-value->js (vector->list v)) ", "))]
|
||||
[(hash? v)
|
||||
(format "{~a}"
|
||||
(string-join
|
||||
(for/list ([(k val) (in-hash v)])
|
||||
(format "~a: ~a" (jsmaker-runtime-key->js k) (jsmaker-runtime-value->js val)))
|
||||
", "))]
|
||||
[else
|
||||
(error 'js "cannot interpolate Racket value as JavaScript literal: ~e" v)]))
|
||||
|
||||
;; The js macro translates a practical Racket-expression subset to JavaScript.
|
||||
;; It is intentionally syntax-driven: the Racket expressions are not evaluated.
|
||||
|
||||
@@ -201,6 +251,44 @@
|
||||
(set! tmp-counter (add1 tmp-counter))
|
||||
(format "__~a_~a" prefix tmp-counter))
|
||||
|
||||
;; Runtime interpolation escape hatch. Before compiling the syntax to a
|
||||
;; datum, the public macros replace each (eval <racket-expr>) subform by a
|
||||
;; unique placeholder symbol. The generated macro result is then a Racket
|
||||
;; expression that replaces those placeholders by JavaScript literals computed
|
||||
;; from <racket-expr> in the use-site lexical context. This is intentionally
|
||||
;; different from JavaScript eval, which remains available as
|
||||
;; (send window eval ...) or by binding/calling another identifier.
|
||||
(define (compile-racket-eval expr)
|
||||
(fail "internal eval placeholder; public macro should rewrite eval first" expr))
|
||||
|
||||
(define (replace-racket-evals stx)
|
||||
(define counter 0)
|
||||
(define evals '())
|
||||
(define (fresh-eval-placeholder)
|
||||
(set! counter (add1 counter))
|
||||
(string->symbol (format "__rkt_eval_slot_~a__" counter)))
|
||||
(define (walk d)
|
||||
(match d
|
||||
[(list 'quote _) d]
|
||||
[(list 'quasiquote _) d]
|
||||
[(list 'eval expr)
|
||||
(define ph (fresh-eval-placeholder))
|
||||
(set! evals (append evals (list (list (symbol->string ph) expr))))
|
||||
ph]
|
||||
[(? pair?) (cons (walk (car d)) (walk (cdr d)))]
|
||||
[(? vector?) (list->vector (map walk (vector->list d)))]
|
||||
[_ d]))
|
||||
(values (walk (syntax->datum stx)) evals))
|
||||
|
||||
(define (wrap-compiled-js stx compiled evals)
|
||||
(for/fold ([acc #`#,compiled]) ([ev (in-list evals)])
|
||||
(define placeholder (first ev))
|
||||
(define racket-expr-datum (second ev))
|
||||
#`(string-replace #,acc
|
||||
#,placeholder
|
||||
(jsmaker-runtime-value->js
|
||||
#,(datum->syntax stx racket-expr-datum)))))
|
||||
|
||||
;; Racket conditionals treat only #f as false. JavaScript would also
|
||||
;; reject 0, "", null and undefined in condition position, so every
|
||||
;; Racket test is compiled through this helper.
|
||||
@@ -384,17 +472,36 @@
|
||||
(define content
|
||||
(case kind
|
||||
[(let)
|
||||
;; Evaluate all RHS expressions before introducing the JS let bindings.
|
||||
;; This avoids JavaScript TDZ bugs for Racket code such as
|
||||
;; (let ([x x]) ...), where the RHS must see the outer x.
|
||||
(define tmps (for/list ([_ (in-list bindings)]) (fresh "let_value")))
|
||||
(define rhs-stmts
|
||||
(for/list ([tmp (in-list tmps)] [b (in-list bindings)])
|
||||
(format "const ~a = ~a;" tmp (compile-expr (binding-rhs b)))))
|
||||
(define bind-stmts
|
||||
(for/list ([tmp (in-list tmps)] [b (in-list bindings)])
|
||||
(format "let ~a = ~a;" (id->js (binding-id b)) tmp)))
|
||||
(join-lines (append rhs-stmts (list (block (join-lines (append bind-stmts (list (body-code))))))))]
|
||||
;; Racket let evaluates all RHS expressions before introducing the new
|
||||
;; bindings. The common case can still be emitted directly when no RHS
|
||||
;; mentions one of the newly-bound identifiers. If it does, use temps
|
||||
;; to avoid JavaScript TDZ bugs for code such as (let ([x x]) ...),
|
||||
;; where the RHS must see the outer x.
|
||||
(define ids (map binding-id bindings))
|
||||
(define (mentions-id? x id)
|
||||
(cond
|
||||
[(symbol? x) (eq? x id)]
|
||||
[(pair? x) (or (mentions-id? (car x) id) (mentions-id? (cdr x) id))]
|
||||
[(vector? x) (for/or ([e (in-vector x)]) (mentions-id? e id))]
|
||||
[else #f]))
|
||||
(define (mentions-any-bound-id? rhs)
|
||||
(for/or ([id (in-list ids)]) (mentions-id? rhs id)))
|
||||
(cond
|
||||
[(for/or ([b (in-list bindings)]) (mentions-any-bound-id? (binding-rhs b)))
|
||||
(define tmps (for/list ([_ (in-list bindings)]) (fresh "let_value")))
|
||||
(define rhs-stmts
|
||||
(for/list ([tmp (in-list tmps)] [b (in-list bindings)])
|
||||
(format "const ~a = ~a;" tmp (compile-expr (binding-rhs b)))))
|
||||
(define bind-stmts
|
||||
(for/list ([tmp (in-list tmps)] [b (in-list bindings)])
|
||||
(format "let ~a = ~a;" (id->js (binding-id b)) tmp)))
|
||||
(join-lines (append rhs-stmts (list (block (join-lines (append bind-stmts (list (body-code))))))))]
|
||||
[else
|
||||
(join-lines
|
||||
(append
|
||||
(for/list ([b (in-list bindings)])
|
||||
(format "let ~a = ~a;" (id->js (binding-id b)) (compile-expr (binding-rhs b))))
|
||||
(list (body-code))))])]
|
||||
[(let*)
|
||||
;; Emit the common case directly:
|
||||
;; (let* ([x 10] [y (+ x x)]) body)
|
||||
@@ -1280,6 +1387,8 @@ if (~a !== false) return ~a;" tmp (compile-expr arg) tmp tmp)))
|
||||
(format "(~a = ~a)" (compile-assignment-target target) (compile-expr rhs))]
|
||||
[(list 'return) "undefined"]
|
||||
[(list 'return e) (compile-expr e)]
|
||||
[(list 'eval racket-expr)
|
||||
(compile-racket-eval racket-expr)]
|
||||
[(list f args ...) (compile-call f args)]
|
||||
[_ (literal->js d)]))
|
||||
|
||||
@@ -1289,9 +1398,11 @@ if (~a !== false) return ~a;" tmp (compile-expr arg) tmp tmp)))
|
||||
(define-syntax (js stx)
|
||||
(syntax-case stx ()
|
||||
[(_ form ...)
|
||||
(datum->syntax stx (compile-top (syntax->datum #'(form ...))))]))
|
||||
(let-values ([(clean-datum evals) (replace-racket-evals #'(form ...))])
|
||||
(wrap-compiled-js stx (compile-top clean-datum) evals))]))
|
||||
|
||||
(define-syntax (js/expression stx)
|
||||
(syntax-case stx ()
|
||||
[(_ form)
|
||||
(datum->syntax stx (compile-expr (syntax->datum #'form)))]))
|
||||
(let-values ([(clean-datum evals) (replace-racket-evals #'form)])
|
||||
(wrap-compiled-js stx (compile-expr clean-datum) evals))]))
|
||||
|
||||
+53
-10
@@ -83,8 +83,11 @@ self-call.
|
||||
|
||||
The generator is best understood as a source-to-source translator over syntax.
|
||||
The input is converted to datum form and matched against the supported surface
|
||||
language. The Racket code is not evaluated. This has a few important
|
||||
consequences:
|
||||
language. The Racket code is normally not evaluated. The deliberate exception
|
||||
is @racket[(eval racket-expr)], which interpolates a Racket value into the
|
||||
JavaScript source text. The expression is evaluated in the lexical context of
|
||||
the @racket[js] or @racket[js/expression] use and its value is emitted as a
|
||||
JavaScript literal. This has a few important consequences:
|
||||
|
||||
@compact-items[
|
||||
@item{Only forms known to js-maker are translated specially. Unknown calls are
|
||||
@@ -100,6 +103,46 @@ consequences:
|
||||
helper functions/IIFEs to preserve semantics.}
|
||||
]
|
||||
|
||||
@subsection{Racket value interpolation with eval}
|
||||
|
||||
The form @racket[(eval racket-expr)] is an interpolation escape hatch inherited
|
||||
from the original transformer. It evaluates @racket[racket-expr] as Racket in
|
||||
the use-site lexical context and then splices the resulting value into the
|
||||
JavaScript source as a literal. This makes surrounding Racket bindings visible
|
||||
to the interpolation expression.
|
||||
|
||||
@(rkt+js
|
||||
#<<RKT
|
||||
(let ([x 10]
|
||||
[y 20])
|
||||
(js (let ([a (eval (* x y))])
|
||||
(return (* a a)))))
|
||||
RKT
|
||||
#<<JS
|
||||
{
|
||||
let a = 200;
|
||||
return (a * a);
|
||||
}
|
||||
JS
|
||||
)
|
||||
|
||||
@(rkt+js
|
||||
#<<RKT
|
||||
(js/expression (array (eval (+ 1 2))
|
||||
(eval (string-append "a" "b"))))
|
||||
RKT
|
||||
#<<JS
|
||||
[3, "ab"]
|
||||
JS
|
||||
)
|
||||
|
||||
This is not JavaScript @tt{eval}. To call JavaScript @tt{eval}, call the
|
||||
JavaScript function explicitly, for example @racket[(send window eval "1 + 2")].
|
||||
Racket-side @racket[eval] is best used for constants, generated literal data,
|
||||
and small configuration values that are known while the JavaScript source is
|
||||
being constructed. It should not be used for run-time browser state, DOM access,
|
||||
or user input.
|
||||
|
||||
@section{Supported expression and statement forms}
|
||||
|
||||
The following Racket forms are supported as either expressions, statements, or
|
||||
@@ -116,6 +159,7 @@ both, depending on context:
|
||||
@item{@racket[when], @racket[unless], @racket[while], @racket[for],
|
||||
@racket[for/list], @racket[for/vector], and @racket[for/fold].}
|
||||
@item{@racket[with-handlers] for the generic @racket[exn?] case.}
|
||||
@item{@racket[(eval racket-expr)] for Racket-side value interpolation into JavaScript literals.}
|
||||
@item{Interop forms such as @racket[send], @racket[new], @racket[js-ref],
|
||||
@racket[js-dot], @racket[set-prop!], @racket[delete-prop!],
|
||||
@racket[js-delete], @racket[array], @racket[object],
|
||||
@@ -732,14 +776,13 @@ harness rather than hiding raw JavaScript inside the feature implementation.
|
||||
|
||||
@section{Further examples}
|
||||
|
||||
The companion document @filepath{scrbl/usecases.scrbl} contains practical
|
||||
examples such as DOM manipulation, @tt{Set}, currying, object destructuring,
|
||||
timers, @tt{fetch}, sorting, binary search, and map-based counting. Each use
|
||||
case shows the Racket/js-maker source, representative JavaScript output, and the
|
||||
behavior tested by the regression suite.
|
||||
The companion @hyperlink["../usecases/index.html"]{use-case manual} contains
|
||||
practical examples such as DOM manipulation, @tt{Set}, currying, object
|
||||
destructuring, timers, @tt{fetch}, sorting, binary search, and map-based
|
||||
counting. Each use case shows the Racket/js-maker source, representative
|
||||
JavaScript output, and the behavior tested by the regression suite.
|
||||
|
||||
When the documentation is rendered with @exec{raco scribble}, the use-case
|
||||
manual is normally emitted as a separate document next to this one. The explicit
|
||||
file name is used here instead of a cross-document reference because the latter
|
||||
The source file for that companion manual is @filepath{scrbl/usecases.scrbl}.
|
||||
The relative link above is used instead of @racket[other-doc] because the latter
|
||||
can render as an unresolved @tt{(part ... "top")} tag when the two documents are
|
||||
built outside Racket's installed documentation index.
|
||||
|
||||
@@ -63,6 +63,24 @@
|
||||
(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")
|
||||
(js-expression-test 'compile-time-eval-data
|
||||
(js/expression (array (eval (list 1 2 3))
|
||||
(eval (string-append "a" "b"))))
|
||||
"[[1,2,3],\"ab\"]")
|
||||
(let ([x 10]
|
||||
[y 20])
|
||||
(js-expression-test 'runtime-eval-lexical-let
|
||||
(js/expression (let ([a (eval (* x y))]) (* a a)))
|
||||
"40000"))
|
||||
(js-expression-test 'substring (js/expression (substring "abcdef" 1 4)) "\"bcd\"")
|
||||
(js-expression-test 'equal-list (js/expression (equal? (list 1 2) (list 1 2))) "true")
|
||||
(js-expression-test 'cond-test-only (js/expression (cond [0] [else 2])) "0")
|
||||
|
||||
Reference in New Issue
Block a user