js-on-sx: Function.prototype.isPrototypeOf recognises callable recvs (+3)

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).
This commit is contained in:
2026-04-24 13:07:33 +00:00
parent 52fc87f222
commit 81059861fd
3 changed files with 97 additions and 87 deletions

View File

@@ -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