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

View File

@@ -1213,6 +1213,20 @@ cat > "$TMPFILE" << 'EPOCHS'
(epoch 3709) (epoch 3709)
(eval "(js-eval \"var a=[1,2,3]; a.keys().join(',')\")") (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 ──────────── ;; ── Phase 11.coerce2: Number coercion edge cases ────────────
(epoch 4000) (epoch 4000)
(eval "(js-eval \"Number('abc')\")") (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 3708 "arr.toSorted" '"1,1,3,4,5"'
check 3709 "arr.keys" '"0,1,2"' 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 ───────────── # ── Phase 11.coerce2: Number coercion edge cases ─────────────
check 4001 "isNaN(Number('abc'))" 'true' check 4001 "isNaN(Number('abc'))" 'true'
check 4002 "parseInt leading digits" '123' check 4002 "parseInt leading digits" '123'