js-on-sx: Function.prototype.call/apply/bind
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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))))
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user