| title | Expressions |
|---|---|
| description | Complete RF expression grammar, literal types, operators, precedence, typing rules, JSON encoding, and rejected syntax. |
RF expressions are a strict subset of HCL expressions. The accepted shape depends on position:
| Position | Purpose | Accepted expression family |
|---|---|---|
Rule or member match.where |
Test one resource instance's available values | Predicate |
Policy assert |
Test facts of a selected architecture stage within a Form | Policy assertion |
as, to, Policy target references |
Name a semantic symbol | Typed reference only |
via, by |
Read instance or saved-plan evidence | Traversal only |
| Static string fields | Metadata or closed enum | Constant expression producing a string |
target.rules, target.dialects |
Static target filters | Nonempty list with position-specific item type |
General HCL expression evaluation is not available in where or assert.
Static string fields use separate constant evaluation described below.
| Type | Native examples | Accepted positions | Notes |
|---|---|---|---|
| String | "prod", static heredoc |
Predicate comparison operand | Valid UTF-8; no interpolation |
| Boolean | true, false |
Predicate and Policy assertion | Lowercase keywords |
| Integer-valued number | 0, 3, 1.0, 1e3 |
Predicate comparison and Policy assertion | Exact integer value within signed 64-bit range |
| Floating point | 1.5 |
None | Rejected |
| Null | null |
None | Rejected |
| Collection or object | [], {} |
No general value position | Only dedicated static list fields accept lists |
RF validates numeric value, not spelling: 1.0 and 1e3 are accepted
because their values are exact integers. 1.5 is not. Although runtime scalar
evidence can contain signed integers, native RF source has no unary minus
operator. Authorable number literals are therefore non-negative. -1 is
rejected as an unsupported unary expression.
Static metadata strings may be empty only where the block-specific table permits
it. For example, definition description may be empty, while Policy
message may not.
A match.where predicate tests the instance currently considered by its
surrounding Rule or member match. Unknown or sensitive instance values stay
unknown, so a predicate cannot prove that such a candidate matches.
predicate = boolean-literal
| "!", predicate
| "(", predicate, ")"
| predicate, ("&&" | "||"), predicate
| comparison ;
comparison = predicate-operand, comparison-operator, predicate-operand ;
predicate-operand
= string-literal
| boolean-literal
| integer-valued-number-literal
| source-traversal ;
comparison-operator
= "==" | "!=" | "<" | "<=" | ">" | ">=" ;Examples:
where = source.enabled == true
where = source.mode == "ACTIVE"
where = source.replicas >= 2
where = source.enabled == true && source.replicas >= 2
where = !(source.mode == "DISABLED")
Rules:
- only
source.*traversal is accepted; - a bare Boolean literal is accepted;
- bare traversal is not a predicate;
- equality and inequality require the same runtime scalar type;
- ordering requires numbers;
- missing, unknown, collection-valued, or type-incompatible evidence makes the comparison unknown;
- only a known
trueaccepts a Rule candidate.
The compiler cannot always know a traversal's result type. A syntactically accepted
comparison such as source.enabled > true becomes unknown at evaluation,
rather than inventing ordering for Booleans.
Policy assertions operate on queries over facts in the selected architecture stage of a Form. They cannot traverse raw plan or state values.
assertion = boolean-value ;
boolean-value = boolean-literal
| exists-call
| "!", boolean-value
| "(", boolean-value, ")"
| boolean-value, ("&&" | "||"), boolean-value
| boolean-value, ("==" | "!="), boolean-value
| number-value, numeric-operator, number-value ;
number-value = integer-valued-number-literal
| length-call
| "(", number-value, ")" ;
numeric-operator
= "==" | "!=" | "<" | "<=" | ">" | ">=" ;
exists-call = "exists", "(", query, ")" ;
length-call = "length", "(", query, ")" ;
query = contexts-call | relations-call | contributions-call ;Grammar is type-directed and precedence follows table below. Because a comparison produces a Boolean, its result can participate in another Boolean equality or logical operation. HCL left associativity makes both expressions valid:
assert = true == false == false
assert = 1 < 2 == true
Practical examples:
assert = true
assert = exists(contexts(rf.context.network))
assert = length(contexts(rf.context.network)) >= 1
assert = !exists(relations(aws.relation.subscribes-to))
assert = (
exists(contexts(rf.context.network)) &&
length(contributions(aws.rule.s3-bucket-versioning)) == 0
)
This complete Pack verifies recursive Boolean results:
policy_pack "expression-results" {
version = "0.1.0"
}
policy "recursive-booleans" {
target {
rules = [aws.rule.subnet]
}
assert = (
true == false == false &&
1 < 2 == true
)
message = "Recursive Boolean expressions must remain true."
}
exists and length each take exactly one query call. A query call cannot be
stored, compared directly, nested in another function, or passed as a general
collection. See Built-ins for exact signatures.
| Operator | Operand type | Result | Predicate | Policy |
|---|---|---|---|---|
! |
Boolean | Boolean | Yes | Yes |
&&, || |
Boolean, Boolean | Boolean | Yes | Yes |
==, != |
Same scalar type | Boolean | String, Boolean, integer | Boolean or number |
<, <=, >, >= |
Integer, integer | Boolean | Yes | Yes |
No implicit conversion exists. "3" == 3 is never true. In Policy source it
is rejected by static typing; in predicate evaluation mismatched runtime types
produce unknown.
From highest to lowest:
| Precedence | Operators |
|---|---|
| 1 | Parentheses |
| 2 | Unary ! |
| 3 | <, <=, >, >= |
| 4 | ==, != |
| 5 | && |
| 6 | || |
Binary operators are left-associative. Use parentheses when mixing comparisons
or Boolean operators. A chained comparison such as 1 < source.count < 5 is
invalid; write source.count > 1 && source.count < 5.
Parentheses do not add an expression node to the compiled RF artifact; they only control grouping.
where = (source.enabled == true) && (source.replicas >= 2)
Fields such as version, description, message, kind, type,
provider constraints, and fact-match strategy use HCL constant evaluation
separately from predicate/assertion grammar.
Result must be known, non-null string in empty evaluation context. Therefore:
- quoted strings and static heredocs are accepted;
- parentheses and constant string conditionals are accepted;
- a pure
"${expression}"wrapper is accepted only when wrapped constant result is string; - variables and runtime traversals are unavailable;
- function calls are unavailable;
- multi-part interpolation is rejected.
For native .rf.hcl, acceptance is result-based: any HCL expression meeting
that rule is accepted, except a multi-part template. This constant-expression
surface is separate from closed where and assert grammars. In
.rf.json, static string fields are ordinary JSON strings.
description = "Production workload"
kind = true ? "resource" : "data"
message = <<-EOT
Workloads must declare a runtime.
EOT
Direct literals are canonical and recommended. Dynamic templates are rejected:
description = "Workload for ${source.environment}"
This complete Dialect demonstrates accepted constant string conditionals:
dialect "constant-example" {
version = true ? "0.1.0" : "9.9.9"
provider "hashicorp/example" {
version = true ? ">= 1.0.0" : "< 1.0.0"
}
}
concept "application" {
description = true ? "A deployable application." : "Unused"
}
rule "application" {
match {
kind = true ? "resource" : "data"
type = true ? "example_application" : "other"
}
as = concept.application
}
HCL JSON stores expression-valued fields in strings using "${...}":
{
"match": {
"type": "example_service",
"where": "${source.enabled == true}"
},
"as": "${concept.application}"
}Native .rf.hcl also accepts a pure "${expression}" wrapper for
full-expression fields and unwraps it to the enclosed value. Direct native syntax is
canonical:
where = source.enabled == true
Boolean and numeric JSON values can directly carry literal expressions. Raw
string fields such as message remain ordinary JSON strings. Rootform does
not parse arbitrary JSON strings as RF expressions.
In where and assert, Rootform language 0.1.0 rejects:
- arithmetic:
+,-,*,/,%; - unary minus;
- conditional expressions in
whereandassert; - multi-part templates and interpolation mixed with literal text;
- tuple, list, set, map, and object values outside dedicated list fields;
- comprehensions and
forexpressions; - splats;
- dynamic or string indexes;
- relative traversals;
- arbitrary function calls;
- expanded function arguments;
- bare architecture queries.
Examples:
where = source.enabled
where = source.replicas >= -1
where = source.cpu > 1.5
assert = contexts(rf.context.network)
assert = "yes"
assert = length(contexts(rf.context.network))
assert = length(contexts(rf.context.network)) + 1 > 1
Depending on position, these produce INVALID_EXPRESSION,
PREDICATE_UNRESOLVED, or POLICY_INVALID.
| Bound | Limit |
|---|---|
| Authored expression source | 4,096 bytes |
| Compiled expression tree depth | 64 |
| Compiled expression nodes | 1,024 per expression |
Exceeded source shape fails compilation. Compiled Policy Pack has additional aggregate limits under Diagnostics and limits.