Evaluation

Evaluation uses data supplied by the host and has no side effects. The result of a complete filter is interpreted as a boolean. Null safety is intentional: it trades some ability to distinguish the source of a null for simpler queries and fewer runtime failures.

Implicit boolean conversion

When a non-boolean value occurs in a boolean context, convert it by its runtime type:

Value type True when
bool The value is true
number The value is non-null and not zero
string The value is non-null and not empty
list The value is non-null and contains at least one element
object The value is non-null and contains at least one field or entry
bytes The value is non-null and not empty
duration The value is non-null and not zero
timestamp The value is non-null, including the Unix epoch
null Never

Thus null, 0, "", false, {}, and [] are falsy.

age name

This filter requires a non-null, nonzero age and a non-null, nonempty name. An implementation may simplify these checks using schema knowledge while preserving results. Empty bytes and zero durations are also falsy; the Unix epoch is a valid timestamp and is truthy.

Prefix ! negates the converted boolean. For a nullable boolean flag:

flag Boolean conversion !flag
true true false
false false true
null false true

Therefore !flag means !(flag!=null flag!=false) when explicit null checks have their intended meaning. It is equivalent to flag=false for a non-nullable boolean, but that equivalence does not hold for a nullable boolean under the comparison rule below.

Null propagation

Null-safe access, ordinary receiver calls, and arithmetic propagate null:

Subexpression Result
null_obj.foo null when null_obj is null
labels.missing null when the map key is absent
null:size null
null+null null

A propagated null is indistinguishable from an explicit null value by ordinary value comparison. No implicit exception-handling expression is needed for these operations. The methods type(), json(), and zero-argument string() are exceptions: on a null receiver, each returns the string "null". exists() returns false for null or the internal no-value state from a failed guard. Other methods propagate no-value, except predicate callback conversion in any/all. Ordinary methods propagate null arguments. Membership methods in(any...) and list:contains(any...) can inspect a null search argument; their receiver still follows ordinary null propagation. object:has(string) checks key presence even when the stored value is null.

Arithmetic and operator domains

Arithmetic and unary minus operate on numbers; remainder requires int64. String concatenation and timestamp/duration operations use their named methods, rather than arithmetic overloads. Regex operators require string operands. Other statically incompatible combinations are rejected; applicable union or Any operations use runtime checks and return null for incompatible combinations.

Comparisons and null

Equality and inequality with an explicit null literal are null tests. All other comparisons involving a null operand return false. This gives:

Expression Input Result
x=null x is null true
x!=null x is null false
x!=null x is non-null true
x=true x is null false
x!=true x is null false
x~"A" x is null false
x!~"A" x is null false
!(x=true) x is null true

This makes !(x=y) and x!=y equivalent only for appropriate non-null operands. The same distinction applies to !~. A null obtained by evaluating another expression is not an explicit null test: if both x and y are null, x=y is false. Recognize explicit null tests before optimization; constant folding must not change a comparison into a different null-testing operation. Grouping an explicit null literal does not change its status: x=(null) is still an explicit null test. An expression that merely computes null is not.

RE2 matching uses ~ and !~, with the pattern on the right. These operators and matches() search for a matching substring. Use anchors such as ^ and $ to require a full-string match. Invalid patterns yield null, including under !~; non-match is not a way to turn an invalid pattern into true. HFF's reference regular-expression syntax follows RE2 except that the byte escape \C is excluded and is an invalid pattern, so it yields null. Backends may support a smaller regex subset; they must publish those restrictions and reject unsupported patterns rather than claim full HFF regex support. Numeric subtypes compare by numeric value. Otherwise, equality compares values of the same type. Lists and objects compare recursively by contents; stored null members compare equal to stored null members. This structural rule does not override top-level null comparisons. Lists compare length and elements in order. Objects compare their actual key sets and associated values, independent of struct/map schema representation. A missing key differs from a present null key. Within structural comparisons, different value families are unequal; numeric subtypes still compare numerically. Membership and uniqueness use this same structural value equality, including null search values. Ordering supports numbers, strings, timestamps, and durations. Statically incompatible comparisons are compile-time errors; dynamically incompatible operands from Any or unions yield null. Strings compare lexicographically by Unicode code point, without normalization.

Structural comparisons require the corresponding ListEquality or ObjectEquality capability; StructuralEquality selects both. Compilation checks the types that may actually be compared, recursively through compatible nested values. This applies to equality and inequality, in(), list:contains(), list:unique(), and list:distinct(). Comparing scalar list elements does not require list equality; comparing list values does. Explicit null tests are not structural comparisons and require no structural equality permission. For Any and unions, all statically possible composite comparison families must be enabled. An explicit type guard narrows that set: value:[list[string]]:unique compares scalar elements, whereas value:[list[string]]=["a"] compares list values and requires list equality.

Guarded paths

Ordinary access cannot establish where a null originated:

nullable.nullvalue=null

This may match because nullable is null, even if its nullvalue field was never reached. When the receiver is known to be non-null, foo.nullvalue=null can test the field directly. At the top level, nullvalue=null tests the variable's nullness without a parent path.

Explicit type guards

The postfix expression value:[Type] checks a runtime value against a type constraint and narrows the expression's static type. It never converts or rewrites the value. A matching value is returned unchanged; a mismatch produces internal no-value. A type guard on a value that is already no-value propagates no-value. It can guard a schema value or a computed expression, and no-value propagation follows the same rules as other guarded expressions.

For example, value:[string]:contains("x") calls string containment only when the guarded value is a string. Otherwise the guard yields no-value, so the call does not execute and the surrounding predicate converts to false. The existing no-value exception remains in effect: exists() reports false for no-value. type(), json(), and zero-argument string() propagate no-value unchanged. A guard narrowing a union or Any lets compilation check subsequent methods against the narrowed type. Without that guard, every statically applicable method signature must be permitted by capabilities, even when a particular runtime value would not use every signature. This is a compile-time permission rule; a disabled candidate does not become null or a runtime capability error.

