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"