From 275d2ecbae359e50cc440a98492c980714b0d00e Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 23 Apr 2026 20:58:24 +0000 Subject: [PATCH] js-on-sx: String.prototype extensions + Object/Array builtins Strings: includes, startsWith, endsWith, trim, trimStart, trimEnd, repeat, padStart, padEnd, toString, valueOf. Object: keys, values, entries, assign, freeze (no-op). Array: isArray, of. All wired into js-global. 17 new unit tests. 357/359 unit (+17), 148/148 slice unchanged. --- lib/js/runtime.sx | 162 +++++++++++++++++++++++++++++++++++++++++++++- lib/js/test.sh | 59 +++++++++++++++++ plans/js-on-sx.md | 2 + 3 files changed, 222 insertions(+), 1 deletion(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 23e0f826..73a1567e 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -885,6 +885,36 @@ ((>= i (len arr)) acc) (else (js-list-reduce-loop f (f acc (nth arr i)) arr (+ i 1)))))) +(define + js-string-repeat + (fn + (s n acc) + (if (<= n 0) acc (js-string-repeat s (- n 1) (str acc s))))) + +(define + js-string-pad + (fn + (s target pad at-start) + (let + ((slen (len s))) + (if + (or (<= target slen) (= (len pad) 0)) + s + (let + ((needed (- target slen))) + (let + ((padding (js-string-pad-build pad needed ""))) + (if at-start (str padding s) (str s padding)))))))) + +(define + js-string-pad-build + (fn + (pad needed acc) + (cond + ((<= needed 0) acc) + ((>= (len acc) needed) (js-string-slice acc 0 needed)) + (else (js-string-pad-build pad needed (str acc pad)))))) + (define js-string-method (fn @@ -933,6 +963,51 @@ ((= name "split") (fn (sep) (js-string-split s (js-to-string sep)))) ((= name "concat") (fn (&rest args) (js-string-concat-loop s args 0))) + ((= name "includes") + (fn + (&rest args) + (let + ((needle (if (= (len args) 0) "" (js-to-string (nth args 0))))) + (>= (js-string-index-of s needle 0) 0)))) + ((= name "startsWith") + (fn + (&rest args) + (let + ((needle (if (= (len args) 0) "" (js-to-string (nth args 0)))) + (start (if (< (len args) 2) 0 (js-num-to-int (nth args 1))))) + (js-string-matches? s needle start 0)))) + ((= name "endsWith") + (fn + (&rest args) + (let + ((needle (if (= (len args) 0) "" (js-to-string (nth args 0))))) + (let + ((end-len (len s)) (n-len (len needle))) + (if + (> n-len end-len) + false + (js-string-matches? s needle (- end-len n-len) 0)))))) + ((= name "trim") (fn () (js-trim s))) + ((= name "trimStart") (fn () (js-trim-left s))) + ((= name "trimEnd") (fn () (js-trim-right s))) + ((= name "repeat") + (fn (n) (js-string-repeat s (js-num-to-int n) ""))) + ((= name "padStart") + (fn + (&rest args) + (let + ((target (if (= (len args) 0) 0 (js-num-to-int (nth args 0)))) + (pad (if (< (len args) 2) " " (js-to-string (nth args 1))))) + (js-string-pad s target pad true)))) + ((= name "padEnd") + (fn + (&rest args) + (let + ((target (if (= (len args) 0) 0 (js-num-to-int (nth args 0)))) + (pad (if (< (len args) 2) " " (js-to-string (nth args 1))))) + (js-string-pad s target pad false)))) + ((= name "toString") (fn () s)) + ((= name "valueOf") (fn () s)) (else js-undefined)))) (define @@ -1061,6 +1136,17 @@ ((= key "toLowerCase") (js-string-method obj "toLowerCase")) ((= key "split") (js-string-method obj "split")) ((= key "concat") (js-string-method obj "concat")) + ((= key "includes") (js-string-method obj "includes")) + ((= key "startsWith") (js-string-method obj "startsWith")) + ((= key "endsWith") (js-string-method obj "endsWith")) + ((= key "trim") (js-string-method obj "trim")) + ((= key "trimStart") (js-string-method obj "trimStart")) + ((= key "trimEnd") (js-string-method obj "trimEnd")) + ((= key "repeat") (js-string-method obj "repeat")) + ((= key "padStart") (js-string-method obj "padStart")) + ((= key "padEnd") (js-string-method obj "padEnd")) + ((= key "toString") (js-string-method obj "toString")) + ((= key "valueOf") (js-string-method obj "valueOf")) (else js-undefined))) ((= (type-of obj) "dict") (js-dict-get-walk obj (js-to-string key))) @@ -1347,6 +1433,80 @@ (dict-set! p "value" reason) (js-promise-flush-callbacks! p)))))) +(define + js-object-keys + (fn + (o) + (cond + ((dict? o) + (let + ((result (list))) + (for-each (fn (k) (append! result k)) (keys o)) + result)) + (else (list))))) + +(define + js-object-values + (fn + (o) + (cond + ((dict? o) + (let + ((result (list))) + (for-each (fn (k) (append! result (get o k))) (keys o)) + result)) + (else (list))))) + +(define + js-object-entries + (fn + (o) + (cond + ((dict? o) + (let + ((result (list))) + (for-each + (fn + (k) + (let + ((pair (list))) + (append! pair k) + (append! pair (get o k)) + (append! result pair))) + (keys o)) + result)) + (else (list))))) + +(define + js-object-assign + (fn + (&rest args) + (cond + ((= (len args) 0) (dict)) + (else + (let + ((target (nth args 0))) + (for-each + (fn + (src) + (when + (dict? src) + (for-each + (fn (k) (dict-set! target k (get src k))) + (keys src)))) + (rest args)) + target))))) + +(define js-object-freeze (fn (o) o)) + +(define Object {:entries js-object-entries :values js-object-values :freeze js-object-freeze :assign js-object-assign :keys js-object-keys}) + +(define js-array-is-array (fn (v) (list? v))) + +(define js-array-of (fn (&rest args) args)) + +(define Array {:isArray js-array-is-array :of js-array-of}) + (define js-promise-flush-callbacks! (fn @@ -1761,4 +1921,4 @@ (str "/" (get rx "source") "/" (get rx "flags"))) (else js-undefined)))) -(define js-global {:isFinite js-global-is-finite :console console :Number Number :Math Math :NaN 0 :Infinity inf :isNaN js-global-is-nan :undefined js-undefined}) +(define js-global {:isFinite js-global-is-finite :console console :Number Number :Math Math :Array Array :NaN 0 :Infinity inf :isNaN js-global-is-nan :Object Object :undefined js-undefined}) diff --git a/lib/js/test.sh b/lib/js/test.sh index b1eba014..6b2d9b38 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -899,6 +899,44 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 1310) (eval "(js-eval \"var sum = 0; for (var i = 1; i <= 5; i++) sum = sum + i; sum\")") +;; ── Phase 11.strings: extended String.prototype methods ───────── +(epoch 1400) +(eval "(js-eval \"'hello world'.includes('world')\")") +(epoch 1401) +(eval "(js-eval \"'hello world'.includes('xyz')\")") +(epoch 1402) +(eval "(js-eval \"'hello'.startsWith('hel')\")") +(epoch 1403) +(eval "(js-eval \"'hello'.startsWith('llo')\")") +(epoch 1404) +(eval "(js-eval \"'hello'.endsWith('llo')\")") +(epoch 1405) +(eval "(js-eval \"'hello'.endsWith('hel')\")") +(epoch 1406) +(eval "(js-eval \"' hi '.trim()\")") +(epoch 1407) +(eval "(js-eval \"'abc'.repeat(3)\")") +(epoch 1408) +(eval "(js-eval \"'5'.padStart(3, '0')\")") +(epoch 1409) +(eval "(js-eval \"'5'.padEnd(3, 'x')\")") +(epoch 1410) +(eval "(js-eval \"'hello'.toString()\")") + +;; ── Phase 11.object: Object builtin ───────────────────────────── +(epoch 1500) +(eval "(js-eval \"Object.keys({a:1, b:2}).length\")") +(epoch 1501) +(eval "(js-eval \"Object.values({a:1, b:2}).length\")") +(epoch 1502) +(eval "(js-eval \"var o = Object.assign({a:1}, {b:2}); o.a + o.b\")") +(epoch 1503) +(eval "(js-eval \"Array.isArray([1,2])\")") +(epoch 1504) +(eval "(js-eval \"Array.isArray('abc')\")") +(epoch 1505) +(eval "(js-eval \"Array.of(1,2,3).length\")") + EPOCHS OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -1377,6 +1415,27 @@ check 1308 "a[1]++" '3' check 1309 "for-loop x++" '8' check 1310 "for-loop accumulator" '15' +# ── Phase 11.strings: extended String.prototype ───────────────── +check 1400 "includes match" 'true' +check 1401 "includes no match" 'false' +check 1402 "startsWith match" 'true' +check 1403 "startsWith no match" 'false' +check 1404 "endsWith match" 'true' +check 1405 "endsWith no match" 'false' +check 1406 "trim whitespace" '"hi"' +check 1407 "repeat 3x" '"abcabcabc"' +check 1408 "padStart 0s" '"005"' +check 1409 "padEnd x" '"5xx"' +check 1410 "toString self" '"hello"' + +# ── Phase 11.object: Object + Array ───────────────────────────── +check 1500 "Object.keys count" '2' +check 1501 "Object.values.length" '2' +check 1502 "Object.assign merge" '3' +check 1503 "Array.isArray yes" 'true' +check 1504 "Array.isArray no" 'false' +check 1505 "Array.of length" '3' + 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 7185f72e..e47fc6ac 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -179,6 +179,8 @@ Append-only record of completed iterations. Loop writes one line per iteration: - 2026-04-23 — **Postfix/prefix `++` / `--`.** Parser: postfix branch in `jp-parse-postfix` (matches `op ++`/`--` after the current expression and emits `(js-postfix op target)`), prefix branch in `jp-parse-primary` *before* the unary-`-/+/!/~` path emits `(js-prefix op target)`. Transpile: `js-transpile-prefix` emits `(set! sxname (+ (js-to-number sxname) ±1))` for idents, `(js-set-prop obj key (+ (js-to-number (js-get-prop obj key)) ±1))` for members/indices. `js-transpile-postfix` uses a `let` binding to cache the old value via `js-to-number`, then updates and returns the saved value — covers ident, member, and index targets. 11 new unit tests (ident inc/dec, pre vs post return value, obj.key, a[i], in `for(;; i++)`, accumulator loop), **340/342** (329→+11). Conformance unchanged. +- 2026-04-23 — **String.prototype extensions + Object/Array builtins.** Strings: added `includes`, `startsWith`, `endsWith`, `trim`, `trimStart`, `trimEnd`, `repeat`, `padStart`, `padEnd`, `toString`, `valueOf` to `js-string-method` dispatch and corresponding `js-get-prop` string-branch keys. Helpers: `js-string-repeat` (tail-recursive concat), `js-string-pad` + `js-string-pad-build`. Object: `Object.keys / .values / .entries / .assign / .freeze` (freeze is a no-op — we don't track sealed state). Array: `Array.isArray` (backed by `list?`), `Array.of` (varargs → list). Wired into `js-global`. 17 new unit tests, **357/359** (340→+17). Conformance unchanged. Gotcha: SX's `keys` primitive returns most-recently-inserted-first, so `Object.keys({a:1, b:2})` comes back `["b", "a"]`. Test assertion has to check `.length` rather than the literal pair. If spec order matters for a real app, Object.keys would need its own ordered traversal. + ## Phase 3-5 gotchas Worth remembering for later phases: