js-on-sx: regex literal lex+parse+transpile+runtime stub

Lexer: js-regex-context? disambiguates / based on prior token;
read-regex handles [...] classes and \ escapes. Emits
{:type "regex" :value {:pattern :flags}}.

Parser: new primary branch → (js-regex pat flags).

Transpile: (js-regex-new pat flags).

Runtime: js-regex? predicate, js-regex-new builds tagged dict with
source/flags/global/ignoreCase/multiline/sticky/unicode/dotAll/
hasIndices/lastIndex. js-regex-invoke-method dispatches .test/.exec/
.toString. js-invoke-method detects regex receivers. Stub engine
uses js-string-index-of; __js_regex_platform__ + override! let a
real engine plug in later.

Runner: repeatable --filter flags (OR'd).

308/310 unit (+30 regex tests), 148/148 slice unchanged.
This commit is contained in:
2026-04-23 20:27:19 +00:00
parent 6b0334affe
commit ce46420c2e
7 changed files with 337 additions and 4 deletions

View File

@@ -745,6 +745,90 @@ cat > "$TMPFILE" << 'EPOCHS'
(epoch 938)
(eval "(js-eval \"`[${''}-${''}]`\")")
;; ── Phase 11.regex: regex literal lexing ────────────────────────
;; Simple regex at start of file
(epoch 1000)
(eval "(get (nth (js-tokenize \"/abc/\") 0) :type)")
(epoch 1001)
(eval "(get (get (nth (js-tokenize \"/abc/\") 0) :value) :pattern)")
(epoch 1002)
(eval "(get (get (nth (js-tokenize \"/abc/\") 0) :value) :flags)")
;; With flags
(epoch 1003)
(eval "(get (get (nth (js-tokenize \"/a+/gi\") 0) :value) :flags)")
;; Character class with embedded /
(epoch 1004)
(eval "(get (get (nth (js-tokenize \"/[/]/\") 0) :value) :pattern)")
;; Escaped /
(epoch 1005)
(eval "(get (get (nth (js-tokenize \"/a\\\\/b/\") 0) :value) :pattern)")
;; After `return` keyword → regex
(epoch 1006)
(eval "(get (nth (js-tokenize \"return /x/\") 1) :type)")
;; After `=` op → regex
(epoch 1007)
(eval "(get (nth (js-tokenize \"x = /y/\") 2) :type)")
;; After ident `x` → division (not regex)
(epoch 1008)
(eval "(get (nth (js-tokenize \"a/b\") 1) :type)")
(epoch 1009)
(eval "(get (nth (js-tokenize \"a/b\") 1) :value)")
;; After `)` → division
(epoch 1010)
(eval "(get (nth (js-tokenize \"(a)/b\") 3) :type)")
;; After number → division
(epoch 1011)
(eval "(get (nth (js-tokenize \"1/2\") 1) :type)")
;; Regex /= must still be division-assignment in expr context
(epoch 1012)
(eval "(get (nth (js-tokenize \"x/=2\") 1) :type)")
(epoch 1013)
(eval "(get (nth (js-tokenize \"x/=2\") 1) :value)")
;; Inside function body after statement separator
(epoch 1014)
(eval "(get (nth (js-tokenize \"; /abc/\") 1) :type)")
;; After `throw`
(epoch 1015)
(eval "(get (nth (js-tokenize \"throw /x/\") 1) :type)")
;; ── Phase 11.regex: parser ──────────────────────────────────────
(epoch 1020)
(eval "(first (js-parse-expr \"/abc/\"))")
(epoch 1021)
(eval "(nth (js-parse-expr \"/foo/gi\") 1)")
(epoch 1022)
(eval "(nth (js-parse-expr \"/foo/gi\") 2)")
;; ── Phase 11.regex: transpile ───────────────────────────────────
(epoch 1030)
(eval "(first (js-transpile (js-parse-expr \"/abc/\")))")
;; ── Phase 11.regex: runtime — regex object shape ───────────────
(epoch 1040)
(eval "(get (js-regex-new \"ab\" \"g\") :source)")
(epoch 1041)
(eval "(get (js-regex-new \"ab\" \"g\") :flags)")
(epoch 1042)
(eval "(get (js-regex-new \"ab\" \"g\") :global)")
(epoch 1043)
(eval "(js-regex? (js-regex-new \"ab\" \"\"))")
;; .source / .flags / .global etc via property access
(epoch 1050)
(eval "(js-eval \"/abc/g.source\")")
(epoch 1051)
(eval "(js-eval \"/abc/gi.flags\")")
(epoch 1052)
(eval "(js-eval \"/abc/g.global\")")
(epoch 1053)
(eval "(js-eval \"/abc/i.ignoreCase\")")
;; .test() via stub: substring-based
(epoch 1060)
(eval "(js-eval \"/foo/.test('hello foo')\")")
(epoch 1061)
(eval "(js-eval \"/zzz/.test('hello')\")")
EPOCHS
OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
@@ -1144,6 +1228,48 @@ check 936 'bare ${42}' '"42"'
check 937 "expr in interp" '"len is 12"'
check 938 "empty interps" '"[-]"'
# ── Phase 11.regex: lexer ────────────────────────────────────────
check 1000 "regex at sof → type" '"regex"'
check 1001 "regex pattern" '"abc"'
check 1002 "regex empty flags" '""'
check 1003 "regex with gi flags" '"gi"'
check 1004 "regex class with /" '"[/]"'
check 1005 "regex escaped /" '"a\\/b"'
check 1006 "after return → regex" '"regex"'
check 1007 "after = → regex" '"regex"'
check 1008 "after ident → op" '"op"'
check 1009 "after ident div value" '"/"'
check 1010 "after ) → op" '"op"'
check 1011 "after number → op" '"op"'
check 1012 "x/=2 is /=-assign" '"op"'
check 1013 "x/=2 /= op value" '"/="'
check 1014 "after ; → regex" '"regex"'
check 1015 "after throw → regex" '"regex"'
# ── Phase 11.regex: parser ───────────────────────────────────────
check 1020 "parse /abc/ head" 'js-regex'
check 1021 "parse pattern arg" '"foo"'
check 1022 "parse flags arg" '"gi"'
# ── Phase 11.regex: transpile ────────────────────────────────────
check 1030 "transpile uses js-regex-new" 'js-regex-new'
# ── Phase 11.regex: runtime obj ──────────────────────────────────
check 1040 "regex source" '"ab"'
check 1041 "regex flags" '"g"'
check 1042 "regex global true" 'true'
check 1043 "js-regex? true" 'true'
# ── Phase 11.regex: property access ──────────────────────────────
check 1050 "literal .source" '"abc"'
check 1051 "literal .flags" '"gi"'
check 1052 "literal .global" 'true'
check 1053 "literal .ignoreCase" 'true'
# ── Phase 11.regex: test() ───────────────────────────────────────
check 1060 "test match" 'true'
check 1061 "test no match" 'false'
TOTAL=$((PASS + FAIL))
if [ $FAIL -eq 0 ]; then
echo "$PASS/$TOTAL JS-on-SX tests passed"