From 92c1fc72a56fdd9a65ebec86a362fd43900928bf Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 24 Apr 2026 06:27:18 +0000 Subject: [PATCH] js-on-sx: Function.prototype.call/apply/bind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds js-invoke-function-method dispatched from js-invoke-method when the receiver is a JS function (lambda/function/component/callable-dict) and the method name is one of call/apply/bind/toString/name/length. call and apply bind this around a single call; bind returns a closure with prepended args. toString returns the native-code placeholder. 6 unit tests, 450/452 total (Array.prototype.push.call with arrayLike still fails — tracked as the 455x 'Not callable array-like' scoreboard item which needs array methods to treat dict-with-length as a list). Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/js/runtime.sx | 145 +++++++++++++++++++++++++++++++--------------- lib/js/test.sh | 22 +++++++ 2 files changed, 120 insertions(+), 47 deletions(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index e0bf701b..023d4182 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -54,6 +54,57 @@ ;; Safe number-parser. Tries to call an SX primitive that can parse ;; strings to numbers; on failure returns 0 (stand-in for NaN so the ;; slice doesn't crash). +(define + js-function-method? + (fn + (name) + (or + (= name "call") + (= name "apply") + (= name "bind") + (= name "toString") + (= name "name") + (= name "length")))) + +;; Minimal string->number for the slice. Handles integers, negatives, +;; and simple decimals. Returns 0 on malformed input. +(define + js-invoke-function-method + (fn + (recv key args) + (cond + ((= key "call") + (let + ((this-arg (if (< (len args) 1) :js-undefined (nth args 0))) + (rest + (if + (< (len args) 1) + (list) + (js-list-slice args 1 (len args))))) + (js-call-with-this this-arg recv rest))) + ((= key "apply") + (let + ((this-arg (if (< (len args) 1) :js-undefined (nth args 0))) + (arr (if (< (len args) 2) (list) (nth args 1)))) + (let + ((rest (cond ((= arr nil) (list)) ((js-undefined? arr) (list)) ((list? arr) arr) (else (js-iterable-to-list arr))))) + (js-call-with-this this-arg recv rest)))) + ((= key "bind") + (let + ((this-arg (if (< (len args) 1) :js-undefined (nth args 0))) + (bound + (if + (< (len args) 1) + (list) + (js-list-slice args 1 (len args))))) + (fn + (&rest more) + (js-call-with-this this-arg recv (js-list-concat bound more))))) + ((= key "toString") "function () { [native code] }") + ((= key "name") "") + ((= key "length") 0) + (else :js-undefined)))) + (define js-apply-fn (fn @@ -85,8 +136,6 @@ (nth args 5))) (else (apply callable args)))))) -;; Minimal string->number for the slice. Handles integers, negatives, -;; and simple decimals. Returns 0 on malformed input. (define js-invoke-method (fn @@ -100,7 +149,9 @@ ((m (js-get-prop recv key))) (cond ((not (js-undefined? m)) (js-call-with-this recv m args)) - ((and (dict? recv) (js-object-builtin-method? key)) + ((and (js-function? recv) (js-function-method? key)) + (js-invoke-function-method recv key args)) + ((and (= (type-of recv) "dict") (js-object-builtin-method? key)) (js-invoke-object-method recv key args)) (else (error @@ -143,6 +194,13 @@ (define js-lower-case (fn (s) (js-case-loop s 0 "" false))) +;; 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-case-loop (fn @@ -217,17 +275,12 @@ ((= code 122) "z") (else "")))) -;; 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-method-dyn (fn (recv key args) (js-invoke-method recv key args))) +;; ── String coercion (ToString) ──────────────────────────────────── + (define js-call-plain (fn @@ -254,8 +307,9 @@ ret obj)))))) -;; ── String coercion (ToString) ──────────────────────────────────── +;; ── Arithmetic (JS rules) ───────────────────────────────────────── +;; JS `+`: if either operand is a string → string concat, else numeric. (define js-instanceof (fn @@ -284,9 +338,6 @@ ((not (= (type-of p) "dict")) false) (else (js-instanceof-walk p proto)))))))) -;; ── Arithmetic (JS rules) ───────────────────────────────────────── - -;; JS `+`: if either operand is a string → string concat, else numeric. (define js-in (fn @@ -377,6 +428,7 @@ nil) this)))) +;; Bitwise + logical-not (define ReferenceError (fn @@ -407,14 +459,16 @@ (= t "component") (and (= t "dict") (contains? (keys v) "__callable__")))))) -;; Bitwise + logical-not +;; ── Equality ────────────────────────────────────────────────────── + +;; Strict equality (===): no coercion; js-undefined matches js-undefined. (define __js_proto_table__ (dict)) (define __js_next_id__ (dict)) -;; ── 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. (dict-set! __js_next_id__ "n" 0) (define @@ -428,9 +482,11 @@ (else (let ((p (dict))) (begin (dict-set! __js_proto_table__ id p) p))))))) -;; 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-reset-ctor-proto! (fn @@ -445,11 +501,6 @@ (ctor proto) (let ((id (js-ctor-id ctor))) (dict-set! __js_proto_table__ id proto)))) -;; ── Relational comparisons ──────────────────────────────────────── - -;; Abstract relational comparison from ES5. -;; Numbers compare numerically; two strings compare lexicographically; -;; mixed types coerce both to numbers. (define js-ctor-id (fn @@ -505,6 +556,13 @@ js-string-to-number (fn (s) (cond ((= s "") 0) (else (js-parse-num-safe s))))) +;; ── 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 js-parse-num-safe (fn (s) (cond (else (js-num-from-string s))))) (define @@ -517,19 +575,17 @@ ((= trimmed "") 0) (else (js-parse-decimal trimmed 0 0 1 false 0)))))) -;; ── 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 js-trim (fn (s) (js-trim-left (js-trim-right s)))) +;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs). (define js-trim-left (fn (s) (let ((n (len s))) (js-trim-left-at s n 0)))) +;; ── 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-trim-left-at (fn @@ -539,15 +595,13 @@ ((js-is-space? (char-at s i)) (js-trim-left-at s n (+ i 1))) (else (substr s i n))))) -;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs). (define js-trim-right (fn (s) (let ((n (len s))) (js-trim-right-at s n)))) -;; ── Short-circuit logical ops ───────────────────────────────────── +;; ── console.log ─────────────────────────────────────────────────── -;; `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). +;; Trivial bridge. `log-info` is available on OCaml; fall back to print. (define js-trim-right-at (fn @@ -561,9 +615,8 @@ js-is-space? (fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r")))) -;; ── console.log ─────────────────────────────────────────────────── +;; ── Math object ─────────────────────────────────────────────────── -;; Trivial bridge. `log-info` is available on OCaml; fall back to print. (define js-parse-decimal (fn @@ -596,7 +649,6 @@ false 0))) (else (* sign (if frac? (/ acc fdiv) acc))))))) - (define js-is-digit? (fn @@ -613,9 +665,6 @@ (= c "7") (= c "8") (= c "9"))))) - -;; ── Math object ─────────────────────────────────────────────────── - (define js-digit-val (fn @@ -667,16 +716,18 @@ ((or (= (type-of a) "string") (= (type-of b) "string")) (str (js-to-string a) (js-to-string b))) (else (+ (js-to-number a) (js-to-number b)))))) -(define js-sub (fn (a b) (- (js-to-number a) (js-to-number b)))) -(define js-mul (fn (a b) (* (js-to-number a) (js-to-number b)))) -(define js-div (fn (a b) (/ (js-to-number a) (js-to-number b)))) ; deterministic placeholder for tests +(define js-sub (fn (a b) (- (js-to-number a) (js-to-number b)))) ; deterministic placeholder for tests -(define js-mod (fn (a b) (mod (js-to-number a) (js-to-number b)))) +(define js-mul (fn (a b) (* (js-to-number a) (js-to-number b)))) ;; 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-div (fn (a b) (/ (js-to-number a) (js-to-number b)))) + +(define js-mod (fn (a b) (mod (js-to-number a) (js-to-number b)))) + (define js-pow (fn (a b) (pow (js-to-number a) (js-to-number b)))) (define js-neg (fn (a) (- 0 (js-to-number a)))) diff --git a/lib/js/test.sh b/lib/js/test.sh index d074c0c4..c33b666f 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -1147,6 +1147,20 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 3301) (eval "(js-eval \"var o = {a:1, b:2}; delete o.a; o.b\")") +;; ── Phase 11.fnmethod: Function.prototype.call/apply/bind ───── +(epoch 3400) +(eval "(js-eval \"function greet3(n) { return 'hi ' + n; } greet3.call(null, 'ada')\")") +(epoch 3401) +(eval "(js-eval \"function greet4(n) { return 'hi ' + n; } greet4.apply(null, ['bob'])\")") +(epoch 3402) +(eval "(js-eval \"function sum3(a,b,c) { return a+b+c; } var bnd2 = sum3.bind(null, 1, 2); bnd2(3)\")") +(epoch 3403) +(eval "(js-eval \"var obj2 = {x: 42}; function getX() { return this.x; } getX.call(obj2)\")") +(epoch 3404) +(eval "(js-eval \"function id(x) { return x; } id.apply(null, [7])\")") +(epoch 3405) +(eval "(js-eval \"var arr2 = [1,2]; Array.prototype.push.call(arr2, 10, 20); arr2.length\")") + EPOCHS @@ -1770,6 +1784,14 @@ check 3201 "obj multi rename" '3' check 3300 "delete obj.x" 'true' check 3301 "delete obj.a keeps b" '2' +# ── Phase 11.fnmethod: call/apply/bind ──────────────────────── +check 3400 "fn.call basic" '"hi ada"' +check 3401 "fn.apply basic" '"hi bob"' +check 3402 "fn.bind partial" '6' +check 3403 "fn.call this-binding" '42' +check 3404 "fn.apply arg-unpack" '7' +check 3405 "Array.prototype.push.call arr" '4' + TOTAL=$((PASS + FAIL)) if [ $FAIL -eq 0 ]; then echo "✓ $PASS/$TOTAL JS-on-SX tests passed"