eval refitted
This commit is contained in:
@@ -1,12 +1,62 @@
|
|||||||
#lang racket/base
|
#lang racket/base
|
||||||
|
|
||||||
(require (for-syntax racket/base
|
(require racket/string
|
||||||
|
(for-syntax racket/base
|
||||||
racket/list
|
racket/list
|
||||||
racket/match
|
racket/match
|
||||||
racket/string))
|
racket/string))
|
||||||
|
|
||||||
(provide js js/expression)
|
(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.
|
;; The js macro translates a practical Racket-expression subset to JavaScript.
|
||||||
;; It is intentionally syntax-driven: the Racket expressions are not evaluated.
|
;; It is intentionally syntax-driven: the Racket expressions are not evaluated.
|
||||||
|
|
||||||
@@ -201,6 +251,44 @@
|
|||||||
(set! tmp-counter (add1 tmp-counter))
|
(set! tmp-counter (add1 tmp-counter))
|
||||||
(format "__~a_~a" prefix 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
|
;; Racket conditionals treat only #f as false. JavaScript would also
|
||||||
;; reject 0, "", null and undefined in condition position, so every
|
;; reject 0, "", null and undefined in condition position, so every
|
||||||
;; Racket test is compiled through this helper.
|
;; Racket test is compiled through this helper.
|
||||||
@@ -384,17 +472,36 @@
|
|||||||
(define content
|
(define content
|
||||||
(case kind
|
(case kind
|
||||||
[(let)
|
[(let)
|
||||||
;; Evaluate all RHS expressions before introducing the JS let bindings.
|
;; Racket let evaluates all RHS expressions before introducing the new
|
||||||
;; This avoids JavaScript TDZ bugs for Racket code such as
|
;; bindings. The common case can still be emitted directly when no RHS
|
||||||
;; (let ([x x]) ...), where the RHS must see the outer x.
|
;; mentions one of the newly-bound identifiers. If it does, use temps
|
||||||
(define tmps (for/list ([_ (in-list bindings)]) (fresh "let_value")))
|
;; to avoid JavaScript TDZ bugs for code such as (let ([x x]) ...),
|
||||||
(define rhs-stmts
|
;; where the RHS must see the outer x.
|
||||||
(for/list ([tmp (in-list tmps)] [b (in-list bindings)])
|
(define ids (map binding-id bindings))
|
||||||
(format "const ~a = ~a;" tmp (compile-expr (binding-rhs b)))))
|
(define (mentions-id? x id)
|
||||||
(define bind-stmts
|
(cond
|
||||||
(for/list ([tmp (in-list tmps)] [b (in-list bindings)])
|
[(symbol? x) (eq? x id)]
|
||||||
(format "let ~a = ~a;" (id->js (binding-id b)) tmp)))
|
[(pair? x) (or (mentions-id? (car x) id) (mentions-id? (cdr x) id))]
|
||||||
(join-lines (append rhs-stmts (list (block (join-lines (append bind-stmts (list (body-code))))))))]
|
[(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*)
|
[(let*)
|
||||||
;; Emit the common case directly:
|
;; Emit the common case directly:
|
||||||
;; (let* ([x 10] [y (+ x x)]) body)
|
;; (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))]
|
(format "(~a = ~a)" (compile-assignment-target target) (compile-expr rhs))]
|
||||||
[(list 'return) "undefined"]
|
[(list 'return) "undefined"]
|
||||||
[(list 'return e) (compile-expr e)]
|
[(list 'return e) (compile-expr e)]
|
||||||
|
[(list 'eval racket-expr)
|
||||||
|
(compile-racket-eval racket-expr)]
|
||||||
[(list f args ...) (compile-call f args)]
|
[(list f args ...) (compile-call f args)]
|
||||||
[_ (literal->js d)]))
|
[_ (literal->js d)]))
|
||||||
|
|
||||||
@@ -1289,9 +1398,11 @@ if (~a !== false) return ~a;" tmp (compile-expr arg) tmp tmp)))
|
|||||||
(define-syntax (js stx)
|
(define-syntax (js stx)
|
||||||
(syntax-case stx ()
|
(syntax-case stx ()
|
||||||
[(_ form ...)
|
[(_ 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)
|
(define-syntax (js/expression stx)
|
||||||
(syntax-case stx ()
|
(syntax-case stx ()
|
||||||
[(_ form)
|
[(_ 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 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
|
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
|
language. The Racket code is normally not evaluated. The deliberate exception
|
||||||
consequences:
|
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[
|
@compact-items[
|
||||||
@item{Only forms known to js-maker are translated specially. Unknown calls are
|
@item{Only forms known to js-maker are translated specially. Unknown calls are
|
||||||
@@ -100,6 +103,46 @@ consequences:
|
|||||||
helper functions/IIFEs to preserve semantics.}
|
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}
|
@section{Supported expression and statement forms}
|
||||||
|
|
||||||
The following Racket forms are supported as either expressions, statements, or
|
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],
|
@item{@racket[when], @racket[unless], @racket[while], @racket[for],
|
||||||
@racket[for/list], @racket[for/vector], and @racket[for/fold].}
|
@racket[for/list], @racket[for/vector], and @racket[for/fold].}
|
||||||
@item{@racket[with-handlers] for the generic @racket[exn?] case.}
|
@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],
|
@item{Interop forms such as @racket[send], @racket[new], @racket[js-ref],
|
||||||
@racket[js-dot], @racket[set-prop!], @racket[delete-prop!],
|
@racket[js-dot], @racket[set-prop!], @racket[delete-prop!],
|
||||||
@racket[js-delete], @racket[array], @racket[object],
|
@racket[js-delete], @racket[array], @racket[object],
|
||||||
@@ -732,14 +776,13 @@ harness rather than hiding raw JavaScript inside the feature implementation.
|
|||||||
|
|
||||||
@section{Further examples}
|
@section{Further examples}
|
||||||
|
|
||||||
The companion document @filepath{scrbl/usecases.scrbl} contains practical
|
The companion @hyperlink["../usecases/index.html"]{use-case manual} contains
|
||||||
examples such as DOM manipulation, @tt{Set}, currying, object destructuring,
|
practical examples such as DOM manipulation, @tt{Set}, currying, object
|
||||||
timers, @tt{fetch}, sorting, binary search, and map-based counting. Each use
|
destructuring, timers, @tt{fetch}, sorting, binary search, and map-based
|
||||||
case shows the Racket/js-maker source, representative JavaScript output, and the
|
counting. Each use case shows the Racket/js-maker source, representative
|
||||||
behavior tested by the regression suite.
|
JavaScript output, and the behavior tested by the regression suite.
|
||||||
|
|
||||||
When the documentation is rendered with @exec{raco scribble}, the use-case
|
The source file for that companion manual is @filepath{scrbl/usecases.scrbl}.
|
||||||
manual is normally emitted as a separate document next to this one. The explicit
|
The relative link above is used instead of @racket[other-doc] because the latter
|
||||||
file name is used here instead of a cross-document reference because the latter
|
|
||||||
can render as an unresolved @tt{(part ... "top")} tag when the two documents are
|
can render as an unresolved @tt{(part ... "top")} tag when the two documents are
|
||||||
built outside Racket's installed documentation index.
|
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 '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 (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 '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 '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")
|
(js-expression-test 'cond-test-only (js/expression (cond [0] [else 2])) "0")
|
||||||
|
|||||||
Reference in New Issue
Block a user