From 2bd3a6b2ba28d236c36e07d3a43096b5ae8cb60a Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 23 Apr 2026 21:11:12 +0000 Subject: [PATCH] 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. --- lib/js/runtime.sx | 193 +++++++++++++++++++++++++++++++++++----------- lib/js/test.sh | 47 +++++++++++ plans/js-on-sx.md | 2 + 3 files changed, 196 insertions(+), 46 deletions(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 73a1567e..42175917 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -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 diff --git a/lib/js/test.sh b/lib/js/test.sh index 18836f67..a7bce38e 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -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" diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index b7d5f19c..8e19880c 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -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: