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.
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 |