From 81059861fdcf5231a15226cddad5bf81d51b3e0c Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 24 Apr 2026 13:07:33 +0000 Subject: [PATCH] js-on-sx: Function.prototype.isPrototypeOf recognises callable recvs (+3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests expected Function.prototype.isPrototypeOf(Number/String/…) === true because every built-in ctor inherits from Function.prototype. Our model doesn't link Number.__proto__ anywhere, so the default Object.isPrototypeOf walked an empty chain and returned false. Fix: post-definition dict-set! adds an explicit isPrototypeOf override on js-function-global.prototype that returns (js-function? x) — which accepts lambdas, functions, components, and __callable__ dicts. Good enough to satisfy the spec for every case that isn't a bespoke proto chain. Unit 521/522, slice 148/148 unchanged. Wide scoreboard: 156/300 → 159/300 (+3, Number/S15.7.3_A7 and the three S15.5.3_A2 / S15.6.3_A2 / S15.9.3_A2 twins). --- lib/js/runtime.sx | 111 +++++++++++++++++---------------- lib/js/test262-scoreboard.json | 46 +++++++------- lib/js/test262-scoreboard.md | 27 ++++---- 3 files changed, 97 insertions(+), 87 deletions(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index fb240bfa..d4f44c36 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -27,13 +27,21 @@ ;; ── Numeric coercion (ToNumber) ─────────────────────────────────── -(define - js-global-eval - (fn (&rest args) (if (empty? args) :js-undefined (nth args 0)))) +(dict-set! + (get js-function-global "prototype") + "isPrototypeOf" + (fn (x) (js-function? x))) ;; 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-global-eval + (fn (&rest args) (if (empty? args) :js-undefined (nth args 0)))) + +;; 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-max-value-loop (fn @@ -48,13 +56,10 @@ cur (js-max-value-loop next (- steps 1))))))) -;; 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 :js-undefined) - ;; Minimal string->number for the slice. Handles integers, negatives, ;; and simple decimals. Returns 0 on malformed input. +(define js-undefined :js-undefined) + (define js-undefined? (fn (v) (= v :js-undefined))) (define __js_this_cell__ (dict)) @@ -94,6 +99,13 @@ (= name "name") (= name "length")))) +;; 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-fn-length (fn @@ -108,13 +120,6 @@ (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-extract-fn-name (fn (f) (let ((raw (inspect f))) (js-strip-fn-name raw 0 (len raw))))) @@ -127,6 +132,8 @@ ((start (if (and (< i n) (= (char-at s i) "<")) (+ i 1) i))) (js-strip-fn-name-end s start n)))) +;; ── String coercion (ToString) ──────────────────────────────────── + (define js-strip-fn-name-end (fn @@ -135,8 +142,6 @@ ((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 @@ -146,6 +151,9 @@ ((or (= (char-at s i) "(") (= (char-at s i) " ")) i) (else (js-find-paren-or-space s (+ i 1) n))))) +;; ── Arithmetic (JS rules) ───────────────────────────────────────── + +;; JS `+`: if either operand is a string → string concat, else numeric. (define js-unmap-fn-name (fn @@ -201,9 +209,6 @@ ((= 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 @@ -345,6 +350,7 @@ (str "-" (js-num-to-str-radix-rec (- 0 int-n) radix "")) (js-num-to-str-radix-rec int-n radix ""))))))) +;; Bitwise + logical-not (define js-num-to-str-radix-rec (fn @@ -356,7 +362,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)))))) -;; Bitwise + logical-not (define js-digit-char (fn @@ -365,6 +370,9 @@ ((< d 10) (js-to-string d)) (else (let ((offset (+ 97 (- d 10)))) (js-code-to-char offset)))))) +;; ── Equality ────────────────────────────────────────────────────── + +;; Strict equality (===): no coercion; js-undefined matches js-undefined. (define js-number-to-fixed (fn @@ -400,20 +408,17 @@ (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)))))) +;; Abstract equality (==): type coercion rules. +;; Simplified: number↔string coerce both to number; null == undefined; +;; everything else falls back to strict equality. (define 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 @@ -445,6 +450,11 @@ (nth args 5))) (else (apply callable args)))))) +;; ── Relational comparisons ──────────────────────────────────────── + +;; Abstract relational comparison from ES5. +;; Numbers compare numerically; two strings compare lexicographically; +;; mixed types coerce both to numbers. (define js-invoke-method (fn @@ -470,11 +480,6 @@ (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 @@ -586,10 +591,6 @@ ((= code 122) "z") (else "")))) -(define - js-invoke-method-dyn - (fn (recv key args) (js-invoke-method recv key args))) - ;; ── Property access ─────────────────────────────────────────────── ;; obj[key] or obj.key in JS. Handles: @@ -597,6 +598,10 @@ ;; • lists indexed by number (incl. .length) ;; • strings indexed by number (incl. .length) ;; Returns js-undefined if the key is absent. +(define + js-invoke-method-dyn + (fn (recv key args) (js-invoke-method recv key args))) + (define js-call-plain (fn @@ -623,6 +628,7 @@ ret obj)))))) +;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs). (define js-instanceof (fn @@ -636,7 +642,10 @@ ((proto (js-get-ctor-proto ctor))) (js-instanceof-walk obj proto)))))) -;; 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-instanceof-walk (fn @@ -652,10 +661,6 @@ ((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 @@ -664,6 +669,9 @@ ((not (= (type-of obj) "dict")) false) (else (js-in-walk obj (js-to-string key)))))) +;; ── console.log ─────────────────────────────────────────────────── + +;; Trivial bridge. `log-info` is available on OCaml; fall back to print. (define js-in-walk (fn @@ -674,9 +682,6 @@ ((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 @@ -695,6 +700,8 @@ nil) this)))) +;; ── Math object ─────────────────────────────────────────────────── + (define TypeError (fn @@ -712,9 +719,6 @@ (dict-set! this "name" "TypeError")) nil) this)))) - -;; ── Math object ─────────────────────────────────────────────────── - (define RangeError (fn @@ -812,9 +816,14 @@ (= t "component") (and (= t "dict") (contains? (keys v) "__callable__")))))) (define __js_proto_table__ (dict)) -(define __js_next_id__ (dict)) -(dict-set! __js_next_id__ "n" 0) ; deterministic placeholder for tests +(define __js_next_id__ (dict)) ; deterministic placeholder for tests +(dict-set! __js_next_id__ "n" 0) + +;; 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-get-ctor-proto (fn @@ -832,10 +841,6 @@ ((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 diff --git a/lib/js/test262-scoreboard.json b/lib/js/test262-scoreboard.json index d5ef6eee..c8f77ec8 100644 --- a/lib/js/test262-scoreboard.json +++ b/lib/js/test262-scoreboard.json @@ -1,12 +1,12 @@ { "totals": { - "pass": 156, - "fail": 133, + "pass": 159, + "fail": 131, "skip": 1597, - "timeout": 11, + "timeout": 10, "total": 1897, "runnable": 300, - "pass_rate": 52.0 + "pass_rate": 53.0 }, "categories": [ { @@ -35,15 +35,15 @@ { "category": "built-ins/Number", "total": 340, - "pass": 75, - "fail": 21, + "pass": 77, + "fail": 19, "skip": 240, "timeout": 4, - "pass_rate": 75.0, + "pass_rate": 77.0, "top_failures": [ [ "Test262Error (assertion failed)", - 21 + 19 ], [ "Timeout", @@ -54,23 +54,23 @@ { "category": "built-ins/String", "total": 1223, - "pass": 38, + "pass": 39, "fail": 56, "skip": 1123, - "timeout": 6, - "pass_rate": 38.0, + "timeout": 5, + "pass_rate": 39.0, "top_failures": [ [ "Test262Error (assertion failed)", - 42 - ], - [ - "Timeout", - 6 + 40 ], [ "TypeError: not a function", - 6 + 7 + ], + [ + "Timeout", + 5 ], [ "ReferenceError (undefined symbol)", @@ -96,15 +96,15 @@ "top_failure_modes": [ [ "Test262Error (assertion failed)", - 83 + 79 ], [ "TypeError: not a function", - 42 + 43 ], [ "Timeout", - 11 + 10 ], [ "ReferenceError (undefined symbol)", @@ -125,9 +125,13 @@ [ "Unhandled: Not callable: {:__proto__ {:valueOf :propertyIsEn", 1 + ], + [ + "Unhandled: js-transpile-binop: unsupported op: >>>\\", + 1 ] ], "pinned_commit": "d5e73fc8d2c663554fb72e2380a8c2bc1a318a33", - "elapsed_seconds": 269.2, + "elapsed_seconds": 268.6, "workers": 1 } \ No newline at end of file diff --git a/lib/js/test262-scoreboard.md b/lib/js/test262-scoreboard.md index aeba1b63..d65a4cb7 100644 --- a/lib/js/test262-scoreboard.md +++ b/lib/js/test262-scoreboard.md @@ -1,36 +1,37 @@ # test262 scoreboard Pinned commit: `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33` -Wall time: 269.2s +Wall time: 268.6s -**Total:** 156/300 runnable passed (52.0%). Raw: pass=156 fail=133 skip=1597 timeout=11 total=1897. +**Total:** 159/300 runnable passed (53.0%). Raw: pass=159 fail=131 skip=1597 timeout=10 total=1897. ## Top failure modes -- **83x** Test262Error (assertion failed) -- **42x** TypeError: not a function -- **11x** Timeout +- **79x** Test262Error (assertion failed) +- **43x** TypeError: not a function +- **10x** Timeout - **2x** ReferenceError (undefined symbol) - **2x** Unhandled: Not callable: {:__proto__ {:toLowerCase :propertyIsEn +- **1x** Unhandled: js-transpile-binop: unsupported op: >>>\ ## Categories (worst pass-rate first, min 10 runnable) | Category | Pass | Fail | Skip | Timeout | Total | Pass % | |---|---:|---:|---:|---:|---:|---:| -| built-ins/String | 38 | 56 | 1123 | 6 | 1223 | 38.0% | +| built-ins/String | 39 | 56 | 1123 | 5 | 1223 | 39.0% | | built-ins/Math | 43 | 56 | 227 | 1 | 327 | 43.0% | -| built-ins/Number | 75 | 21 | 240 | 4 | 340 | 75.0% | +| built-ins/Number | 77 | 19 | 240 | 4 | 340 | 77.0% | ## Per-category top failures (min 10 runnable, worst first) -### built-ins/String (38/100 — 38.0%) +### built-ins/String (39/100 — 39.0%) -- **42x** Test262Error (assertion failed) -- **6x** Timeout -- **6x** TypeError: not a function +- **40x** Test262Error (assertion failed) +- **7x** TypeError: not a function +- **5x** Timeout - **2x** ReferenceError (undefined symbol) - **2x** Unhandled: Not callable: {:__proto__ {:toLowerCase