|
| 1 | +# Destructuring Planner |
| 2 | + |
| 3 | +The destructuring planner pre-computes how Rego assignments, function parameters, loop indices, and `some ... in` expressions bind variables. By materializing explicit plans during compilation, the interpreter can execute complex binding patterns without re-inspecting the abstract syntax tree (AST) each time an expression runs. |
| 4 | + |
| 5 | +``` |
| 6 | ++--------------+ +----------------------------+ +-------------------+ |
| 7 | +| AST walker | ---> | Destructuring planner core | ---> | BindingPlans table | |
| 8 | ++--------------+ +----------------------------+ +-------------------+ |
| 9 | + | | ^ | |
| 10 | + | v | v |
| 11 | + | +------------------+ +-------------------+ |
| 12 | + | | ScopeContext | <--------> | Planner utilities | |
| 13 | + | +------------------+ +-------------------+ |
| 14 | + v |
| 15 | ++------------------+ |
| 16 | +| Scheduler output | |
| 17 | ++------------------+ |
| 18 | +
|
| 19 | +Downstream compiler passes reuse the same plans: |
| 20 | +
|
| 21 | +``` |
| 22 | +BindingPlans table |
| 23 | + | |
| 24 | + +--> Rego VM compiler (RVM) for bytecode emission |
| 25 | + +--> Type propagation pass |
| 26 | + +--> Constant folding and other analyzers |
| 27 | +``` |
| 28 | +``` |
| 29 | + |
| 30 | +## Planner building blocks |
| 31 | + |
| 32 | +### Scope awareness |
| 33 | + |
| 34 | +The planner relies on `ScopeContext` implementations to answer two questions for every variable candidate: |
| 35 | + |
| 36 | +| Question | Method | Why it matters | |
| 37 | +| :----------------------------------------- | :----------------------------- | :--------------------------------------------------------------------- | |
| 38 | +| "Is this name currently unbound?" | `is_var_unbound(var, scoping)` | Determines whether a symbol becomes a new binding or should be treated as an equality check. | |
| 39 | +| "Has this scope already introduced the name?" | `has_same_scope_binding(var)` | Blocks same-scope rebinding for `:=` while still permitting shadowing in child scopes. | |
| 40 | + |
| 41 | +The planner uses two scoping modes: |
| 42 | + |
| 43 | +| Scoping mode | Description | Used by | |
| 44 | +| :-------------- | :-------------------------------------------------------------------------- | :---------------------------------------------------------- | |
| 45 | +| `RespectParent` | Honors existing bindings. Only treats names that are not yet visible as new bindings. | `=` comparisons, loop indices, `some ... in` value/key plans. | |
| 46 | +| `AllowShadowing` | Allows new bindings even if the name is defined in an ancestor scope. | Function parameters, `:=` LHS, `some ... in` overlay contexts. | |
| 47 | + |
| 48 | +### Plan families |
| 49 | + |
| 50 | +Three layers of plan types describe the complete binding strategy. |
| 51 | + |
| 52 | +#### `DestructuringPlan` |
| 53 | + |
| 54 | +| Variant | Purpose | Notes on bindings | |
| 55 | +| :------------------------------------- | :------------------------------------------------- | :-------------------------------------------------------------- | |
| 56 | +| `Var(span)` | Bind the complete value to the variable at `span`. | Adds the variable to the current scope. | |
| 57 | +| `Ignore` | Consume a wildcard (`_`). | No bindings emitted. | |
| 58 | +| `EqualityExpr(expr)` | Require runtime equality with a dynamic expression. | Used when a candidate variable is already bound. | |
| 59 | +| `EqualityValue(value)` | Require equality with a literal known at compile time. | Enables static structural checks. | |
| 60 | +| `Array { element_plans }` | Destructure arrays element-by-element. | Recursively nests `DestructuringPlan` values. | |
| 61 | +| `Object { field_plans, dynamic_fields }` | Destructure objects. Literal keys use `field_plans`; dynamic keys appear in `dynamic_fields`. | Ensures literal shape compatibility during planning. | |
| 62 | + |
| 63 | +#### `AssignmentPlan` |
| 64 | + |
| 65 | +| Variant | Triggers | Binding behavior | |
| 66 | +| :--------------- | :-------------------------- | :-------------------------------------------------------------------------------- | |
| 67 | +| `ColonEquals` | `:=` | Only LHS may introduce bindings; RHS must match structure/literals. Same-scope rebinding raises an error. | |
| 68 | +| `EqualsBindLeft` | `=` where LHS has free vars | Binds the LHS pattern after structural + literal checks. | |
| 69 | +| `EqualsBindRight` | `=` where RHS has free vars | Symmetric to `EqualsBindLeft`. | |
| 70 | +| `EqualsBothSides` | `=` where both sides have free vars | Flattens matching sub-expressions into `(value_expr, plan)` pairs and orders them using dependency analysis. | |
| 71 | +| `EqualityCheck` | `=` with no free vars | Pure equality comparison. | |
| 72 | +| `WildcardMatch` | `=` when either side is `_` | Short-circuits to avoid materializing a plan. | |
| 73 | + |
| 74 | +#### `BindingPlan` |
| 75 | + |
| 76 | +| Variant | Created by | Typical consumers | |
| 77 | +| :----------- | :---------------------------------- | :-------------------------------------------------- | |
| 78 | +| `Assignment` | `create_assignment_binding_plan` | Rule bodies for `:=` and `=`. | |
| 79 | +| `LoopIndex` | `create_loop_index_binding_plan` | Hoisted loops and comprehensions. | |
| 80 | +| `Parameter` | `create_parameter_binding_plan` | Functions and rule heads. | |
| 81 | +| `SomeIn` | `create_some_in_binding_plan` | `some key, value in collection` statements. | |
| 82 | + |
| 83 | +## Planner workflow |
| 84 | + |
| 85 | +1. **Entry point selection** — The compiler pass decides which helper to call based on the AST node (assignment, comprehension, function parameter, etc.). |
| 86 | +2. **Pattern inspection** — `create_destructuring_plan` walks the candidate pattern and records which names would become new bindings under the selected scoping rules. |
| 87 | +3. **Conflict detection** — The planner asks the context for same-scope bindings and raises `VariableAlreadyDefined` when a duplicate `:=` appears in the same block. |
| 88 | +4. **Structural validation** — Helpers such as `ensure_structural_compatibility` and `ensure_literal_match` verify that literal shapes are consistent. |
| 89 | +5. **Plan assembly** — The resulting `DestructuringPlan`, `AssignmentPlan`, or higher-level `BindingPlan` is stored in the binding lookup table for quick interpreter access. |
| 90 | + |
| 91 | +### Example flow |
| 92 | + |
| 93 | +``` |
| 94 | +[Rule body] -- := --> [create_assignment_binding_plan] |
| 95 | + | |
| 96 | + v |
| 97 | + [create_destructuring_plan] |
| 98 | + | |
| 99 | + +------v--------------+ |
| 100 | + | ScopeContext checks | |
| 101 | + +------+--------------+ |
| 102 | + | |
| 103 | + +-----------v-----------+ |
| 104 | + | AssignmentPlan::ColonEquals | |
| 105 | + +-----------+-----------+ |
| 106 | + | |
| 107 | + stores in BindingPlans table |
| 108 | +``` |
| 109 | + |
| 110 | +## Worked examples |
| 111 | + |
| 112 | +Each example shows the original Rego snippet, the resulting binding plan, and highlights of the emitted bindings. |
| 113 | + |
| 114 | +### 1. Nested `:=` patterns |
| 115 | + |
| 116 | +```rego |
| 117 | +package test |
| 118 | +
|
| 119 | +result := { |
| 120 | + "outer": outer, |
| 121 | + "inner": inner, |
| 122 | + "tag": tag, |
| 123 | +} if { |
| 124 | + [outer, {"meta": {"inner": inner, "tag": tag}}] := [ |
| 125 | + "alpha", |
| 126 | + {"meta": {"inner": "omega", "tag": "v1"}}, |
| 127 | + ] |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +Plan overview: |
| 132 | + |
| 133 | +``` |
| 134 | +BindingPlan::Assignment |
| 135 | +└── AssignmentPlan::ColonEquals |
| 136 | + ├── lhs_expr: array pattern |
| 137 | + └── lhs_plan: DestructuringPlan::Array |
| 138 | + ├── [0] -> Var("outer") |
| 139 | + └── [1] -> DestructuringPlan::Object |
| 140 | + └── key "meta": DestructuringPlan::Object |
| 141 | + ├── key "inner": Var("inner") |
| 142 | + └── key "tag": Var("tag") |
| 143 | +``` |
| 144 | + |
| 145 | +| New binding | Source span | Notes | |
| 146 | +| --- | --- | --- | |
| 147 | +| `outer` | LHS array index 0 | New symbol in scope. | |
| 148 | +| `inner` | Object field `meta.inner` | Shares scope with `outer`. | |
| 149 | +| `tag` | Object field `meta.tag` | Must not reappear in same `:=` block. | |
| 150 | + |
| 151 | +### 2. Symmetric `=` binding |
| 152 | + |
| 153 | +```rego |
| 154 | +package test |
| 155 | +
|
| 156 | +values := [[left_id, right_id, val] | |
| 157 | + some left, right, left_id, right_id, val |
| 158 | + data.transitions[_] = [left, right] |
| 159 | + [{"id": left_id, "next": {"target": right_id}}, {"id": right_id, "payload": {"value": val}}] = [left, right] |
| 160 | +] |
| 161 | +``` |
| 162 | + |
| 163 | +Plan fragments: |
| 164 | + |
| 165 | +``` |
| 166 | +BindingPlan::Assignment |
| 167 | +└── AssignmentPlan::EqualsBothSides |
| 168 | + └── element_pairs (ordered) |
| 169 | + 1. value_expr -> rhs[0] |
| 170 | + plan -> DestructuringPlan::Object |
| 171 | + key "id" -> Var("left_id") |
| 172 | + key "next" -> DestructuringPlan::Object { key "target" -> Var("right_id") } |
| 173 | + 2. value_expr -> rhs[1] |
| 174 | + plan -> DestructuringPlan::Object |
| 175 | + key "id" -> Var("right_id") |
| 176 | + key "payload" -> DestructuringPlan::Object { key "value" -> Var("val") } |
| 177 | +``` |
| 178 | + |
| 179 | +Dependency ordering ensures `left_id` is available before `right_id`/`val` comparisons run. |
| 180 | + |
| 181 | +### 3. Function parameter destructuring |
| 182 | + |
| 183 | +```rego |
| 184 | +package test |
| 185 | +
|
| 186 | +# f([id, payload]) := payload |
| 187 | +f([id, payload]) := result { |
| 188 | + result := payload |
| 189 | +} |
| 190 | +``` |
| 191 | + |
| 192 | +``` |
| 193 | +BindingPlan::Parameter |
| 194 | +└── param_expr: array pattern |
| 195 | + destructuring_plan: |
| 196 | + Array |
| 197 | + ├── [0] -> Var("id") |
| 198 | + └── [1] -> Var("payload") |
| 199 | +``` |
| 200 | + |
| 201 | +Both bindings use `ScopingMode::AllowShadowing`, allowing `id` or `payload` to shadow outer names when the function executes. |
| 202 | + |
| 203 | +### 4. `some ... in` loop |
| 204 | + |
| 205 | +```rego |
| 206 | +package test |
| 207 | +
|
| 208 | +some user, record in data.users |
| 209 | +record.role == "admin" |
| 210 | +``` |
| 211 | + |
| 212 | +Plan summary: |
| 213 | + |
| 214 | +``` |
| 215 | +BindingPlan::SomeIn |
| 216 | +├── collection_expr: data.users |
| 217 | +├── key_plan: DestructuringPlan::Var("user") |
| 218 | +└── value_plan: DestructuringPlan::Var("record") |
| 219 | +``` |
| 220 | + |
| 221 | +Tables for bindings: |
| 222 | + |
| 223 | +| Element | Plan | New bindings | |
| 224 | +| --- | --- | --- | |
| 225 | +| `key_plan` | `Var("user")` | Introduces `user` if unbound. | |
| 226 | +| `value_plan` | `Var("record")` | Introduces `record`. | |
| 227 | + |
| 228 | +Literal arrays used in `collection_expr` are checked so the planner can report mismatched element shapes upfront. |
| 229 | + |
| 230 | +### 5. Rebinding error detection |
| 231 | + |
| 232 | +```rego |
| 233 | +package test |
| 234 | +
|
| 235 | +flag := true if { |
| 236 | + value := "initial" |
| 237 | + value := "shadowed" |
| 238 | +} |
| 239 | +``` |
| 240 | + |
| 241 | +``` |
| 242 | +BindingPlan::Assignment |
| 243 | +└── AssignmentPlan::ColonEquals (lhs := value) |
| 244 | +``` |
| 245 | + |
| 246 | +During planning, the second `:=` consults `has_same_scope_binding("value")` which returns `true`. The planner emits `BindingPlannerError::VariableAlreadyDefined` and compilation reports: |
| 247 | + |
| 248 | +``` |
| 249 | +error: var `value` used before definition below |
| 250 | +``` |
| 251 | + |
| 252 | +## Interpreter handoff |
| 253 | + |
| 254 | +Planned bindings are stored in the same lookup tables as hoisted loops. At runtime the interpreter: |
| 255 | + |
| 256 | +1. Fetches the `BindingPlan` using `(module_id, expr_idx)`. |
| 257 | +2. Executes the plan, binding or validating values without re-walking the AST. |
| 258 | +3. Falls back to legacy evaluation if a plan is missing (useful for incremental compilation or mixed modules). |
| 259 | + |
| 260 | +This division keeps the hot execution path small while letting the compiler perform aggressive validation and error reporting ahead of time. |
0 commit comments