Skip to content

RF expressions are a strict subset of HCL expressions. The accepted shape depends on position:

PositionPurposeAccepted expression family
Rule or member match.whereTest one resource instance's available valuesPredicate
Policy assertTest facts of a selected architecture stage within a FormPolicy assertion
as, to, Policy target referencesName a semantic symbolTyped reference only
via, byRead instance or saved-plan evidenceTraversal only
Static string fieldsMetadata or closed enumConstant expression producing a string
target.rules, target.dialectsStatic target filtersNonempty 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.

Literal types

TypeNative examplesAccepted positionsNotes
String"prod", static heredocPredicate comparison operandValid UTF-8; no interpolation
Booleantrue, falsePredicate and Policy assertionLowercase keywords
Integer-valued number0, 3, 1.0, 1e3Predicate comparison and Policy assertionExact integer value within signed 64-bit range
Floating point1.5NoneRejected
NullnullNoneRejected
Collection or object[], {}No general value positionOnly 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.

Predicate expressions

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.

EBNF
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:

valid predicates RF
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 true accepts 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

Policy assertions operate on queries over facts in the selected architecture stage of a Form. They cannot traverse raw plan or state values.

EBNF
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:

RF
assert = true == false == false
assert = 1 < 2 == true

Practical examples:

valid Policy assertions RF
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:

expression-results/pack.rf.hcl RF
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.

Operators and types

OperatorOperand typeResultPredicatePolicy
!BooleanBooleanYesYes
&&, ||Boolean, BooleanBooleanYesYes
==, !=Same scalar typeBooleanString, Boolean, integerBoolean or number
<, <=, >, >=Integer, integerBooleanYesYes

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.

Precedence and associativity

From highest to lowest:

PrecedenceOperators
1Parentheses
2Unary !
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

Parentheses do not add an expression node to the compiled RF artifact; they only control grouping.

RF
where = (source.enabled == true) && (source.replicas >= 2)

Static string expressions

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.

valid static strings RF
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:

invalid static string RF
description = "Workload for ${source.environment}"

This complete Dialect demonstrates accepted constant string conditionals:

constant-strings/dialect.rf.hcl RF
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
}

JSON expression carrier

HCL JSON stores expression-valued fields in strings using "${...}":

JSON
{
"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:

RF
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.

Rejected full-expression families

In where and assert, Rootform language 0.1.0 rejects:

  • arithmetic: +, -, *, /, %;
  • unary minus;
  • conditional expressions in where and assert;
  • multi-part templates and interpolation mixed with literal text;
  • tuple, list, set, map, and object values outside dedicated list fields;
  • comprehensions and for expressions;
  • splats;
  • dynamic or string indexes;
  • relative traversals;
  • arbitrary function calls;
  • expanded function arguments;
  • bare architecture queries.

Examples:

invalid expressions RF
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.

Bounds

BoundLimit
Authored expression source4,096 bytes
Compiled expression tree depth64
Compiled expression nodes1,024 per expression

Exceeded source shape fails compilation. Compiled Policy Pack has additional aggregate limits under Diagnostics and limits.