Native OCaml Rego/OPA policy engine
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

ocaml-rego: parser & lexer fixes for OPA conformance

- accept future-keyword identifiers in dotted refs so `import
future.keywords.in` and `import rego.v1` parse as ordinary imports.
- add `r=ref_expr op=assign_op v=expr b=rule_body` and the body-less
variant so `p = v { body }` and `p = v` are parsed as complete rules
instead of falling through to RuleDefault.
- introduce `infix_expr_no_in` / `expr_no_in` and a literal-level
`k, v in c` form (plus the parenthesised atom variant) so the
membership `in` operator and its keyed form both parse with the
expected precedence.
- treat the `contains` builtin as an identifier in atom position so
`contains("...", "...")` calls don't collide with the partial-set
`contains` keyword.
- in the lexer, peek past a synthetic SEMICOLON before `else` so
`} else` across a newline doesn't break the rule body's else_chain.

+114 -18
+44 -10
lib/lexer.ml
··· 93 93 true 94 94 | _ -> false 95 95 96 - type state = { lexbuf : Sedlexing.lexbuf; mutable prev : Parser.token } 96 + type state = { 97 + lexbuf : Sedlexing.lexbuf; 98 + mutable prev : Parser.token; 99 + mutable buffered : Parser.token option; 100 + (** Peeked-ahead token so we can suppress the synthetic SEMICOLON before 101 + [else] (the only Rego keyword that legitimately follows a rule body's 102 + [}] across a newline). *) 103 + } 104 + 105 + let v src = 106 + { 107 + lexbuf = Sedlexing.Utf8.from_string src; 108 + prev = Parser.EOF; 109 + buffered = None; 110 + } 97 111 98 - let v src = { lexbuf = Sedlexing.Utf8.from_string src; prev = Parser.EOF } 99 112 let positions st = Sedlexing.lexing_positions st.lexbuf 100 113 101 114 let rec raw_token lexbuf = ··· 144 157 raise (Error (Fmt.str "unexpected character: %S" s)) 145 158 | _ -> raise (Error "unexpected end of input") 146 159 160 + (** Skip newlines until we get a real token; returns that token. *) 161 + let rec next_real_token lexbuf = 162 + match raw_token lexbuf with 163 + | `Newline -> next_real_token lexbuf 164 + | `Token t -> t 165 + 147 166 let rec token st = 148 - match raw_token st.lexbuf with 149 - | `Token tok -> 167 + match st.buffered with 168 + | Some tok -> 169 + st.buffered <- None; 150 170 st.prev <- tok; 151 171 tok 152 - | `Newline -> 153 - if can_end_stmt st.prev then begin 154 - (* Don't update prev — the semicolon is synthetic *) 155 - Parser.SEMICOLON 156 - end 157 - else token st (* skip newline *) 172 + | None -> ( 173 + match raw_token st.lexbuf with 174 + | `Token tok -> 175 + st.prev <- tok; 176 + tok 177 + | `Newline -> 178 + if can_end_stmt st.prev then 179 + let next = next_real_token st.lexbuf in 180 + (* A rule body's [}] is always followed by a newline before 181 + [else]; suppressing the synthetic [;] there keeps the 182 + grammar simpler than absorbing it inside [else_chain]. *) 183 + if next = Parser.ELSE then begin 184 + st.buffered <- Some next; 185 + token st 186 + end 187 + else begin 188 + st.buffered <- Some next; 189 + Parser.SEMICOLON 190 + end 191 + else token st)
+70 -8
lib/parser.mly
··· 40 40 %nonassoc ASSIGN COLONEQ 41 41 %left PIPE 42 42 %left AMPERSAND 43 + %nonassoc IN_OP 43 44 %nonassoc EQEQ BANGEQ LT GT LE GE 44 45 %left PLUS MINUS 45 46 %left STAR SLASH PERCENT 46 47 (* Dummy token for reducing term_expr → infix_expr at lower precedence 47 48 than LPAREN/LBRACK postfix operators, resolving the shift/reduce 48 49 conflicts in rule heads and contains_key. *) 49 - %nonassoc below_LPAREN below_SEMI 50 - %nonassoc LPAREN LBRACK SEMICOLON 50 + %nonassoc below_LPAREN below_SEMI below_LBRACE 51 + %nonassoc LPAREN LBRACK SEMICOLON LBRACE 51 52 52 53 %start <Ast.rego_module> rego_module 53 54 %start <Ast.expr> expr_only ··· 80 81 81 82 (* ref_only: a restricted ref that is only dotted names (no [ ] or call). 82 83 Used in package_decl and import_decl where [ would be ambiguous with 83 - rule heads, and ( would be ambiguous with function definitions. *) 84 + rule heads, and ( would be ambiguous with function definitions. 85 + 86 + Path segments accept the future-keyword identifiers ([in], [every], 87 + [contains], [if]) so [import future.keywords.in] and [import rego.v1] 88 + parse as ordinary dotted refs. *) 84 89 ref_only: 85 - | i=IDENT { Var (sp, i) } 86 - | r=ref_only DOT i=IDENT { RefDot (sp, r, i) } 90 + | i=ident_or_kw { Var (sp, i) } 91 + | r=ref_only DOT i=ident_or_kw { RefDot (sp, r, i) } 92 + ; 93 + 94 + ident_or_kw: 95 + | i=IDENT { i } 96 + | IN { "in" } 97 + | EVERY { "every" } 98 + | CONTAINS { "contains" } 99 + | IF { "if" } 87 100 ; 88 101 89 102 (* ── Rules ─────────────────────────────────────────────────────────────── *) ··· 91 104 rule: 92 105 | DEFAULT r=ref_expr op=assign_op v=expr 93 106 { RuleDefault (sp, r, [], op, v) } 107 + | DEFAULT r=ref_expr LPAREN a=separated_list(COMMA, expr) RPAREN 108 + op=assign_op v=expr 109 + { RuleDefault (sp, r, a, op, v) } 94 110 | r=ref_expr b=rule_body es=else_chain 95 111 { RuleSpec (sp, RuleHeadCompr (sp, r, None), b :: es) } 96 112 | r=ref_expr IF b=rule_body es=else_chain 97 113 { RuleSpec (sp, RuleHeadCompr (sp, r, None), b :: es) } 114 + | r=ref_expr op=assign_op v=expr b=rule_body es=else_chain 115 + { RuleSpec (sp, RuleHeadCompr (sp, r, 116 + Some { ra_span = sp; ra_op = op; ra_value = v }), b :: es) } 98 117 | r=ref_expr op=assign_op v=expr IF b=rule_body es=else_chain 99 118 { RuleSpec (sp, RuleHeadCompr (sp, r, 100 119 Some { ra_span = sp; ra_op = op; ra_value = v }), b :: es) } 101 - | r=ref_expr op=assign_op v=expr 102 - { RuleDefault (sp, r, [], op, v) } 120 + | r=ref_expr op=assign_op v=expr %prec below_LBRACE 121 + { RuleSpec (sp, RuleHeadCompr (sp, r, 122 + Some { ra_span = sp; ra_op = op; ra_value = v }), 123 + [{ rb_span = sp; rb_assign = None; 124 + rb_query = { q_span = sp; q_stmts = [] } }]) } 103 125 | r=ref_expr CONTAINS k=contains_key IF b=rule_body 104 126 { RuleSpec (sp, RuleHeadSet (sp, r, Some k), [b]) } 105 127 | r=ref_expr CONTAINS k=contains_key %prec below_LPAREN 106 128 { RuleSpec (sp, RuleHeadSet (sp, r, Some k), []) } 107 129 | r=ref_expr LPAREN a=separated_list(COMMA, expr) RPAREN 130 + op=assign_op v=expr b=rule_body es=else_chain 131 + { RuleSpec (sp, RuleHeadFunc (sp, r, a, 132 + Some { ra_span = sp; ra_op = op; ra_value = v }), b :: es) } 133 + | r=ref_expr LPAREN a=separated_list(COMMA, expr) RPAREN 108 134 op=assign_op v=expr IF b=rule_body es=else_chain 109 135 { RuleSpec (sp, RuleHeadFunc (sp, r, a, 110 136 Some { ra_span = sp; ra_op = op; ra_value = v }), b :: es) } 111 137 | r=ref_expr LPAREN a=separated_list(COMMA, expr) RPAREN 112 - op=assign_op v=expr 138 + op=assign_op v=expr %prec below_LBRACE 113 139 { RuleSpec (sp, RuleHeadFunc (sp, r, a, 114 140 Some { ra_span = sp; ra_op = op; ra_value = v }), []) } 115 141 | r=ref_expr LPAREN a=separated_list(COMMA, expr) RPAREN b=rule_body es=else_chain ··· 166 192 { SomeIn (sp, None, Var (sp, v), e) } 167 193 | SOME k=IDENT COMMA v=IDENT IN e=expr 168 194 { SomeIn (sp, Some (Var (sp, k)), Var (sp, v), e) } 195 + | k=expr_no_in COMMA v=expr_no_in IN c=expr 196 + { Expr (sp, Membership (sp, Some k, v, c)) } 169 197 | EVERY v=IDENT IN e=expr b=rule_body 170 198 { Every (sp, None, v, e, b.rb_query) } 171 199 | EVERY k=IDENT COMMA v=IDENT IN e=expr b=rule_body ··· 206 234 | l=infix_expr PIPE r=infix_expr { BinExpr (sp, Union, l, r) } 207 235 | l=infix_expr ASSIGN r=infix_expr { AssignExpr (sp, Assign, l, r) } 208 236 | l=infix_expr COLONEQ r=infix_expr { AssignExpr (sp, ColonEq, l, r) } 237 + | l=infix_expr IN r=infix_expr %prec IN_OP { Membership (sp, None, l, r) } 238 + ; 239 + 240 + (* Same as [infix_expr] but without the IN production. Used as the left 241 + and right sides of the keyed [k, v in c] form so that an IN there 242 + never gets greedily consumed into a value position. *) 243 + infix_expr_no_in: 244 + | t=term_expr { t } %prec below_LPAREN 245 + | l=infix_expr_no_in PLUS r=infix_expr_no_in { ArithExpr (sp, Add, l, r) } 246 + | l=infix_expr_no_in MINUS r=infix_expr_no_in { ArithExpr (sp, Sub, l, r) } 247 + | l=infix_expr_no_in STAR r=infix_expr_no_in { ArithExpr (sp, Mul, l, r) } 248 + | l=infix_expr_no_in SLASH r=infix_expr_no_in { ArithExpr (sp, Div, l, r) } 249 + | l=infix_expr_no_in PERCENT r=infix_expr_no_in { ArithExpr (sp, Mod, l, r) } 250 + | l=infix_expr_no_in EQEQ r=infix_expr_no_in { BoolExpr (sp, Eq, l, r) } 251 + | l=infix_expr_no_in BANGEQ r=infix_expr_no_in { BoolExpr (sp, Ne, l, r) } 252 + | l=infix_expr_no_in LT r=infix_expr_no_in { BoolExpr (sp, Lt, l, r) } 253 + | l=infix_expr_no_in GT r=infix_expr_no_in { BoolExpr (sp, Gt, l, r) } 254 + | l=infix_expr_no_in LE r=infix_expr_no_in { BoolExpr (sp, Le, l, r) } 255 + | l=infix_expr_no_in GE r=infix_expr_no_in { BoolExpr (sp, Ge, l, r) } 256 + | l=infix_expr_no_in AMPERSAND r=infix_expr_no_in { BinExpr (sp, Intersection, l, r) } 257 + | l=infix_expr_no_in PIPE r=infix_expr_no_in { BinExpr (sp, Union, l, r) } 258 + ; 259 + 260 + expr_no_in: 261 + | e=infix_expr_no_in { e } 262 + | MINUS e=infix_expr_no_in { UnaryExpr (sp, e) } 209 263 ; 210 264 211 265 (* Expression without PIPE — used inside comprehensions where | is the ··· 231 285 | l=no_pipe_infix AMPERSAND r=no_pipe_infix { BinExpr (sp, Intersection, l, r) } 232 286 | l=no_pipe_infix ASSIGN r=no_pipe_infix { AssignExpr (sp, Assign, l, r) } 233 287 | l=no_pipe_infix COLONEQ r=no_pipe_infix { AssignExpr (sp, ColonEq, l, r) } 288 + | l=no_pipe_infix IN r=no_pipe_infix %prec IN_OP { Membership (sp, None, l, r) } 234 289 ; 235 290 236 291 (* ── Terms ─────────────────────────────────────────────────────────────── *) ··· 283 338 | s=STRING { String (sp, s) } 284 339 | s=RAWSTRING { RawString (sp, s) } 285 340 | i=IDENT { Var (sp, i) } 341 + (* The OPA [contains] / [print] builtins clash with the [contains] 342 + keyword and the implicit [print] introduced for tracing. Accept 343 + them as identifiers here so [contains(s, sub)] and [print(...)] 344 + parse as ordinary calls rather than keywords. *) 345 + | CONTAINS { Var (sp, "contains") } 286 346 | LPAREN e=expr RPAREN { e } 347 + | LPAREN k=expr_no_in COMMA v=expr_no_in IN c=expr RPAREN 348 + { Membership (sp, Some k, v, c) } 287 349 | LBRACK RBRACK { Array (sp, []) } 288 350 | LBRACK e=no_pipe_expr rest=brack_rest { rest sp e } 289 351 | LBRACE RBRACE { Object (sp, []) }