Skip to content

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

{ Rule_A1() Rule_A2() }(:result) || { Rule_B1() Rule_B2() }(:result)

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 :var wins. Remaining blocks are not evaluated.
  • A block fails if an Assert / AssertNot fires inside it. The chain then moves to the next block.
  • successfulMapping is true if 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 of 8521 ("UNKNOWN") so it always produces something.

We want the precise mapping where possible; otherwise the broad one.

logic: { UsagiMap("map3.csv")[] }(:gender)
logic: || { UsagiMap("map4.csv", 8521)[] }(:gender)

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

Rule1() || Rule2()

still works exactly as before — see DSL Guide → Conditional Chaining for the original semantics. Block-level || is purely an extension.

See also

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.