Explicitly guarding a nullable receiver removes the ambiguity:

nullable!=null nullable.nullvalue=null

Postfix ! provides shorthand for those guards. In predicate position the following expressions have the stated expansions:

n1!.n2!.n3!.nullvalue=null
n1!=null n1.n2!=null n1.n2.n3!=null n1.n2.n3.nullvalue=null
n1!.n2!.n3!.nullvalue:exists
n1!=null n1.n2!=null n1.n2.n3!=null n1.n2.n3.nullvalue!=null

A failed guard must make these guarded predicates false. Simply replacing a failed guard with null would be incorrect for a subsequent =null comparison. Guards can also occur in value expressions. A failed guard produces an internal no-value state, distinct from null and inaccessible as a literal or host value. Subsequent access, arithmetic, and method calls propagate no-value. Comparisons involving no-value return false, including =null and !=null; boolean conversion maps no-value to false.

A successful guard always returns the original value, in every expression context. x! therefore retains normal truthiness when used as a predicate: zero, false, and empty values remain falsy. Use x:exists to ask whether a value is present and non-null. This method returns false for null and no-value, and true for every other value, including zero, false, and empty collections. It is an explicit exception to ordinary no-value propagation. Thus n1!.n2!.value:exists preserves the path's presence checks without giving postfix ! a context-dependent result.

Prefix NOT applies after boolean conversion, just as for other values. Thus !(a!.b=null) is true when the guard fails. Logical AND and OR combine the converted booleans; a failed guard in one branch does not invalidate another OR branch. No-value in an argument, list element, or object value propagates to the containing call or collection. Elements and entries are never silently dropped. No-value in a transformation or fold callback propagates to the entire method. Predicate callbacks in any() and all() instead apply boolean conversion, so no-value is a false predicate result for that element. Ordinary null callback results are retained by map(); flatmap() requires a list result and yields null if a callback produces another value type.

HFF restricts queries to filters over schema-declared data. Each predicate's comparison left operand, or its bare value expression, must depend on a schema-declared variable. The check applies before constant optimization.

Complete query Accepted? Reason
1+1 No Constant computation
10<20 No Constant comparison
10<age No The left operand does not relate to a field
age>10 Yes Field comparison
age+10<10 Yes The left operand derives from a field
age name Yes Predicates over declared data

Literals remain valid as operands, arguments, and collection contents. The restriction must therefore be checked in the semantic compiler rather than by removing literal expressions from the parser. Inner constant expressions are allowed: age>(10+2), (10+age)<20, and list:map(x=>1) are valid. A constant predicate such as true does not become valid merely because another AND or OR branch references a field. Callback bodies may produce constants; the containing method receiver supplies field dependence. Walk the filter's AND, OR, and NOT nodes through grouping and validate each leaf predicate. Once inside the value expression of a leaf, comparisons in arguments, indices, literal members, and callbacks do not start new field-dependence checks. For example, flag=(1<2) is valid, while age>10 | true is not. Empty input remains the explicit match-all exception.

Other runtime failures

Invalid conversions (including JSON, base64, timestamp, and number parsing), out-of-range or non-integer list indices, division by zero, integer overflow, and non-finite floating-point results yield null. Float parsing and arithmetic use the selected backend's declared approximate model; see the float.* details in its compatibility Profile. These rules also apply to constant operands: optimization must not turn an evaluation failure into a compile-time error. Syntax errors, statically invalid types, and unavailable features remain compile-time errors.

Services configure resource limits, including query length, depth, evaluation steps, and generated element counts. Exceeding a limit produces an explicit evaluation-cancellation result, distinct from null and from a completed boolean result. Partial evaluation must not be presented as a completed filter result. MaxJSONDepth defaults to 64 and counts the JSON root as depth one. It bounds JSON parsing and serialization, including JSON output produced by list/object string(). Exceeding it aborts with a limit error.

Evaluation order

AND and OR evaluate left-to-right and short-circuit once their boolean result is determined. Unevaluated branches consume no evaluation budget. Ordinary method calls evaluate their receiver and arguments left-to-right. bool:and(bool) and bool:or(bool) may skip an argument when the boolean receiver already determines the result, preserving ordinary null-receiver propagation. Arithmetic operands, call arguments, and collection entries are evaluated left-to-right. Once propagation determines the result, remaining evaluations may be omitted; omitted expressions consume no evaluation budget. Null list members and object values are ordinary data and do not themselves nullify a collection. A no-value member does propagate, allowing evaluation of remaining members to stop. Presence inspection and null-search methods retain their explicit exceptions. For example, a no-value receiver skips an ordinary method's arguments, but exists() consumes that receiver and returns false.

Method argument types remain strict; truthiness conversion belongs to logical operators and predicate contexts, including any/all predicate callbacks.

A backend must preserve results and document the execution constraints it can guarantee. It must not claim local-runtime step accounting or branch-evaluation guarantees that its processing system cannot enforce.

Presence and predicate examples

The following value examples distinguish presence from truthiness. The schema must admit the stated inputs; required-input validation still applies.

Expression Input Result
x x=0 false as a filter
x! x=0 false as a filter; the guarded value is still 0
x:exists x=0 true
x:exists x=false true
x:exists x=null false
parent!.value:exists parent=null false
items:any(x=>x.active) items=[] false
items:all(x=>x.active) items=[] true
items:any(x=>x!.active) items=[null,{"active":true}] true
items:all(x=>x!.active) items=[null,{"active":true}] false
Explore HFF
Search documentation

Search the language specification and guides.