diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index c88b500f..c77c475f 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -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 diff --git a/lib/js/test.sh b/lib/js/test.sh index 8625521e..a9e7a83c 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -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'