Skip to content

RFC-002 — Rule expression grammar

Status: ratified. Source: atomprd/specs/rfcs/RFC-002-rule-expression-grammar.md.

rule.expression is a string in a constrained mini-grammar — not arbitrary JavaScript / Python. The grammar is small, side-effect-free, and statically analysable.

expr ::= literal | ref | call | binop | unaryop | paren
literal ::= number | string | boolean | "null"
ref ::= identifier ("." identifier)*
call ::= identifier "(" [expr ("," expr)*] ")"
binop ::= expr op expr
unaryop ::= "!" expr | "-" expr
paren ::= "(" expr ")"
op ::= "+" | "-" | "*" | "/" | "%" | "&&" | "||" | "==" | "!=" | "<" | "<=" | ">" | ">="
length(state.draftHabitName) > 0
state.user.role == "admin" && state.feature_flag.streak_enabled
contains(state.allowed_emails, state.draftEmail)
state.amount >= 0 && state.amount <= 1000000
  • length(s) — string or array length.
  • contains(arr, x) — array membership.
  • startsWith(s, prefix) / endsWith(s, suffix).
  • isEmpty(x) / isNotEmpty(x).
  • match(s, pattern) — regex match (validator restricts pattern to a safe subset).
  • now() — ISO timestamp (codegen pins this at build time, not runtime, for determinism).
  • Function definitions.
  • Loops.
  • Mutation.
  • IO.
  • Reflection / eval.
  • Static analysability. The cloud authoring UI flags rules referencing undefined state slots / fields before they ship.
  • Safe to lower. Codegen targets translate the AST to TS / Python / Go without an embedded interpreter.
  • No “while true” footguns. A rule expression terminates trivially.

rule.expression is the predicate. rule.behavior.enforced_at is where the predicate is checked (UI button disabled, pre-call gate, validator).