js-on-sx: Object.prototype methods on function receivers

String.prototype.toUpperCase.hasOwnProperty('length') was failing with
'TypeError: hasOwnProperty is not a function' because js-invoke-method's
dict-with-builtin fallback only matched 'dict' receivers, not functions.

New js-invoke-function-objproto branch handles hasOwnProperty (checks
name/length/prototype keys), toString, valueOf, isPrototypeOf,
propertyIsEnumerable, toLocaleString. Fires from js-invoke-method when
recv is js-function? and key is in the Object.prototype builtin set.

Unblocks many String.prototype tests that check
.hasOwnProperty('length') on the prototype methods.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 09:33:56 +00:00
parent ade87c0744
commit 1c0a71517c

View File

@@ -202,6 +202,24 @@
(js-to-string key)
" is not a function (on number)"))))))
(define
js-invoke-function-objproto
(fn
(recv key args)
(cond
((= key "hasOwnProperty")
(let
((k (if (empty? args) "" (js-to-string (nth args 0)))))
(or (= k "name") (= k "length") (= k "prototype"))))
((= key "toString") "function () { [native code] }")
((= key "valueOf") recv)
((= key "isPrototypeOf") false)
((= key "propertyIsEnumerable") false)
((= key "toLocaleString") "function () { [native code] }")
(else :js-undefined))))
;; ── String coercion (ToString) ────────────────────────────────────
(define
js-invoke-boolean-method
(fn
@@ -216,8 +234,6 @@
(js-to-string key)
" is not a function (on boolean)"))))))
;; ── String coercion (ToString) ────────────────────────────────────
(define
js-num-to-str-radix
(fn
@@ -236,6 +252,9 @@
(str "-" (js-num-to-str-radix-rec (- 0 int-n) radix ""))
(js-num-to-str-radix-rec int-n radix "")))))))
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define
js-num-to-str-radix-rec
(fn
@@ -247,9 +266,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))))))
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define
js-digit-char
(fn
@@ -349,6 +365,8 @@
((not (js-undefined? m)) (js-call-with-this recv m args))
((and (js-function? recv) (js-function-method? key))
(js-invoke-function-method recv key args))
((and (js-function? recv) (js-object-builtin-method? key))
(js-invoke-function-objproto recv key args))
((and (= (type-of recv) "dict") (js-object-builtin-method? key))
(js-invoke-object-method recv key args))
(else
@@ -367,6 +385,7 @@
(= name "valueOf")
(= name "toLocaleString"))))
;; Bitwise + logical-not
(define
js-invoke-object-method
(fn
@@ -388,14 +407,13 @@
((= name "toLocaleString") "[object Object]")
(else js-undefined))))
;; Bitwise + logical-not
(define js-upper-case (fn (s) (js-case-loop s 0 "" true)))
(define js-lower-case (fn (s) (js-case-loop s 0 "" false)))
;; ── Equality ──────────────────────────────────────────────────────
;; Strict equality (===): no coercion; js-undefined matches js-undefined.
(define js-lower-case (fn (s) (js-case-loop s 0 "" false)))
(define
js-case-loop
(fn
@@ -411,6 +429,9 @@
((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?))))))))
;; Abstract equality (==): type coercion rules.
;; Simplified: number↔string coerce both to number; null == undefined;
;; everything else falls back to strict equality.
(define
js-code-to-char
(fn
@@ -470,13 +491,15 @@
((= code 122) "z")
(else ""))))
;; Abstract equality (==): type coercion rules.
;; Simplified: number↔string coerce both to number; null == undefined;
;; everything else falls back to strict equality.
(define
js-invoke-method-dyn
(fn (recv key args) (js-invoke-method recv key args)))
;; ── Relational comparisons ────────────────────────────────────────
;; Abstract relational comparison from ES5.
;; Numbers compare numerically; two strings compare lexicographically;
;; mixed types coerce both to numbers.
(define
js-call-plain
(fn
@@ -488,11 +511,6 @@
(js-call-with-this :js-undefined (get fn-val "__callable__") args))
(else (js-call-with-this :js-undefined fn-val args)))))
;; ── Relational comparisons ────────────────────────────────────────
;; Abstract relational comparison from ES5.
;; Numbers compare numerically; two strings compare lexicographically;
;; mixed types coerce both to numbers.
(define
js-new-call
(fn
@@ -572,6 +590,13 @@
nil)
this))))
;; ── 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
TypeError
(fn
@@ -590,13 +615,6 @@
nil)
this))))
;; ── 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
RangeError
(fn
@@ -633,6 +651,7 @@
nil)
this))))
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
(define
ReferenceError
(fn
@@ -651,7 +670,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
js-function?
(fn
@@ -664,19 +686,17 @@
(= t "component")
(and (= t "dict") (contains? (keys v) "__callable__"))))))
;; ── 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_proto_table__ (dict))
(define __js_next_id__ (dict))
;; ── console.log ───────────────────────────────────────────────────
;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
(define __js_next_id__ (dict))
(dict-set! __js_next_id__ "n" 0)
;; ── Math object ───────────────────────────────────────────────────
(define
js-get-ctor-proto
(fn
@@ -687,9 +707,6 @@
((dict-has? __js_proto_table__ id) (get __js_proto_table__ id))
(else
(let ((p (dict))) (begin (dict-set! __js_proto_table__ id p) p)))))))
;; ── Math object ───────────────────────────────────────────────────
(define
js-reset-ctor-proto!
(fn
@@ -764,7 +781,8 @@
(else (js-nan-value))))))
(define
js-is-numeric-string?
(fn (s) (js-is-numeric-loop s 0 false false false)))
(fn (s) (js-is-numeric-loop s 0 false false false))) ; deterministic placeholder for tests
(define
js-is-numeric-loop
(fn
@@ -792,14 +810,14 @@
(or (= prev "e") (= prev "E"))
(js-is-numeric-loop s (+ i 1) sawdigit sawdot sawe)
false)))
(else false))))))) ; deterministic placeholder for tests
(define js-parse-num-safe (fn (s) (cond (else (js-num-from-string s)))))
(else false)))))))
;; 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-parse-num-safe (fn (s) (cond (else (js-num-from-string s)))))
(define
js-num-from-string
(fn