diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 45b89225..fb240bfa 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -115,6 +115,95 @@ ;; sign — 1 or -1 ;; frac? — are we past the decimal point ;; fdiv — divisor used to scale fraction digits (only if frac?) +(define + js-extract-fn-name + (fn (f) (let ((raw (inspect f))) (js-strip-fn-name raw 0 (len raw))))) + +(define + js-strip-fn-name + (fn + (s i n) + (let + ((start (if (and (< i n) (= (char-at s i) "<")) (+ i 1) i))) + (js-strip-fn-name-end s start n)))) + +(define + js-strip-fn-name-end + (fn + (s start n) + (let + ((end (js-find-paren-or-space s start n))) + (let ((name (js-string-slice s start end))) (js-unmap-fn-name name))))) + +;; ── String coercion (ToString) ──────────────────────────────────── + +(define + js-find-paren-or-space + (fn + (s i n) + (cond + ((>= i n) n) + ((or (= (char-at s i) "(") (= (char-at s i) " ")) i) + (else (js-find-paren-or-space s (+ i 1) n))))) + +(define + js-unmap-fn-name + (fn + (name) + (cond + ((= name "js-math-abs") "abs") + ((= name "js-math-floor") "floor") + ((= name "js-math-ceil") "ceil") + ((= name "js-math-round") "round") + ((= name "js-math-max") "max") + ((= name "js-math-min") "min") + ((= name "js-math-random") "random") + ((= name "js-math-sqrt") "sqrt") + ((= name "js-math-pow") "pow") + ((= name "js-math-trunc") "trunc") + ((= name "js-math-sign") "sign") + ((= name "js-math-cbrt") "cbrt") + ((= name "js-math-hypot") "hypot") + ((= name "js-number-is-finite") "isFinite") + ((= name "js-number-is-nan") "isNaN") + ((= name "js-number-is-integer") "isInteger") + ((= name "js-number-is-safe-integer") "isSafeInteger") + ((= name "js-global-is-finite") "isFinite") + ((= name "js-global-is-nan") "isNaN") + ((= name "js-string-from-char-code") "fromCharCode") + ((= name "js-array-is-array") "isArray") + ((= name "js-array-of") "of") + ((= name "js-array-from") "from") + ((= name "js-object-keys") "keys") + ((= name "js-object-values") "values") + ((= name "js-object-entries") "entries") + ((= name "js-object-assign") "assign") + ((= name "js-object-freeze") "freeze") + ((= name "js-object-get-prototype-of") "getPrototypeOf") + ((= name "js-object-set-prototype-of") "setPrototypeOf") + ((= name "js-object-create") "create") + ((= name "js-object-define-property") "defineProperty") + ((= name "js-object-define-properties") "defineProperties") + ((= name "js-object-get-own-property-names") "getOwnPropertyNames") + ((= name "js-object-get-own-property-descriptor") + "getOwnPropertyDescriptor") + ((= name "js-object-get-own-property-descriptors") + "getOwnPropertyDescriptors") + ((= name "js-object-is-extensible") "isExtensible") + ((= name "js-object-is-frozen") "isFrozen") + ((= name "js-object-is-sealed") "isSealed") + ((= name "js-object-seal") "seal") + ((= name "js-object-prevent-extensions") "preventExtensions") + ((= name "js-object-is") "is") + ((= name "js-object-from-entries") "fromEntries") + ((= name "js-object-has-own") "hasOwn") + ((= name "js-to-number") "Number") + ((= name "js-to-boolean") "Boolean") + (else name)))) + +;; ── Arithmetic (JS rules) ───────────────────────────────────────── + +;; JS `+`: if either operand is a string → string concat, else numeric. (define js-count-real-params (fn @@ -162,7 +251,7 @@ (&rest more) (js-call-with-this this-arg recv (js-list-concat bound more))))) ((= key "toString") "function () { [native code] }") - ((= key "name") "") + ((= key "name") (js-extract-fn-name recv)) ((= key "length") (js-fn-length recv)) (else :js-undefined)))) @@ -179,8 +268,6 @@ (fn (&rest args) (js-invoke-function-method recv "bind" args))) (else :js-undefined)))) -;; ── String coercion (ToString) ──────────────────────────────────── - (define js-invoke-number-method (fn @@ -226,9 +313,6 @@ ((= key "toLocaleString") "function () { [native code] }") (else :js-undefined)))) -;; ── Arithmetic (JS rules) ───────────────────────────────────────── - -;; JS `+`: if either operand is a string → string concat, else numeric. (define js-invoke-boolean-method (fn @@ -272,6 +356,7 @@ ((d (mod n radix)) (rest (js-math-trunc (/ n radix)))) (js-num-to-str-radix-rec rest radix (str (js-digit-char d) acc)))))) +;; Bitwise + logical-not (define js-digit-char (fn @@ -315,6 +400,9 @@ (js-to-string (js-math-trunc frac-part)) d)))))))))))) +;; ── Equality ────────────────────────────────────────────────────── + +;; Strict equality (===): no coercion; js-undefined matches js-undefined. (define js-pow-int (fn (b e) (if (<= e 0) 1 (* b (js-pow-int b (- e 1)))))) @@ -323,6 +411,9 @@ js-pad-int-str (fn (s n) (if (>= (len s) n) s (js-pad-int-str (str "0" s) n)))) +;; Abstract equality (==): type coercion rules. +;; Simplified: number↔string coerce both to number; null == undefined; +;; everything else falls back to strict equality. (define js-apply-fn (fn @@ -354,7 +445,6 @@ (nth args 5))) (else (apply callable args)))))) -;; Bitwise + logical-not (define js-invoke-method (fn @@ -380,6 +470,11 @@ (error (str "TypeError: " (js-to-string key) " is not a function"))))))))) +;; ── Relational comparisons ──────────────────────────────────────── + +;; Abstract relational comparison from ES5. +;; Numbers compare numerically; two strings compare lexicographically; +;; mixed types coerce both to numbers. (define js-object-builtin-method? (fn @@ -392,9 +487,6 @@ (= name "valueOf") (= name "toLocaleString")))) -;; ── Equality ────────────────────────────────────────────────────── - -;; Strict equality (===): no coercion; js-undefined matches js-undefined. (define js-invoke-object-method (fn @@ -418,9 +510,6 @@ (define js-upper-case (fn (s) (js-case-loop s 0 "" true))) -;; Abstract equality (==): type coercion rules. -;; Simplified: number↔string coerce both to number; null == undefined; -;; everything else falls back to strict equality. (define js-lower-case (fn (s) (js-case-loop s 0 "" false))) (define @@ -438,11 +527,6 @@ ((cv (cond ((and to-upper? (>= cc 97) (<= cc 122)) (js-code-to-char (- cc 32))) ((and (not to-upper?) (>= cc 65) (<= cc 90)) (js-code-to-char (+ cc 32))) (else c)))) (js-case-loop s (+ i 1) (str acc cv) to-upper?)))))))) -;; ── Relational comparisons ──────────────────────────────────────── - -;; Abstract relational comparison from ES5. -;; Numbers compare numerically; two strings compare lexicographically; -;; mixed types coerce both to numbers. (define js-code-to-char (fn @@ -506,6 +590,13 @@ js-invoke-method-dyn (fn (recv key args) (js-invoke-method recv key args))) +;; ── Property access ─────────────────────────────────────────────── + +;; obj[key] or obj.key in JS. Handles: +;; • dicts keyed by string +;; • lists indexed by number (incl. .length) +;; • strings indexed by number (incl. .length) +;; Returns js-undefined if the key is absent. (define js-call-plain (fn @@ -545,6 +636,7 @@ ((proto (js-get-ctor-proto ctor))) (js-instanceof-walk obj proto)))))) +;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs). (define js-instanceof-walk (fn @@ -560,6 +652,10 @@ ((not (= (type-of p) "dict")) false) (else (js-instanceof-walk p proto)))))))) +;; ── Short-circuit logical ops ───────────────────────────────────── + +;; `a && b` in JS: if a is truthy return b else return a. The thunk +;; form defers evaluation of b — the transpiler passes (fn () b). (define js-in (fn @@ -568,13 +664,6 @@ ((not (= (type-of obj) "dict")) false) (else (js-in-walk obj (js-to-string key)))))) -;; ── Property access ─────────────────────────────────────────────── - -;; obj[key] or obj.key in JS. Handles: -;; • dicts keyed by string -;; • lists indexed by number (incl. .length) -;; • strings indexed by number (incl. .length) -;; Returns js-undefined if the key is absent. (define js-in-walk (fn @@ -585,6 +674,9 @@ ((dict-has? obj "__proto__") (js-in-walk (get obj "__proto__") skey)) (else false)))) +;; ── console.log ─────────────────────────────────────────────────── + +;; Trivial bridge. `log-info` is available on OCaml; fall back to print. (define Error (fn @@ -621,7 +713,8 @@ nil) this)))) -;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs). +;; ── Math object ─────────────────────────────────────────────────── + (define RangeError (fn @@ -639,11 +732,6 @@ (dict-set! this "name" "RangeError")) nil) this)))) - -;; ── Short-circuit logical ops ───────────────────────────────────── - -;; `a && b` in JS: if a is truthy return b else return a. The thunk -;; form defers evaluation of b — the transpiler passes (fn () b). (define SyntaxError (fn @@ -661,7 +749,6 @@ (dict-set! this "name" "SyntaxError")) nil) this)))) - (define ReferenceError (fn @@ -679,10 +766,6 @@ (dict-set! this "name" "ReferenceError")) nil) this)))) - -;; ── console.log ─────────────────────────────────────────────────── - -;; Trivial bridge. `log-info` is available on OCaml; fall back to print. (define URIError (fn @@ -700,7 +783,6 @@ (if (empty? args) "" (js-to-string (nth args 0)))) (dict-set! this "name" "URIError"))) this)))) - (define EvalError (fn @@ -718,9 +800,6 @@ (if (empty? args) "" (js-to-string (nth args 0)))) (dict-set! this "name" "EvalError"))) this)))) - -;; ── Math object ─────────────────────────────────────────────────── - (define js-function? (fn @@ -734,7 +813,8 @@ (and (= t "dict") (contains? (keys v) "__callable__")))))) (define __js_proto_table__ (dict)) (define __js_next_id__ (dict)) -(dict-set! __js_next_id__ "n" 0) +(dict-set! __js_next_id__ "n" 0) ; deterministic placeholder for tests + (define js-get-ctor-proto (fn @@ -751,6 +831,11 @@ (let ((p (dict))) (begin (dict-set! __js_proto_table__ id p) p))))))))) + +;; The global object — lookup table for JS names that aren't in the +;; SX env. Transpiled idents look up locally first; globals here are a +;; fallback, but most slice programs reference `console`, `Math`, +;; `undefined` as plain symbols, which we bind as defines above. (define js-reset-ctor-proto! (fn @@ -758,11 +843,13 @@ (let ((id (js-ctor-id ctor)) (p (dict))) (begin (dict-set! __js_proto_table__ id p) p)))) + (define js-set-ctor-proto! (fn (ctor proto) (let ((id (js-ctor-id ctor))) (dict-set! __js_proto_table__ id proto)))) + (define js-ctor-id (fn @@ -771,6 +858,7 @@ ((and (= (type-of ctor) "dict") (dict-has? ctor "__ctor_id__")) (get ctor "__ctor_id__")) (else (inspect ctor))))) + (define js-typeof (fn @@ -786,7 +874,7 @@ ((= (type-of v) "component") "function") ((and (= (type-of v) "dict") (contains? (keys v) "__callable__")) "function") - (else "object")))) ; deterministic placeholder for tests + (else "object")))) (define js-to-boolean @@ -800,10 +888,6 @@ ((= v "") false) (else true)))) -;; The global object — lookup table for JS names that aren't in the -;; SX env. Transpiled idents look up locally first; globals here are a -;; fallback, but most slice programs reference `console`, `Math`, -;; `undefined` as plain symbols, which we bind as defines above. (define js-to-number (fn @@ -2180,7 +2264,7 @@ ((and (js-function? obj) (or (= key "prototype") (= key "name") (= key "length") (= key "call") (= key "apply") (= key "bind"))) (cond ((= key "prototype") (js-get-ctor-proto obj)) - ((= key "name") "") + ((= key "name") (js-extract-fn-name obj)) ((= key "length") (js-fn-length obj)) (else (js-invoke-function-bound obj key)))) (else js-undefined))))