From 38e937657309ff380d512382c185a2a7a6cca8c3 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 24 Apr 2026 09:54:42 +0000 Subject: [PATCH] js-on-sx: Function global stub (constructor throws, prototype has stubs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several tests check 'new Function("return 1")' — we can't actually implement that (would need runtime JS eval). Now Function is a dict with __callable__ that throws TypeError, and a prototype containing call/apply/ bind/toString/length/name stubs so code that probes Function.prototype doesn't crash with 'Undefined symbol: Function'. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/js/runtime.sx | 110 ++++++++++++++++++++++---------------------- lib/js/transpile.sx | 1 + 2 files changed, 57 insertions(+), 54 deletions(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 278878a5..d33bf4a5 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -23,12 +23,17 @@ ;; ── Boolean coercion (ToBoolean) ────────────────────────────────── +(define js-function-global {:__callable__ (fn (&rest args) (error "TypeError: Function constructor not supported")) :prototype {:call (fn (&rest args) :js-undefined) :length 0 :bind (fn (&rest args) (fn () :js-undefined)) :toString (fn () "function () { [native code] }") :apply (fn (&rest args) :js-undefined) :name ""}}) + +;; ── Numeric coercion (ToNumber) ─────────────────────────────────── + (define js-global-eval (fn (&rest args) (if (empty? args) :js-undefined (nth args 0)))) -;; ── Numeric coercion (ToNumber) ─────────────────────────────────── - +;; Parse a JS-style string to a number. For the slice we just delegate +;; to SX's number parser via `str->num`/`parse-number`. Empty string → 0 +;; per JS (technically ToNumber("") === 0). (define js-max-value-loop (fn @@ -43,18 +48,15 @@ cur (js-max-value-loop next (- steps 1))))))) -;; Parse a JS-style string to a number. For the slice we just delegate -;; to SX's number parser via `str->num`/`parse-number`. Empty string → 0 -;; per JS (technically ToNumber("") === 0). -(define js-undefined :js-undefined) - ;; Safe number-parser. Tries to call an SX primitive that can parse ;; strings to numbers; on failure returns 0 (stand-in for NaN so the ;; slice doesn't crash). -(define js-undefined? (fn (v) (= v :js-undefined))) +(define js-undefined :js-undefined) ;; Minimal string->number for the slice. Handles integers, negatives, ;; and simple decimals. Returns 0 on malformed input. +(define js-undefined? (fn (v) (= v :js-undefined))) + (define __js_this_cell__ (dict)) (define @@ -106,6 +108,13 @@ (js-fn-length (get f "__callable__"))) (else 0))))) +;; parse a decimal number from a trimmed non-empty string. +;; s — source +;; i — cursor +;; acc — integer part so far (or total for decimals) +;; sign — 1 or -1 +;; frac? — are we past the decimal point +;; fdiv — divisor used to scale fraction digits (only if frac?) (define js-count-real-params (fn @@ -120,13 +129,6 @@ 0 (+ 1 (js-count-real-params (rest params))))))))) -;; parse a decimal number from a trimmed non-empty string. -;; s — source -;; i — cursor -;; acc — integer part so far (or total for decimals) -;; sign — 1 or -1 -;; frac? — are we past the decimal point -;; fdiv — divisor used to scale fraction digits (only if frac?) (define js-invoke-function-method (fn @@ -177,6 +179,8 @@ (fn (&rest args) (js-invoke-function-method recv "bind" args))) (else :js-undefined)))) +;; ── String coercion (ToString) ──────────────────────────────────── + (define js-invoke-number-method (fn @@ -206,8 +210,6 @@ (js-to-string key) " is not a function (on number)")))))) -;; ── String coercion (ToString) ──────────────────────────────────── - (define js-invoke-function-objproto (fn @@ -224,6 +226,9 @@ ((= 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 @@ -238,9 +243,6 @@ (js-to-string key) " is not a function (on boolean)")))))) -;; ── Arithmetic (JS rules) ───────────────────────────────────────── - -;; JS `+`: if either operand is a string → string concat, else numeric. (define js-num-to-str-radix (fn @@ -352,6 +354,7 @@ (nth args 5))) (else (apply callable args)))))) +;; Bitwise + logical-not (define js-invoke-method (fn @@ -377,7 +380,6 @@ (error (str "TypeError: " (js-to-string key) " is not a function"))))))))) -;; Bitwise + logical-not (define js-object-builtin-method? (fn @@ -390,6 +392,9 @@ (= name "valueOf") (= name "toLocaleString")))) +;; ── Equality ────────────────────────────────────────────────────── + +;; Strict equality (===): no coercion; js-undefined matches js-undefined. (define js-invoke-object-method (fn @@ -411,16 +416,13 @@ ((= name "toLocaleString") "[object Object]") (else js-undefined)))) -;; ── Equality ────────────────────────────────────────────────────── - -;; Strict equality (===): no coercion; js-undefined matches js-undefined. (define js-upper-case (fn (s) (js-case-loop s 0 "" true))) -(define js-lower-case (fn (s) (js-case-loop s 0 "" false))) - ;; 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 js-case-loop (fn @@ -436,6 +438,11 @@ ((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 @@ -495,11 +502,6 @@ ((= code 122) "z") (else "")))) -;; ── Relational comparisons ──────────────────────────────────────── - -;; Abstract relational comparison from ES5. -;; Numbers compare numerically; two strings compare lexicographically; -;; mixed types coerce both to numbers. (define js-invoke-method-dyn (fn (recv key args) (js-invoke-method recv key args))) @@ -566,6 +568,13 @@ ((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 @@ -576,13 +585,6 @@ ((dict-has? obj "__proto__") (js-in-walk (get obj "__proto__") skey)) (else false)))) -;; ── 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 Error (fn @@ -619,6 +621,7 @@ nil) this)))) +;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs). (define RangeError (fn @@ -637,7 +640,10 @@ nil) this)))) -;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs). +;; ── 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 @@ -656,10 +662,6 @@ 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 ReferenceError (fn @@ -678,6 +680,9 @@ nil) this)))) +;; ── console.log ─────────────────────────────────────────────────── + +;; Trivial bridge. `log-info` is available on OCaml; fall back to print. (define js-function? (fn @@ -690,15 +695,11 @@ (= t "component") (and (= t "dict") (contains? (keys v) "__callable__")))))) -;; ── console.log ─────────────────────────────────────────────────── - -;; Trivial bridge. `log-info` is available on OCaml; fall back to print. (define __js_proto_table__ (dict)) -(define __js_next_id__ (dict)) - ;; ── Math object ─────────────────────────────────────────────────── +(define __js_next_id__ (dict)) (dict-set! __js_next_id__ "n" 0) (define js-get-ctor-proto @@ -768,7 +769,8 @@ ((= v false) 0) ((= (type-of v) "number") v) ((= (type-of v) "string") (js-string-to-number v)) - (else 0)))) + (else 0)))) ; deterministic placeholder for tests + (define js-string-to-number (fn @@ -781,16 +783,16 @@ ((= trimmed "+Infinity") (js-infinity-value)) ((= trimmed "-Infinity") (- 0 (js-infinity-value))) ((js-is-numeric-string? trimmed) (js-parse-num-safe trimmed)) - (else (js-nan-value)))))) ; deterministic placeholder for tests - -(define - js-is-numeric-string? - (fn (s) (js-is-numeric-loop s 0 false false false))) + (else (js-nan-value)))))) ;; 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-is-numeric-string? + (fn (s) (js-is-numeric-loop s 0 false false false))) + (define js-is-numeric-loop (fn diff --git a/lib/js/transpile.sx b/lib/js/transpile.sx index 3124f8e2..42c9dd4d 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -161,6 +161,7 @@ ((= name "Infinity") (list (js-sym "js-infinity-value"))) ((= name "eval") (js-sym "js-global-eval")) ((= name "globalThis") (js-sym "js-global")) + ((= name "Function") (js-sym "js-function-global")) (else (js-sym name))))) ;; ── Unary ops ─────────────────────────────────────────────────────