* Fix four bugs surfaced by the commonmark-app example
- regex: bounded quantifiers {n,m} no longer expand exponentially. The
desugaring inlined the continuation per optional level, doubling it each
time, so {0,61} built a PEG the compiler expanded to ~2^61 nodes and hung.
Each level is now its own grammar rule referenced by name (jolt-3xur).
- strings are a seqable of chars for vec/set/into, matching seq. They went
through realize-for-iteration, which had no string case, so into/set got
raw bytes (code points) and set threw; vec used string/from-bytes (1-char
strings). realize-for-iteration now maps make-char over the bytes, and
core-vec matches (jolt-dl4s).
- clojure.string/split takes Java Pattern.split limit semantics: negative
keeps trailing empties, 0/omitted drops them (a no-match result stays
[input]), positive caps the part count with the remainder unsplit. It used
to delegate the limit to take, so a negative limit returned [] and the
2-arg form never trimmed trailing empties (jolt-ik3a).
- System/exit is registered (maps to os/exit), so (System/exit n) works
(jolt-w2wf).
* regex: single-digit backreferences \1..\9 (jolt-xtss)
\1..\9 now match the text captured by the corresponding group, so
patterns like ([-*_])\1\1 or (\w+) \1 work. The parser records which
groups are referenced; a referenced group additionally captures its text
under a tag and the backref compiles to a PEG (backmatch). Only referenced
groups change — they match possessively (the CPS-over-possessive-PEG engine
can't backtrack into a tagged capture), so backtracking back into a
backreferenced group isn't supported (rare). Unreferenced groups keep full
backtracking and position-based result capture unchanged.
---------
Co-authored-by: Yogthos <yogthos@gmail.com>
83 lines
5.2 KiB
Text
83 lines
5.2 KiB
Text
# Specification: regular expressions — #"…" literals and the re-* fns.
|
|
# (Whole area previously unspecced; some cases adapted from jank reader-macro/regex.)
|
|
(use ../support/harness)
|
|
|
|
(defspec "regex / literals & predicate"
|
|
["regex? literal" "true" "(regex? #\"\\d+\")"]
|
|
["regex? non-regex" "false" "(regex? \"\\d+\")"]
|
|
["escaped digits" "\"42\"" "(re-find #\"\\d+\" \"x42y\")"]
|
|
["escaped ws/non-ws" "\"x a\"" "(re-find #\"\\S\\s\\S\" \"x a b y\")"])
|
|
|
|
(defspec "regex / re-find"
|
|
["match" "\"123\"" "(re-find #\"\\d+\" \"abc123def\")"]
|
|
["no match nil" "nil" "(re-find #\"\\d+\" \"abc\")"]
|
|
["with groups" "[\"a1\" \"a\" \"1\"]" "(re-find #\"([a-z])(\\d)\" \"--a1--\")"]
|
|
["first match only" "\"1\"" "(re-find #\"\\d\" \"1 2 3\")"])
|
|
|
|
(defspec "regex / re-matches"
|
|
["full match" "\"123\"" "(re-matches #\"\\d+\" \"123\")"]
|
|
["partial = nil" "nil" "(re-matches #\"\\d+\" \"123abc\")"]
|
|
["groups" "[\"12\" \"1\" \"2\"]" "(re-matches #\"(\\d)(\\d)\" \"12\")"]
|
|
["no match nil" "nil" "(re-matches #\"x+\" \"yyy\")"])
|
|
|
|
(defspec "regex / re-seq"
|
|
["all matches" "(quote (\"1\" \"22\" \"333\"))" "(re-seq #\"\\d+\" \"a1b22c333\")"]
|
|
["empty when none" "nil" "(seq (re-seq #\"z\" \"abc\"))"]
|
|
["words" "(quote (\"foo\" \"bar\"))" "(re-seq #\"\\w+\" \"foo bar\")"])
|
|
|
|
(defspec "regex / re-pattern & string ops"
|
|
["re-pattern build" "\"hi\"" "(re-find (re-pattern \"\\\\w+\") \"hi!\")"]
|
|
["re-pattern is regex?" "true" "(regex? (re-pattern \"a\"))"]
|
|
["split on regex" "[\"a\" \"b\" \"c\"]" "(do (require '[clojure.string :as s]) (s/split \"a1b2c\" #\"\\d\"))"]
|
|
["replace regex" "\"X-X\"" "(do (require '[clojure.string :as s]) (s/replace \"a-b\" #\"[a-z]\" \"X\"))"]
|
|
["replace $1" "\"[a][b]\"" "(do (require '[clojure.string :as s]) (s/replace \"ab\" #\"([a-z])\" \"[$1]\"))"])
|
|
|
|
# Unicode property classes (jolt-xlp), byte-PEG approximation: ASCII exact,
|
|
# any high byte (inside a UTF-8 sequence) counts as a LETTER for \p{L}.
|
|
# Acceptance target was cuerdas (kebab/snake/capital now pass conformance).
|
|
(defspec "regex / \\p property classes"
|
|
["p{L} ascii" "\"hello\"" `(re-matches #"^\p{L}+$" "hello")`]
|
|
["p{L} utf-8" "true" `(boolean (re-matches #"^\p{L}+$" "héllo"))`]
|
|
["p{L} rejects digits" "false" `(boolean (re-matches #"^\p{L}+$" "ab1"))`]
|
|
["p{N}" "(quote (\"12\" \"345\"))" `(re-seq #"\p{N}+" "a12b345")`]
|
|
["P{N} negation" "\"abc\"" `(re-matches #"^\P{N}+$" "abc")`]
|
|
["inside class" "\"a-1_b\"" `(re-matches #"^[\p{N}\p{L}_-]+$" "a-1_b")`]
|
|
["p{Lu}/p{Ll}" "\"aB\"" `(re-matches #"^\p{Ll}\p{Lu}$" "aB")`]
|
|
["p{Z} space" "\" \"" `(re-matches #"(?u)^[\s\p{Z}]+$" " ")`]
|
|
["p{Ps}/p{Pe}" "\"(x)\"" `(re-matches #"^\p{Ps}x\p{Pe}$" "(x)")`]
|
|
["(?u) accepted" "\"hi\"" `(re-matches #"(?u)^hi$" "hi")`]
|
|
["unknown class throws" :throws `(re-pattern "\p{Greek}")`])
|
|
|
|
# java.util.regex.Pattern statics (jolt-47b): compile returns jolt's native
|
|
# regex value (so .split / str-ops accept it), MULTILINE maps to (?m), quote
|
|
# escapes metachars. Plus the regex String methods migratus uses.
|
|
(defspec "regex / Pattern statics & String regex methods"
|
|
["Pattern/compile is a regex" "true" "(regex? (Pattern/compile \"a.c\"))"]
|
|
["compiled .split" "[\"a\" \"b\" \"c\"]" "(.split (Pattern/compile \",\") \"a,b,c\")"]
|
|
["str/replace w/ Pattern" "\"ab\"" "(do (require '[clojure.string :as s]) (s/replace \"a1b2\" (Pattern/compile \"[0-9]\") \"\"))"]
|
|
["Pattern/MULTILINE ^" "true" "(boolean (re-find (Pattern/compile \"^x\" Pattern/MULTILINE) \"y\\nx\"))"]
|
|
["Pattern/quote literal" "true" "(boolean (re-find (re-pattern (Pattern/quote \"a.c\")) \"za.cy\"))"]
|
|
["Pattern/quote not meta" "false" "(boolean (re-find (re-pattern (Pattern/quote \"a.c\")) \"zabcy\"))"]
|
|
[".matches whole string" "true" "(.matches \"abc\" \"a.c\")"]
|
|
[".matches partial -> false" "false" "(.matches \"abcd\" \"a.c\")"]
|
|
[".replaceAll regex" "\"a-b-c\"" "(.replaceAll \"a_b_c\" \"_\" \"-\")"]
|
|
[".replaceFirst regex" "\"a-b_c\"" "(.replaceFirst \"a_b_c\" \"_\" \"-\")"])
|
|
|
|
(defspec "regex / bounded quantifiers (jolt-3xur)"
|
|
["exact {n}" "\"aaa\"" "(re-matches #\"a{3}\" \"aaa\")"]
|
|
["range {n,m} max" "\"aaaa\"" "(re-find #\"a{2,4}\" \"aaaaa\")"]
|
|
["zero lower {0,n}" "\"aa\"" "(re-find #\"a{0,3}\" \"aa\")"]
|
|
["{n,} unbounded" "\"aaaa\"" "(re-find #\"a{2,}\" \"aaaa\")"]
|
|
# nested bounded quantifiers must not blow up the PEG compiler — this used to
|
|
# expand to ~2^61 nodes and hang at compile time.
|
|
["nested bounds compile" "true"
|
|
"(boolean (re-matches #\"[a-z](?:[a-z]{0,61}[a-z])?(?:-[a-z]{0,61}[a-z])*\" \"abc-def\"))"])
|
|
|
|
(defspec "regex / backreferences (jolt-xtss)"
|
|
["repeated char" "true" "(boolean (re-find #\"(.)\\1\" \"abba\"))"]
|
|
["no repeat = nil" "nil" "(re-find #\"(.)\\1\" \"abc\")"]
|
|
["thematic-break run" "true" "(boolean (re-matches #\"([-*_])\\1\\1\" \"---\"))"]
|
|
["mismatched run" "nil" "(re-matches #\"([-*_])\\1\\1\" \"-*_\")"]
|
|
["repeated word" "[\"the the\" \"the\"]" "(re-find #\"(\\w+) \\1\" \"say the the word\")"]
|
|
["group still captures" "[\"x=x\" \"x\"]" "(re-matches #\"(\\w+)=\\1\" \"x=x\")"]
|
|
["html tag pair" "true" "(boolean (re-matches #\"<(\\w+)>.*</\\1>\" \"<b>hi</b>\"))"])
|