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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user