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 (let
((m (js-get-prop recv key))) ((m (js-get-prop recv key)))
(cond (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 (error
(str "TypeError: " (js-to-string key) " is not a function"))) (str "TypeError: " (js-to-string key) " is not a function")))))))))
(else (js-call-with-this recv m args))))))))
(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))) (define js-upper-case (fn (s) (js-case-loop s 0 "" true)))
@@ -179,6 +214,13 @@
((= code 122) "z") ((= code 122) "z")
(else "")))) (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 (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)))
@@ -192,13 +234,6 @@
(error "TypeError: undefined is not a function")) (error "TypeError: undefined is not a function"))
(else (js-call-with-this :js-undefined fn-val args))))) (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 (define
js-new-call js-new-call
(fn (fn
@@ -214,6 +249,8 @@
ret ret
obj)))))) obj))))))
;; ── String coercion (ToString) ────────────────────────────────────
(define (define
js-instanceof js-instanceof
(fn (fn
@@ -242,8 +279,9 @@
((not (= (type-of p) "dict")) false) ((not (= (type-of p) "dict")) false)
(else (js-instanceof-walk p proto)))))))) (else (js-instanceof-walk p proto))))))))
;; ── String coercion (ToString) ──────────────────────────────────── ;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define (define
js-in js-in
(fn (fn
@@ -262,9 +300,6 @@
((dict-has? obj "__proto__") (js-in-walk (get obj "__proto__") skey)) ((dict-has? obj "__proto__") (js-in-walk (get obj "__proto__") skey))
(else false)))) (else false))))
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
;; JS `+`: if either operand is a string → string concat, else numeric.
(define (define
Error Error
(fn (fn
@@ -363,11 +398,14 @@
((t (type-of v))) ((t (type-of v)))
(or (= t "lambda") (= t "function") (= t "component"))))) (or (= t "lambda") (= t "function") (= t "component")))))
;; Bitwise + logical-not
(define __js_proto_table__ (dict)) (define __js_proto_table__ (dict))
(define __js_next_id__ (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) (dict-set! __js_next_id__ "n" 0)
(define (define
@@ -381,9 +419,9 @@
(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)))))))
;; ── 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-reset-ctor-proto! js-reset-ctor-proto!
(fn (fn
@@ -398,9 +436,11 @@
(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))))
;; 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-ctor-id js-ctor-id
(fn (fn
@@ -424,11 +464,6 @@
((= (type-of v) "native-fn") "function") ((= (type-of v) "native-fn") "function")
(else "object")))) (else "object"))))
;; ── Relational comparisons ────────────────────────────────────────
;; Abstract relational comparison from ES5.
;; Numbers compare numerically; two strings compare lexicographically;
;; mixed types coerce both to numbers.
(define (define
js-to-boolean js-to-boolean
(fn (fn
@@ -470,12 +505,6 @@
((= trimmed "") 0) ((= trimmed "") 0)
(else (js-parse-decimal trimmed 0 0 1 false 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 ─────────────────────────────────────────────── ;; ── Property access ───────────────────────────────────────────────
;; obj[key] or obj.key in JS. Handles: ;; obj[key] or obj.key in JS. Handles:
@@ -483,6 +512,12 @@
;; • lists indexed by number (incl. .length) ;; • lists indexed by number (incl. .length)
;; • strings indexed by number (incl. .length) ;; • strings indexed by number (incl. .length)
;; Returns js-undefined if the key is absent. ;; 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 (define
js-trim-left-at js-trim-left-at
(fn (fn
@@ -492,10 +527,15 @@
((js-is-space? (char-at s i)) (js-trim-left-at s n (+ i 1))) ((js-is-space? (char-at s i)) (js-trim-left-at s n (+ i 1)))
(else (substr s i n))))) (else (substr s i n)))))
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
(define (define
js-trim-right js-trim-right
(fn (s) (let ((n (len s))) (js-trim-right-at s n)))) (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 (define
js-trim-right-at js-trim-right-at
(fn (fn
@@ -505,15 +545,13 @@
((js-is-space? (char-at s (- n 1))) (js-trim-right-at s (- n 1))) ((js-is-space? (char-at s (- n 1))) (js-trim-right-at s (- n 1)))
(else (substr s 0 n))))) (else (substr s 0 n)))))
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
(define (define
js-is-space? js-is-space?
(fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r")))) (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 ;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
;; form defers evaluation of b — the transpiler passes (fn () b).
(define (define
js-parse-decimal js-parse-decimal
(fn (fn
@@ -564,9 +602,8 @@
(= c "8") (= c "8")
(= c "9"))))) (= c "9")))))
;; ── console.log ─────────────────────────────────────────────────── ;; ── Math object ───────────────────────────────────────────────────
;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
(define (define
js-digit-val js-digit-val
(fn (fn
@@ -583,7 +620,6 @@
((= c "8") 8) ((= c "8") 8)
((= c "9") 9) ((= c "9") 9)
(else 0)))) (else 0))))
(define (define
js-to-string js-to-string
(fn (fn
@@ -596,9 +632,6 @@
((= (type-of v) "string") v) ((= (type-of v) "string") v)
((= (type-of v) "number") (js-number-to-string v)) ((= (type-of v) "number") (js-number-to-string v))
(else (str v))))) (else (str v)))))
;; ── Math object ───────────────────────────────────────────────────
(define (define
js-template-concat js-template-concat
(fn (&rest parts) (js-template-concat-loop parts 0 ""))) (fn (&rest parts) (js-template-concat-loop parts 0 "")))
@@ -624,16 +657,18 @@
(else (+ (js-to-number a) (js-to-number 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-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-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-div (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-pow (fn (a b) (pow (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 ;; 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 ;; SX env. Transpiled idents look up locally first; globals here are a
;; fallback, but most slice programs reference `console`, `Math`, ;; fallback, but most slice programs reference `console`, `Math`,
;; `undefined` as plain symbols, which we bind as defines above. ;; `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-pos (fn (a) (js-to-number a)))
(define js-not (fn (a) (not (js-to-boolean a)))) (define js-not (fn (a) (not (js-to-boolean a))))
@@ -762,6 +797,19 @@
(error "Reduce of empty array with no initial value") (error "Reduce of empty array with no initial value")
(js-list-reduce-loop (nth args 0) (nth arr 0) arr 1))) (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))))) (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)))) (else js-undefined))))
(define pop-last! (fn (lst) nil)) (define pop-last! (fn (lst) nil))
@@ -885,6 +933,53 @@
((>= i (len arr)) acc) ((>= i (len arr)) acc)
(else (js-list-reduce-loop f (f acc (nth arr i)) arr (+ i 1)))))) (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 (define
js-string-repeat js-string-repeat
(fn (fn
@@ -1118,6 +1213,12 @@
((= key "filter") (js-array-method obj "filter")) ((= key "filter") (js-array-method obj "filter"))
((= key "forEach") (js-array-method obj "forEach")) ((= key "forEach") (js-array-method obj "forEach"))
((= key "reduce") (js-array-method obj "reduce")) ((= 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))) (else js-undefined)))
((= (type-of obj) "string") ((= (type-of obj) "string")
(cond (cond

View File

@@ -951,6 +951,36 @@ cat > "$TMPFILE" << 'EPOCHS'
(epoch 1605) (epoch 1605)
(eval "(js-eval \"var r=0; switch(1){case 1: r=10; case 2: r=20; break;} r\")") (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 EPOCHS
OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) 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 1604 "switch on string" '"yes"'
check 1605 "switch fallthrough chains" '20' 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)) TOTAL=$((PASS + FAIL))
if [ $FAIL -eq 0 ]; then if [ $FAIL -eq 0 ]; then
echo "$PASS/$TOTAL JS-on-SX tests passed" 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 — **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 ## Phase 3-5 gotchas
Worth remembering for later phases: Worth remembering for later phases: