From 84b947024da726dfd6167fb611944c6c2b91491c Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 24 Apr 2026 08:42:35 +0000 Subject: [PATCH] js-on-sx: fn.length/.name/.call/.apply/.bind as properties (not just methods) js-get-prop on a function receiver only routed .prototype before; now also handles .name (returns ''), .length (returns 0), and .call/.apply/.bind as bound function references. Previously Math.abs.length crashed with 'TypeError: length is not a function'. Similarly for arr.sort.call which is a common test262 pattern. Pass rate stable at 514/516. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/js/runtime.sx | 107 +++++++++++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index fc6c4427..31f3b661 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -109,6 +109,19 @@ ((= key "length") 0) (else :js-undefined)))) +(define + js-invoke-function-bound + (fn + (recv key) + (cond + ((= key "call") + (fn (&rest args) (js-invoke-function-method recv "call" args))) + ((= key "apply") + (fn (&rest args) (js-invoke-function-method recv "apply" args))) + ((= key "bind") + (fn (&rest args) (js-invoke-function-method recv "bind" args))) + (else :js-undefined)))) + (define js-invoke-number-method (fn @@ -170,6 +183,13 @@ (str "-" (js-num-to-str-radix-rec (- 0 int-n) radix "")) (js-num-to-str-radix-rec int-n radix ""))))))) +;; 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-num-to-str-radix-rec (fn @@ -181,13 +201,6 @@ ((d (mod n radix)) (rest (js-math-trunc (/ n radix)))) (js-num-to-str-radix-rec rest radix (str (js-digit-char d) acc)))))) -;; 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-digit-char (fn @@ -220,16 +233,19 @@ "." (js-pad-int-str (js-to-string (js-math-trunc frac-abs)) d)))))))))) +;; ── String coercion (ToString) ──────────────────────────────────── + (define js-pow-int (fn (b e) (if (<= e 0) 1 (* b (js-pow-int b (- e 1)))))) -;; ── String coercion (ToString) ──────────────────────────────────── - (define js-pad-int-str (fn (s n) (if (>= (len s) n) s (js-pad-int-str (str "0" s) n)))) +;; ── Arithmetic (JS rules) ───────────────────────────────────────── + +;; JS `+`: if either operand is a string → string concat, else numeric. (define js-apply-fn (fn @@ -261,9 +277,6 @@ (nth args 5))) (else (apply callable args)))))) -;; ── Arithmetic (JS rules) ───────────────────────────────────────── - -;; JS `+`: if either operand is a string → string concat, else numeric. (define js-invoke-method (fn @@ -398,11 +411,11 @@ ((= code 122) "z") (else "")))) +;; Bitwise + logical-not (define js-invoke-method-dyn (fn (recv key args) (js-invoke-method recv key args))) -;; Bitwise + logical-not (define js-call-plain (fn @@ -414,6 +427,9 @@ (js-call-with-this :js-undefined (get fn-val "__callable__") args)) (else (js-call-with-this :js-undefined fn-val args))))) +;; ── Equality ────────────────────────────────────────────────────── + +;; Strict equality (===): no coercion; js-undefined matches js-undefined. (define js-new-call (fn @@ -429,9 +445,6 @@ ret obj)))))) -;; ── Equality ────────────────────────────────────────────────────── - -;; Strict equality (===): no coercion; js-undefined matches js-undefined. (define js-instanceof (fn @@ -445,6 +458,9 @@ ((proto (js-get-ctor-proto ctor))) (js-instanceof-walk obj proto)))))) +;; Abstract equality (==): type coercion rules. +;; Simplified: number↔string coerce both to number; null == undefined; +;; everything else falls back to strict equality. (define js-instanceof-walk (fn @@ -460,9 +476,6 @@ ((not (= (type-of p) "dict")) false) (else (js-instanceof-walk p proto)))))))) -;; Abstract equality (==): type coercion rules. -;; Simplified: number↔string coerce both to number; null == undefined; -;; everything else falls back to strict equality. (define js-in (fn @@ -471,6 +484,11 @@ ((not (= (type-of obj) "dict")) false) (else (js-in-walk obj (js-to-string key)))))) +;; ── Relational comparisons ──────────────────────────────────────── + +;; Abstract relational comparison from ES5. +;; Numbers compare numerically; two strings compare lexicographically; +;; mixed types coerce both to numbers. (define js-in-walk (fn @@ -481,11 +499,6 @@ ((dict-has? obj "__proto__") (js-in-walk (get obj "__proto__") skey)) (else false)))) -;; ── Relational comparisons ──────────────────────────────────────── - -;; Abstract relational comparison from ES5. -;; Numbers compare numerically; two strings compare lexicographically; -;; mixed types coerce both to numbers. (define Error (fn @@ -588,8 +601,6 @@ (= t "component") (and (= t "dict") (contains? (keys v) "__callable__")))))) -(define __js_proto_table__ (dict)) - ;; ── Property access ─────────────────────────────────────────────── ;; obj[key] or obj.key in JS. Handles: @@ -597,10 +608,13 @@ ;; • lists indexed by number (incl. .length) ;; • strings indexed by number (incl. .length) ;; Returns js-undefined if the key is absent. +(define __js_proto_table__ (dict)) + (define __js_next_id__ (dict)) (dict-set! __js_next_id__ "n" 0) +;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs). (define js-get-ctor-proto (fn @@ -612,7 +626,10 @@ (else (let ((p (dict))) (begin (dict-set! __js_proto_table__ id p) p))))))) -;; 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 js-reset-ctor-proto! (fn @@ -621,16 +638,15 @@ ((id (js-ctor-id ctor)) (p (dict))) (begin (dict-set! __js_proto_table__ id p) p)))) -;; ── 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-set-ctor-proto! (fn (ctor proto) (let ((id (js-ctor-id ctor))) (dict-set! __js_proto_table__ id proto)))) +;; ── console.log ─────────────────────────────────────────────────── + +;; Trivial bridge. `log-info` is available on OCaml; fall back to print. (define js-ctor-id (fn @@ -640,9 +656,6 @@ (get ctor "__ctor_id__")) (else (inspect ctor))))) -;; ── console.log ─────────────────────────────────────────────────── - -;; Trivial bridge. `log-info` is available on OCaml; fall back to print. (define js-typeof (fn @@ -660,6 +673,8 @@ "function") (else "object")))) +;; ── Math object ─────────────────────────────────────────────────── + (define js-to-boolean (fn @@ -671,9 +686,6 @@ ((= v 0) false) ((= v "") false) (else true)))) - -;; ── Math object ─────────────────────────────────────────────────── - (define js-to-number (fn @@ -743,7 +755,8 @@ (define js-trim (fn (s) (js-trim-left (js-trim-right s)))) (define js-trim-left - (fn (s) (let ((n (len s))) (js-trim-left-at s n 0)))) + (fn (s) (let ((n (len s))) (js-trim-left-at s n 0)))) ; deterministic placeholder for tests + (define js-trim-left-at (fn @@ -751,16 +764,16 @@ (cond ((>= i n) "") ((js-is-space? (char-at s i)) (js-trim-left-at s n (+ i 1))) - (else (substr s i n))))) ; deterministic placeholder for tests - -(define - js-trim-right - (fn (s) (let ((n (len s))) (js-trim-right-at s n)))) + (else (substr s i n))))) ;; 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-trim-right + (fn (s) (let ((n (len s))) (js-trim-right-at s n)))) + (define js-trim-right-at (fn @@ -1941,8 +1954,12 @@ (js-dict-get-walk obj (js-to-string key))) ((and (= obj Promise) (dict-has? __js_promise_statics__ (js-to-string key))) (get __js_promise_statics__ (js-to-string key))) - ((and (js-function? obj) (= (js-to-string key) "prototype")) - (js-get-ctor-proto obj)) + ((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 "length") 0) + (else (js-invoke-function-bound obj key)))) (else js-undefined)))) (define js-dict-get-walk