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:
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user