Fallback chains (||)¶
The single-rule || operator (Rule1() || Rule2()) extends to whole
blocks of rules. Block-level fallback is the right tool when the
fallback path needs more than one rule — e.g. lookup, normalise,
assert, all as a unit.
Syntax¶
Each block declares its output with (:var). All blocks in the
chain must declare the same variable, with the same type — the
parser checks this at compile time.
Semantics¶
- Blocks are evaluated left to right.
- The first block that produces a value for
:varwins. Remaining blocks are not evaluated. - A block fails if an
Assert/AssertNotfires inside it. The chain then moves to the next block. successfulMappingistrueif any block succeeded.
The single-rule form Rule1() || Rule2() is shorthand for
{ Rule1() }(:implicit) || { Rule2() }(:implicit). Both behave
identically — the block form just lets you put more than one rule
in each path.
Worked example — try one Usagi map, fall back to another¶
CDM person.gender_concept_id. Two Usagi files exist:
map3.csv— a curated, high-precision file. May not have a match.map4.csv— a broader fallback with a default of8521("UNKNOWN") so it always produces something.
We want the precise mapping where possible; otherwise the broad one.
Read it as: try map3 first; if no match, try map4 (which has a
default and so always succeeds). Both paths produce a multi-result
into the same variable :gender, which the table-level LoopOver
then expands into one row per match.
Multi-rule block fallback¶
Where the fallback needs more than one rule, the block form becomes essential:
{
:raw_code => Map([..., NULL])
AssertNot(NULL)
}(:concept_id)
||
{
:raw_code => UsagiMap("vocab-map.csv")[]
AssertVocabDomain("Condition", TRUE)
}(:concept_id)
If the hand-curated Map either misses the input or produces
NULL, the AssertNot(NULL) fires and the first block fails — at
which point the Usagi-based block runs and validates that the result
is in the expected domain.
Type uniformity¶
All blocks must agree on the type of the declared output. The compile-time check catches mistakes like:
{ Map([["A", 1]]) }(:x) # produces INT
|| { Map([["B", "fallback"]]) }(:x) # produces STRING — error
The mismatch is rejected at parse time.
Single-rule || is unchanged¶
still works exactly as before — see
DSL Guide → Conditional Chaining
for the original semantics. Block-level || is purely an extension.
See also¶
- Conditional Chaining — the single-rule form.
- Conditionals — when
If/Elseis a better fit (different inputs, comparison-driven branching). - Parallel collection (
++) — the dual: collect every block's result instead of just the first success.
Source tests¶
tests/test089/ — block fallback between two UsagiMap calls;
the second has a default value so it always succeeds.
tests/test090/ — variations with named variables and multi-result.