js-on-sx: Array.prototype includes/find/some/every/reverse + Object fallbacks

Array: includes, find, findIndex, some, every, reverse via
tail-recursive helpers.

Object: hasOwnProperty, isPrototypeOf, propertyIsEnumerable,
toString, valueOf, toLocaleString fallback in js-invoke-method
when js-get-prop returns undefined. Lets o.hasOwnProperty('k')
work on plain dicts.

376/378 unit (+13), 148/148 slice unchanged.
This commit is contained in:
2026-04-23 21:11:12 +00:00
parent 9d3e54029a
commit 2bd3a6b2ba
3 changed files with 196 additions and 46 deletions

View File

@@ -96,10 +96,45 @@
(let
((m (js-get-prop recv key)))
(cond
((js-undefined? m)
((not (js-undefined? m)) (js-call-with-this recv m args))
((and (dict? recv) (js-object-builtin-method? key))
(js-invoke-object-method recv key args))
(else
(error
(str "TypeError: " (js-to-string key) " is not a function")))
(else (js-call-with-this recv m args))))))))
(str "TypeError: " (js-to-string key) " is not a function")))))))))
(define
js-object-builtin-method?
(fn
(name)
(or
(= name "hasOwnProperty")
(= name "isPrototypeOf")
(= name "propertyIsEnumerable")
(= name "toString")
(= name "valueOf")
(= name "toLocaleString"))))
(define
js-invoke-object-method
(fn
(recv name args)
(cond
((= name "hasOwnProperty")
(if
(= (len args) 0)
false
(contains? (keys recv) (js-to-string (nth args 0)))))
((= name "isPrototypeOf") false)
((= name "propertyIsEnumerable")
(if
(= (len args) 0)
false
(contains? (keys recv) (js-to-string (nth args 0)))))
((= name "toString") "[object Object]")
((= name "valueOf") recv)
((= name "toLocaleString") "[object Object]")
(else js-undefined))))
(define js-upper-case (fn (s) (js-case-loop s 0 "" true)))
@@ -179,6 +214,13 @@
((= 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)))
@@ -192,13 +234,6 @@
(error "TypeError: undefined is not a function"))
(else (js-call-with-this :js-undefined fn-val args)))))
;; 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-new-call
(fn
@@ -214,6 +249,8 @@
ret
obj))))))
;; ── String coercion (ToString) ────────────────────────────────────
(define
js-instanceof
(fn
@@ -242,8 +279,9 @@
((not (= (type-of p) "dict")) false)
(else (js-instanceof-walk p proto))))))))
;; ── String coercion (ToString) ────────────────────────────────────
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define
js-in
(fn
@@ -262,9 +300,6 @@
((dict-has? obj "__proto__") (js-in-walk (get obj "__proto__") skey))
(else false))))
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define
Error
(fn
@@ -363,11 +398,14 @@
((t (type-of v)))
(or (= t "lambda") (= t "function") (= t "component")))))
;; Bitwise + logical-not
(define __js_proto_table__ (dict))
(define __js_next_id__ (dict))
;; Bitwise + logical-not
;; ── Equality ──────────────────────────────────────────────────────
;; Strict equality (===): no coercion; js-undefined matches js-undefined.
(dict-set! __js_next_id__ "n" 0)
(define
@@ -381,9 +419,9 @@
(else
(let ((p (dict))) (begin (dict-set! __js_proto_table__ id p) p)))))))
;; ── 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-reset-ctor-proto!
(fn
@@ -398,9 +436,11 @@
(ctor proto)
(let ((id (js-ctor-id ctor))) (dict-set! __js_proto_table__ id proto))))
;; 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-ctor-id
(fn
@@ -424,11 +464,6 @@
((= (type-of v) "native-fn") "function")
(else "object"))))
;; ── Relational comparisons ────────────────────────────────────────
;; Abstract relational comparison from ES5.
;; Numbers compare numerically; two strings compare lexicographically;
;; mixed types coerce both to numbers.
(define
js-to-boolean
(fn
@@ -470,12 +505,6 @@
((= trimmed "") 0)
(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-left
(fn (s) (let ((n (len s))) (js-trim-left-at s n 0))))
;; ── Property access ───────────────────────────────────────────────
;; obj[key] or obj.key in JS. Handles:
@@ -483,6 +512,12 @@
;; • 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))))
(define
js-trim-left
(fn (s) (let ((n (len s))) (js-trim-left-at s n 0))))
(define
js-trim-left-at
(fn
@@ -492,10 +527,15 @@
((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 ─────────────────────────────────────
;; `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-right-at
(fn
@@ -505,15 +545,13 @@
((js-is-space? (char-at s (- n 1))) (js-trim-right-at s (- n 1)))
(else (substr s 0 n)))))
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
(define
js-is-space?
(fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r"))))
;; ── 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-parse-decimal
(fn
@@ -564,9 +602,8 @@
(= c "8")
(= c "9")))))
;; ── console.log ───────────────────────────────────────────────────
;; ── Math object ───────────────────────────────────────────────────
;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
(define
js-digit-val
(fn
@@ -583,7 +620,6 @@
((= c "8") 8)
((= c "9") 9)
(else 0))))
(define
js-to-string
(fn
@@ -596,9 +632,6 @@
((= (type-of v) "string") v)
((= (type-of v) "number") (js-number-to-string v))
(else (str v)))))
;; ── Math object ───────────────────────────────────────────────────
(define
js-template-concat
(fn (&rest parts) (js-template-concat-loop parts 0 "")))
@@ -624,16 +657,18 @@
(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))))
(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)))) ; deterministic placeholder for tests
(define js-div (fn (a b) (/ (js-to-number a) (js-to-number b)))) ; deterministic placeholder for tests
(define js-neg (fn (a) (- 0 (js-to-number a))))
(define js-mod (fn (a b) (mod (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-pow (fn (a b) (pow (js-to-number a) (js-to-number b))))
(define js-neg (fn (a) (- 0 (js-to-number a))))
(define js-pos (fn (a) (js-to-number a)))
(define js-not (fn (a) (not (js-to-boolean a))))
@@ -762,6 +797,19 @@
(error "Reduce of empty array with no initial value")
(js-list-reduce-loop (nth args 0) (nth arr 0) arr 1)))
(else (js-list-reduce-loop (nth args 0) (nth args 1) arr 0)))))
((= name "includes")
(fn
(&rest args)
(if
(= (len args) 0)
false
(>= (js-list-index-of arr (nth args 0) 0) 0))))
((= name "find") (fn (f) (js-list-find-loop f arr 0)))
((= name "findIndex") (fn (f) (js-list-find-index-loop f arr 0)))
((= name "some") (fn (f) (js-list-some-loop f arr 0)))
((= name "every") (fn (f) (js-list-every-loop f arr 0)))
((= name "reverse")
(fn () (js-list-reverse-loop arr (- (len arr) 1) (list))))
(else js-undefined))))
(define pop-last! (fn (lst) nil))
@@ -885,6 +933,53 @@
((>= i (len arr)) acc)
(else (js-list-reduce-loop f (f acc (nth arr i)) arr (+ i 1))))))
(define
js-list-find-loop
(fn
(f arr i)
(cond
((>= i (len arr)) js-undefined)
((js-to-boolean (f (nth arr i))) (nth arr i))
(else (js-list-find-loop f arr (+ i 1))))))
(define
js-list-find-index-loop
(fn
(f arr i)
(cond
((>= i (len arr)) -1)
((js-to-boolean (f (nth arr i))) i)
(else (js-list-find-index-loop f arr (+ i 1))))))
(define
js-list-some-loop
(fn
(f arr i)
(cond
((>= i (len arr)) false)
((js-to-boolean (f (nth arr i))) true)
(else (js-list-some-loop f arr (+ i 1))))))
(define
js-list-every-loop
(fn
(f arr i)
(cond
((>= i (len arr)) true)
((not (js-to-boolean (f (nth arr i)))) false)
(else (js-list-every-loop f arr (+ i 1))))))
(define
js-list-reverse-loop
(fn
(arr i acc)
(cond
((< i 0) acc)
(else
(begin
(append! acc (nth arr i))
(js-list-reverse-loop arr (- i 1) acc))))))
(define
js-string-repeat
(fn
@@ -1118,6 +1213,12 @@
((= key "filter") (js-array-method obj "filter"))
((= key "forEach") (js-array-method obj "forEach"))
((= key "reduce") (js-array-method obj "reduce"))
((= key "includes") (js-array-method obj "includes"))
((= key "find") (js-array-method obj "find"))
((= key "findIndex") (js-array-method obj "findIndex"))
((= key "some") (js-array-method obj "some"))
((= key "every") (js-array-method obj "every"))
((= key "reverse") (js-array-method obj "reverse"))
(else js-undefined)))
((= (type-of obj) "string")
(cond

View File

@@ -951,6 +951,36 @@ cat > "$TMPFILE" << 'EPOCHS'
(epoch 1605)
(eval "(js-eval \"var r=0; switch(1){case 1: r=10; case 2: r=20; break;} r\")")
;; ── Phase 11.array: more Array.prototype ────────────────────────
(epoch 1700)
(eval "(js-eval \"[1,2,3].includes(2)\")")
(epoch 1701)
(eval "(js-eval \"[1,2,3].includes(5)\")")
(epoch 1702)
(eval "(js-eval \"[1,2,3].find((x)=>x>1)\")")
(epoch 1703)
(eval "(js-eval \"[1,2,3].findIndex((x)=>x>1)\")")
(epoch 1704)
(eval "(js-eval \"[1,2,3].some((x)=>x>2)\")")
(epoch 1705)
(eval "(js-eval \"[1,2,3].some((x)=>x>5)\")")
(epoch 1706)
(eval "(js-eval \"[1,2,3].every((x)=>x>0)\")")
(epoch 1707)
(eval "(js-eval \"[1,2,3].every((x)=>x>2)\")")
(epoch 1708)
(eval "(js-eval \"[1,2,3].reverse().join(',')\")")
;; ── Phase 11.objmethod: hasOwnProperty + toString ───────────────
(epoch 1800)
(eval "(js-eval \"var o = {x:1}; o.hasOwnProperty('x')\")")
(epoch 1801)
(eval "(js-eval \"var o = {x:1}; o.hasOwnProperty('y')\")")
(epoch 1802)
(eval "(js-eval \"({}).toString()\")")
(epoch 1803)
(eval "(js-eval \"({x:1}).valueOf().x\")")
EPOCHS
OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
@@ -1458,6 +1488,23 @@ check 1603 "switch fallthrough stops on break" '12'
check 1604 "switch on string" '"yes"'
check 1605 "switch fallthrough chains" '20'
# ── Phase 11.array: more Array.prototype ────────────────────────
check 1700 "Array.includes yes" 'true'
check 1701 "Array.includes no" 'false'
check 1702 "Array.find" '2'
check 1703 "Array.findIndex" '1'
check 1704 "Array.some yes" 'true'
check 1705 "Array.some no" 'false'
check 1706 "Array.every yes" 'true'
check 1707 "Array.every no" 'false'
check 1708 "Array.reverse" '"3,2,1"'
# ── Phase 11.objmethod: hasOwnProperty ──────────────────────────
check 1800 "hasOwnProperty yes" 'true'
check 1801 "hasOwnProperty no" 'false'
check 1802 "({}).toString()" '"[object Object]"'
check 1803 "obj.valueOf().x" '1'
TOTAL=$((PASS + FAIL))
if [ $FAIL -eq 0 ]; then
echo "$PASS/$TOTAL JS-on-SX tests passed"

View File

@@ -183,6 +183,8 @@ Append-only record of completed iterations. Loop writes one line per iteration:
- 2026-04-23 — **switch / case / default.** Parser: new `jp-parse-switch-stmt` (expect `switch (expr) { cases }`), `jp-parse-switch-cases` (walks clauses: `case val:`, `default:`), `jp-parse-switch-body` (collects stmts until next `case`/`default`/`}`). AST: `(js-switch discr (("case" val body-stmts) ("default" nil body-stmts) ...))`. Transpile: wraps body in `(call/cc (fn (__break__) (let ((__discr__ …) (__matched__ false)) …)))`. Each case clause becomes `(when (or __matched__ (js-strict-eq __discr__ val)) (set! __matched__ true) body-stmts)` — implements JS fall-through naturally (once a case matches, all following cases' `when` fires via `__matched__`). Default is a separate `(when (not __matched__) default-body)` appended at the end. `break` inside a case body already transpiles to `(__break__ nil)` and jumps out via the `call/cc`. 6 new unit tests (match, no-match default, fall-through stops on break, string discriminant, empty-body fall-through chain), **363/365** (357→+6). Conformance unchanged.
- 2026-04-23 — **More Array.prototype + Object fallbacks (`hasOwnProperty` etc).** Array: `includes`, `find`, `findIndex`, `some`, `every`, `reverse` (in `js-array-method` dispatch + `js-get-prop` list-branch keys). Helpers: `js-list-find-loop / -find-index-loop / -some-loop / -every-loop / -reverse-loop` all tail-recursive, no `while` because SX doesn't have one. Object fallbacks: `js-invoke-method` now falls back to `js-invoke-object-method` for dicts when js-get-prop returns undefined AND the method name is in the builtin set (`hasOwnProperty`, `isPrototypeOf`, `propertyIsEnumerable`, `toString`, `valueOf`, `toLocaleString`). `hasOwnProperty` checks `(contains? (keys recv) (js-to-string k))`. This lets `o.hasOwnProperty('x')` work on plain dicts without having to install an Object.prototype. 13 new tests, **376/378** (363→+13). Conformance unchanged.
## Phase 3-5 gotchas
Worth remembering for later phases: