diff --git a/bend2/base.bend b/bend2/base.bend
index 35475bf56..e7e0d9bf4 100644
--- a/bend2/base.bend
+++ b/bend2/base.bend
@@ -83,6 +83,29 @@ type Map is Kind(a):
MLeaf{key: String, val: V}
MNode{pos: Nat, lo: Map, hi: Map}
+type Regex.Node is Data:
+ RSet{neg: Bool, rs: List<&2, Sigma<&2, &2, U32, _ => U32>>}
+ RCat{a: Regex.Node, b: Regex.Node}
+ RAlt{a: Regex.Node, b: Regex.Node}
+ RRep{r: Regex.Node, k: Nat, lazy: Bool}
+ RCap{n: Nat, r: Regex.Node}
+ RAsr{k: Nat}
+ REmpty{}
+
+type Regex.Inst is Data:
+ ISet{neg: Bool, rs: List<&2, Sigma<&2, &2, U32, _ => U32>>}
+ ISplit{x: Nat, y: Nat}
+ IJmp{x: Nat}
+ ISave{slot: Nat}
+ IAsr{k: Nat}
+ IMatch{}
+
+type Regex is Data:
+ Regex{code: List<&2, Regex.Inst>, groups: Nat}
+
+type Regex.Match is Data:
+ Match{start: Nat, end: Nat, groups: List<&2, Maybe<&2, Sigma<&2, &2, Nat, _ => Nat>>>}
+
# Effects
# -------
@@ -2208,6 +2231,468 @@ def List.show(~a: Quant, ~A: Kind(a), ~f: A -> String, xs: List) ->
case h <> t:
"[" ++ f(h) ++ List.show.go(~a, ~A, ~f, t)
+# Regex
+# -----
+
+# CPython 3.11's `re` under re.ASCII, in part: literals, `.`, sets, the
+# escapes `\d \w \s \D \W \S \b \B` and the control ones, `^ $`, groups,
+# `(?:`, `|`, and `* + ?`, greedy or lazy. It runs on a Pike VM, linear in
+# the subject; offsets count code points. A pattern out of this part fails
+# to compile, as does a `*` or `+` on a body that can match empty.
+
+def Regex.Ranges() -> Data:
+ List<&2, Sigma<&2, &2, U32, _ => U32>>
+
+def Regex.words() -> Regex.Ranges():
+ [(48, 57), (65, 90), (95, 95), (97, 122)]
+
+# An escape as the chars it takes; None for a letter or digit that is not
+# one of them
+def Regex.esc(+c: Char) -> Maybe<&2, Regex.Ranges()>:
+ match c:
+ case 'd':
+ Some{[(48, 57)]}
+ case 'D':
+ Some{[(0, 47), (58, 1114111)]}
+ case 's':
+ Some{[(9, 13), (32, 32)]}
+ case 'S':
+ Some{[(0, 8), (14, 31), (33, 1114111)]}
+ case 'w':
+ Some{Regex.words()}
+ case 'W':
+ Some{[(0, 47), (58, 64), (91, 94), (96, 96), (123, 1114111)]}
+ case 'a':
+ Some{[(7, 7)]}
+ case 'b':
+ Some{[(8, 8)]}
+ case 't':
+ Some{[(9, 9)]}
+ case 'n':
+ Some{[(10, 10)]}
+ case 'v':
+ Some{[(11, 11)]}
+ case 'f':
+ Some{[(12, 12)]}
+ case 'r':
+ Some{[(13, 13)]}
+ case Chr{+x}:
+ Bool.pick(Maybe<&2, Regex.Ranges()>,
+ Char.is_alpha(Chr{x}) || Char.is_digit(Chr{x}), None{}, Some{[(x, x)]})
+
+# A set so far: its ranges, whether they are valid, and the mode: 3 at the
+# start (a `]` there is a literal), 1 after an item (a `-` may make it a
+# range's start), 2 after that `-`, 0 after a range
+def Regex.Set() -> Data:
+ Sigma<&2, &2, Regex.Ranges(), _ => Sigma<&2, &2, Bool, _ => Nat>>
+
+def Regex.set.add(x: Maybe<&2, Regex.Ranges()>, st: Regex.Set()) -> Regex.Set():
+ match x st:
+ case Some{[(+b, +c)]}, ((+a, d) <> rs, (ok, 2n)):
+ ((a, c) <> rs, (ok && U32.is_eq(a, d) && U32.is_eq(b, c) && U32.is_le(a, b), 0n))
+ case Some{x}, (rs, (ok, 2n)):
+ (rs, (False{}, 0n))
+ case Some{x}, (rs, (ok, m)):
+ (List.append(&2, Sigma<&2, &2, U32, _ => U32>, x, rs), (ok, 1n))
+ case None{}, (rs, (ok, m)):
+ (rs, (False{}, 1n))
+
+# Scans a set up to its `]`
+def Regex.set(s: String, st: Regex.Set()) ->
+ Result<&2, &2, String, Sigma<&2, &2, Regex.Ranges(), _ => String>>:
+ match s st:
+ case SCon{']', t}, (rs, (ok, 3n)):
+ Regex.set(t, ([(93, 93)], (True{}, 1n)))
+ case SCon{']', t}, (rs, (False{}, m)):
+ Fail{"bad character range or escape"}
+ case SCon{']', t}, (rs, (ok, 2n)):
+ Done{((45, 45) <> rs, t)}
+ case SCon{']', t}, (rs, r):
+ Done{(rs, t)}
+ case SCon{'-', t}, (rs, (ok, 1n)):
+ Regex.set(t, (rs, (ok, 2n)))
+ case SCon{'\\', SCon{+c, t}}, st:
+ Regex.set(t, Regex.set.add(Regex.esc(c), st))
+ case SCon{Chr{+c}, t}, st:
+ Regex.set(t, Regex.set.add(Some{[(c, c)]}, st))
+ case SNil{}, st:
+ Fail{"unterminated character set"}
+
+# A group open: (its number, 0 for `(?:`, (the alternatives before its last
+# `|`, the items after it))
+def Regex.Frame() -> Data:
+ Sigma<&2, &2, Nat, _ => Sigma<&2, &2, Maybe<&2, Regex.Node>, _ => Regex.Node>>
+
+# (the pattern left, (the groups so far, the frames, innermost first))
+def Regex.Parse() -> Data:
+ Result<&2, &2, String,
+ Sigma<&2, &2, String, _ => Sigma<&2, &2, Nat, _ => List<&2, Regex.Frame()>>>>
+
+def Regex.alt(alts: Maybe<&2, Regex.Node>, r: Regex.Node) -> Regex.Node:
+ match alts:
+ case None{}:
+ r
+ case Some{a}:
+ RAlt{a, r}
+
+def Regex.nullable(r: Regex.Node) -> Bool:
+ match r:
+ case RSet{neg, rs}:
+ False{}
+ case RCat{a, b}:
+ Regex.nullable(a) && Regex.nullable(b)
+ case RAlt{a, b}:
+ Regex.nullable(a) || Regex.nullable(b)
+ case RRep{x, 1n, lazy}:
+ Regex.nullable(x)
+ case RCap{n, x}:
+ Regex.nullable(x)
+ case _:
+ True{}
+
+def Regex.atom(t: String, g: Nat, fs: List<&2, Regex.Frame()>, r: Regex.Node) ->
+ Regex.Parse():
+ match fs:
+ case (n, (alts, cat)) <> fs:
+ Done{(t, (g, (n, (alts, RCat{cat, r})) <> fs))}
+ case Nil{}:
+ Fail{"unbalanced parenthesis"}
+
+def Regex.atom.set(g: Nat, fs: List<&2, Regex.Frame()>, neg: Bool,
+ r: Result<&2, &2, String, Sigma<&2, &2, Regex.Ranges(), _ => String>>
+) -> Regex.Parse():
+ match r:
+ case Fail{e}:
+ Fail{e}
+ case Done{(rs, t)}:
+ Regex.atom(t, g, fs, RSet{neg, rs})
+
+def Regex.atom.esc(t: String, g: Nat, fs: List<&2, Regex.Frame()>,
+ m: Maybe<&2, Regex.Ranges()>) -> Regex.Parse():
+ match m:
+ case None{}:
+ Fail{"bad escape"}
+ case Some{rs}:
+ Regex.atom(t, g, fs, RSet{False{}, rs})
+
+# `*` `+` `?` (k = 0, 1, 2) on the last item
+def Regex.rep(t: String, g: Nat, fs: List<&2, Regex.Frame()>, +k: Nat,
+ lazy: Bool) -> Regex.Parse():
+ match fs:
+ case (n, (alts, RCat{cat, RRep{r, j, l}})) <> fs:
+ Fail{"multiple repeat"}
+ case (n, (alts, RCat{cat, RAsr{j}})) <> fs:
+ Fail{"nothing to repeat"}
+ case (n, (alts, RCat{cat, +r})) <> fs:
+ Bool.pick(Regex.Parse(), Regex.nullable(r) && Nat.is_lt(k, 2n),
+ Fail{"repeat of a pattern that can match empty"},
+ Done{(t, (g, (n, (alts, RCat{cat, RRep{r, k, lazy}})) <> fs))})
+ case fs:
+ Fail{"nothing to repeat"}
+
+def Regex.step(s: String, +g: Nat, fs: List<&2, Regex.Frame()>) ->
+ Regex.Parse():
+ match s fs:
+ case SCon{'(', SCon{'?', SCon{':', t}}}, fs:
+ Done{(t, (g, (0n, (None{}, REmpty{})) <> fs))}
+ case SCon{'(', SCon{'?', t}}, fs:
+ Fail{"unsupported group extension"}
+ case SCon{'(', t}, fs:
+ Done{(t, (1n+g, (1n+g, (None{}, REmpty{})) <> fs))}
+ case SCon{')', t}, (n, (alts, cat)) <> ((m, (up, pre)) <> fs):
+ Done{(t, (g, (m, (up, RCat{pre, RCap{n, Regex.alt(alts, cat)}})) <> fs))}
+ case SCon{')', t}, fs:
+ Fail{"unbalanced parenthesis"}
+ case SCon{'|', t}, (n, (alts, cat)) <> fs:
+ Done{(t, (g, (n, (Some{Regex.alt(alts, cat)}, REmpty{})) <> fs))}
+ case SCon{'*', SCon{'?', t}}, fs:
+ Regex.rep(t, g, fs, 0n, True{})
+ case SCon{'*', t}, fs:
+ Regex.rep(t, g, fs, 0n, False{})
+ case SCon{'+', SCon{'?', t}}, fs:
+ Regex.rep(t, g, fs, 1n, True{})
+ case SCon{'+', t}, fs:
+ Regex.rep(t, g, fs, 1n, False{})
+ case SCon{'?', SCon{'?', t}}, fs:
+ Regex.rep(t, g, fs, 2n, True{})
+ case SCon{'?', t}, fs:
+ Regex.rep(t, g, fs, 2n, False{})
+ case SCon{'{', t}, fs:
+ Fail{"counted repeats are not supported"}
+ case SCon{'[', SCon{'^', t}}, fs:
+ Regex.atom.set(g, fs, True{}, Regex.set(t, ([], (True{}, 3n))))
+ case SCon{'[', t}, fs:
+ Regex.atom.set(g, fs, False{}, Regex.set(t, ([], (True{}, 3n))))
+ case SCon{'.', t}, fs:
+ Regex.atom(t, g, fs, RSet{True{}, [(10, 10)]})
+ case SCon{'^', t}, fs:
+ Regex.atom(t, g, fs, RAsr{0n})
+ case SCon{'$', t}, fs:
+ Regex.atom(t, g, fs, RAsr{1n})
+ case SCon{'\\', SCon{'b', t}}, fs:
+ Regex.atom(t, g, fs, RAsr{2n})
+ case SCon{'\\', SCon{'B', t}}, fs:
+ Regex.atom(t, g, fs, RAsr{3n})
+ case SCon{'\\', SCon{+c, t}}, fs:
+ Regex.atom.esc(t, g, fs, Regex.esc(c))
+ case SCon{'\\', t}, fs:
+ Fail{"bad escape (end of pattern)"}
+ case SCon{Chr{+c}, t}, fs:
+ Regex.atom(t, g, fs, RSet{False{}, [(c, c)]})
+ case SNil{}, fs:
+ Done{(SNil{}, (g, fs))}
+
+def Regex.split(lazy: Bool, x: Nat, y: Nat) -> Regex.Inst:
+ match lazy:
+ case False{}:
+ ISplit{x, y}
+ case True{}:
+ ISplit{y, x}
+
+# The code for r placed at pc
+def Regex.emit(r: Regex.Node, +pc: Nat) -> List<&2, Regex.Inst>:
+ match r:
+ case RSet{neg, rs}:
+ [ISet{neg, rs}]
+ case RCat{a, b}:
+ +xs = Regex.emit(a, pc)
+ List.append(&2, Regex.Inst, xs, Regex.emit(b, Nat.add(pc, List.length(&2, Regex.Inst, xs))))
+ case RAlt{a, b}:
+ +xs = Regex.emit(a, 1n+pc)
+ +y = Nat.add(2n+pc, List.length(&2, Regex.Inst, xs))
+ +ys = Regex.emit(b, y)
+ ISplit{1n+pc, y} <> List.append(&2, Regex.Inst, xs,
+ IJmp{Nat.add(y, List.length(&2, Regex.Inst, ys))} <> ys)
+ case RRep{x, 0n, lazy}:
+ +xs = Regex.emit(x, 1n+pc)
+ Regex.split(lazy, 1n+pc, Nat.add(2n+pc, List.length(&2, Regex.Inst, xs)))
+ <> List.append(&2, Regex.Inst, xs, [IJmp{pc}])
+ case RRep{x, 1n, lazy}:
+ +xs = Regex.emit(x, pc)
+ List.append(&2, Regex.Inst, xs,
+ [Regex.split(lazy, pc, Nat.add(1n+pc, List.length(&2, Regex.Inst, xs)))])
+ case RRep{x, k, lazy}:
+ +xs = Regex.emit(x, 1n+pc)
+ Regex.split(lazy, 1n+pc, Nat.add(1n+pc, List.length(&2, Regex.Inst, xs))) <> xs
+ case RCap{0n, x}:
+ Regex.emit(x, pc)
+ case RCap{+n, x}:
+ ISave{Nat.double(n)} <> List.append(&2, Regex.Inst, Regex.emit(x, 1n+pc),
+ [ISave{1n+Nat.double(n)}])
+ case RAsr{k}:
+ [IAsr{k}]
+ case REmpty{}:
+ []
+
+# One char per step, so the fuel never runs out
+def Regex.parse(fuel: Nat, r: Regex.Parse()) -> Result<&2, &2, String, Regex>:
+ match fuel r:
+ case _ Fail{e}:
+ Fail{e}
+ case _ Done{(SNil{}, (g, [(n, (alts, cat))]))}:
+ Done{Regex{ISave{0n} <> Regex.emit(Regex.alt(alts, cat), 1n), g}}
+ case _ Done{(SNil{}, r)}:
+ Fail{"missing ), unterminated subpattern"}
+ case 1n+f Done{(s, (g, fs))}:
+ Regex.parse(f, Regex.step(s, g, fs))
+ case 0n r:
+ Fail{"out of fuel"}
+
+def Regex.compile(+p: String) -> Result<&2, &2, String, Regex>:
+ Regex.parse(1n+String.length(p), Done{(p, (0n, [(0n, (None{}, REmpty{}))]))})
+
+def Regex.Caps() -> Data:
+ List<&2, Maybe<&2, Nat>>
+
+def Regex.Thread() -> Data:
+ Sigma<&2, &2, Nat, _ => Regex.Caps()>
+
+def Regex.Threads() -> Data:
+ List<&2, Regex.Thread()>
+
+# A step's place: (its offset, (the char before, the subject from here))
+def Regex.Ctx() -> Data:
+ Sigma<&2, &2, Nat, _ => Sigma<&2, &2, Maybe<&2, U32>, _ => String>>
+
+# The threads alive, in priority order, and the best match so far
+def Regex.Run() -> Data:
+ Sigma<&2, &2, Regex.Threads(), _ => Maybe<&2, Regex.Caps()>>
+
+# A closure: (the threads to visit, (the pcs visited, the next run))
+def Regex.Close() -> Data:
+ Sigma<&2, &2, Regex.Threads(), _ => Sigma<&2, &2, List<&2, Nat>, _ => Regex.Run()>>
+
+def Regex.in(+c: U32, rs: Regex.Ranges()) -> Bool:
+ match rs:
+ case Nil{}:
+ False{}
+ case (a, b) <> t:
+ U32.is_le(a, c) && U32.is_le(c, b) || Regex.in(c, t)
+
+def Regex.word(m: Maybe<&2, U32>) -> Bool:
+ match m:
+ case None{}:
+ False{}
+ case Some{c}:
+ Regex.in(c, Regex.words())
+
+def Regex.bound(p: Maybe<&2, U32>, t: String) -> Bool:
+ match t:
+ case SNil{}:
+ Regex.word(p)
+ case SCon{Chr{c}, u}:
+ Bool.xor(Regex.word(p), Regex.word(Some{c}))
+
+# Assertion k: 0 `^`, 1 `$`, 2 `\b`, 3 `\B`, 4 the end. On "" `\B` fails
+# too, as in CPython
+def Regex.asr(k: Nat, cx: Regex.Ctx()) -> Bool:
+ match k cx:
+ case 0n, (i, (None{}, t)):
+ True{}
+ case 1n, (i, (p, SNil{})):
+ True{}
+ case 1n, (i, (p, SCon{'\n', SNil{}})):
+ True{}
+ case 2n, (i, (p, t)):
+ Regex.bound(p, t)
+ case 3n, (i, (None{}, SNil{})):
+ False{}
+ case 3n, (i, (p, t)):
+ Bool.not(Regex.bound(p, t))
+ case 4n, (i, (p, SNil{})):
+ True{}
+ case _, _:
+ False{}
+
+def Regex.pos(cx: Regex.Ctx()) -> Nat:
+ match cx:
+ case (i, r):
+ i
+
+def Regex.eat(neg: Bool, rs: Regex.Ranges(), cx: Regex.Ctx()) -> Bool:
+ match cx:
+ case (i, (p, SCon{Chr{c}, t})):
+ Bool.xor(neg, Regex.in(c, rs))
+ case _:
+ False{}
+
+# Visits thread (pc, caps) at cx: dup if pc was visited here, inst the
+# code at pc. A set passes the thread to the next char; a match cuts every
+# thread below it
+def Regex.visit(+cx: Regex.Ctx(), +pc: Nat, +caps: Regex.Caps(),
+ work: Regex.Threads(), seen: List<&2, Nat>, dup: Bool,
+ inst: Maybe<&2, Regex.Inst>, now: Regex.Run()) -> Regex.Close():
+ match dup inst now:
+ case True{}, _, now:
+ (work, (seen, now))
+ case False{}, Some{ISplit{x, y}}, now:
+ ((x, caps) <> ((y, caps) <> work), (pc <> seen, now))
+ case False{}, Some{IJmp{x}}, now:
+ ((x, caps) <> work, (pc <> seen, now))
+ case False{}, Some{ISave{k}}, now:
+ ((1n+pc, List.set(&2, Maybe<&2, Nat>, caps, k, Some{Regex.pos(cx)})) <> work,
+ (pc <> seen, now))
+ case False{}, Some{IAsr{k}}, now:
+ (List.filter.put(Regex.Thread(), (1n+pc, caps), work, Regex.asr(k, cx)),
+ (pc <> seen, now))
+ case False{}, Some{ISet{neg, rs}}, (ts, best):
+ (work, (pc <> seen, (List.filter.put(Regex.Thread(), (1n+pc, caps), ts,
+ Regex.eat(neg, rs, cx)), best)))
+ case False{}, Some{IMatch{}}, (ts, best):
+ ([], (seen, (ts, Some{caps})))
+ case False{}, _, now:
+ (work, (pc <> seen, now))
+
+# Each pc is visited once per step, so 3 * (1 + |code|) visits drain it
+def Regex.close(fuel: Nat, +code: List<&2, Regex.Inst>, +cx: Regex.Ctx(),
+ st: Regex.Close()) -> Regex.Run():
+ match fuel st:
+ case 1n+f, ((+pc, caps) <> work, (+seen, run)):
+ Regex.close(f, code, cx, Regex.visit(cx, pc, caps, work, seen,
+ List.contains(~Nat, ~Nat.is_eq, seen, pc), List.get(&2, Regex.Inst, code, pc),
+ run))
+ case _, (work, (seen, (ts, best))):
+ (List.reverse(&2, Regex.Thread(), ts), best)
+
+# The threads alive at cx, and below them, while no match is known, the
+# fresh one
+def Regex.tick(+code: List<&2, Regex.Inst>, fuel: Nat, start: Regex.Threads(),
+ cx: Regex.Ctx(), st: Regex.Run()) -> Regex.Run():
+ match st:
+ case (ts, +best):
+ Regex.close(fuel, code, cx, (List.append(&2, Regex.Thread(), ts,
+ Bool.pick(Regex.Threads(), Maybe.is_some(&2, Regex.Caps(), best), [],
+ start)), ([], ([], best))))
+
+# Walks s from 0; the first `at` chars only give the char before. An
+# anchored run drops the fresh thread after `at`
+def Regex.run(s: String, at: Nat, +code: List<&2, Regex.Inst>, +fuel: Nat,
+ +start: Regex.Threads(), +anc: Bool, +i: Nat, p: Maybe<&2, U32>,
+ st: Regex.Run()) -> Regex.Run():
+ match s at st:
+ case SCon{Chr{c}, t}, 1n+a, st:
+ Regex.run(t, a, code, fuel, start, anc, 1n+i, Some{c}, st)
+ case s, 0n, (Nil{}, Some{caps}):
+ ([], Some{caps})
+ case SCon{Chr{+c}, +t}, 0n, st:
+ Regex.run(t, 0n, code, fuel, Bool.pick(Regex.Threads(), anc, [], start),
+ anc, 1n+i, Some{c}, Regex.tick(code, fuel, start, (i, (p, SCon{Chr{c}, t})), st))
+ case SNil{}, a, st:
+ Regex.tick(code, fuel, start, (i, (p, SNil{})), st)
+
+def Regex.spans(caps: Regex.Caps()) -> List<&2, Maybe<&2, Sigma<&2, &2, Nat, _ => Nat>>>:
+ match caps:
+ case Some{a} <> (Some{b} <> t):
+ Some{(a, b)} <> Regex.spans(t)
+ case x <> (y <> t):
+ None{} <> Regex.spans(t)
+ case _:
+ []
+
+def Regex.found(r: Regex.Run()) -> Maybe<&2, Regex.Match>:
+ match r:
+ case (ts, Some{Some{a} <> (Some{b} <> gs)}):
+ Some{Match{a, b, Regex.spans(gs)}}
+ case _:
+ None{}
+
+def Regex.search(re: Regex, s: String, at: Nat, full: Bool, anc: Bool) ->
+ Maybe<&2, Regex.Match>:
+ match re:
+ case Regex{code, n}:
+ +prog = List.append(&2, Regex.Inst, code, Bool.pick(List<&2, Regex.Inst>, full,
+ [IAsr{4n}, ISave{1n}, IMatch{}], [ISave{1n}, IMatch{}]))
+ Regex.found(Regex.run(s, at, prog, Nat.mul(3n, 1n+List.length(&2, Regex.Inst, prog)),
+ [(0n, List.replicate(Maybe<&2, Nat>, Nat.double(1n+n), None{}))], anc, 0n,
+ None{}, ([], None{})))
+
+# The first match at or after `at`, as Pattern.search(s, at)
+def Regex.exec(re: Regex, s: String, at: Nat) -> Maybe<&2, Regex.Match>:
+ Regex.search(re, s, at, False{}, False{})
+
+# A match starting at `at`, as Pattern.match(s, at)
+def Regex.match_at(re: Regex, s: String, at: Nat) -> Maybe<&2, Regex.Match>:
+ Regex.search(re, s, at, False{}, True{})
+
+# A match of all of s, as Pattern.fullmatch(s)
+def Regex.fullmatch(re: Regex, s: String) -> Maybe<&2, Regex.Match>:
+ Regex.search(re, s, 0n, True{}, True{})
+
+def Regex.slice(s: String, g: Maybe<&2, Maybe<&2, Sigma<&2, &2, Nat, _ => Nat>>>) ->
+ Maybe<&2, String>:
+ match g:
+ case Some{Some{(+a, b)}}:
+ Some{String.take(String.drop(s, a), Nat.sub(b, a))}
+ case _:
+ None{}
+
+# Group k of a match on s as text, 0 the whole match; None if it took no
+# part
+def Regex.group(s: String, m: Regex.Match, k: Nat) -> Maybe<&2, String>:
+ match m:
+ case Match{a, b, gs}:
+ Regex.slice(s, List.get(&2, Maybe<&2, Sigma<&2, &2, Nat, _ => Nat>>, Some{(a, b)} <> gs, k))
+
# Array
# -----
diff --git a/tests/regex/captures.bend b/tests/regex/captures.bend
new file mode 100644
index 000000000..275952273
--- /dev/null
+++ b/tests/regex/captures.bend
@@ -0,0 +1,43 @@
+# Captures: a group keeps the last value it took, even once a later
+# iteration skips it; a group that took no part is None, one that matched
+# empty is Some; offsets count code points, so a supplementary char counts
+# once; Regex.group is a span's text: group 0 the match, None past the last
+# group or for one that took no part
+import Base
+
+def go(r: Result<&2, &2, String, Regex>, s: String) -> Maybe<&2, Regex.Match>:
+ match r:
+ case Fail{e}:
+ None{}
+ case Done{re}:
+ Regex.exec(re, s, 0n)
+
+def m(p: String, s: String) -> Maybe<&2, Regex.Match>:
+ go(Regex.compile(p), s)
+
+def txt.go(m: Maybe<&2, Regex.Match>, s: String, k: Nat) -> Maybe<&2, String>:
+ match m:
+ case None{}:
+ None{}
+ case Some{x}:
+ Regex.group(s, x, k)
+
+def txt(r: Result<&2, &2, String, Regex>, +s: String, k: Nat) -> Maybe<&2, String>:
+ match r:
+ case Fail{e}:
+ None{}
+ case Done{re}:
+ txt.go(Regex.exec(re, s, 0n), s, k)
+
+def t(p: String, s: String, k: Nat) -> Maybe<&2, String>:
+ txt(Regex.compile(p), s, k)
+
+def main() -> Sigma<&2, &2, List<&2, Maybe<&2, Regex.Match>>, _ => List<&2, Maybe<&2, String>>>:
+ ([m("(a(b)?)+", "aba"), m("((a)|b)+", "ab"), m("(a)|b", "b"), m("a(b)?c", "ac"),
+ m("a(b*)c", "ac"), m("(a|(b))*", "ab"), m(".", "😀x"), m("x😀y", "ax😀yb"),
+ m("[😀-🙏]", "🙂"), m("(😀)+", "é😀😀")],
+ [t("(a(b)?)+", "xaba", 0n), t("(a(b)?)+", "xaba", 1n), t("(a(b)?)+", "xaba", 2n),
+ t("(a(b)?)+", "xaba", 3n), t("a(b)?c", "ac", 1n), t("a(b*)c", "ac", 1n),
+ t("x(😀+)y", "ax😀😀yb", 1n), t("(\\w+)@(\\w+)", "mail ann@bend now", 2n)])
+
+#|([Some{Match{0n, 3n, [Some{(2n, 3n)}, Some{(1n, 2n)}]}}, Some{Match{0n, 2n, [Some{(1n, 2n)}, Some{(0n, 1n)}]}}, Some{Match{0n, 1n, [None{}]}}, Some{Match{0n, 2n, [None{}]}}, Some{Match{0n, 2n, [Some{(1n, 1n)}]}}, Some{Match{0n, 2n, [Some{(1n, 2n)}, Some{(1n, 2n)}]}}, Some{Match{0n, 1n, []}}, Some{Match{1n, 4n, []}}, Some{Match{0n, 1n, []}}, Some{Match{1n, 3n, [Some{(2n, 3n)}]}}], [Some{"aba"}, Some{"a"}, Some{"b"}, None{}, None{}, Some{""}, Some{"😀😀"}, Some{"bend"}])
diff --git a/tests/regex/exec.bend b/tests/regex/exec.bend
new file mode 100644
index 000000000..10fab0944
--- /dev/null
+++ b/tests/regex/exec.bend
@@ -0,0 +1,31 @@
+# Regex.exec is Pattern.search(s, at), Regex.match_at is match(s, at),
+# Regex.fullmatch is fullmatch(s): leftmost-first alternation, a
+# catastrophic pattern terminates, lazy repeats, $ before a final newline
+# only, ^ only at the real start and \b seeing the char before at, at past
+# the end clamped to it
+import Base
+
+def go(r: Result<&2, &2, String, Regex>, s: String, at: Nat, k: Nat) -> Maybe<&2, Regex.Match>:
+ match r k:
+ case Fail{e}, _:
+ None{}
+ case Done{re}, 0n:
+ Regex.exec(re, s, at)
+ case Done{re}, 1n:
+ Regex.match_at(re, s, at)
+ case Done{re}, 2n+_:
+ Regex.fullmatch(re, s)
+
+def m(p: String, s: String, at: Nat, k: Nat) -> Maybe<&2, Regex.Match>:
+ go(Regex.compile(p), s, at, k)
+
+def main() -> List<&2, Maybe<&2, Regex.Match>>:
+ [m("a|ab", "ab", 0n, 0n), m("^(a+)+$", "aaaaaaaaaaaaaaaaaaaab", 0n, 0n),
+ m("(a|b)*?c", "abc", 0n, 0n), m("a+?", "aaa", 0n, 0n), m("a$", "a\n", 0n, 0n),
+ m("a$", "a\n\n", 0n, 0n), m("a", "ba", 1n, 0n), m("^a", "aa", 1n, 0n),
+ m("\\ba", "ba", 1n, 0n), m("\\Ba", "ba", 1n, 0n), m("", "", 0n, 0n),
+ m("x", "abc", 5n, 0n), m("", "ab", 5n, 0n), m("a", "ba", 0n, 1n),
+ m("a", "ba", 1n, 1n), m("b|$", "ab", 2n, 1n), m("a+", "aa\n", 0n, 2n),
+ m("a*?", "aa", 0n, 2n), m("a|ab", "ab", 0n, 2n), m("a$", "a\n", 0n, 2n)]
+
+#|[Some{Match{0n, 1n, []}}, None{}, Some{Match{0n, 3n, [Some{(1n, 2n)}]}}, Some{Match{0n, 1n, []}}, Some{Match{0n, 1n, []}}, None{}, Some{Match{1n, 2n, []}}, None{}, None{}, Some{Match{1n, 2n, []}}, Some{Match{0n, 0n, []}}, None{}, Some{Match{2n, 2n, []}}, None{}, Some{Match{1n, 2n, []}}, Some{Match{2n, 2n, []}}, None{}, Some{Match{0n, 2n, []}}, Some{Match{0n, 2n, []}}, None{}]
diff --git a/tests/regex/laws.bend b/tests/regex/laws.bend
new file mode 100644
index 000000000..bbd0aa78b
--- /dev/null
+++ b/tests/regex/laws.bend
@@ -0,0 +1,84 @@
+# Laws tying the three entry points together, three per (pattern, subject,
+# at): exec is the first match_at from at on, span and groups alike (search
+# is anchored matching tried at each start in turn); match_at(i) is None or
+# starts at i, for every i; fullmatch is None or spans the whole subject. A
+# pattern that fails to compile is a False, never a skipped row
+import Base
+
+def eq.span(a: Maybe<&2, Sigma<&2, &2, Nat, _ => Nat>>, b: Maybe<&2, Sigma<&2, &2, Nat, _ => Nat>>) -> Bool:
+ match a b:
+ case None{}, None{}:
+ True{}
+ case Some{(i, j)}, Some{(k, l)}:
+ Bool.and(Nat.is_eq(i, k), Nat.is_eq(j, l))
+ case _, _:
+ False{}
+
+def eq.spans(a: List<&2, Maybe<&2, Sigma<&2, &2, Nat, _ => Nat>>>, b: List<&2, Maybe<&2, Sigma<&2, &2, Nat, _ => Nat>>>) -> Bool:
+ match a b:
+ case Nil{}, Nil{}:
+ True{}
+ case x <> xs, y <> ys:
+ Bool.and(eq.span(x, y), eq.spans(xs, ys))
+ case _, _:
+ False{}
+
+def eq(a: Maybe<&2, Regex.Match>, b: Maybe<&2, Regex.Match>) -> Bool:
+ match a b:
+ case None{}, None{}:
+ True{}
+ case Some{Match{i, j, gs}}, Some{Match{k, l, hs}}:
+ eq.spans(Some{(i, j)} <> gs, Some{(k, l)} <> hs)
+ case _, _:
+ False{}
+
+def first(k: Nat, m: Maybe<&2, Regex.Match>, +re: Regex, +s: String, +i: Nat) -> Maybe<&2, Regex.Match>:
+ match k m:
+ case _, Some{x}:
+ Some{x}
+ case 0n, None{}:
+ None{}
+ case 1n+k, None{}:
+ first(k, Regex.match_at(re, s, 1n+i), re, s, 1n+i)
+
+def starts(m: Maybe<&2, Regex.Match>, i: Nat) -> Bool:
+ match m:
+ case None{}:
+ True{}
+ case Some{Match{a, b, gs}}:
+ Nat.is_eq(a, i)
+
+def anchored(ks: List<&2, Nat>, +re: Regex, +s: String) -> Bool:
+ match ks:
+ case Nil{}:
+ True{}
+ case +i <> t:
+ Bool.and(starts(Regex.match_at(re, s, i), i), anchored(t, re, s))
+
+def whole(m: Maybe<&2, Regex.Match>, n: Nat) -> Bool:
+ match m:
+ case None{}:
+ True{}
+ case Some{Match{a, b, gs}}:
+ Bool.and(Nat.is_eq(a, 0n), Nat.is_eq(b, n))
+
+def laws(r: Result<&2, &2, String, Regex>, +s: String, +at: Nat) -> List<&2, Bool>:
+ match r:
+ case Fail{e}:
+ [False{}]
+ case Done{+re}:
+ +n = String.length(s)
+ [eq(Regex.exec(re, s, at), first(Nat.sub(n, at), Regex.match_at(re, s, at), re, s, at)),
+ anchored(List.range(1n+n), re, s), whole(Regex.fullmatch(re, s), n)]
+
+def l(p: String, +s: String, +at: Nat) -> List<&2, Bool>:
+ laws(Regex.compile(p), s, at)
+
+def main() -> List<&2, List<&2, Bool>>:
+ [l("|a", "aba", 0n), l("a*", "baac", 1n), l("a*?", "aa", 0n), l("(a)|b", "cab", 0n),
+ l("\\b", "ab cd", 2n), l("\\Bb", "ab b", 0n), l("^a|b$", "ab\n", 1n), l("x", "abc", 0n),
+ l("", "", 0n), l("(?:a|ab)(c|bcd)?", "xabcdab", 0n), l("(a(b)?)+", "abab", 1n),
+ l("[^\\s]+", "é😀 x", 0n), l("😀*", "a😀😀b", 1n), l("a$", "a\n", 0n),
+ l("abcd|a", "abab", 0n)]
+
+#|[[True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}], [True{}, True{}, True{}]]
diff --git a/tests/regex/oracle.bend b/tests/regex/oracle.bend
new file mode 100644
index 000000000..0a362e39b
--- /dev/null
+++ b/tests/regex/oracle.bend
@@ -0,0 +1,369 @@
+# the CPython oracle: every row is (pattern, subject, at), run as
+# search(s, at) | match(s, at) | fullmatch(s); a match prints its spans,
+# group 0 first and - for a group that took no part, a failure none, a
+# pattern that fails to compile err. The pins are CPython 3.11's `re`
+# under re.ASCII on the same rows (160 rows, seed 7)
+import Base
+
+def Row() -> Data:
+ Sigma<&2, &2, String, _ => Sigma<&2, &2, String, _ => Nat>>
+
+def span(g: Maybe<&2, Sigma<&2, &2, Nat, _ => Nat>>) -> String:
+ match g:
+ case Some{(a, b)}:
+ Nat.show(a) ++ "-" ++ Nat.show(b)
+ case None{}:
+ "-"
+
+def spans.go(gs: List<&2, Maybe<&2, Sigma<&2, &2, Nat, _ => Nat>>>) -> String:
+ match gs:
+ case Nil{}:
+ ""
+ case g <> t:
+ " " ++ span(g) ++ spans.go(t)
+
+def spans(m: Maybe<&2, Regex.Match>) -> String:
+ match m:
+ case Some{Match{a, b, gs}}:
+ span(Some{(a, b)}) ++ spans.go(gs)
+ case None{}:
+ "none"
+
+def row(r: Result<&2, &2, String, Regex>, +s: String, +at: Nat) -> String:
+ match r:
+ case Fail{e}:
+ "err"
+ case Done{+re}:
+ spans(Regex.exec(re, s, at)) ++ " | " ++ spans(Regex.match_at(re, s, at))
+ ++ " | " ++ spans(Regex.fullmatch(re, s))
+
+def rows(xs: List<&2, Row()>) -> String:
+ match xs:
+ case Nil{}:
+ ""
+ case (p, (s, at)) <> t:
+ row(Regex.compile(p), s, at) ++ "\n" ++ rows(t)
+
+def main() -> IO(Unit):
+ IO.print(rows([
+ ("\\[$(?:([-A-C{{]b$)()??2|[_]+?\\W?\\w??)", ("é", 0n)),
+ ("\\B(\\S,)?b", ("1bA", 3n)),
+ ("|\\W", ("-é", 2n)),
+ ("(())|\\b😀+?\\-+?", (" b\n a", 0n)),
+ ("(||)|\\*??_", ("\n", 1n)),
+ ("(?:\\S(?:[^-(a-c ]+\\b|c*?|[]\\d ]*?)??)", ("bA\n", 0n)),
+ ("|a?c(?:)??|.+?()?é*", ("éé", 0n)),
+ ("[\\q]", ("\t", 0n)),
+ ("(\\W$)*?", ("-\tbéb\t\ta", 0n)),
+ ("(?:||é?\\*)", ("aa,2\t\tcB", 7n)),
+ ("c??.", ("aA-ac", 0n)),
+ ("(?:(?:A.[--/\\w])\\s())B", ("\t😀\n2aBab", 0n)),
+ ("", ("aAaB", 0n)),
+ ("[a-\\w]", ("Bba\tAb", 0n)),
+ ("$+", ("1\tc1", 0n)),
+ ("(\\w?)??\\B", ("1cA2cb _", 0n)),
+ ("[\\q]", ("-baa", 5n)),
+ ("\\$", ("aB_2é2A", 0n)),
+ ("", (" a\nB\t", 0n)),
+ ("", ("ab 😀", 0n)),
+ ("a)", ("a😀\t", 1n)),
+ ("[b((]|", ("aaa", 0n)),
+ ("^é\\d+|", ("a\nBaBAcA", 7n)),
+ ("a+?([],Ba-c]+?|[^b-b\\w)][+]?$|😀(\\D)+?)(?:[0-9A]._*)", ("", 0n)),
+ ("\\w*? +^", ("", 1n)),
+ ("\\W\\s", ("B A1", 3n)),
+ ("", ("\t\né", 1n)),
+ ("(?:())(..(?:[]2A-C\\b]?[??-]+)??|)]|", ("", 0n)),
+ ("😀+?}", (" BA", 0n)),
+ ("a)", ("B2A\n\tAé", 0n)),
+ ("", ("", 0n)),
+ ("|(?:( 😀),|(?:éb?[^--/a-c1\\S]|[^]b\\né]|.+_*)(.??|[b-bb-b]?[\\n-\\r\\W\\\\]))+?\\\\", (", B-\nb\n", 0n)),
+ ("2", ("\t", 0n)),
+ ("(?:\\b(|)([--/\\\\]?B.??|[^-0-9][a-cbb-b][^-\\n-\\r])+?)\\b(([^2é\\S]\\D)^)", ("-11-", 0n)),
+ (".* ((\\b\\w[$]??)^(?:[?-]| ??))", ("b _bbAca", 0n)),
+ ("", (",é1-1😀", 3n)),
+ ("\\wAb*?", ("a", 0n)),
+ ("\\S}+(?:a+cb+?|([b-b\\n-\\r?].)*|B\\b)", ("😀bBé _B ", 0n)),
+ ("(|2+?(😀[^-a-ca-c])|(?:[^--/_]+?B?_|é\\[[a-c]|[^|}0-9]*?\\\\]))B*😀*", ("a\ta", 0n)),
+ (" ", (" é\n", 0n)),
+ ("\\b|.", ("ba", 0n)),
+ ("aa?", ("céa b", 0n)),
+ ("", ("1", 0n)),
+ ("[0-90-9]1?", ("a-\tb_\n", 0n)),
+ ("([b-b])b", ("A\nb", 0n)),
+ ("|[c\\b]$|,^[+é]", ("b2,", 0n)),
+ ("2*|(?:bb*)|(([^--/*(]\\**?-+?|,)*?(?:[-\\d*]|é?[-A-CA+]))*?", ("b \t", 0n)),
+ (".(^(,|\\|*?)?|-[--/?\\Db-b])??", ("A12\n\n", 0n)),
+ ("a", ("b\t_😀a", 0n)),
+ ("[_]b+?", ("é", 0n)),
+ ("", ("bébB-Ab", 7n)),
+ ("A^(?:1*.+?())?|\\w2😀", ("1-2", 0n)),
+ ("[\\](]*(?:^)", (" é😀a 1ba", 0n)),
+ ("|ab(aB??.)?", ("😀\tab-😀 ", 0n)),
+ ("[a", ("\n-", 0n)),
+ ("a?", ("c", 0n)),
+ ("\\S", ("", 0n)),
+ (",*\\.", ("", 0n)),
+ ("a|\\wb*?\\D+|[--/|]+?1", (" ", 0n)),
+ ("", ("BB-b😀", 0n)),
+ ("[]?\\w?]??", ("a ,c", 0n)),
+ ("|😀+[2a-c0-9]??", ("a\n_", 0n)),
+ ("[^-\\]-]+", ("😀😀cBB-b", 0n)),
+ ("(?:()??\\d+?.)^", (" b,b_AAb", 0n)),
+ ("", ("", 1n)),
+ ("(*a)", ("ac1😀😀", 0n)),
+ ("..", ("a,", 0n)),
+ ("", ("b,b a", 0n)),
+ ("_(b)", ("baB", 0n)),
+ ("\\s\\w?", ("é\n1", 0n)),
+ ("A[^0-9]*\\D", ("1b_-a-cB", 0n)),
+ ("a+", ("Bbcbc😀1", 0n)),
+ ("[\\d-z]", ("a\t_21\n", 0n)),
+ ("b|()?(a(?:\\W??[A-C]?[- \\n-\\r+]|))", ("Bbbb bé😀", 0n)),
+ ("a*?😀\\?|2+|", ("\tb_A-", 0n)),
+ ("(\\d|,?(a*[\\d]||[]B\\]0-9]+?\\B)_)*?|(?:(?:_2[a-c]||bbc*))??[-B\\w\\n-\\r]??|b\\D[\\]|-]*?", ("1bé1b-😀", 0n)),
+ ("(a|b", ("A2c-a", 0n)),
+ ("-\\S|[]+cA]$,+", ("\tab_2", 0n)),
+ ("(-+[A-C\\d}]?)||1()", ("😀Bé\tb😀c-", 0n)),
+ ("||", ("b", 2n)),
+ ("\\.+[0-90-9{](|([1-])a.??)|()(^[]\\ba-c]([^a-c][^B+_]))(,|\\Bb?)|([1_]|c[b-b]|_+?)?A*?[^]0-9\\n-\\rb-]", ("b aé", 0n)),
+ ("-+[0-9]??\\?", ("a- AB", 0n)),
+ (",😀??\\w|(?:$)[0-9]??", ("a\t,", 0n)),
+ ("$", ("bé-bb", 4n)),
+ ("()|", ("1cb1", 0n)),
+ ("\\b()??(?:(\\W[-|][]a]|[^}\\s.]*)\\B)??|\\w+^((?:|))", ("", 0n)),
+ ("\\.??1c", ("2\t,-éb a", 0n)),
+ ("b[--/A-Cb-bB]*\\B", ("BbAbcé", 0n)),
+ ("", ("\tB\n", 1n)),
+ ("||b+\\d+?\\D*?", ("bc😀\nab_a", 0n)),
+ ("a+?*", ("", 0n)),
+ ("\\tba?", ("_\t\t", 2n)),
+ ("a\\", ("b_é2b", 0n)),
+ ("(?:([\\n-\\r$]*\\?*?\\d?)??,a|[b]|.([\\n-\\r].😀|\\[?|b)?2+)2[^?_]", (" , 😀1b😀", 0n)),
+ ("2(?:[^--/.*])", ("éabB", 0n)),
+ ("(c()??|()?|[b-b\\b1] )22|", ("cb", 0n)),
+ ("$2|a*,", ("\t\tBbB", 0n)),
+ ("((a)", ("A ", 0n)),
+ ("", ("\tcébB_bb", 0n)),
+ ("_*?\\s?(?:)??", ("", 0n)),
+ ("(_1*?)|\\s??", ("1a2a", 0n)),
+ ("(a*?)??b?(\\s)?|[^--/éb2](|)?é+|(?:b)?[--/_-][]a-c]", ("a,é1é", 2n)),
+ ("..b+?", ("", 0n)),
+ ("a?|", ("é2,é2,", 0n)),
+ ("\\B(?:😀a+|B*?)|$(?:$b|(.*?[$])*.+?(b| )|(?:2[b-b2]*)*?[*\\\\.]??)??(?:\\)\\B)?", ("😀", 2n)),
+ ("a\\W? |\\B\\t+", ("", 0n)),
+ ("$+", ("ac2\naé-", 2n)),
+ ("[1 c].+?", ("", 0n)),
+ ("c(?:ba?\\B)?[-A-C{]|(|)??", ("aé", 1n)),
+ ("$", ("c\t", 0n)),
+ ("*a", ("", 0n)),
+ ("a??(2+?($[$é\\b])*?|}😀)a", ("1😀éa", 0n)),
+ ("(||(?:[\\-0-9]+?^)([^]A][1\\b]*?[^A-Cb-b]|^))(A?)", ("Aab", 0n)),
+ ("\\n$(|()?|[]1b-b-]\\w)|a*?", ("", 0n)),
+ ("|\\??|\\Bb", ("a,😀b", 3n)),
+ ("bA|\\B^.+?|a-?", ("\nab\n", 3n)),
+ ("[]b-b]|(bb[\\D]*?|\\W+(?:^\\t_+)+?)*\\]", ("1_1 ", 0n)),
+ ("[-a-c](?:([^\\n-\\r(]A\\B)(?:Ba*a|1*)[0-9$]|[]}]\\+*?-*|[^,A]_(?:)??)*(?:)|.[-B]*", ("c2", 0n)),
+ ("a**", ("😀- ", 0n)),
+ ("(?:)??", ("b Ba", 0n)),
+ ("(|(?:||[B\\]][^A\\Wé])a|(?:.b|[)]+[-}\\S][a\\D2]*|)?)($\\b|[].]([]\\wé]+)|[A-C(\\n])", (" aaa,ac😀", 0n)),
+ ("\\S?(?:\\t*?\\W+?a)??.+?", ("\n\nb", 0n)),
+ ("a", ("aAb", 0n)),
+ ("a+?*", ("AAaé😀-", 0n)),
+ ("\\n😀\\D?", ("a", 0n)),
+ ("||\\S\\\\", ("cbc-baé", 0n)),
+ ("b", ("111b", 0n)),
+ ("", ("a", 0n)),
+ ("c+?\\.+?b*?|", ("1,", 0n)),
+ ("(?:|\\S*?)??[\\n-\\r]", ("aA2aé", 0n)),
+ ("(a|b", ("2_aéb,", 0n)),
+ ("😀+?||(.b+?)", ("BaacB", 0n)),
+ ("|[^)-]😀*b|.*?,\\\\?", ("_2B", 0n)),
+ ("", ("😀1", 0n)),
+ ("[z-a]", ("a", 0n)),
+ ("()??|$([^_1b-b].)|", ("", 0n)),
+ ("(([^\\s][-\\Sc] |$\\b[^]\\-]+?)b*?\\w|)?$", ("b_b 2a_", 0n)),
+ ("[^]", (",", 0n)),
+ ("[c\\b-]", ("_\t\nccc\n", 0n)),
+ ("()\\???|", ("B,", 3n)),
+ ("", ("-bb\t", 0n)),
+ ("()", ("B😀AbB-", 0n)),
+ ("", ("a1bb", 0n)),
+ ("", ("A\n\t😀_\té", 3n)),
+ ("(a", ("bBA-_\t", 0n)),
+ ("()b[c]", ("__ ", 0n)),
+ ("\\t+", ("a\n😀,2😀", 0n)),
+ ("(|.)$|c", ("Bba,", 0n)),
+ ("a??()A", ("a", 0n)),
+ ("", ("_21-\nB2a", 0n)),
+ ("", ("", 0n)),
+ ("\\wa+?", (" 1aA1a", 0n)),
+ ("(*a)", (",,A", 0n)),
+ ("((😀|)?)c??B|", (",21", 0n)),
+ ("", ("a1a2\t", 0n)),
+ ("(?:)??1?-", ("\tA,2", 3n)),
+ ("2?}b", ("", 0n)),
+ ("\\w()?^", ("bé-bA", 1n)),
+ ("[\\]-]+?b*|.éA", ("\nb22b2c-", 0n)),
+ ("(?:)??(?:_[--/b-b]+?)+?|B|.??()??", ("éa", 0n))]))
+
+#|none | none | none
+#|none | none | none
+#|2-2 | 2-2 | none
+#|0-0 0-0 0-0 | 0-0 0-0 0-0 | none
+#|1-1 1-1 | 1-1 1-1 | none
+#|0-1 | 0-1 | none
+#|0-0 - | 0-0 - | 0-2 1-1
+#|err
+#|0-0 - | 0-0 - | none
+#|7-7 | 7-7 | none
+#|0-1 | 0-1 | none
+#|none | none | none
+#|0-0 | 0-0 | none
+#|err
+#|err
+#|0-1 0-1 | 0-1 0-1 | none
+#|err
+#|none | none | none
+#|0-0 | 0-0 | none
+#|0-0 | 0-0 | none
+#|err
+#|0-0 | 0-0 | none
+#|7-7 | 7-7 | none
+#|none | none | none
+#|none | none | none
+#|none | none | none
+#|1-1 | 1-1 | none
+#|0-0 - - | 0-0 - - | 0-0 - -
+#|none | none | none
+#|err
+#|0-0 | 0-0 | 0-0
+#|0-0 - - | 0-0 - - | none
+#|none | none | none
+#|none | none | none
+#|none | none | none
+#|3-3 | 3-3 | none
+#|none | none | none
+#|none | none | none
+#|0-0 0-0 - | 0-0 0-0 - | none
+#|0-1 | 0-1 | none
+#|0-0 | 0-0 | none
+#|2-3 | none | none
+#|0-0 | 0-0 | none
+#|none | none | none
+#|none | none | none
+#|0-0 | 0-0 | none
+#|0-0 - - | 0-0 - - | none
+#|0-1 - - | 0-1 - - | none
+#|4-5 | none | none
+#|none | none | none
+#|7-7 | 7-7 | none
+#|none | none | none
+#|0-0 | 0-0 | none
+#|0-0 - | 0-0 - | none
+#|err
+#|0-0 | 0-0 | none
+#|none | none | none
+#|none | none | none
+#|none | none | none
+#|0-0 | 0-0 | none
+#|0-0 | 0-0 | none
+#|0-0 | 0-0 | none
+#|0-5 | 0-5 | none
+#|none | none | none
+#|0-0 | 0-0 | 0-0
+#|err
+#|0-2 | 0-2 | 0-2
+#|0-0 | 0-0 | none
+#|none | none | none
+#|1-3 | none | none
+#|none | none | none
+#|none | none | none
+#|err
+#|1-2 - - | none | none
+#|0-0 | 0-0 | none
+#|0-0 - - | 0-0 - - | none
+#|err
+#|none | none | none
+#|0-0 - - | 0-0 - - | none
+#|1-1 | 1-1 | none
+#|1-2 - - - - - - - | none | none
+#|none | none | none
+#|3-3 | none | none
+#|5-5 | none | none
+#|0-0 0-0 | 0-0 0-0 | none
+#|none | none | none
+#|none | none | none
+#|1-4 | none | none
+#|1-1 | 1-1 | none
+#|0-0 | 0-0 | none
+#|err
+#|none | none | none
+#|err
+#|none | none | none
+#|none | none | none
+#|0-0 - - - | 0-0 - - - | none
+#|none | none | none
+#|err
+#|0-0 | 0-0 | none
+#|0-0 | 0-0 | 0-0
+#|0-0 - | 0-0 - | none
+#|2-2 - - - | 2-2 - - - | none
+#|none | none | none
+#|0-0 | 0-0 | none
+#|1-1 - - | 1-1 - - | none
+#|none | none | none
+#|err
+#|none | none | none
+#|1-1 - | 1-1 - | none
+#|2-2 | none | none
+#|err
+#|none | none | none
+#|0-1 0-0 - 0-1 | 0-1 0-0 - 0-1 | none
+#|0-0 - - | 0-0 - - | 0-0 - -
+#|3-3 | 3-3 | none
+#|none | none | none
+#|none | none | none
+#|0-1 - | 0-1 - | none
+#|err
+#|0-0 | 0-0 | none
+#|none | none | none
+#|2-3 | none | none
+#|0-1 | 0-1 | none
+#|err
+#|none | none | none
+#|0-0 | 0-0 | none
+#|3-4 | none | none
+#|0-0 | 0-0 | none
+#|0-0 | 0-0 | none
+#|none | none | none
+#|err
+#|0-0 - | 0-0 - | none
+#|0-0 | 0-0 | none
+#|0-0 | 0-0 | none
+#|err
+#|0-0 - - | 0-0 - - | 0-0 - -
+#|7-7 7-7 - | none | none
+#|err
+#|3-4 | none | none
+#|2-2 2-2 | 2-2 2-2 | none
+#|0-0 | 0-0 | none
+#|0-0 0-0 | 0-0 0-0 | none
+#|0-0 | 0-0 | none
+#|3-3 | 3-3 | none
+#|err
+#|none | none | none
+#|none | none | none
+#|3-4 3-4 | none | none
+#|none | none | none
+#|0-0 | 0-0 | none
+#|0-0 | 0-0 | 0-0
+#|1-3 | none | none
+#|err
+#|0-0 - - | 0-0 - - | none
+#|0-0 | 0-0 | none
+#|none | none | none
+#|none | none | none
+#|none | none | none
+#|7-8 | none | none
+#|0-0 - | 0-0 - | none
diff --git a/tests/regex/parse.bend b/tests/regex/parse.bend
new file mode 100644
index 000000000..37e413a82
--- /dev/null
+++ b/tests/regex/parse.bend
@@ -0,0 +1,20 @@
+# Regex.compile: a pattern becomes Pike VM code after ISave{0n} (a run
+# appends the ISave{1n} and IMatch) with its group count; groups number by
+# their open paren, a lazy repeat's split tries the exit first. A bad
+# pattern fails with CPython's message less the offending text, and the
+# slice's cuts, which CPython accepts, fail too: counted repeats (and any
+# `{` outside a set, a literal to CPython when no count follows), group
+# extensions, \x, \A, possessive repeats, a repeat of a body that can
+# match empty
+import Base
+
+def main() -> List<&2, Result<&2, &2, String, Regex>>:
+ [Regex.compile(""), Regex.compile("a|b"), Regex.compile("a*?"),
+ Regex.compile("(a)(?:b)"), Regex.compile("[^a-c\\d]"), Regex.compile("^\\b.$"),
+ Regex.compile("a**"), Regex.compile("*a"), Regex.compile("^*"), Regex.compile("(a"),
+ Regex.compile("a)"), Regex.compile("[a"), Regex.compile("[z-a]"), Regex.compile("\\q"),
+ Regex.compile("a\\"), Regex.compile("a{2}"), Regex.compile("{"), Regex.compile("(?i)a"),
+ Regex.compile("(?Pa)"), Regex.compile("\\x41"), Regex.compile("\\A"),
+ Regex.compile("a*+"), Regex.compile("(a*)*"), Regex.compile("(?:)+")]
+
+#|[Done{Regex{[ISave{0n}], 0n}}, Done{Regex{[ISave{0n}, ISplit{2n, 4n}, ISet{False{}, [(97, 97)]}, IJmp{5n}, ISet{False{}, [(98, 98)]}], 0n}}, Done{Regex{[ISave{0n}, ISplit{4n, 2n}, ISet{False{}, [(97, 97)]}, IJmp{1n}], 0n}}, Done{Regex{[ISave{0n}, ISave{2n}, ISet{False{}, [(97, 97)]}, ISave{3n}, ISet{False{}, [(98, 98)]}], 1n}}, Done{Regex{[ISave{0n}, ISet{True{}, [(48, 57), (97, 99)]}], 0n}}, Done{Regex{[ISave{0n}, IAsr{0n}, IAsr{2n}, ISet{True{}, [(10, 10)]}, IAsr{1n}], 0n}}, Fail{"multiple repeat"}, Fail{"nothing to repeat"}, Fail{"nothing to repeat"}, Fail{"missing ), unterminated subpattern"}, Fail{"unbalanced parenthesis"}, Fail{"unterminated character set"}, Fail{"bad character range or escape"}, Fail{"bad escape"}, Fail{"bad escape (end of pattern)"}, Fail{"counted repeats are not supported"}, Fail{"counted repeats are not supported"}, Fail{"unsupported group extension"}, Fail{"unsupported group extension"}, Fail{"bad escape"}, Fail{"bad escape"}, Fail{"multiple repeat"}, Fail{"repeat of a pattern that can match empty"}, Fail{"repeat of a pattern that can match empty"}]
diff --git a/tests/regex/smoke.bend b/tests/regex/smoke.bend
new file mode 100644
index 000000000..b495447cc
--- /dev/null
+++ b/tests/regex/smoke.bend
@@ -0,0 +1,29 @@
+# Independent subjects cross one ! transfer: each is its own Regex.exec
+# and Regex.match_at, the pairs fork in parallel, and the results keep the
+# list's order
+import Base
+
+def one(+re: Regex, +s: String) -> List<&2, Maybe<&2, Regex.Match>>:
+ [Regex.exec(re, s, 0n), Regex.match_at(re, s, 1n)]
+
+def each(+re: Regex, xs: List<&2, String>) -> List<&2, List<&2, Maybe<&2, Regex.Match>>>:
+ match xs:
+ case Nil{}:
+ []
+ case s <> tail:
+ h t = one(re, s) each(re, tail)
+ h <> t
+
+def all(r: Result<&2, &2, String, Regex>, xs: List<&2, String>) -> List<&2, List<&2, Maybe<&2, Regex.Match>>>:
+ match r:
+ case Fail{e}:
+ []
+ case Done{re}:
+ each(re, xs)
+
+def main() -> List<&2, List<&2, Maybe<&2, Regex.Match>>>:
+ all!(Regex.compile("(\\w+)@(\\w+)\\.(?:com|org)|^$"),
+ ["ann@bend.org", "no match here", "", "é😀 , c@d.com", "xa@b.org\n", "a@b.net",
+ String.append(String.repeat("pad ", 40n), "z@y.com")])
+
+#|[[Some{Match{0n, 12n, [Some{(0n, 3n)}, Some{(4n, 8n)}]}}, Some{Match{1n, 12n, [Some{(1n, 3n)}, Some{(4n, 8n)}]}}], [None{}, None{}], [Some{Match{0n, 0n, [None{}, None{}]}}, Some{Match{0n, 0n, [None{}, None{}]}}], [Some{Match{4n, 15n, [Some{(4n, 7n)}, Some{(8n, 11n)}]}}, None{}], [Some{Match{0n, 8n, [Some{(0n, 2n)}, Some{(3n, 4n)}]}}, Some{Match{1n, 8n, [Some{(1n, 2n)}, Some{(3n, 4n)}]}}], [None{}, None{}], [Some{Match{160n, 167n, [Some{(160n, 161n)}, Some{(162n, 163n)}]}}, None{}]]