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{}]]