js-on-sx: fn.length reflects actual arity via lambda-params

Previously fn.length always returned 0 — so the 'length value' test262 tests
failed. Now js-fn-length inspects the lambda's parameter list (via
lambda-params primitive) and counts non-rest params. For functions/components
and callable dicts it still returns 0 (can't introspect arity in those cases).

6 new unit tests, 520/522 total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 09:14:56 +00:00
parent c22f553146
commit 85a329e8d6
2 changed files with 99 additions and 49 deletions

View File

@@ -88,6 +88,34 @@
(= name "name")
(= name "length"))))
(define
js-fn-length
(fn
(f)
(let
((t (type-of f)))
(cond
((= t "lambda") (js-count-real-params (lambda-params f)))
((= t "function") 0)
((= t "component") 0)
((and (= t "dict") (contains? (keys f) "__callable__"))
(js-fn-length (get f "__callable__")))
(else 0)))))
(define
js-count-real-params
(fn
(params)
(cond
((empty? params) 0)
(else
(let
((first (first params)))
(if
(= first "&rest")
0
(+ 1 (js-count-real-params (rest params)))))))))
(define
js-invoke-function-method
(fn
@@ -122,9 +150,16 @@
(js-call-with-this this-arg recv (js-list-concat bound more)))))
((= key "toString") "function () { [native code] }")
((= key "name") "")
((= key "length") 0)
((= key "length") (js-fn-length recv))
(else :js-undefined))))
;; 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-bound
(fn
@@ -167,13 +202,6 @@
(js-to-string key)
" is not a function (on number)"))))))
;; 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-boolean-method
(fn
@@ -188,6 +216,8 @@
(js-to-string key)
" is not a function (on boolean)"))))))
;; ── String coercion (ToString) ────────────────────────────────────
(define
js-num-to-str-radix
(fn
@@ -217,8 +247,9 @@
((d (mod n radix)) (rest (js-math-trunc (/ n radix))))
(js-num-to-str-radix-rec rest radix (str (js-digit-char d) acc))))))
;; ── String coercion (ToString) ────────────────────────────────────
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define
js-digit-char
(fn
@@ -262,9 +293,6 @@
(js-to-string (js-math-trunc frac-part))
d))))))))))))
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define
js-pow-int
(fn (b e) (if (<= e 0) 1 (* b (js-pow-int b (- e 1))))))
@@ -360,11 +388,14 @@
((= 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)))
;; Bitwise + logical-not
;; ── Equality ──────────────────────────────────────────────────────
;; Strict equality (===): no coercion; js-undefined matches js-undefined.
(define
js-case-loop
(fn
@@ -439,9 +470,9 @@
((= code 122) "z")
(else ""))))
;; ── Equality ──────────────────────────────────────────────────────
;; Strict equality (===): no coercion; js-undefined matches js-undefined.
;; 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)))
@@ -457,9 +488,11 @@
(js-call-with-this :js-undefined (get fn-val "__callable__") args))
(else (js-call-with-this :js-undefined fn-val args)))))
;; Abstract equality (==): type coercion rules.
;; Simplified: number↔string coerce both to number; null == undefined;
;; everything else falls back to strict equality.
;; ── 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
@@ -488,11 +521,6 @@
((proto (js-get-ctor-proto ctor)))
(js-instanceof-walk obj proto))))))
;; ── Relational comparisons ────────────────────────────────────────
;; Abstract relational comparison from ES5.
;; Numbers compare numerically; two strings compare lexicographically;
;; mixed types coerce both to numbers.
(define
js-instanceof-walk
(fn
@@ -562,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
RangeError
(fn
@@ -598,13 +633,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
ReferenceError
(fn
@@ -623,6 +651,7 @@
nil)
this))))
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
(define
js-function?
(fn
@@ -635,15 +664,17 @@
(= t "component")
(and (= t "dict") (contains? (keys v) "__callable__"))))))
(define __js_proto_table__ (dict))
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
(define __js_next_id__ (dict))
;; ── 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.
(dict-set! __js_next_id__ "n" 0)
(define
@@ -657,9 +688,8 @@
(else
(let ((p (dict))) (begin (dict-set! __js_proto_table__ id p) p)))))))
;; ── console.log ───────────────────────────────────────────────────
;; ── Math object ───────────────────────────────────────────────────
;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
(define
js-reset-ctor-proto!
(fn
@@ -667,15 +697,11 @@
(let
((id (js-ctor-id ctor)) (p (dict)))
(begin (dict-set! __js_proto_table__ id p) p))))
(define
js-set-ctor-proto!
(fn
(ctor proto)
(let ((id (js-ctor-id ctor))) (dict-set! __js_proto_table__ id proto))))
;; ── Math object ───────────────────────────────────────────────────
(define
js-ctor-id
(fn
@@ -766,8 +792,14 @@
(or (= prev "e") (= prev "E"))
(js-is-numeric-loop s (+ i 1) sawdigit sawdot sawe)
false)))
(else false)))))))
(else false))))))) ; deterministic placeholder for tests
(define js-parse-num-safe (fn (s) (cond (else (js-num-from-string s)))))
;; 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-num-from-string
(fn
@@ -776,14 +808,10 @@
((trimmed (js-trim s)))
(cond
((= trimmed "") 0)
(else (js-parse-decimal trimmed 0 0 1 false 0)))))) ; deterministic placeholder for tests
(else (js-parse-decimal trimmed 0 0 1 false 0))))))
(define js-trim (fn (s) (js-trim-left (js-trim-right s))))
;; 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-left
(fn (s) (let ((n (len s))) (js-trim-left-at s n 0))))
@@ -1985,7 +2013,7 @@
(cond
((= key "prototype") (js-get-ctor-proto obj))
((= key "name") "")
((= key "length") 0)
((= key "length") (js-fn-length obj))
(else (js-invoke-function-bound obj key))))
(else js-undefined))))
(define

View File

@@ -1213,6 +1213,20 @@ cat > "$TMPFILE" << 'EPOCHS'
(epoch 3709)
(eval "(js-eval \"var a=[1,2,3]; a.keys().join(',')\")")
;; ── Phase 11.fnlen: fn.length uses arity from lambda-params ─
(epoch 4100)
(eval "(js-eval \"function f(){} f.length\")")
(epoch 4101)
(eval "(js-eval \"function f(a){} f.length\")")
(epoch 4102)
(eval "(js-eval \"function f(a,b,c){} f.length\")")
(epoch 4103)
(eval "(js-eval \"function f(a, ...rest){} f.length\")")
(epoch 4104)
(eval "(js-eval \"Math.abs.length\")")
(epoch 4105)
(eval "(js-eval \"Math.floor.length\")")
;; ── Phase 11.coerce2: Number coercion edge cases ────────────
(epoch 4000)
(eval "(js-eval \"Number('abc')\")")
@@ -1969,6 +1983,14 @@ check 3707 "arr.toReversed" '"2,1,3"'
check 3708 "arr.toSorted" '"1,1,3,4,5"'
check 3709 "arr.keys" '"0,1,2"'
# ── Phase 11.fnlen: fn.length arity ───────────────────────────
check 4100 "fn () length" '0'
check 4101 "fn (a) length" '1'
check 4102 "fn (a,b,c) length" '3'
check 4103 "fn rest length" '1'
check 4104 "Math.abs.length" '1'
check 4105 "Math.floor.length" '1'
# ── Phase 11.coerce2: Number coercion edge cases ─────────────
check 4001 "isNaN(Number('abc'))" 'true'
check 4002 "parseInt leading digits" '123'