Typed IR Reference

HFF compiles each query into an immutable, backend-independent typed program. This reference describes the Program entry point and the node tree's types, source spans, guards, callbacks, and payload accessors. Backends inspect this IR without depending on parser nodes; a syntax tree cannot be evaluated or translated to SQL.

Only HFF's compiler constructs valid programs and nodes. A node is an opaque handle; callers cannot mutate compiler storage. Programs and their values are safe to reuse concurrently.

Program and node accessors

The generated Go Reference lists the exact signatures and source comments for Program, Node, Method, and Binding. Use this page to interpret those accessors as a semantic tree:

Purpose Accessors
Enter the compiled tree and inspect its configuration Program.Root(), Source(), Schema(), Capabilities(), Limits(), UnlimitedLimits()
Identify a node's operation and result Node.Valid(), Kind(), Type(), Span(), MayNoValue()
Traverse children in evaluation order ChildCount(), Child(i)
Read literal, path, field, and operator payloads Literal(), Path(), Field(), Operator()
Inspect a resolved call or a type guard Method(), Constraint()
Inspect lexical scope and object entries Binding(), Parameters(), Captures(), Keys()

The zero Program and Node are invalid. A valid empty query has a MatchAll root. Child returns false for an invalid index. A payload accessor returns false for an inapplicable kind. Returned slices are copies; Value, Type, Method, and Binding are immutable values.

Program.Source returns the original query. Schema, Capabilities, and resource-limit accessors report the configuration used to compile it. Span is the node's half-open UTF-8 byte interval in the source. Type is the resolved result type, including explicit nullable union members. MayNoValue separately reports whether a failed guard can yield internal no-value. No-value is not a host-constructible value or schema type.

Path is available on variable and field nodes and returns the static literal path as a copy; dynamic indices remain child nodes. Field gives the literal field name on a field node. Operator gives the selected operator on binary and null-test nodes. Literal returns explicit literals, including null.

Node kinds and child order

NodeKind identifies these operations. The order of children and payloads is a compatibility contract. Child order follows source evaluation order. Keep existing kind values stable; append new kinds without renumbering or reusing values.

Kind Children, in order Payload accessors
InvalidNode None None; zero value only
MatchAllNode None None
LiteralNode None Literal
VariableNode None Path, Binding
ParameterNode None Binding
FieldNode Receiver Path, Field
IndexNode Receiver, index expression None
GuardNode Guarded value None
NullTestNode Tested value Operator (= or !=)
BinaryNode Left operand, right operand Operator
NegateNode Operand None
BoolNode Value None
AndNode Operands, left to right None
OrNode Operands, left to right None
NotNode Operand None
ListNode Elements in source order None
ObjectNode Entry values in source order Keys, aligned by index with children
CallNode Receiver, then arguments in source order Method
CallbackNode Body Parameters, Captures
TypeGuardNode Guarded value Constraint

A callback passed as a call argument occupies that argument's child position. Its parameter and capture bindings are metadata; its body is its child. An object's Keys slice aligns with the value children and is returned by copy. Groups that only affect precedence do not need runtime nodes; their source range remains represented by the compiled node span.

Backends must reject kinds and operators they do not support. Unknown kinds must fail safely so an additive IR change cannot silently produce an incorrect query. Optimization must preserve the child order and language behavior below.

Methods and callback bindings

A Method exposes its call name and its declared signatures. Each Signature contains a stable ID, receiver type, parameter types, result type, and variadic flag. For a CallNode, Method.Signatures() contains only the signatures statically applicable to that call, not the entire method registry. A concrete receiver may have one applicable signature; an Any or union receiver may have multiple family signatures for runtime dispatch. Every applicable signature must be enabled or compilation fails with a capability error. A TypeGuardNode narrows the receiver before method resolution, so only the signatures applicable to the narrowed type are checked. A runtime value incompatible with all applicable signatures yields null. A null-only receiver has no non-null family to select a signature, so all known applicable candidates must be enabled before null propagates at runtime. Each signature ID identifies its concrete family and overload. Returned signatures and parameter slices are copies. Backends use these IDs to dispatch without repeating common type resolution; they must preserve required runtime checks for dynamic types.

Constraint returns the user-specified type expression attached to a TypeGuardNode; it does not return the node's inferred result type. Type() reports the intersection with the receiver's known type, preserving known structural or element precision and nullable members. The runtime checker uses the original constraint. On a match, the node preserves the original value; a mismatch produces no-value and MayNoValue() is true. The accessor returns false for other node kinds.

Each Binding exposes its ID, source Name, and inferred Type. Both root variable nodes and callback parameter nodes return their binding through Binding(). A binding ID is unique within one Program across root variables and callback parameters, including nested and shadowed callbacks. Zero is invalid, and a payload accessor returns false when the ID is zero. IDs from different programs have no shared identity. Parameter bindings are listed in declaration order. Captures identify outer bindings by their existing IDs, so a backend need not resolve names or rebuild lexical scope.

Preserve language behavior

The IR keeps explicit null tests, guards, boolean conversion, and logical short-circuit nodes visible. x=null is a null test because the right side is an explicit null literal; x=(null) has the same meaning. A comparison with an expression that merely evaluates to null is an ordinary comparison. Optimizers must retain this distinction.

A failed guard produces no-value, distinct from null. MayNoValue allows a backend to see where that state can occur. Later access, comparisons, and calls follow the propagation rules in the language specification; exists() is an explicit presence check. AND and OR evaluate children left to right and skip later children once the result is determined. Calls evaluate the receiver and arguments in order; collections evaluate entries in source order. A backend must preserve these semantics when lowering or optimizing a program.

Explore HFF
Search documentation

Search the language specification and guides.