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.
This commit is contained in:
2026-04-23 20:58:24 +00:00
parent 6c4001a299
commit 275d2ecbae
3 changed files with 222 additions and 1 deletions

View File

@@ -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})

View File

@@ -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"

View File

@@ -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: