BOUNDARY: write a regex as data, get a Pattern the existing subsystem already knows how to run.
(api/->expr :java [:cat :start [:+ :digit] :end]) ;; => {:ok "^\d+$"}
(api/register! #:pattern{:id :audit/probe} [:cat "ready" [:? "?"]]) (pattern/match? :audit/probe "ready?")
A Construct projects INTO the existing Pattern value object rather than
replacing it: :pattern/expr is filled from the construct, :pattern/construct
records what it was built from, and everything downstream — the registry's
validation, IPatternEngine, match?, scan — is untouched. A pattern
written as a literal string keeps working exactly as before.
register! goes through pattern.registry/register-pattern! so a construct
that projects to an invalid Pattern is refused at the same gate as every
other pattern.
BOUNDARY: write a regex as data, get a Pattern the existing subsystem
already knows how to run.
(api/->expr :java [:cat :start [:+ :digit] :end])
;; => {:ok "^\\d+$"}
(api/register! #:pattern{:id :audit/probe} [:cat "ready" [:? "?"]])
(pattern/match? :audit/probe "ready?")
A Construct projects INTO the existing `Pattern` value object rather than
replacing it: `:pattern/expr` is filled from the construct, `:pattern/construct`
records what it was built from, and everything downstream — the registry's
validation, `IPatternEngine`, `match?`, `scan` — is untouched. A pattern
written as a literal string keeps working exactly as before.
`register!` goes through `pattern.registry/register-pattern!` so a construct
that projects to an invalid Pattern is refused at the same gate as every
other pattern.PROMOTE layer: lift authored regal syntax into a validated Construct, project it back, and say what a Construct requires of its target.
Everything here is pure and total — a malformed form yields a Result describing why, never an exception and never a silent repair.
(normalize [:cat "a" [:alt "b" "c"]]) => {:ok {…}} (->regal normalized) => [:cat "a" [:alt "b" "c"]] (required-capabilities normalized) => #{}
required-capabilities is what makes dialect support a set-difference
decidable BEFORE emission rather than a try/catch around a compile.
PROMOTE layer: lift authored regal syntax into a validated Construct, project
it back, and say what a Construct requires of its target.
Everything here is pure and total — a malformed form yields a Result
describing why, never an exception and never a silent repair.
(normalize [:cat "a" [:alt "b" "c"]]) => {:ok {…}}
(->regal normalized) => [:cat "a" [:alt "b" "c"]]
(required-capabilities normalized) => #{}
`required-capabilities` is what makes dialect support a set-difference
decidable BEFORE emission rather than a `try`/`catch` around a compile.The DIP swap point: a regex target language as data, plus the one gate every emitter passes through.
For a Construct a dialect cannot express, emit returns
(r/err :construct/unsupported …). It NEVER emits an approximation and never
drops a node. A :lookahead quietly discarded for RE2 produces a DIFFERENT
LANGUAGE that still compiles and still returns hits — the worst outcome
available, because nothing reports it.
emit is a plain function that checks capabilities and THEN calls
-render. An implementation supplies rendering only; it cannot skip,
weaken or forget the check, because it never sees it. One definition, N
implementations.
The dialect profile is an ARGUMENT throughout — resolved from the registry at call time, never captured at wiring time.
The DIP swap point: a regex target language as data, plus the one gate every emitter passes through. ## The substitutability contract For a Construct a dialect cannot express, `emit` returns `(r/err :construct/unsupported …)`. It NEVER emits an approximation and never drops a node. A `:lookahead` quietly discarded for RE2 produces a DIFFERENT LANGUAGE that still compiles and still returns hits — the worst outcome available, because nothing reports it. ## Why the gate is here and not in the emitter `emit` is a plain function that checks capabilities and THEN calls `-render`. An implementation supplies rendering only; it cannot skip, weaken or forget the check, because it never sees it. One definition, N implementations. The dialect profile is an ARGUMENT throughout — resolved from the registry at call time, never captured at wiring time.
POSIX ERE — the dialect grep -E speaks, rendered without regal.
regal has no ERE flavor, and its :java/:ecma output is not ERE: a nested
alternation comes out as (?:a|b), which POSIX ERE has no syntax for. So
this is the one dialect that renders from the Construct directly. It is
small because ERE is small — literals, classes, alternation, greedy
quantifiers, anchors, and one grouping syntax.
rg rejects what it cannot do: an unsupported pattern exits 2 with a
message. GNU grep 3.11 does not. Measured:
grep -E '\d+' matches a literal d — no warning, exit 0
grep -E '(?:a|b)c' matches abc — a warning, exit 0
grep -E 'ab*?' greedy — silently not lazy
grep -E '(?=x)' never matches — a warning, exit 1
Every one of those is a DIFFERENT LANGUAGE that still runs and still returns hits. For rg the capability gate converts a stderr parse into data; for grep it converts a silently wrong answer into a named refusal, which is the stronger reason the gate exists at all.
:posix-ere therefore provides: nothingIts capability set is empty. \d must be written [:class [\0 \9]],
which renders [0-9]; a tab must be a literal tab inside a :literal, not
:tab; a :capture is refused because ERE's only group is a capturing one,
so any structural group this emitter needs would shift the very index
:match/groups promises.
Everything the gate lets through is rendered exactly, or not at all.
POSIX ERE — the dialect `grep -E` speaks, rendered without regal. ## Why this emitter is hand-written regal has no ERE flavor, and its `:java`/`:ecma` output is not ERE: a nested alternation comes out as `(?:a|b)`, which POSIX ERE has no syntax for. So this is the one dialect that renders from the Construct directly. It is small because ERE is small — literals, classes, alternation, greedy quantifiers, anchors, and one grouping syntax. ## Why the gate matters more here than anywhere else `rg` rejects what it cannot do: an unsupported pattern exits 2 with a message. GNU grep 3.11 does not. Measured: grep -E '\d+' matches a literal `d` — no warning, exit 0 grep -E '(?:a|b)c' matches `abc` — a warning, exit 0 grep -E 'ab*?' greedy — silently not lazy grep -E '(?=x)' never matches — a warning, exit 1 Every one of those is a DIFFERENT LANGUAGE that still runs and still returns hits. For rg the capability gate converts a stderr parse into data; for grep it converts a silently wrong answer into a named refusal, which is the stronger reason the gate exists at all. ## What `:posix-ere` therefore provides: nothing Its capability set is empty. `\d` must be written `[:class [\0 \9]]`, which renders `[0-9]`; a tab must be a literal tab inside a `:literal`, not `:tab`; a `:capture` is refused because ERE's only group is a capturing one, so any structural group this emitter needs would shift the very index `:match/groups` promises. Everything the gate lets through is rendered exactly, or not at all.
COLLECT layer: an anti-corruption layer over lambdaisland/regal, and the three dialect profiles it can render.
Regal is the emitter we buy rather than build — it owns escaping, precedence and grouping, and it is the only code here that knows what a regex looks like. Its vocabulary does not cross into the domain: it throws, we return a Result; it names flavors, we name dialects.
Measured on 0.1.175: regal does NOT gate on flavor. [:atomic "ab"] renders
as (?>ab) under :ecma, where JavaScript has no atomic groups, and an
unknown flavor is accepted silently. The capability gate in
construct.dialect is therefore load-bearing, not ceremony.
:java every capability flavor :java9 :ecma no atomic groups flavor :ecma :re2 no lookaround, no atomic flavor :ecma (rg, sd, RE2, Rust regex)
:posix-ere is deliberately absent: ERE has no non-capturing group, so
regal's (?:…) output is not ERE even after a capability gate. It needs a
real second emitter, which this design declines to build.
COLLECT layer: an anti-corruption layer over lambdaisland/regal, and the three dialect profiles it can render. Regal is the emitter we buy rather than build — it owns escaping, precedence and grouping, and it is the only code here that knows what a regex looks like. Its vocabulary does not cross into the domain: it throws, we return a Result; it names flavors, we name dialects. ## What regal does not do, and this layer must Measured on 0.1.175: regal does NOT gate on flavor. `[:atomic "ab"]` renders as `(?>ab)` under `:ecma`, where JavaScript has no atomic groups, and an unknown flavor is accepted silently. The capability gate in `construct.dialect` is therefore load-bearing, not ceremony. ## Dialects shipped :java every capability flavor :java9 :ecma no atomic groups flavor :ecma :re2 no lookaround, no atomic flavor :ecma (rg, sd, RE2, Rust regex) `:posix-ere` is deliberately absent: ERE has no non-capturing group, so regal's `(?:…)` output is not ERE even after a capability gate. It needs a real second emitter, which this design declines to build.
Malli value objects for regex CONSTRUCTS — a regex expression reified as data.
A construct is AUTHORED in lambdaisland/regal's vector syntax — [:cat "a" [:alt "b" "c"]] — and NORMALIZED into the recursive map value object this
namespace defines. The authored form is what callers write; the normalized
form is what carries contracts, generators and tests. construct.core
promotes one into the other and projects back.
The split is forced: malli refuses a :ref reachable inside a sequence
schema (::m/potentially-recursive-seqex), and regal's head-plus-variadic
vector IS a sequence schema.
A Capability names a regex feature whose availability differs across target
languages. A Dialect declares the set it PROVIDES; a construct's required
set is computed from its shape. Support is a set-difference, decidable before
emission — never a try/catch around a compile.
These schemas are the single source: they drive the m/=> contracts and the
synthesized property/mutation tests.
Malli value objects for regex CONSTRUCTS — a regex expression reified as data. ## Two forms, one language A construct is AUTHORED in lambdaisland/regal's vector syntax — `[:cat "a" [:alt "b" "c"]]` — and NORMALIZED into the recursive map value object this namespace defines. The authored form is what callers write; the normalized form is what carries contracts, generators and tests. `construct.core` promotes one into the other and projects back. The split is forced: malli refuses a `:ref` reachable inside a sequence schema (`::m/potentially-recursive-seqex`), and regal's head-plus-variadic vector IS a sequence schema. ## Capabilities A `Capability` names a regex feature whose availability differs across target languages. A `Dialect` declares the set it PROVIDES; a construct's required set is computed from its shape. Support is a set-difference, decidable before emission — never a `try`/`catch` around a compile. These schemas are the single source: they drive the `m/=>` contracts and the synthesized property/mutation tests.
Facade for the pattern subsystem: name what you are looking for, get an answer, never mention an engine.
(pattern/register-pattern! #:pattern{:id :audit/probe :expr "ready\?"}) (pattern/match? :audit/probe source) (pattern/scan :audit/probe source)
The regex engine registers itself on load, so :regex is available without
ceremony; *default-engine* selects what an engine-less Pattern uses.
Re-exports are DELEGATING fns, never def-aliases — a def alias of a protocol method freezes its method cache.
Facade for the pattern subsystem: name what you are looking for, get an
answer, never mention an engine.
(pattern/register-pattern! #:pattern{:id :audit/probe :expr "ready\\?"})
(pattern/match? :audit/probe source)
(pattern/scan :audit/probe source)
The regex engine registers itself on load, so `:regex` is available without
ceremony; `*default-engine*` selects what an engine-less Pattern uses.
Re-exports are DELEGATING fns, never def-aliases — a def alias of a protocol
method freezes its method cache.Protocol for the pattern subsystem.
IPatternEngine is the DIP seam between consumers (audits, linters, log
scanners) and concrete matching engines (java.util.regex today; a structural
s-expression matcher, a glob matcher, or a tree-sitter query engine next).
Consumers depend on this protocol and on a Pattern value. They never name an engine, so swapping or adding one changes no call site.
An engine MUST:
Result from every operation — never throw for a bad pattern.:pattern/unsupported-flag) rather than
ignore it: a silently dropped flag is a wrong answer.:match/end exclusive,
with (subs subject start end) equal to :match/text.engine-id keyword.Protocol for the pattern subsystem. ## ISP boundary `IPatternEngine` is the DIP seam between consumers (audits, linters, log scanners) and concrete matching engines (java.util.regex today; a structural s-expression matcher, a glob matcher, or a tree-sitter query engine next). Consumers depend on this protocol and on a Pattern value. They never name an engine, so swapping or adding one changes no call site. ## Engine contract An engine MUST: - Return `Result` from every operation — never throw for a bad pattern. - Reject a flag it cannot honour (`:pattern/unsupported-flag`) rather than ignore it: a silently dropped flag is a wrong answer. - Report matches in subject order, non-overlapping, `:match/end` exclusive, with `(subs subject start end)` equal to `:match/text`. - Use a stable, unique `engine-id` keyword.
java.util.regex adapter for IPatternEngine.
The default engine. Pure promoters (flags->bitmask, ->matches) carry the
logic; the record is a thin Result-returning shell over them.
java.util.regex adapter for IPatternEngine. The default engine. Pure promoters (`flags->bitmask`, `->matches`) carry the logic; the record is a thin Result-returning shell over them.
Engine and named-pattern registries — the DIP swap points.
Two open sets, two registries:
engines id -> IPatternEngine ;; how to match patterns id -> Pattern ;; what to look for
A consumer names a pattern (:audit/readiness-probe) instead of carrying a
literal expression, so one definition serves every call site and a fix lands
in one place. resolve-pattern is PURE — the index is an argument, which is
what makes it testable and what lets a caller run against a private index.
Engine and named-pattern registries — the DIP swap points. Two open sets, two registries: engines id -> IPatternEngine ;; how to match patterns id -> Pattern ;; what to look for A consumer names a pattern (`:audit/readiness-probe`) instead of carrying a literal expression, so one definition serves every call site and a fix lands in one place. `resolve-pattern` is PURE — the index is an argument, which is what makes it testable and what lets a caller run against a private index.
Malli value objects for the pattern subsystem.
A Pattern is a portable, ENGINE-AGNOSTIC description of what to look for: an expression plus flags, optionally named by an id. A Match is one located occurrence — text plus its half-open [start end) offsets in the subject.
These schemas are the single source: they drive the m/=> contracts and the
synthesized property/mutation tests.
Malli value objects for the pattern subsystem. A Pattern is a portable, ENGINE-AGNOSTIC description of what to look for: an expression plus flags, optionally named by an id. A Match is one located occurrence — text plus its half-open [start end) offsets in the subject. These schemas are the single source: they drive the `m/=>` contracts and the synthesized property/mutation tests.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |