Skip to content

Support OPA v1 'if' rules as single rules - #23

Merged
FallenAngel97 merged 1 commit into
FallenAngel97:masterfrom
lovesegfault:fix-v1-if-rules
Jul 6, 2026
Merged

Support OPA v1 'if' rules as single rules#23
FallenAngel97 merged 1 commit into
FallenAngel97:masterfrom
lovesegfault:fix-v1-if-rules

Conversation

@lovesegfault

Copy link
Copy Markdown
Contributor

Fixes #20.

Problem

A v1 rule like

allow := true if {
    input.admin
}

parsed as two rules: a value head (allow with a := true body) plus a phantom rule whose head variable is if. Since OPA 1.0 rejects bodied rules without if, virtually every current-syntax policy produced these split trees. The same split affected else chains, and else := 2 if { ... } clauses could swallow following rules.

Approach

The head grammar was missing the plain-var value form (var [":=" term] ["if"] from the OPA spec) — the bracket/args head forms already carried [rule-head-comp] [if], but a plain allow := true if had nowhere to go.

  • New rule alternative for the v1 comp+if form: value bound in the head (rule_head_v1, aliased to rule_head so consumers see the usual node), then a first body restricted to a braced query or single literal (rule_body_v1, aliased to rule_body). Restricting the first body means a following v0 rule that happens to be named if (a := 1 then if := 2) cannot be fused in — its := 2 is not a valid v1 body, so that GLR path dies and the two-rule parse wins. Dynamic precedence prefers the one-rule parse over the phantom split.
  • rule_head drops prec.right: it statically resolved var • := toward the head path, which would break plain constants (x := 1) once the comp+if form exists. The head forms instead share one prec.dynamic(1) so brackets, args, and if stay in the head rather than demoting to body literals (pinned by the new v0_heads.txt corpus cases at end-of-file, where the tie-break used to be arbitrary).
  • rule_head_comp accepts full expressions (f(x) := x + 1 if x > 0, roles := input.roles | {"anonymous"} if { ... }), not just plain terms.
  • The rule body value branch accepts = alongside :=, fixing plain unification constants (a = 1, a = 1 { true }) and terminal else = 2 clauses.
  • else clauses take an optional if (else := 2 if { ... }, else if input.b).

Corpus

  • conditionals and ordered (else) previously codified the split-parse trees; they now assert single rules.
  • union (Parsing error - union #8) and reference heads (Parsing error - reference heads #7) remain broken per their own issues; their expectations re-pin the current recovery trees (the reference heads recovery actually improves — one rule with the ERROR confined to the dotted segments instead of a phantom if split).
  • New v1_rules.txt (9 cases: braced/unbraced bodies, else-if chains, default+conditional, infix values, unification forms, constants staying separate) and v0_heads.txt (5 cases guarding v0 head shapes and the rule-named-if non-fusion).

53/53 corpus tests pass. src/ regenerated with tree-sitter generate.

Known remaining gaps

  • Bodiless partial-set rules (deny contains "msg" with no body) still swallow the following statement — unchanged from before; fixing it trips over the grammar's repeat1 body requirement and deserves its own change.
  • A v0 function literally named if directly following a constant rule can fuse into it. if is a reserved word in v1, so this only affects legacy policies defining a function named if.

Note: this branch is independent of #22; whichever merges second just needs tree-sitter generate re-run to resolve the generated-file overlap.

@FallenAngel97

Copy link
Copy Markdown
Owner

Hello @lovesegfault !

Thank you for your PR.

Can you please rebase it with the latest master? I fixed the yarn.lock, why it was failing - it was resolving for some reason towards the npm of Apple

@lovesegfault

Copy link
Copy Markdown
Contributor Author

done! :)

A v1 rule like `allow := true if { input.admin }` previously parsed
as two rules: a value head plus a phantom rule whose head variable
was `if`. Since OPA 1.0 rejects bodied rules without `if`, virtually
every current-syntax policy produced these split trees.

Grammar changes:

- A new rule alternative covers the v1 comp+if form (value bound in
  the head, `if`, then a braced query or single literal). Restricting
  the first body keeps a following v0 rule that happens to be named
  `if` from being fused in. Dynamic precedence prefers the one-rule
  parse over the phantom split.
- rule_head drops its prec.right wrapper, which statically resolved
  `var :=` toward the head and prevented GLR from exploring the
  constant-rule parse; the head forms instead share an explicit
  dynamic precedence so brackets, args, and `if` stay in the head
  rather than demoting to body literals.
- rule_head_comp accepts full expressions as values
  (`f(x) := x + 1 if ...`), not just plain terms.
- The rule_body value branch accepts `=` alongside `:=`, fixing plain
  unification constants (`a = 1`) and terminal `else = 2` clauses.
- else clauses take an optional `if` (`else := 2 if { ... }`).

Corpus: `conditionals` and `ordered (else)` expectations previously
codified the split-parse trees and now assert single rules; `union`
(FallenAngel97#8) and `reference heads` (FallenAngel97#7) remain broken per their own issues
and re-pin the current recovery trees; new v1_rules.txt and
v0_heads.txt cases cover the v1 forms and guard the v0 head shapes.

Known remaining gaps: bodiless partial-set rules
(`deny contains "msg"` with no body) still swallow the following
statement, as before this change; a v0 function literally named `if`
directly after a constant rule can fuse into it (`if` is a reserved
word in v1).

Fixes FallenAngel97#20
@FallenAngel97
FallenAngel97 merged commit c2a8c8f into FallenAngel97:master Jul 6, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OPA v1 'if' rules parse as two rules (phantom rule with head var 'if')

2 participants