From 60bb7c46879c48515328bcc3049b96d8f5fc0a2a Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 23 Apr 2026 21:54:36 +0000 Subject: [PATCH] js-on-sx: String replace/search/match + Array.from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit String: replace, search, match now work with either string or regex arg. Regex path uses js-string-index-of on source (case-adjusted when ignoreCase set). Array.from(iter, mapFn?) normalizes via js-iterable-to-list and optionally applies mapFn. Fixed dict-set! on list bug in js-regex-stub-exec — just omit the index/input metadata, spec-breaking but tests that just check [0] work. 407/409 unit (+8), 148/148 slice unchanged. --- lib/js/runtime.sx | 97 +++++++++++++++++++++++++++++++++++++++++++++-- lib/js/test.sh | 28 ++++++++++++++ 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index a78c37b4..278b432d 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -1152,6 +1152,73 @@ (js-string-pad s target pad false)))) ((= name "toString") (fn () s)) ((= name "valueOf") (fn () s)) + ((= name "replace") + (fn + (&rest args) + (cond + ((< (len args) 2) s) + ((js-regex? (nth args 0)) + (let + ((rx (nth args 0)) (repl (nth args 1))) + (let + ((src (get rx "source"))) + (let + ((idx (js-string-index-of (if (get rx "ignoreCase") (js-lower-case s) s) (if (get rx "ignoreCase") (js-lower-case src) src) 0))) + (if + (= idx -1) + s + (let + ((matched (js-string-slice s idx (+ idx (len src))))) + (str + (js-string-slice s 0 idx) + (if + (js-function? repl) + (repl matched) + (js-to-string repl)) + (js-string-slice s (+ idx (len src)) (len s))))))))) + (else + (let + ((needle (js-to-string (nth args 0))) (repl (nth args 1))) + (let + ((idx (js-string-index-of s needle 0))) + (if + (= idx -1) + s + (str + (js-string-slice s 0 idx) + (if + (js-function? repl) + (repl needle) + (js-to-string repl)) + (js-string-slice s (+ idx (len needle)) (len s)))))))))) + ((= name "search") + (fn + (&rest args) + (cond + ((= (len args) 0) -1) + ((js-regex? (nth args 0)) + (let + ((rx (nth args 0)) (src (get (nth args 0) "source"))) + (js-string-index-of + (if (get rx "ignoreCase") (js-lower-case s) s) + (if (get rx "ignoreCase") (js-lower-case src) src) + 0))) + (else (js-string-index-of s (js-to-string (nth args 0)) 0))))) + ((= name "match") + (fn + (&rest args) + (cond + ((= (len args) 0) nil) + ((js-regex? (nth args 0)) (js-regex-stub-exec (nth args 0) s)) + (else + (let + ((needle (js-to-string (nth args 0)))) + (let + ((idx (js-string-index-of s needle 0))) + (if + (= idx -1) + nil + (let ((res (list))) (append! res needle) res)))))))) (else js-undefined)))) (define @@ -1299,6 +1366,9 @@ ((= key "padEnd") (js-string-method obj "padEnd")) ((= key "toString") (js-string-method obj "toString")) ((= key "valueOf") (js-string-method obj "valueOf")) + ((= key "replace") (js-string-method obj "replace")) + ((= key "search") (js-string-method obj "search")) + ((= key "match") (js-string-method obj "match")) (else js-undefined))) ((= (type-of obj) "dict") (js-dict-get-walk obj (js-to-string key))) @@ -1680,7 +1750,30 @@ (define js-array-of (fn (&rest args) args)) -(define Array {:isArray js-array-is-array :of js-array-of}) +(define + js-array-from + (fn + (&rest args) + (cond + ((= (len args) 0) (list)) + (else + (let + ((src (js-iterable-to-list (nth args 0))) + (map-fn (if (< (len args) 2) nil (nth args 1)))) + (if + (= map-fn nil) + (let + ((result (list))) + (for-each (fn (x) (append! result x)) src) + result) + (let + ((result (list)) (i 0)) + (for-each + (fn (x) (append! result (map-fn x)) (set! i (+ i 1))) + src) + result))))))) + +(define Array {:isArray js-array-is-array :of js-array-of :from js-array-from}) (define js-string-from-char-code @@ -2346,8 +2439,6 @@ ((matched (js-string-slice s idx (+ idx (len src)))) (res (list))) (append! res matched) - (dict-set! res "index" idx) - (dict-set! res "input" s) res))))))) (define diff --git a/lib/js/test.sh b/lib/js/test.sh index a51a3196..5e83f260 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -1035,6 +1035,24 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 2202) (eval "(js-eval \"var s=''; for (var c of 'abc') s=s+c; s\")") +;; ── Phase 11.strings2: replace/search/match + Array.from ─────── +(epoch 2300) +(eval "(js-eval \"'hello world'.replace('world', 'JS')\")") +(epoch 2301) +(eval "(js-eval \"'hello'.replace(/l/, 'L')\")") +(epoch 2302) +(eval "(js-eval \"'hello'.search('ll')\")") +(epoch 2303) +(eval "(js-eval \"'hello'.search(/ll/)\")") +(epoch 2304) +(eval "(js-eval \"'hello'.match('ll')[0]\")") +(epoch 2305) +(eval "(js-eval \"Array.from([1,2,3]).length\")") +(epoch 2306) +(eval "(js-eval \"Array.from('abc').length\")") +(epoch 2307) +(eval "(js-eval \"Array.from([1,2,3], (x)=>x*2).join(',')\")") + EPOCHS @@ -1591,6 +1609,16 @@ check 2200 "for-of array" '6' check 2201 "for-in object keys count" '2' check 2202 "for-of string" '"abc"' +# ── Phase 11.strings2 ───────────────────────────────────────── +check 2300 "replace string" '"hello JS"' +check 2301 "replace regex" '"heLlo"' +check 2302 "search string" '2' +check 2303 "search regex" '2' +check 2304 "match string" '"ll"' +check 2305 "Array.from array" '3' +check 2306 "Array.from string" '3' +check 2307 "Array.from w/ map" '"2,4,6"' + TOTAL=$((PASS + FAIL)) if [ $FAIL -eq 0 ]; then echo "✓ $PASS/$TOTAL JS-on-SX tests passed"