Paths & Filters
This page explains how a rule picks one value, one row, or several rows out of the data it was given.
The path grammar
String literals
A string literal may be written with single quotes or double quotes — 'abc' and "abc" are
the same literal. The two forms are fully equivalent: a double-quoted literal is accepted
anywhere a single-quoted one is (CONCAT("a", "b") is exactly as legal as CONCAT('a', 'b')),
generally, across the whole grammar — this is not a step-reference-only special case, and a
reader should not infer a narrower rule just because double quotes are only shown below inside a
step reference. Both forms take the same backslash escapes and neither spans a newline.
The double-quoted form exists because the ratified step-reference spelling (see the reserved
steps namespace, described further down in this section, ratified at plan 02-08 Task 0
option-a) — steps["checkout/items/validate-total"].amount — requires one: a step path contains forward
slashes, and quoting it is how the grammar tells a literal path segment apart from everything else
inside the brackets. Adding a second, position-dependent string-literal concept just for that one
spelling was the worse option, so the lexer instead keeps one string-literal concept, accepted
in both spellings, everywhere. This was purely additive — every pre-existing single-quoted literal
is untouched.
Every access form resolves through one path resolver, shared by a context path (input.amount), a
step reference, and a bare field name inside a filter predicate:
- Property access:
input.amount,steps["…"].amount— a plain dotted field name. - Index access:
positions[0]— the bracket is classified as an index when its content's declared type is a number. - Slice access:
positions[1:3]— either bound may be omitted, meaning the collection's own start or end. - Wildcard access:
positions[*]— maps over every element. - Filter access:
positions[price > 100]— the bracket is classified as a filter when its content's declared type is boolean; a bare field name inside the predicate (price) resolves against the element currently under test, and is only meaningful inside a predicate.
A bracket whose content types as any — a bare, not-yet-typed data path such as
positions[input.i] — is a coded path syntax error in this phase: the index-versus-filter
classification is entirely static, decided from the bracket content's declared return type, and a
bare data path has no declared type until Phase 3's inference exists. A data-driven index is not
expressible until then.
Out-of-range access is strict, never a clamp. positions[99] on a three-element array
fails, and so does a slice reaching past the end — a clamp would silently return a different
collection than the author asked for. Because access is strict, an index or slice bound may be an
arbitrary expression, not only a literal (positions[0:LENGTH(positions) - 1]), so an author can
always compute a bound from the collection's own length rather than relying on a clamp. A bound that
is not a whole number, or whose magnitude is too large to name a position, is a distinct coded error
from out-of-range — "index 3.5 is out of range" would send an author to look at the wrong thing.
Negative indices count from the end — positions[-1] is the last element, and a
negative slice bound behaves correspondingly. On an empty array this is still out of range, per the
strict rule above.
A filter or wildcard over a non-array is a save-time error where the declared schema proves the
target is not an array, and yields an empty result at runtime where the type is only known
dynamically — the same "catch it statically, stay survivable dynamically" shape equality
uses. A filter always returns an array, including for exactly one match, and an empty
result is an empty array, never an error — "no positions over a hundred" is a normal business
answer. An author who wants a single value uses GET or FIND_BY instead, both documented on the
Collections page.
The reserved steps namespace is unshadowable: an input field literally named steps
never captures it, and stays reachable as input.steps. The concrete spelling —
steps["checkout/items/validate-total"].amount — is a one-way door: it appears inside persisted
ruleset documents, so changing it later would be a migration of stored customer data, not a
refactor. Whether a referenced step actually exists, and what type it yields, is resolved by Phase
3's graph binding — this phase fixes only the grammar, the AST node, and the type-checker hook.