Syntax
HFF has one syntax across services and backends. Capability selection changes which expressions compile, not which expressions can be parsed.
Input must be valid UTF-8. Escapes must decode to Unicode scalar values; paired surrogate escapes are decoded together, and unpaired surrogates are rejected. Strings use Unicode code points without implicit normalization. String length is a code-point count, not a byte or grapheme count.
Names and access
foo
foo.age
foo.father.name
`service.name`
`null`
foo names a schema-declared variable. A dot accesses a field of its receiver.
Backticks quote a name that would otherwise contain punctuation or be interpreted
as a literal keyword. Thus `service.name` is one name, while
service.name accesses name on service. `null` names a variable;
null is the null literal.
Unquoted names are case-sensitive ASCII identifiers: the first character is a
letter or underscore; following characters are letters, digits, or underscores.
Other names use backticks. The words true, false, and null are reserved
literal keywords. Inside a quoted name, backslash escapes only a backtick or
another backslash; other escapes are syntax errors. Quoted names may contain
any Unicode scalar value, including line breaks, and may be empty. Bare words are variable
references, not implicit string literals: use name="Alice" for a comparison.
Values
10
1.23
"a"
true
false
null
{"a": 10}
[1, "a", true]
{"a": "12h":duration}
[1:string]
The value families are number, string, bool, null, object, and list. Bytes,
timestamps, and durations can be supplied by the host or produced by methods.
Object values and list elements can contain expressions and method calls.
The last example uses the string() conversion in the method catalog.
Numbers and double-quoted strings use JSON lexical forms, including numeric
exponents and JSON string escapes. Negative numbers use unary minus. Empty
collections are allowed. Duplicate object literal keys are compile-time errors.
Comments and trailing commas are not supported.
Integer spelling selects int64; a decimal point or exponent selects float64.
Access punctuation, call punctuation, and prefix/postfix operators are compact:
a .b, a :len, and ! a are not alternative spellings.
Operators
| Form | Meaning |
|---|---|
!a |
Logical NOT after boolean conversion |
a|b |
Logical OR |
a b |
Logical AND |
a=b, a!=b |
Equality, inequality |
a~b, a!~b |
RE2 regular-expression match, non-match |
a<b, a<=b |
Less than, less than or equal |
a>b, a>=b |
Greater than, greater than or equal |
a+b, a-b |
Addition, subtraction |
-a |
Arithmetic negation |
a*b, a/b, a%b |
Multiplication, division, remainder |
Binary value expressions are compact: do not put spaces between an operand and
an arithmetic or comparison operator. Spaces between predicates mean AND.
This is a readability rule: a compact expression forms one visual condition,
and whitespace separates conditions. age>10 name="Alice" exposes these units
without requiring the reader to infer which spaces are inside an expression.
age > 10 is rejected. a -b joins predicates; a-b is subtraction.
foo.age>10 foo.father.name~"^A"
Spaces, tabs, and line breaks between predicates mean AND. Whitespace inside
string literals does not join predicates. Padding is allowed around |, inside
grouping parentheses, and around collection and argument separators; arithmetic
and comparison operators must remain adjacent to their operands. Empty or
whitespace-only input matches every record.
Grouping and precedence
AND binds more tightly than OR. Parentheses control grouping.
(age>=10 | age<=5) name="Alice"
age>=10 | age<=5 name="Alice"
For a record with age=10 and name="Bob", the first filter is false and the
second is true. The second groups as age>=10 | (age<=5 name="Alice").
Prefix ! applies to a comparison as a whole:
!sex="Female"
!(sex="Female")
These two expressions have the same grouping. They are equivalent to
sex!="Female" for non-null values; for null, the negated equality is true
and the inequality is false.
The precedence table, from weakest to strongest, is:
| Level | Operations | Associativity |
|---|---|---|
| 1 | | |
Left |
| 2 | Predicate separation (AND) | Left |
| 3 | Prefix ! |
Right |
| 4 | =, !=, ~, !~, <, <=, >, >= |
Non-associative |
| 5 | +, - |
Left |
| 6 | *, /, % |
Left |
| 7 | Prefix - |
Right |
| 8 | Field access, index access, method calls, postfix ! |
Left |
age+10<20 means (age+10)<20, and !a b means (!a) b.
A chain such as a<b<c is rejected; write separate predicates instead.
Indexing
a[i]
a[0]
a[-1]
List indices accept numbers whose represented value is an integer, including
1.0. They are zero-based. A negative index counts backward from the end:
-1 denotes the last element. Out-of-range and non-integer indices yield null.
Objects also support string-key indexing: labels[key]. Missing keys yield
null. Statically incompatible receiver/index types are rejected; incompatible
dynamic combinations yield null. Struct dot access requires a declared field;
a dynamic string-key lookup can report absence at runtime.
Explicit type guards
value:[string]
value:[list[string]|null]
value:[map[number]]
value:[string]:contains("x")
The postfix form value:[Type] checks a value against a type expression. The
colon immediately inside the brackets distinguishes a type guard from ordinary
indexing. The primitive type names are any, null, bool, number, int,
float, string, bytes, timestamp, duration, object, and list. int
denotes int64, and float denotes float64. list[T] and map[T] constrain
collection values; T|U denotes a union, and parentheses group a union when
needed. These names describe HFF types.
The guard preserves the original value when its type matches and produces
internal no-value when it does not. It is an expression and can guard a
computed value. Subsequent calls are resolved using the narrowed type. The
type-guard capability is selected by access:type-guard; parsing remains the
same for every capability set.
Method calls
a:b(a1, a2, a3)
name:lower:startswith("a")
name:len=name:len()
created_at:year=created_at:year()
The expression before : is the receiver. Arguments are comma-separated.
Whether a method is available depends on the receiver's schema type, the
configured capabilities, and the selected backend. See methods.
When a call has no arguments, its parentheses may be omitted: name:len means
the same call as name:len(). This also applies when a method has only optional
arguments, so created_at:year uses the method's default time zone just like
created_at:year(). Parentheses remain valid. Omitting required arguments is a
compile-time error. Ordinary free-function calls are not part of the supplied
design.
Arguments, indices, collection values, grouped expressions, and callback bodies
can contain logical expressions as well as arithmetic and comparisons. For
example, items:map(x=>x>10) produces booleans and flag=(1<2) compares to a
computed boolean. Method argument types are still checked strictly.
Higher-order methods use named arrow callbacks:
people:map(x=>x.age)
scores:foldl(0,(acc,x)=>acc+x)
scores:foldr(0,(x,acc)=>x+acc)
Parameters are inferred from the receiver and accumulator. Callbacks may capture
outer schema variables. They are method arguments, not general function values;
HFF provides no function declarations or recursion. Padding around => is
allowed. Callback scope and nesting are specified in the type-system contract.
Guarded paths
Postfix ! checks that the value exists and is not null; it is distinct from
prefix logical NOT. It can guard successive receivers:
n1!.n2!.n3!.nullvalue=null
n1!.n2!.n3!.nullvalue:exists
Their required predicate expansions are given in evaluation. Guards are also supported within value expressions, with the no-value propagation described in evaluation.
PEGO grammar and typed parsing
The syntax is defined using a .pego grammar with declared AST
types and actions, from which PEGO generates a typed Go parser using -types.
Typed parsing produces syntax nodes; it does not replace HFF's schema checking.
PEGO supports explicit grammar rules, typed structs and unions, and Pratt
operator levels. HFF must preserve meaningful predicate whitespace: a blanket
Pratt skip ws would lose the AND operator. The grammar should separate logical
predicate joining from compact value expressions and treat collection and
argument whitespace locally.
The grammar must encode precedence, distinguish prefix and postfix !,
recognize longer operators before their prefixes, and require complete input
consumption. Parsing must remain identical under every capability configuration;
unsupported features are diagnosed after parsing.