Introduction
This specification defines the behavior of a standalone expression
evaluator. In practice, this evaluator is intended to function as a
component within a larger template engine.
While the template engine manages higher-level concerns - such as
file I/O, tag orchestration ({% ... %}), and the provision
of a global execution context - this document governs the internal logic
of Expressions. By decoupling the expression grammar
from the engine’s tag architecture, we ensure that data resolution,
mathematical operations, and filter pipelines remain consistent
regardless of the tag or context in which they appear.
Terminology
Template: The source document or string containing a mix
of static text (to be rendered literally) and dynamic
Markup. It serves as the primary unit of execution for
the engine.
Template Engine: The software system that orchestrates
the transformation of a Template into a rendered
output. It is responsible for parsing the surrounding
Markup, managing the execution context (environment),
and invoking the expression evaluator to resolve
Expressions, Filters, and
Lambdas as defined in this specification. While the
engine handles high-level concerns like file loading and
Tag execution, it relies on the Unified Expression
Grammar for all data resolution and computation.
Expression: An expression is the fundamental unit of
computation within a template, representing a sequence of identifiers,
literals, and operators that resolves to a value. They appear within
output delimiters to render data directly to the page (e.g.,
{{ user.name | upcase }}), inside conditional tags to
govern template logic (e.g., {% if item.price > 100 %}),
and as the data sources for iterations (e.g.,
{% for product in collections.frontpage %})
For each of the preceding examples, this tables isolates the
expression part of the markup to illustrate some of the places an
expression can appear.
| Markup |
Expression |
{{ user.name | upcase }} |
user.name | upcase |
{% if item.price > 100 %} |
item.price > 100 |
{% for product in collections.frontpage %} |
collections.frontpage |
Markup: The collective term for the specialized syntax
(delimited by {{ and {%) embedded within a
template. Markup distinguishes dynamic instructions - such as output
statements and tags - from static content.
Tag: A structural command used to govern control flow,
iteration, or state management (e.g., if, for,
assign). While this specification defines the grammar of
expressions within tags, the implementation of the tags
themselves is considered part of the surrounding architecture.:
Filter: A named, total function registered in the
environment that transforms a value. Filters are invoked within a
pipeline using the pipe (|) syntax, where they accept the
result of the preceding expression as their primary input.
Lambda: An anonymous inline function. Lambda expressions
allow for user-defined filters and custom mapping or sorting logic to be
applied to data structures.
Total Evaluation: An expression language has total
evaluation if every syntactically valid expression evaluates to a value
and does not raise a runtime error. Every operator, filter, and
conversion must produce a value for every possible input.
Total Function: A function is total if it is defined for
every possible input in its domain - the set of inputs the function
accepts.
History
This specification is based on Shopify’s Liquid project
and other implementations derived from Shopify’s reference
implementation.
Historically, Liquid expressions have varied subtly depending on
context - certain tags treated operators or filters differently, and
precedence rules were not globally consistent.
This document aims to replace ad hoc behavior with a single,
composable, context-independent grammar and a clear evaluation model.
All operators, filters, and grouping constructs are valid in any
expression context, with a single, well-defined precedence
hierarchy.
Compatibility with existing Liquid implementations is explicitly not
a goal.
Overview of Unified Expressions
An expression is a composable sequence of components that, when
evaluated against a context, produces a single \(RuntimeValue\). This language is total and
immutable: every syntactically valid expression resolves to a value, and
no expression can modify the state of the execution context.
Formally, for every expression \(e\)
and environment \(\rho\):
\[⟦ e ⟧(\rho) \in
RuntimeValue\]
Expressions are composed of the following six building blocks:
Literals
Literals are textual representations of fixed values encoded directly
in the template source, including:
- Primitives:
Strings (UTF-8), Numbers
(High-precision decimals), Booleans
(true/false), and Null.
- Collections:
Array literals ([1, 2, 3])
and Object literals ({ key: value }).
- Ranges:
Range literals (1..5) which
evaluate to an inclusive sequence of integers.
Variables and Paths
Variables are identifiers used to retrieve data from the execution
context. They support deep resolution via segmented path syntax:
- Dot Notation:
user.profile.name
- Bracket Notation:
user["profile"]["name"] or
items[index]. If a variable or any segment of a path does
not exist, the expression resolves to \(Nothing\).
Operators
Operators perform functional computations or logical comparisons.
- Arithmetic: Standard mathematical operations (
+,
-, *, /, %) and
unary negation (-).
- Comparison: Equality (
==, !=) and
relational (<, <=, >,
>=) checks.
- Logical: Boolean logic (
and, or,
not) used for truthiness branching.
- Coalescence: The
orElse operator provides a fallback
specifically for \(Nothing\)
values.
- Membership: The
in and contains operators
for checking if a value exists within a collection or a substring exists
within a string.
Filters and Pipelines
Filters are named transformations applied to a value via the pipe
(|) syntax. They are designed to be chained into pipelines,
where the output of one filter serves as the primary input for the next
(e.g., product.price | times: 1.1 | round: 2). Filters may
accept:
- Positional Arguments:
filter: arg1, arg2
- Keyword Arguments:
filter: key="value"
Lambdas
Lambdas are anonymous, inline functions (e.g.,
item => item.price > 10). They are first-class
objects that can be “called” as a user-defined filter with the pipe
operator (|), or passed as arguments to filters to delegate
logic back to the expression engine, enabling powerful operations like
custom sorting, mapping, and searching within collections.
Control Expressions
- Conditional (Ternary): The
... if ... else ...
construct allows for inline branching based on the truthiness of a
condition (e.g., score if score > 50 else "Fail").
- Grouping: Parentheses
( ... ) are used to override
default operator precedence or to clarify the evaluation order of
complex nested expressions.
Type
System for Template Expressions
The expression language operates on a data model that is conceptually
“JSON-like”. Every expression, when evaluated, resolves to a value
belonging to one of the fundamental types defined in this section.
The core types - Null, Boolean,
Number, String, Array, and
Object - map directly to JSON types. Any valid JSON
document can be represented naturally within this expression
language.
All values in the data model are immutable. Operations that appear to
“modify” a value (such as filter applications) always return a new
value.
To allow templates to interact with complex application logic, the
language supports “Drops”. These are opaque extension types provided by
the host environment that can implement specific protocols for
iteration, property access, or equality.
Values
\(DataValue\) defines all possible
kinds of value that can exist inside the template data model.
\[
\begin{aligned}
DataValue = \;& Null \\
| \;& Boolean \\
| \;& Number \\
| \;& String \\
| \;& Array\langle DataValue \rangle \\
| \;& Object\langle String \to DataValue \rangle \\
\end{aligned}
\]
\(RuntimeValue\) describes the
result of evaluating an expression. Note that \(DataValue\) is the subset of \(RuntimeValue\) that does not contain \(Drop\) or \(Nothing\).
\[
\begin{aligned}
RuntimeValue = \;& DataValue \\
| \;& Array\langle RuntimeValue \rangle \\
| \;& Object\langle String \to RuntimeValue \rangle \\
| \;& Drop \\
| \;& Nothing
\end{aligned}
\]
\(Nothing\) represents the absence
of a value produced during evaluation and is distinct from \(Null\) (or implementation-specific
nil, None, undefined etc.).
Numeric Types
\(Number\) represents a decimal
numeric value with arbitrary precision and exact decimal semantics.
Implementations MUST perform numeric operations using a decimal
arithmetic model. Binary floating-point (e.g., IEEE-754 double) MUST NOT
be used as the semantic numeric model.
An implementation MAY use binary floating-point internally, but
observable behavior MUST match exact decimal arithmetic.
Integer values are a subset of Number. A number is
considered an integer when its decimal representation has no fractional
component.
Decimal Arithmetic Model
The language defines numbers using base-10 decimal semantics:
- Exact representation of finite decimal literals.
- Exact addition, subtraction, and multiplication.
- Exact division when representable as a finite decimal.
- Deterministic rounding when required.
This ensures:
0.1 + 0.2 == 0.3 → true
in all conforming implementations.
Implementations MUST NOT introduce binary floating-point rounding
artifacts.
Example (required behavior):
0.1 + 0.2
MUST evaluate to a number equal to decimal 0.3.
It MUST NOT produce:
0.30000000000000004
Division Semantics
Division may produce a non-terminating decimal expansion.
Example:
1 / 3
TODO: Loosen precision requirements
Implementations MUST use decimal division with a minimum precision of
28 decimal digits and MUST round using round-half-even (banker’s
rounding), unless a higher precision is supported.
The precision used MUST be consistent within an evaluation.
Numeric Equality
Numeric equality is mathematical equality after decimal
normalization.
Examples:
1 == 1.0 → true
0.30 == 0.3 → true
String Conversion
\(ToString(Number)\) MUST produce a
canonical decimal representation:
- No scientific notation.
- No unnecessary trailing zeros.
- No trailing decimal point.
TODO: true division
TODO: no decimal point when operands are integers and result is
whole
Examples:
1 → "1"
1.0 → "1"
0.300 → "0.3"
1000 → "1000"
Iterables
Some evaluation algorithms operate on iterable values. An
\(Iterable\) is either an \(Array\) or \(Sequence\):
\[
Iterable = Array\langle RuntimeValue \rangle | Sequence
\]
Where \(Sequence\) is a \(Drop\) implementing the Sequence
protocol.
Array and sequence drops MUST behave identically with respect to
iteration semantics.
Data Type Summary
| Type |
Description |
JSON Equivalent |
| Nothing |
Represents the absence of a data value. |
N/A |
| Null |
Represents a deliberate “empty” data value. |
null |
| Boolean |
Logical true or false. |
true, false |
| Number |
An exact decimal representation (see Section 2.2). |
number |
| String |
A sequence of Unicode characters. |
string |
| Array |
An ordered list of values. |
array |
| Object |
A collection of key-value pairs (where keys are Strings). |
object |
| Drop |
A host-provided extension type. |
N/A |
| Iterable |
An array, or drop implementing the sequence protocol |
N/A |
Extension Types (Drops)
Implementations may expose developer-defined objects known as Drops.
A Drop is an object that can be coerced into a data value when required,
with the help of a context hint.
\(Drop\) is defined as an object
implementing \(ToPrimitive\).
\[
ToPrimitive : Drop × ContextHint → RuntimeValue
\]
Where:
\[
ContextHint ∈ { data, numeric, string, boolean, array, object }
\]
With the following constraints:
\[
\begin{aligned}
ToPrimitive(drop, data) \;& → DataValue \\
ToPrimitive(drop, boolean) \;& → Boolean | Nothing \\
ToPrimitive(drop, numeric) \;& → Number | Nothing \\
ToPrimitive(drop, string) \;& → String | Nothing \\
ToPrimitive(drop, array) \;& → Array\langle RuntimeValue \rangle |
Nothing \\
ToPrimitive(drop, object) \;& → Object\langle String \to
RuntimeValue \rangle | Nothing \\
\end{aligned}
\]
The result of \(ToPrimitive(drop,
data)\) MUST be a valid \(DataValue\) as defined above, meaning it
MUST NOT contain \(Drop\) at any
depth.
The following table shows when each hint applies.
| Context |
Hint |
| Arithmetic |
numeric |
| String concatenation |
string |
Boolean test (if, and,
or) |
boolean |
| Comparison |
data |
| Filter arguments (general) |
data |
| Array literal spread, eager filter arguments |
array |
| Object literal spread |
object |
Sequence Protocol
A Drop MAY implement the Sequence protocol to facilitate lazy
iteration with the for tag or sequence aware filters.
\(Sequence\) is defined as a \(Drop\) that supports the following abstract
behavior.
\[
\begin{aligned}
length() \;& \to Number \\
slice(offset, limit, reversed) \;& \to Drop \\
iterate() \;& \to Iterator\langle RuntimeValue \rangle \\
\end{aligned}
\]
Constraints:
- \(length()\) MUST reflect the
current logical sequence.
- \(slice()\) MUST return a Drop
implementing the Sequence protocol.
- \(iterate()\) MUST yield exactly
length() elements.
Equality Protocol
A Drop MAY implement the Equality protocol for interaction with
== and != operators, without first coercing to
a data value.
\(Equals\) MUST NOT throw an
error.
\[
Equals : Drop × RuntimeValue → Boolean | Nothing
\]
A Drop implements the Equality protocol if it supports:
\[
Equals(x) -> Boolean | Nothing
\]
Ordering Protocol
A Drop MAY implement the Ordering protocol for interaction with
<, >, <=, and
>= operators, without first coercing to a data
value.
\(LessThan\) MUST NOT throw and
error.
\[
LessThan : Drop × RuntimeValue → Boolean | Nothing
\]
A Drop implements the Ordering protocol if it supports:
\[
LessThan(x) -> Boolean | Nothing
\]
Membership Protocol
A Drop MAY implement the Membership protocol for interaction with
in and contains operators, without first
coercing to a data value.
\(Contains\) MUST NOT throw an
error.
\[
Contains : Drop × RuntimeValue → Boolean | Nothing
\]
A Drop implements the Membership protocol if it supports:
\[
Contains(x) -> Boolean | Nothing
\]
Type Conversion
Automatic type conversion is performed in some contexts. Here we
define abstract conversion functions for runtime values, each of which
is deterministic and never throws an error.
\[
\begin{aligned}
ToBoolean \;&: RuntimeValue \to Boolean \\
ToNumber \;&: RuntimeValue \to Number | Nothing \\
ToString \;&: RuntimeValue \to String \\
ToArray \;&: RuntimeValue \to Array\langle RuntimeValue \rangle
\\
ToObject \;&: RuntimeValue \to Object\langle String \to
RuntimeValue \rangle \\
ToIterable \;&: RuntimeValue \to Sequence | Array\langle
RuntimeValue \rangle \\
\end{aligned}
\]
Implicit conversions occur in the following contexts (each uses the
corresponding abstract conversion function):
TODO: turn this into a table
- Arithmetic and numeric operators:
ToNumber
- Unary
+/-: ToNumber
- String concatenation (filters):
ToString
- Boolean conditions used by
if, ternary if
expressions, and, or, and not:
ToBoolean
- Comparisons that require primitive values:
ToPrimitive(…, data) then structural comparison; numeric
comparisons use ToNumber when both sides are numeric or
coercible to numeric.
for iterable expressions: ToArray /
ToPrimitive(…, iterable)
- Filter arguments (general):
ToPrimitive(…, data) unless
a filter documents a different required hint
ToArray helper and sequence normalization:
ToArray
Truthiness and ToBoolean(x)
The language adopts structural truthiness. Empty strings, empty
arrays, empty objects, zero numbers, null, and absence
(Nothing) are falsy. All other values are truthy. This rule
is uniform across value types and does not depend on host-language
semantics.
Conditions are evaluated by first evaluating the condition expression
and then applying ToBoolean to the result.
The abstract operation ToBoolean is defined to be
identical to IsTruthy.
\[
ToBoolean, IsTruthy : RuntimeValue → Boolean
\]
An evaluation result is truthy if it represents a non-empty,
non-zero, non-null value.
| Input Type |
Result |
| Nothing |
false |
| Null |
false |
| Boolean |
identity |
| Integer |
x ≠ 0 |
| Float |
x ≠ 0.0 |
| String |
length(x) > 0 |
| Array |
length(x) > 0 |
| Object |
size(o) > 0 |
| Drop |
ToPrimitive(x, boolean); false if Nothing |
ToNumber(x)
Returns either Integer or Decimal.
\[
ToNumber : RuntimeValue → Number | Nothing
\]
| Input Type |
Result |
| Integer |
identity |
| Float |
identity |
| Boolean |
true → 1, false → 0 |
| Nothing |
Nothing |
| Null |
Nothing |
| String |
parse numeric literal; if invalid → Nothing |
| Array |
Nothing |
| Object |
Nothing |
| Drop |
ToPrimitive(x, numeric) |
ToString(x)
\[
ToString : RuntimeValue → String
\]
| Input Type |
Result |
| String |
identity |
| Integer |
decimal representation |
| Float |
canonical decimal |
| Boolean |
"true" or "false" |
| Null |
"" |
| Nothing |
"" |
| Array |
JSON-formatted array |
| Object |
JSON-formatted object |
| Drop |
ToPrimitive(x, string); "" if Nothing |
ToArray(x)
\[
ToArray : RuntimeValue → Array<RuntimeValue>
\]
| Input Type |
Result |
| Array |
identity |
| Object |
array of [key, value] pairs |
| String |
Array |
| Null |
[] |
| Nothing |
[] |
| Drop |
ToPrimitive(x, array) or [] if Nothing |
| Any other value |
[x] |
String Semantics
If x is a String, ToArray(x)
produces an array containing the string’s Unicode scalar values in
order.
\[
ToArray(String) → Array<String>
\]
Where each element is a single Unicode scalar value encoded
as a string of length 1 (not grapheme clusters).
Example:
\[
ToArray("cat")
→ ["c", "a", "t"]
\]
Unicode example:
\[
ToArray("😀a")
→ ["😀", "a"]
\]
ToObject(x)
\[
ToObject : RuntimeValue → Object<String → RuntimeValue>
\]
| Input Type |
Result |
| Object |
identity |
| Drop |
ToPrimitive(x, object) or {} if Nothing |
| Any other |
{} |
ToIterable(x)
\[
ToIterable : RuntimeValue → Iterable
\]
| Input Type |
Result |
| Sequence (Drop) |
identity |
| Any other |
ToArray(x) |
Syntax
and Semantics
TODO: overview
The grammar is written using Parsing Expression Grammar
(PEG) semantics following the notation introduced by Bryan
Ford. Rule definitions and operators follow Ford-style PEG notation
(<-, /, *, +,
?, !, &). Literal syntax,
escape sequences, and bounded repetition follow the conventions of Pest
(parser generator).
TODO: intro sentence
Expression ← TernaryExpr
B ← " " / "\t" / "\r" / "\n" // Whitespace
S ← B* // Optional whitespace
C ← IdentifierChar // Alias for any name character
TODO: Elaborate on Nothing.
Implementations MAY surface evaluation of Nothing as errors,
warnings, or diagnostics, but MUST preserve the observable semantics
defined by this specification when execution continues.
Literals
Literal ← NumberLiteral /
StringLiteral /
BooleanLiteral /
NullLiteral /
ArrayLiteral /
ObjectLiteral /
RangeLiteral
Null Literal
Syntax
The language provides two semantically indistinguishable keywords,
null and nil, to support different developer
preferences.
NullLiteral ← ("null" / "nil") !C
Note: The negative lookahead !C ensures that identifiers
beginning with a Null keyword (e.g., nullify) are not
incorrectly parsed as literals.
Semantics
The null literal represents a deliberate “empty” value within the
data model. It is a concrete data value. It is distinct from
Nothing, which is the internal signal used to represent
evaluation failures, missing variables, or out-of-bounds access.
A null literal always evaluates to a value of type Null.
In a boolean context, Null is always “falsy” (see Section 2.6.1).
Examples
| Expression |
Evaluation |
Notes |
null |
null |
Evaluates to the Null type. |
nil |
null |
Identical to null. |
user.id == null |
true |
Returns true if id is explicitly null. |
(null orElse "default") |
null |
The coalescing operator tests for Nothing.
null is a data value. |
(null or "default") |
"default" |
The logical or operator treats Null as a
trigger for the fallback. |
Boolean Literals
Syntax
The language recognizes the lowercase keywords true and
false.
BooleanLiteral ← ("true" / "false") !C
Note: The negative lookahead !C ensures that identifiers
beginning with a boolean keyword (e.g., true_value) are not
incorrectly parsed as literals.
Semantics
Boolean literals represent the two logical truth values. They
evaluate to a value of type Boolean. Boolean keywords are
case-sensitive. True or TRUE are not
recognized as boolean literals and will be interpreted as identifiers
(variable names).
The literal true is always “truthy,” and the literal
false is always “falsy” (see Section 2.6.1).
Examples
| Expression |
Evaluation |
Notes |
true |
true |
Evaluates to the Boolean true value. |
false |
false |
Evaluates to the Boolean false value. |
not true |
false |
The logical not operator inverts the boolean
value. |
(true orElse false) |
true |
The null coalescing operator returns the first non-null/non-nothing
value. |
Numeric Literals
Syntax
Numeric literals take the form of:
- An integer part - must be either a single
0 or a
non-zero digit followed by any number of digits. Leading zeros are not
permitted (e.g., 05 is invalid).
- An optional fractional part - a decimal point followed by one or
more digits.
- An optional exponent part using either
e or
E.
NumberLiteral ← Integer Fraction? Exponent? !C
Integer ← "0" / [1-9] [0-9]*
Fraction ← "." [0-9]+
Exponent ← [eE] [+-]? [0-9]+
Syntactically, the NumberLiteral itself is unsigned.
Negative numbers are represented by applying the unary negation operator
(-) to a literal. See Section 3.4.7.
Semantics
Regardless of the notation used (standard or scientific), all numeric
literals resolve to the Number type and are stored using
exact decimal semantics. An exponent does not “coerce” the number into a
floating-point type; 1e2 and 100 are
semantically and type-identical.
Examples
| Expression |
Evaluation |
Notes |
0 |
0 |
Single zero integer. |
42 |
42 |
Positive integer without leading zeros. |
0.001 |
0.001 |
Decimal fraction. |
1.23e4 |
12300 |
Scientific notation (positive exponent). |
5E-3 |
0.005 |
Scientific notation (negative exponent, case-insensitive). |
-10.5 |
-10.5 |
Unary negation applied to a literal. |
05 |
Invalid |
Leading zeros are not allowed in the integer part. |
String Literals
Syntax
Both single and double quoted strings are supported. Both forms allow
interpolation.
Escape sequences follow JSON syntax, with the addition of an escaped
single quote for single quoted strings, and \${ to escape
string interpolation.
StringLiteral ← DoubleQuoted / SingleQuoted
DoubleQuoted ← "\"" (Interpolation / DoubleEscaped / Unescaped / "'")* "\""
SingleQuoted ← "'" (Interpolation / SingleEscaped / Unescaped / "\"")* "'"
Interpolation ← "${" Expression "}"
DoubleEscaped ← "\\" ( "\"" / Escapable )
SingleEscaped ← "\\" ( "'" / Escapable )
Unescaped ← LiteralDollar / SourceChar
LiteralDollar ← "$" ! "{"
Escapable ← "b" / "f" / "n" / "r" / "t" / "/" / "\\" / ("u" ~ HexChar) / "${"
HexChar ← NonSurrogate / HighSurrogate "\\u" LowSurrogate
HighSurrogate ← [Dd] [AaBb89] [0-9A-Fa-f]{2}
LowSurrogate ← [Dd] [CcDdEeFf] [0-9A-Fa-f]{2}
NonSurrogate ← [AaBbCcEeFf0-9] [0-9A-Fa-f]{3} /
[Dd] [0-7] [0-9A-Fa-f]{2}
SourceChar ← " " /
"!" /
"#" /
"%" /
"&" /
[\u0028-\u0058] /
[\u005D-\uD7FF] /
[\uE000-\u0010FFFF]
Semantics
String literals represent sequences of Unicode scalar values (not
grapheme clusters). They support both simple static text and dynamic
content via interpolation.
When a string is evaluated, from left to right:
- If the segment is unescaped text, append it to the result.
- If the segment is one or more escape sequences, append decoded
escape sequences to the result.
- If the segment is an expression, resolve the expression and convert
it to a string using \(ToString\) (Section 2.6.3), and append the string to the
result.
There is no semantic difference between single- and double-quoted
strings beyond delimiter rules.
Invalid Unicode escape sequences make the string literal invalid at
parse time.
Examples
| Expression |
Evaluation |
Notes |
"Hello" |
"Hello" |
Simple double-quoted string. |
'Alice\'s House' |
"Alice's House" |
Escaped single quote in a single-quoted string. |
"Cost: $100" |
"Cost: $100" |
Raw $ is literal because it isn’t followed by
{. |
"Hi ${user.name}" |
"Hi Alice" |
Interpolates the value of user.name. |
"Value: \${1}" |
"Value: ${1}" |
Escaped ${ prevents interpolation. |
"\u00A9" |
"©" |
Unicode escape for the Copyright symbol. |
"Line\nBreak" |
A string with a newline |
Standard escape sequence. |
"\uD83D\uDE00" |
"😀" |
Decoded surrogate pair. |
Array Literals
Syntax
Array literals consist of a comma separated sequence of expressions,
surrounded by square brackets.
ArrayLiteral ← "[" S (ArrayItem (S "," S ArrayItem)*)? S ","? S "]"
ArrayItem ← SpreadExpr / Expression
SpreadExpr ← "..." Expression
Trailing commas are permitted. If an array literal contains a single
StringLiteral item, it must be followed by a comma to
syntactically differentiate it from a root variable using bracket
notation.
Semantics
An array literal constructs an immutable ordered collections of
values. They are evaluated eagerly from left to right. Each
Expression is resolved to a value and inserted into a new
Array instance, \(Array\langle RuntimeValue
\rangle\).
Arrays are heterogenous; they may contain values of any type,
including other arrays and objects.
The Spread Operator
The spread operator ... allows for the expansion of an
iterable into the array literal.
- The expression following
... is evaluated and coerced
via ToIterable(x) (see Section 2.6.6).
- Each of the elements in the resulting iterable is inserted into the
new array at the current position.
Note that \(ToIterable\) is total;
the spread operator can not fail.
Examples
Given a context: {"low": [1, 2], "high": [4, 5]}
| Expression |
Evaluation |
Notes |
[1, 2, 3] |
[1, 2, 3] |
A simple array of numbers. |
[1, "two", true] |
[1, "two", true] |
A heterogeneous array. |
[...low, 3, ...high] |
[1, 2, 3, 4, 5] |
Spread operators merging two arrays with a literal. |
[1, , 2] |
Invalid |
Empty items between commas are not permitted. |
[1, 2,] |
[1, 2] |
Trailing comma is ignored. |
[...null] |
[] |
Null is coerced to an empty array by \(ToIterable\). |
[1, missing_var, 3] |
[1, Nothing, 3] |
Arrays can contain Nothing. |
Object Literals
Syntax
Object literals consist of a comma-separated sequence of key-value
pairs or spread expressions, surrounded by curly braces.
ObjectLiteral ← "{" S (ObjectItem (S "," S ObjectItem)*)? S ","? S "}"
ObjectItem ← SpreadExpr / KeyValuePair
KeyValuePair ← ObjectKey S ":" S Expression
ObjectKey ← Identifier / QuotedName
QuotedName ← ( "\"" DoubleQuotedName "\"" ) / ( "'" SingleQuotedName "'" )
QuotedName follows the same escaping rules as
StringLiteral but does not support
interpolation. This ensures that object keys are statically
determinable at parse-time.
QuotedName ← DoubleQuotedName / SingleQuotedName
DoubleQuotedName ← "\"" ( DoubleEscapedName / NameSourceChar / "'")* "\""
SingleQuotedName ← "'" ( SingleEscapedName / NameSourceChar / "\"")* "'"
DoubleEscapedName ← "\\" ( "\"" / EscapableNameChar )
SingleEscapedName ← "\\" ( "'" / EscapableNameChar )
EscapableNameChar ← "b" / "f" / "n" / "r" / "t" / "/" / "\\" / ("u" ~ HexChar)
NameSourceChar ← " " /
"!" /
"#" /
"$" /
"%" /
"&" /
[\u0028-\u0058] /
[\u005D-\uD7FF] /
[\uE000-\u0010FFFF]
Semantics
An object literal constructs an immutable collection of key-value
pairs, \(Object\langle String, RuntimeValue
\rangle\).
The literal is evaluated eagerly from left to right. If the same key
is defined multiple times (either via direct assignment or via the
spread operator), the last value evaluated takes
precedence.
Once constructed, the object and its key-set cannot be modified.
Key Resolution
- If a key is an
Identifier, it is treated as a literal
string (e.g., { name: "Alice" } uses the string
"name" as the key).
- If a key is a
QuotedName, the quotes are stripped and
the interior is processed for escapes to produce the string key.
The Spread Operator
The spread operator ... allows for the merging of
another object into the one being constructed.
- The expression following
... is evaluated and coerced
via ToObject(x) (see Section 2.6.5).
- Each key-value pair in the resulting object is inserted into the new
object.
- If the value cannot be coerced into an object, the spread is ignored
(as \(ToObject\) is total).
Examples
Given a context:
{"base": {"id": 1, "role": "guest"}}
| Expression |
Evaluation |
Notes |
{ name: "Bob", age: 30 } |
{"name": "Bob", "age": 30} |
Standard object with identifier keys. |
{ "First Name": "Bob" } |
{"First Name": "Bob"} |
Quoted keys allow for whitespace and reserved characters. |
{ ...base, role: "admin" } |
{"id": 1, "role": "admin"} |
The role key from base is overwritten by
the literal. |
{ a: 1, a: 2 } |
{"a": 2} |
Last key wins rule applies. |
{ ...null, count: 0 } |
{"count": 0} |
Spreading a non-object value results in an empty merge. |
{ "${name}": 1 } |
Invalid |
Object keys do not support interpolation. |
Range Literals
Syntax
Range literals consist of two expressions separated by a double-dot
(..), enclosed in parentheses.
RangeLiteral ← "(" S Expression S ".." S Expression S ")"
Semantics
A range literal constructs an immutable, inclusive sequence of
integers.
- The
start (left) and end (right)
expressions are evaluated eagerly before coercing both values to numbers
using \(ToNumber\) (see Section 2.6.2).
- If a value is a decimal, truncate toward zero.
- If either expression evaluates to
Nothing, the range
literal evaluates to an empty sequence.
- The range is inclusive of both the start and end values.
- If
start > end, the range results in an
empty sequence. It does not count backward.
Implementations MAY represent this value as:
- An eager \(Array\langle RuntimeValue
\rangle\), or
- A \(Drop\) implementing the \(Sequence\) protocol.
The observable behavior MUST be indistinguishable.
Implementations MAY define an upper limit to the number of items in a
range to guard against excessively large array materialization.
Examples
| Expression |
Evaluation |
Notes |
(1..5) |
[1, 2, 3, 4, 5] |
Standard ascending inclusive range. |
(5..1) |
[] |
Start is greater than end; results in empty sequence. |
(1.2..3.8) |
[1, 2, 3] |
Values are coerced to integers before generation. |
(1..1) |
[1] |
Single-item sequence. |
(0..user.count) |
[0, 1, 2] |
Dynamic range (assuming user.count is 2). |
(1..null) |
[] |
null coerces to Nothing, resulting in an
empty range. |
Identifiers
Identifiers are the names used to reference variables, properties,
filters, and keyword arguments.
Syntax
An identifier MUST start with an alphabetic character (a-z, A-Z), an
underscore (_), or a supported non-ASCII Unicode character.
It cannot start with a digit.
After the first character, identifiers may contain any combination of
alphanumeric characters, underscores, and supported Unicode ranges.
Identifier ← IdentifierFirst IdentifierChar* !"?"
IdentifierFirst ← [a-zA-Z_] / [\u{80}-\u{D7FF}] / [\u{E000}-\u{10FFFF}]
IdentifierChar ← IdentifierFirst / [0-9]
Note: The negative lookahead !"?" ensures that an
identifier is not immediately followed by a question mark. This reserves
that specific syntactic pattern for Predicates (see Section 3.3.2.2).
Semantics
Identifiers are strictly case-sensitive. myVariable and
myvariable are treated as two distinct identifiers.
A sequence of characters that would otherwise be a valid identifier
is NOT considered an Identifier if it is followed by a
?. Such sequences are instead parsed as part of a
Predicate. Note that ? is not a valid
identifier character.
Examples
| Identifier |
Validity |
Notes |
user_name |
Valid |
Standard snake_case identifier. |
_secret |
Valid |
Starts with an underscore. |
item2 |
Valid |
Contains a digit (but not at the start). |
dâtâ |
Valid |
Contains non-ASCII Unicode characters. |
2fast |
Invalid |
Cannot start with a digit. |
is_valid? |
Invalid |
Matches the syntax for a Predicate, not an
Identifier. |
Variables and Paths
Syntax
A variable consists of a “root segment” followed by zero or more
segments that make up a “path” to a value.
Segments can be in bracket notation, [<selector>],
or shorthand notation, .some_name. Shorthand notation is
only available for names that are valid identifiers.
Both single and double quoted names are allowed in bracket
notation.
The last segment of a path may be a predicate (Section 3.3.2.2) - an identifier followed by
a question mark ?.
Variable ← VariableRoot Path?
VariableRoot ← Identifier /
"[" S StringLiteral S "]"
Path ← (S Segment)+ Predicate?
Segment ← "." Identifier /
"[" S StringLiteral / Integer / QuerySelector S "]"
QuerySelector ← VariableRoot (S Segment)*
Predicate ← "." IdentifierFirst IdentifierChar "?"
Note: An Identifier cannot include a ?.
A Predicate is syntactically distinguished by its trailing
question mark and its position as the terminal segment of a variable
path.
Semantics
Variable resolution is a sequential process where each part of the
path (the Selector) is resolved against the value
produced by the preceding part.
To determine if a selector can be applied, the language distinguishes
between two categories of values:
Structural Values: Values that can contain other
values. This includes \(Array\), \(Object\), and \(Drop\) that implements a lookup
protocol.
Non-Structural Values: Scalar or terminal values
that do not support internal navigation. This includes \(Strings\), \(Number\), \(Boolean\), \(Null\), and \(Nothing\).
The Resolution Chain
- Resolution starts by looking up the
VariableRoot in the
current execution context.
- For each subsequent
Segment (Dot or Bracket):
- If the current value is Structural, the engine
attempts to resolve the selector against it (e.g., looking up a key in
an Object or an index in an Array). If selector lookup fails (key not
found, index out of bounds or non string/integer selector), the segment
resolves to
Nothing.
- If the current value is Non-Structural, the segment
resolves to
Nothing.
- Path Termination:
- Standard Path: If the variable does not end in a predicate, the
final value of the last segment is the result of the expression. If any
segment in the chain resolved to
Nothing, the entire
variable expression resolves to Nothing.
- Predicate Path: If the variable ends in a
Predicate,
the predicate is invoked using the result of the preceding segment as
its input.
Predicates
A predicate is a named function registered with the environment. All
predicates are total over \(RuntimeValue\) and MUST return \(Boolean\).
\[
Predicate : RuntimeValue → Boolean
\]
For any predicate \(.p?\) and
accompanying abstract function \(IsP\),
x.p? is semantically equivalent to \(IsP(x)\).
Unlike standard segments, Predicates are
specifically designed to handle the absence of a value.
- The predicate is evaluated against the result of the path preceding
it. If the path resolution fails at any point, the predicate receives
Nothing as its input.
- A predicate is always invoked, even if the preceding path
resolution resulted in
Nothing.
- This allows a path like
user.profile.defined? to return
false rather than Nothing if
user.profile does not exist.
- If a predicate does not exist in the environment, the result is
false. Implementations MAY issue a warning or error at
parse time in the event of an unknown predicate.
Predicate Definitions
Here we define abstract predicate functions that are expected to be
implemented.
blank?
Returns true for empty textual or collection values.
Note that Nothing is distinct from Null and is
not considered blank.
IsBlank(x) =
x is Null
OR x is String and trim(x) = ""
OR x is Array and length(x) = 0
OR x is Object and size(x) = 0
OTHERWISE false
The absence of a value (Nothing) is not considered
blank.
empty?
Returns true for values that are empty collections or
empty strings. As with IsBlank, Nothing is not
considered empty.
IsEmpty(x) =
x is String and length(x) = 0
OR x is Array and length(x) = 0
OR x is Object and size(x) = 0
OTHERWISE false
defined?
Returns true if the input is any value other than
Nothing.
IsDefined(Nothing) → false
Otherwise → true
string?
Returns true if the input is of type \(String\).
IsString(x) =
x is String → true
otherwise → false
null?
Returns true if the input is of type \(Null\).
IsNull(x) =
x is Null → true
otherwise → false
number?
Returns true if the input is of type \(Number\).
IsNumber(x) =
x is Number → true
otherwise → false
boolean?
Returns true if the input is of type \(Boolean\).
IsBoolean(x) =
x is Boolean → true
otherwise → false
array?
Returns true if the input is of type \(Array\).
IsArray(x) =
x is Array → true
otherwise → false
object?
Returns true if the input is of type \(Object\).
IsObject(x) =
x is Object → true
otherwise → false
Examples
Given a context: {"user": {"tags": ["admin"]}}
| Expression |
Evaluation |
Notes |
user.tags[0] |
"admin" |
Path through structural values (Object -> Array). |
user.id |
Nothing |
user is structural, but id is
missing. |
user.id.type |
Nothing |
user.id is Nothing (non-structural), so
.type resolves to Nothing. |
user.tags.array? |
true |
Predicate invoked on a valid Array. |
user.id.defined? |
false |
user.id is Nothing. The predicate handles
Nothing and returns false. |
["missing"].defined? |
false |
The root is missing (Nothing), but the predicate still
evaluates to a Boolean. |
Given a context: {"user": {"name": " ", "tags": []}}
| Expression |
Evaluation |
Notes |
user.name.blank? |
true |
String is whitespace only. |
user.name.empty? |
false |
String has length > 0. |
user.tags.empty? |
true |
Array has length 0. |
user.id.defined? |
false |
id is missing (Nothing). |
user.id.null? |
false |
Nothing is not Null. |
user.tags.array? |
true |
Correct type check. |
["missing"].defined? |
false |
Root lookup failed, predicate handles Nothing. |
Operators
The following table lists operators from highest to lowest
precedence. Operators within the same group are evaluated left-to-right
(left-associative).
| Operator Type |
Syntax |
| Primary |
Literals, Variables, ( expr ) |
| Unary |
+, - (Positive/Negative) |
| Multiplicative |
*, /, % |
| Additive |
+, - |
| Pipe (Filter) |
expr | filter |
| Comparison/Membership |
==, !=, <,
>, <=, >=,
contains, in |
| Logical NOT |
not |
| Logical AND |
and |
| Logical OR |
or |
| Nothing Coalesce |
orElse |
| Ternary |
consequence if condition else alternative |
Ternary Expressions
Syntax
The ternary expression follows a “Python-style” postfix conditional
syntax.
TernaryExpression ← CoalesceExpression ( "if" !C CoalesceExpression "else" !C CoalesceExpression )?
The keywords if and else are followed by a
negative lookahead !C to ensure they are not parsed as
prefixes of identifiers.
Semantics
Ternary expressions allow for branching logic within a single line.
They prioritize the “happy path” by placing the consequence first.
- The Condition (
CoalesceExpression) is
evaluated first.
- The result is coerced to a Boolean via
ToBoolean(x).
- If
true, the Consequence is evaluated
and returned; the alternative is short-circuited.
- If
false, the Alternative is evaluated
and returned; the consequence is short-circuited.
Examples
TODO: better examples
| Expression |
Evaluation |
Notes |
"Hi" if true else "Bye" |
"Hi" |
Basic usage. |
a if b else c if d else e |
Invalid |
Syntax error: c if d else e is not a
PipeExpression. |
a if b else (c if d else e) |
— |
Valid: Parentheses restore the Expression context. |
‘a’ | upcase if true else ‘b’ |
"A" |
|
‘a’ if false else ‘b’ | upcase |
"B" |
|
(‘a’ if true else ‘b’) | upcase |
"A" |
|
Nothing Coalescing Operator
Syntax
The Nothing coalescing operator (orElse) is a binary
operator used to provide a fallback for unresolved paths or missing
data.
CoalesceExpression ← OrExpression ( S "orElse" !C S OrExpression )*
Semantics
The orElse operator evaluates its operands from left to
right and returns the first value that is not Nothing.
The operator triggers a fallback only if the
left-hand side evaluates to Nothing (the signal for a
failed resolution or non-existent variable). If the left-hand side is
any value other than Nothing (including false,
0, or ""), the right-hand side is not
evaluated.
orElse binds more loosely than logical or,
meaning a or b orElse c is evaluated as
(a or b) orElse c.
And orElse operator binds more tightly than the pipe
operator (|), so a orElse b | f is evaluated
as (a orElse b) | f.
Examples
Given a context: {"id": 0, "status": null}
| Expression |
Evaluation |
Notes |
id orElse 1 |
0 |
0 is not Nothing. |
status orElse "active" |
null |
null is valid data; no fallback. |
missing_var orElse "fallback" |
"fallback" |
Nothing triggers the fallback. |
user.profile.id orElse 1 |
1 |
Failed path resolution results in Nothing. |
null orElse "fallback" |
null |
Consistent with the “Null is Data” rule. |
Logical Operators
Syntax
Logical operators perform boolean algebra and support short-circuit
evaluation.
OrExpression ← AndExpression ( S "or" !C S AndExpression )*
AndExpression ← PrefixExpression ( S "and" !C S PrefixExpression )*
PrefixExpression ← NotExpression / TestExpression
NotExpression ← "not" !C S PrefixExpression
Semantics
Logical operators rely on the \(IsTruthy(x)\) abstract function to evaluate
operands (see Section 2.6.1).
or returns the first truthy operand. If
all operands are falsy, it returns the result of the last operand.
Short-circuits if a truthy value is found.
and returns the first falsy operand. If
all operands are truthy, it returns the result of the last operand.
Short-circuits if a falsy value is found.
not is a unary operator that returns true
if the operand is falsy, and false if the operand is
truthy.
Examples
| Expression |
Evaluation |
Notes |
true or false |
true |
Standard boolean logic. |
null or "default" |
"default" |
null is falsy. |
0 or "fallback" |
"fallback" |
0 is falsy. |
"hi" and true |
true |
"hi" is truthy; and returns the last
value. |
missing_var and true |
Nothing |
Nothing is falsy; and returns the first
falsy value found. |
not null |
true |
null is falsy, so not null is
true. |
not 0 |
true |
0 is falsy. |
Comparison Operators
Syntax
Comparison operators ==, !=,
<=, >=, <,
> test for equality and ordering.
TestExpression ← PipeExpression ( S TestOperator S PipeExpression )?
TestOperator ← "==" / "!=" / "<=" / ">=" / "<" / ">" / ("in" !C) / ("contains" !C)
Semantics
Comparison is total; if two types are fundamentally
incomparable and no protocol is present, the result is
false rather than an error.
\[
==, !=, <, >, <=, >= : RuntimeValue × RuntimeValue → Boolean
\]
We first define == and <, then
!=, >, <= and
>= in terms of == and
<.
A comparison using the operator == evaluates to true if
the comparison is between:
Nothing and Nothing.
- two values that represent the same data.
- arrays of the same length where each element of the first array is
equal to the corresponding element in the second array.
- objects with the same collection of names and, for each of those
names, the associated values are equal.
- drops:
- If
a is a drop and implements \(Equals\) and a.Equals(b) is
true.
- If
b is a drop and implements \(Equals\) and b.Equals(a) is
true.
- Otherwise coerce both via \(ToPrimitive(…,
data)\) and compare the results.
A comparison using the operator < yields true if the
comparison is between values that are both numbers, both strings or
either is a \(Drop\) and that satisfy
the comparison:
- For numbers: numeric ordering is used. Integers and floating values
are compared by their numeric value; implementations may use a single
representation (e.g. IEEE 754 BigDecimal) but comparisons must behave as
the mathematical numeric ordering (smaller → less-than).
- For strings: by default strings are ordered by Unicode scalar (code
point) order. Implementations MAY provide an option to use the Unicode
Collation Algorithm (UCA) for locale sensitive ordering, but the
canonical spec semantics are Unicode code point ordering to ensure
deterministic results.
- For drops:
- If
a is Drop and implements
LessThan and a.LessThan(b) is true.
- If
b is Drop and implements
LessThan and b.LessThan(a) is true.
- Otherwise coerce both via \(ToPrimitive(…,
data)\) and attempt standard ordering on the results.
!=, >, <= and
>= are defined in terms of == and
<.
\[
\begin{aligned}
x \;\texttt{!=}\; y &\to \text{not}(x \;\texttt{==}\; y) \\
x \;\texttt{>}\; y &\to y \;\texttt{<}\; x \\
x \;\texttt{<=}\; y &\to (x \;\texttt{<}\; y) \;\text{or}\; (x
\;\texttt{==}\; y) \\
x \;\texttt{>=}\; y &\to (y \;\texttt{<}\; x) \;\text{or}\; (x
\;\texttt{==}\; y)
\end{aligned}
\]
Examples
| Expression |
Evaluation |
Notes |
0 == 0.0 |
true |
Numeric normalization. |
"" == null |
false |
Different types, both are falsy but not equal. |
5 > "2" |
false |
Type mismatch defaults to false. |
(1 + 1) == 2 |
true |
Arithmetic is resolved before comparison. |
Membership Operators
Syntax
TestExpression ← PipeExpression ( S TestOperator S PipeExpression )?
TestOperator ← "==" / "!=" / "<=" / ">=" / "<" / ">" / ("in" !C) / ("contains" !C)
Semantics
Membership operators in and contains
determine whether a value appears in a container. Both operators return
\(Boolean\).
element in container
container contains element
These forms are semantically equivalent with operands reversed.
Evaluation proceeds as follows.
- If
container is a Drop that implements the
\(Membership\) protocol, call
contains(element):
- If the result is
Boolean, return that value.
- If the result is
Nothing, continue evaluation.
- If
container and element are both \(String\):
- Test for substring inclusion. Return
true if
element occurs within container, otherwise
false.
- If
container is an Object, membership
tests for the existence of a key.
- Return
true if key exists in the object,
otherwise false.
- Otherwise coerce to an iterable using \(ToIterable\) and iterate the resulting
sequence.
- Return
true if key exists in the sequence,
otherwise false.
Examples
| Expression |
Evaluation |
Notes |
3 in [1,2,3] |
true |
Array is iterable. |
3 in (1..5) |
true |
Range literals are iterable. |
5 in 5 |
true |
5 is coerced to [5] by \(ToIterable\). |
"a" in "cat" |
true |
Substring membership. |
"name" in {name:1} |
true |
Object property membership. |
Pipe Operator (Filters)
A filter is a named total function registered in the
environment. Formally:
\[
\begin{aligned}
FilterFunction \;&: RuntimeValue × List\langle FilterArgument
\rangle → RuntimeValue \\
FilterArgument \;&: RuntimeValue | Lambda
\end{aligned}
\]
Syntax
PipeExpression ← ArithmeticExpression ( S "|" S FilterInvocation )*
FilterInvocation ← Variable ( S ":" S ArgumentList )?
ArgumentList ← Argument ( S "," S Argument )*
Argument ← LambdaArgument / KeywordArgument / ArgumentExpression
KeywordArgument ← Identifier S ( "=" / ":" ) S ( LambdaArgument / ArgumentExpression )
LambdaArgument ← LambdaParameters S ( "=>" / "->" ) S ArgumentExpression
ArgumentExpression ← ArithmeticExpression
Semantics
Every filter application has at least one argument: the
input. This is the value resulting from the expression
immediately to the left of the pipe. In the conceptual mapping to a
standard function, the input always occupies the first positional slot
(index 0).
Conceptually, a filter application such as
input | filter: arg1, key=arg2 is evaluated as
filter(input, arg1, key: arg2).
The evaluation of a PipeExpression follows a linear,
left-to-right transformation of data. Each filter in the chain acts as a
function that accepts the current result of the pipeline as its first
implicit argument.
Arguments
Positional arguments are additional values passed to a filter,
identified by their sequence following the filter name. The first
explicit argument provided in the template is treated as the second
argument to the underlying function (index 1), the next as the third
(index 2), and so on.
Keyword arguments allow values to be passed to specific named
parameters of a filter. Keyword arguments may appear in any order
relative to each other, provided they follow all positional arguments.
The assignment operators = and : are treated
as semantically identical. They serve only to bind the expression value
to the named parameter.
Lambda Expressions
Lambda expressions provide a way to define anonymous, inline
functions that are passed directly to filters. They allow filters to
execute custom logic over collections, such as mapping values, filtering
arrays, or applying custom sorting rules.
- When evaluated by the filter, the body expression has access to the
internal parameters passed into it by the filter.
- The body expression also retains read-access to all variables, and
context values that were available in the surrounding template scope at
the time the lambda was defined.
- If a lambda parameter shares a name with a variable in the
surrounding scope, the lambda parameter strictly shadows the outer
variable for the duration of the lambda’s execution.
Well-Typed Filters
Filter implementations may be accompanied by a
Signature. This signature is a piece of metadata
intended for tooling, static analysis, and ahead-of-time (AOT)
validation.
Signatures allow compilers or IDEs to provide autocomplete, verify
argument counts, and check for the presence of required keyword
arguments before the template is executed. The execution engine MUST NOT
depend on this metadata to perform a filter invocation. The engine’s
role is strictly to evaluate arguments and dispatch the resulting values
to the filter implementation.
Totality Requirement
All filters MUST be total functions. A filter is
considered total if it returns a valid value for every possible
combination of input and arguments.
- A filter must never throw an exception or halt the execution of the
template.
- If a transformation is mathematically or logically impossible (e.g.,
division by zero, or passing a
String to a filter that only
handles Numbers and cannot coerce the value), the filter
MUST return Nothing.
Filters MUST NOT raise errors due to incorrect arity:
Too Few Arguments If a required (non-optional,
non-variadic) parameter has no corresponding argument, the filter
invocation evaluates to Nothing.
Too Many Arguments If extra arguments are
supplied and the filter does not declare a variadic parameter, the
filter invocation evaluates to Nothing.
Variadic Parameters If a variadic parameter is
declared, all remaining arguments are collected into a list and passed
as separate positional arguments or as an array, according to the
implementation’s calling convention.
Filters that are unknown to the environment evaluate to
Nothing. Implementations MAY throw an error or warning at
parse time in the event of an unknown filter.
Examples
| Expression |
Structure |
Notes |
val | upcase |
Simple invocation |
No arguments. |
val | round: 2 |
Positional argument |
A single numeric literal argument. |
val | replace: “a”, “b” |
Multiple arguments |
Comma-separated positionals. |
val | date: format = “%Y” |
Keyword argument |
Uses = for key-value assignment. |
users | map: u => u.name |
Lambda argument |
Single parameter lambda for transformation. |
users | sort: (a, b) => a.age - b.age |
Multi-arg lambda |
Grouped parameters for comparison logic. |
val | f: (other | g) |
Nested Pipe |
Parentheses are required to pipe inside an argument. |
Arithmetic Operators
Syntax
ArithmeticExpression ← AddExpression
AddExpression ← MulExpression ( S ( "+" / "-" ) S MulExpression )*
MulExpression ← UnaryExpression ( S ( "*" / "/" / "%" ) S UnaryExpression )*
UnaryExpression ← NegExpression / PosExpression / PrimaryExpression
NegExpression ← "-" UnaryExpression
PosExpression ← "+" UnaryExpression
Semantics
Arithmetic is performed using the Decimal Arithmetic
Model to ensure exact precision for base-10 decimals, avoiding
the rounding errors typical of binary floating-point systems.
All arithmetic operators are total. Operands are coerced to numbers
via \(ToNumber\). If an operand
resolves to Nothing, the entire arithmetic operation
resolves to Nothing.
+, -, *, /, % : RuntimeValue × RuntimeValue → Number | Nothing
+ , - (unary) : RuntimeValue → Number | Nothing
Arithmetic operators MUST share semantics with their filter
equivalents - plus, minus, times,
etc.
Division operator
Division uses true arithmetic division. Evaluation
proceeds as follows:
- Convert operands using
ToNumber.
- If either conversion yields
Nothing, the result is
Nothing.
- If the right hand side is equal to
0, the result is
Nothing.
- Otherwise compute the decimal quotient using the decimal arithmetic
model defined in Section 2.2.
- Normalize; if the result is mathematically an integer (i.e. it has
no fractional component), the result MUST be represented as an integer
value.
Modulo Operator
The modulo operator computes the remainder of division. Evaluation
proceeds as follows:
- Convert operands using \(ToNumber\).
- If either conversion yields
Nothing, the result is
Nothing.
- If the right hand side is equal to
0, the result is
Nothing.
- Otherwise compute the remainder using \(r
= x' - y' * floor(x' / y')\). The result has the
same sign as the divisor.
- If the result has no fractional component, it MUST be represented as
an integer.
Examples
| Expression |
Evaluation |
Notes |
4 / 2 |
2 |
Integer representation. |
0.1 + 0.2 |
0.3 |
Exact decimal arithmetic. |
10 / 4 |
2.5 |
True division (no integer truncation). |
"Item" + 1 |
Nothing |
"Item" coerces to Nothing. +
is not overloaded for concatenation. |
10 / 0 |
Nothing |
Division by zero is handled safely. |
5 * "abc" |
Nothing |
Incompatible types resolve to Nothing. |
5 % 2 |
1 |
|
4 % 2 |
0 |
|
-5 % 2 |
1 |
|
5 % -2 |
-1 |
|
5.5 % 2 |
1.5 |
|
Primary and Parenthesized Expressions
Syntax
PrimaryExpression ← Literal / ParenthesizedExpression / LambdaExpression / Variable
ParenthesizedExpression ← "(" S Expression S ")" Path?
Semantics
A \(PrimaryExpression\) evaluates to
a \(RuntimeValue\) as follows:
\(Literal\) evaluates to the
corresponding literal value (see Section 3.1).
\(Variable\) is evaluated via
environment lookup and path resolution (see Section 3.3).
\(ParenthesizedExpression\) is
evaluated by evaluating the inner expression, then applying the optional
path (see Section 3.3.2.1).
v = Eval(Expression)
result = ResolvePath(v, Path)
If no path is present, the result is simply v.
Examples
Given a context: {"a": {"c": ["42"]}}
| Expression |
Evaluation |
Notes |
(2 + 3) * 4 |
20 |
Override operator precedence with parentheses. |
(a or b).c |
42 |
Path applied on logical expression. |
Lambda Expressions
Syntax
A lambda expression consists of an argument declaration, an arrow
operator (=>), and a single expression body.
The left side of the arrow defines the parameters. This can be a
single variable name or a parenthesized, comma-separated list of
variable names. Parentheses are required if there are zero parameters or
multiple parameters.
The right side of the arrow is a single expression that determines
the return value of the lambda.
LambdaExpression ← LambdaParameters S ( "=>" / "->" ) S Expression
LambdaParameters ← Identifier / ( "(" S ( Identifier ( S "," S Identifier )* )? S ")" )
Semantics
Lambdas are stateless, meaning they do not capture the lexical
environment in which they are defined.
TODO: via drop protocol