mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat!: Introduce structured destructuring plans for bindings (#485)
- add a dedicated `compiler/destructuring_planner` feature that precomputes binding plans for assignments, parameters, and `some in` expressions - enrich `ScopeContext` with same-scope tracking, local scheduling hints, and module globals so the planner enforces := shadowing rules without blocking parent scopes - wire the planner through compiler, hoist, interpreter, and engine paths while updating binding plan variants and adding query traversal helpers for dependency analysis - document the new planner architecture and ship interpreter regressions that exercise nested destructuring, shadowing, and error reporting Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
committed by
GitHub
parent
25a7ddad0a
commit
1e4ff952e6
260
docs/destructuring.md
Normal file
260
docs/destructuring.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Destructuring Planner
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
+--------------+ +----------------------------+ +-------------------+
|
||||
| AST walker | ---> | Destructuring planner core | ---> | BindingPlans table |
|
||||
+--------------+ +----------------------------+ +-------------------+
|
||||
| | ^ |
|
||||
| v | v
|
||||
| +------------------+ +-------------------+
|
||||
| | ScopeContext | <--------> | Planner utilities |
|
||||
| +------------------+ +-------------------+
|
||||
v
|
||||
+------------------+
|
||||
| Scheduler output |
|
||||
+------------------+
|
||||
|
||||
Downstream compiler passes reuse the same plans:
|
||||
|
||||
```
|
||||
BindingPlans table
|
||||
|
|
||||
+--> Rego VM compiler (RVM) for bytecode emission
|
||||
+--> Type propagation pass
|
||||
+--> Constant folding and other analyzers
|
||||
```
|
||||
```
|
||||
|
||||
## Planner building blocks
|
||||
|
||||
### Scope awareness
|
||||
|
||||
The planner relies on `ScopeContext` implementations to answer two questions for every variable candidate:
|
||||
|
||||
| Question | Method | Why it matters |
|
||||
| :----------------------------------------- | :----------------------------- | :--------------------------------------------------------------------- |
|
||||
| "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. |
|
||||
| "Has this scope already introduced the name?" | `has_same_scope_binding(var)` | Blocks same-scope rebinding for `:=` while still permitting shadowing in child scopes. |
|
||||
|
||||
The planner uses two scoping modes:
|
||||
|
||||
| Scoping mode | Description | Used by |
|
||||
| :-------------- | :-------------------------------------------------------------------------- | :---------------------------------------------------------- |
|
||||
| `RespectParent` | Honors existing bindings. Only treats names that are not yet visible as new bindings. | `=` comparisons, loop indices, `some ... in` value/key plans. |
|
||||
| `AllowShadowing` | Allows new bindings even if the name is defined in an ancestor scope. | Function parameters, `:=` LHS, `some ... in` overlay contexts. |
|
||||
|
||||
### Plan families
|
||||
|
||||
Three layers of plan types describe the complete binding strategy.
|
||||
|
||||
#### `DestructuringPlan`
|
||||
|
||||
| Variant | Purpose | Notes on bindings |
|
||||
| :------------------------------------- | :------------------------------------------------- | :-------------------------------------------------------------- |
|
||||
| `Var(span)` | Bind the complete value to the variable at `span`. | Adds the variable to the current scope. |
|
||||
| `Ignore` | Consume a wildcard (`_`). | No bindings emitted. |
|
||||
| `EqualityExpr(expr)` | Require runtime equality with a dynamic expression. | Used when a candidate variable is already bound. |
|
||||
| `EqualityValue(value)` | Require equality with a literal known at compile time. | Enables static structural checks. |
|
||||
| `Array { element_plans }` | Destructure arrays element-by-element. | Recursively nests `DestructuringPlan` values. |
|
||||
| `Object { field_plans, dynamic_fields }` | Destructure objects. Literal keys use `field_plans`; dynamic keys appear in `dynamic_fields`. | Ensures literal shape compatibility during planning. |
|
||||
|
||||
#### `AssignmentPlan`
|
||||
|
||||
| Variant | Triggers | Binding behavior |
|
||||
| :--------------- | :-------------------------- | :-------------------------------------------------------------------------------- |
|
||||
| `ColonEquals` | `:=` | Only LHS may introduce bindings; RHS must match structure/literals. Same-scope rebinding raises an error. |
|
||||
| `EqualsBindLeft` | `=` where LHS has free vars | Binds the LHS pattern after structural + literal checks. |
|
||||
| `EqualsBindRight` | `=` where RHS has free vars | Symmetric to `EqualsBindLeft`. |
|
||||
| `EqualsBothSides` | `=` where both sides have free vars | Flattens matching sub-expressions into `(value_expr, plan)` pairs and orders them using dependency analysis. |
|
||||
| `EqualityCheck` | `=` with no free vars | Pure equality comparison. |
|
||||
| `WildcardMatch` | `=` when either side is `_` | Short-circuits to avoid materializing a plan. |
|
||||
|
||||
#### `BindingPlan`
|
||||
|
||||
| Variant | Created by | Typical consumers |
|
||||
| :----------- | :---------------------------------- | :-------------------------------------------------- |
|
||||
| `Assignment` | `create_assignment_binding_plan` | Rule bodies for `:=` and `=`. |
|
||||
| `LoopIndex` | `create_loop_index_binding_plan` | Hoisted loops and comprehensions. |
|
||||
| `Parameter` | `create_parameter_binding_plan` | Functions and rule heads. |
|
||||
| `SomeIn` | `create_some_in_binding_plan` | `some key, value in collection` statements. |
|
||||
|
||||
## Planner workflow
|
||||
|
||||
1. **Entry point selection** — The compiler pass decides which helper to call based on the AST node (assignment, comprehension, function parameter, etc.).
|
||||
2. **Pattern inspection** — `create_destructuring_plan` walks the candidate pattern and records which names would become new bindings under the selected scoping rules.
|
||||
3. **Conflict detection** — The planner asks the context for same-scope bindings and raises `VariableAlreadyDefined` when a duplicate `:=` appears in the same block.
|
||||
4. **Structural validation** — Helpers such as `ensure_structural_compatibility` and `ensure_literal_match` verify that literal shapes are consistent.
|
||||
5. **Plan assembly** — The resulting `DestructuringPlan`, `AssignmentPlan`, or higher-level `BindingPlan` is stored in the binding lookup table for quick interpreter access.
|
||||
|
||||
### Example flow
|
||||
|
||||
```
|
||||
[Rule body] -- := --> [create_assignment_binding_plan]
|
||||
|
|
||||
v
|
||||
[create_destructuring_plan]
|
||||
|
|
||||
+------v--------------+
|
||||
| ScopeContext checks |
|
||||
+------+--------------+
|
||||
|
|
||||
+-----------v-----------+
|
||||
| AssignmentPlan::ColonEquals |
|
||||
+-----------+-----------+
|
||||
|
|
||||
stores in BindingPlans table
|
||||
```
|
||||
|
||||
## Worked examples
|
||||
|
||||
Each example shows the original Rego snippet, the resulting binding plan, and highlights of the emitted bindings.
|
||||
|
||||
### 1. Nested `:=` patterns
|
||||
|
||||
```rego
|
||||
package test
|
||||
|
||||
result := {
|
||||
"outer": outer,
|
||||
"inner": inner,
|
||||
"tag": tag,
|
||||
} if {
|
||||
[outer, {"meta": {"inner": inner, "tag": tag}}] := [
|
||||
"alpha",
|
||||
{"meta": {"inner": "omega", "tag": "v1"}},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Plan overview:
|
||||
|
||||
```
|
||||
BindingPlan::Assignment
|
||||
└── AssignmentPlan::ColonEquals
|
||||
├── lhs_expr: array pattern
|
||||
└── lhs_plan: DestructuringPlan::Array
|
||||
├── [0] -> Var("outer")
|
||||
└── [1] -> DestructuringPlan::Object
|
||||
└── key "meta": DestructuringPlan::Object
|
||||
├── key "inner": Var("inner")
|
||||
└── key "tag": Var("tag")
|
||||
```
|
||||
|
||||
| New binding | Source span | Notes |
|
||||
| --- | --- | --- |
|
||||
| `outer` | LHS array index 0 | New symbol in scope. |
|
||||
| `inner` | Object field `meta.inner` | Shares scope with `outer`. |
|
||||
| `tag` | Object field `meta.tag` | Must not reappear in same `:=` block. |
|
||||
|
||||
### 2. Symmetric `=` binding
|
||||
|
||||
```rego
|
||||
package test
|
||||
|
||||
values := [[left_id, right_id, val] |
|
||||
some left, right, left_id, right_id, val
|
||||
data.transitions[_] = [left, right]
|
||||
[{"id": left_id, "next": {"target": right_id}}, {"id": right_id, "payload": {"value": val}}] = [left, right]
|
||||
]
|
||||
```
|
||||
|
||||
Plan fragments:
|
||||
|
||||
```
|
||||
BindingPlan::Assignment
|
||||
└── AssignmentPlan::EqualsBothSides
|
||||
└── element_pairs (ordered)
|
||||
1. value_expr -> rhs[0]
|
||||
plan -> DestructuringPlan::Object
|
||||
key "id" -> Var("left_id")
|
||||
key "next" -> DestructuringPlan::Object { key "target" -> Var("right_id") }
|
||||
2. value_expr -> rhs[1]
|
||||
plan -> DestructuringPlan::Object
|
||||
key "id" -> Var("right_id")
|
||||
key "payload" -> DestructuringPlan::Object { key "value" -> Var("val") }
|
||||
```
|
||||
|
||||
Dependency ordering ensures `left_id` is available before `right_id`/`val` comparisons run.
|
||||
|
||||
### 3. Function parameter destructuring
|
||||
|
||||
```rego
|
||||
package test
|
||||
|
||||
# f([id, payload]) := payload
|
||||
f([id, payload]) := result {
|
||||
result := payload
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
BindingPlan::Parameter
|
||||
└── param_expr: array pattern
|
||||
destructuring_plan:
|
||||
Array
|
||||
├── [0] -> Var("id")
|
||||
└── [1] -> Var("payload")
|
||||
```
|
||||
|
||||
Both bindings use `ScopingMode::AllowShadowing`, allowing `id` or `payload` to shadow outer names when the function executes.
|
||||
|
||||
### 4. `some ... in` loop
|
||||
|
||||
```rego
|
||||
package test
|
||||
|
||||
some user, record in data.users
|
||||
record.role == "admin"
|
||||
```
|
||||
|
||||
Plan summary:
|
||||
|
||||
```
|
||||
BindingPlan::SomeIn
|
||||
├── collection_expr: data.users
|
||||
├── key_plan: DestructuringPlan::Var("user")
|
||||
└── value_plan: DestructuringPlan::Var("record")
|
||||
```
|
||||
|
||||
Tables for bindings:
|
||||
|
||||
| Element | Plan | New bindings |
|
||||
| --- | --- | --- |
|
||||
| `key_plan` | `Var("user")` | Introduces `user` if unbound. |
|
||||
| `value_plan` | `Var("record")` | Introduces `record`. |
|
||||
|
||||
Literal arrays used in `collection_expr` are checked so the planner can report mismatched element shapes upfront.
|
||||
|
||||
### 5. Rebinding error detection
|
||||
|
||||
```rego
|
||||
package test
|
||||
|
||||
flag := true if {
|
||||
value := "initial"
|
||||
value := "shadowed"
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
BindingPlan::Assignment
|
||||
└── AssignmentPlan::ColonEquals (lhs := value)
|
||||
```
|
||||
|
||||
During planning, the second `:=` consults `has_same_scope_binding("value")` which returns `true`. The planner emits `BindingPlannerError::VariableAlreadyDefined` and compilation reports:
|
||||
|
||||
```
|
||||
error: var `value` used before definition below
|
||||
```
|
||||
|
||||
## Interpreter handoff
|
||||
|
||||
Planned bindings are stored in the same lookup tables as hoisted loops. At runtime the interpreter:
|
||||
|
||||
1. Fetches the `BindingPlan` using `(module_id, expr_idx)`.
|
||||
2. Executes the plan, binding or validating values without re-walking the AST.
|
||||
3. Falls back to legacy evaluation if a plan is missing (useful for incremental compilation or mixed modules).
|
||||
|
||||
This division keeps the hot execution path small while letting the compiler perform aggressive validation and error reporting ahead of time.
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::ast::{Expr, Ref};
|
||||
use crate::builtins;
|
||||
use crate::builtins::utils::{ensure_args_count, ensure_numeric};
|
||||
use crate::builtins::utils::{ensure_args_count, ensure_numeric, validate_integer_arg};
|
||||
|
||||
use crate::lexer::Span;
|
||||
use crate::value::Value;
|
||||
@@ -19,77 +19,111 @@ pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn
|
||||
m.insert("bits.xor", (xor, 2));
|
||||
}
|
||||
|
||||
fn and(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
||||
fn and(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
|
||||
let name = "bits.and";
|
||||
ensure_args_count(span, name, params, args, 2)?;
|
||||
|
||||
let v1 = ensure_numeric(name, ¶ms[0], &args[0])?;
|
||||
let v2 = ensure_numeric(name, ¶ms[1], &args[1])?;
|
||||
|
||||
if !validate_integer_arg(name, ¶ms[0], &args[0], &v1, strict, true)?
|
||||
|| !validate_integer_arg(name, ¶ms[1], &args[1], &v2, strict, true)?
|
||||
{
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
|
||||
Ok(match v1.and(&v2) {
|
||||
Some(v) => Value::from(v),
|
||||
_ => Value::Undefined,
|
||||
})
|
||||
}
|
||||
|
||||
fn lsh(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
||||
fn lsh(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
|
||||
let name = "bits.lsh";
|
||||
ensure_args_count(span, name, params, args, 2)?;
|
||||
|
||||
let v1 = ensure_numeric(name, ¶ms[0], &args[0])?;
|
||||
let v2 = ensure_numeric(name, ¶ms[1], &args[1])?;
|
||||
|
||||
if !validate_integer_arg(name, ¶ms[0], &args[0], &v1, strict, true)?
|
||||
|| !validate_integer_arg(name, ¶ms[1], &args[1], &v2, strict, false)?
|
||||
{
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
|
||||
Ok(match v1.lsh(&v2) {
|
||||
Some(v) => Value::from(v),
|
||||
_ => Value::Undefined,
|
||||
})
|
||||
}
|
||||
|
||||
fn negate(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
||||
fn negate(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
|
||||
let name = "bits.negate";
|
||||
ensure_args_count(span, name, params, args, 1)?;
|
||||
|
||||
let v = ensure_numeric(name, ¶ms[0], &args[0])?;
|
||||
|
||||
if !validate_integer_arg(name, ¶ms[0], &args[0], &v, strict, true)? {
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
|
||||
Ok(match v.neg() {
|
||||
Some(v) => Value::from(v),
|
||||
_ => Value::Undefined,
|
||||
})
|
||||
}
|
||||
|
||||
fn or(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
||||
fn or(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
|
||||
let name = "bits.or";
|
||||
ensure_args_count(span, name, params, args, 2)?;
|
||||
|
||||
let v1 = ensure_numeric(name, ¶ms[0], &args[0])?;
|
||||
let v2 = ensure_numeric(name, ¶ms[1], &args[1])?;
|
||||
|
||||
if !validate_integer_arg(name, ¶ms[0], &args[0], &v1, strict, true)?
|
||||
|| !validate_integer_arg(name, ¶ms[1], &args[1], &v2, strict, true)?
|
||||
{
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
|
||||
Ok(match v1.or(&v2) {
|
||||
Some(v) => Value::from(v),
|
||||
_ => Value::Undefined,
|
||||
})
|
||||
}
|
||||
|
||||
fn rsh(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
||||
fn rsh(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
|
||||
let name = "bits.rsh";
|
||||
ensure_args_count(span, name, params, args, 2)?;
|
||||
|
||||
let v1 = ensure_numeric(name, ¶ms[0], &args[0])?;
|
||||
let v2 = ensure_numeric(name, ¶ms[1], &args[1])?;
|
||||
|
||||
if !validate_integer_arg(name, ¶ms[0], &args[0], &v1, strict, true)?
|
||||
|| !validate_integer_arg(name, ¶ms[1], &args[1], &v2, strict, false)?
|
||||
{
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
|
||||
Ok(match v1.rsh(&v2) {
|
||||
Some(v) => Value::from(v),
|
||||
_ => Value::Undefined,
|
||||
})
|
||||
}
|
||||
|
||||
fn xor(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
||||
fn xor(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
|
||||
let name = "bits.xor";
|
||||
ensure_args_count(span, name, params, args, 2)?;
|
||||
|
||||
let v1 = ensure_numeric(name, ¶ms[0], &args[0])?;
|
||||
let v2 = ensure_numeric(name, ¶ms[1], &args[1])?;
|
||||
|
||||
if !validate_integer_arg(name, ¶ms[0], &args[0], &v1, strict, true)?
|
||||
|| !validate_integer_arg(name, ¶ms[1], &args[1], &v2, strict, true)?
|
||||
{
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
|
||||
Ok(match v1.xor(&v2) {
|
||||
Some(v) => Value::from(v),
|
||||
_ => Value::Undefined,
|
||||
|
||||
@@ -45,6 +45,50 @@ pub fn ensure_numeric(fcn: &str, arg: &Expr, v: &Value) -> Result<Number> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_integer_arg(
|
||||
fcn: &str,
|
||||
param: &Ref<Expr>,
|
||||
original_value: &Value,
|
||||
numeric_value: &Number,
|
||||
strict: bool,
|
||||
allow_negative: bool,
|
||||
) -> Result<bool> {
|
||||
if !numeric_value.is_integer() {
|
||||
if strict {
|
||||
bail!(param.span().error(
|
||||
format!("`{fcn}` expects integer arguments. Got `{original_value}`").as_str()
|
||||
));
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !allow_negative {
|
||||
if let Some(int_value) = numeric_value.as_i128() {
|
||||
if int_value < 0 {
|
||||
if strict {
|
||||
bail!(param.span().error(
|
||||
format!("`{fcn}` expects non-negative integer arguments. Got `{original_value}`")
|
||||
.as_str(),
|
||||
));
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
} else if !numeric_value.is_positive() {
|
||||
if strict {
|
||||
bail!(param.span().error(
|
||||
format!(
|
||||
"`{fcn}` expects non-negative integer arguments. Got `{original_value}`"
|
||||
)
|
||||
.as_str(),
|
||||
));
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn ensure_string(fcn: &str, arg: &Expr, v: &Value) -> Result<Rc<str>> {
|
||||
Ok(match &v {
|
||||
Value::String(s) => s.clone(),
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
//! the compilation phase to prepare policies for efficient execution.
|
||||
|
||||
pub mod context;
|
||||
pub mod destructuring_planner;
|
||||
pub mod hoist;
|
||||
|
||||
@@ -32,19 +32,34 @@ pub enum ContextType {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScopeContext {
|
||||
/// Type of context (Rule, Comprehension, Every, Query)
|
||||
#[allow(dead_code)]
|
||||
pub context_type: ContextType,
|
||||
|
||||
/// Variables that are bound in the current scope
|
||||
pub bound_vars: BTreeSet<String>,
|
||||
|
||||
/// Variables that are introduced in this scope (used for conflict detection)
|
||||
pub current_scope_bound_vars: BTreeSet<String>,
|
||||
|
||||
/// Variables that are explicitly marked as unbound (from `some` declarations)
|
||||
pub unbound_vars: BTreeSet<String>,
|
||||
|
||||
/// Variables that are local to this scope and will become bound once assigned
|
||||
pub local_vars: BTreeSet<String>,
|
||||
|
||||
/// Flag indicating whether scheduler scope information was available
|
||||
pub has_scheduler_scope: bool,
|
||||
|
||||
/// Key expression from rule head or object comprehension (for output expression hoisting)
|
||||
#[allow(dead_code)]
|
||||
pub key_expr: Option<ExprRef>,
|
||||
|
||||
/// Value expression from rule assignment or comprehension term (for output expression hoisting)
|
||||
#[allow(dead_code)]
|
||||
pub value_expr: Option<ExprRef>,
|
||||
|
||||
/// Shared set of module-level globals available in this scope
|
||||
pub module_globals: Option<crate::Rc<BTreeSet<String>>>,
|
||||
}
|
||||
|
||||
impl ScopeContext {
|
||||
@@ -53,9 +68,13 @@ impl ScopeContext {
|
||||
Self {
|
||||
context_type: ContextType::Query,
|
||||
bound_vars: BTreeSet::new(),
|
||||
current_scope_bound_vars: BTreeSet::new(),
|
||||
unbound_vars: BTreeSet::new(),
|
||||
local_vars: BTreeSet::new(),
|
||||
has_scheduler_scope: false,
|
||||
key_expr: None,
|
||||
value_expr: None,
|
||||
module_globals: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,9 +84,13 @@ impl ScopeContext {
|
||||
Self {
|
||||
context_type,
|
||||
bound_vars: BTreeSet::new(),
|
||||
current_scope_bound_vars: BTreeSet::new(),
|
||||
unbound_vars: BTreeSet::new(),
|
||||
local_vars: BTreeSet::new(),
|
||||
has_scheduler_scope: false,
|
||||
key_expr: None,
|
||||
value_expr: None,
|
||||
module_globals: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,9 +104,13 @@ impl ScopeContext {
|
||||
Self {
|
||||
context_type,
|
||||
bound_vars: BTreeSet::new(),
|
||||
current_scope_bound_vars: BTreeSet::new(),
|
||||
unbound_vars: BTreeSet::new(),
|
||||
local_vars: BTreeSet::new(),
|
||||
has_scheduler_scope: false,
|
||||
key_expr,
|
||||
value_expr,
|
||||
module_globals: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,9 +124,13 @@ impl ScopeContext {
|
||||
Self {
|
||||
context_type,
|
||||
bound_vars: self.bound_vars.clone(),
|
||||
current_scope_bound_vars: BTreeSet::new(),
|
||||
unbound_vars: self.unbound_vars.clone(),
|
||||
local_vars: self.local_vars.clone(),
|
||||
has_scheduler_scope: self.has_scheduler_scope,
|
||||
key_expr,
|
||||
value_expr,
|
||||
module_globals: self.module_globals.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,13 +138,18 @@ impl ScopeContext {
|
||||
pub fn bind_variable(&mut self, var_name: &str) {
|
||||
if var_name != "_" {
|
||||
self.bound_vars.insert(var_name.to_string());
|
||||
self.current_scope_bound_vars.insert(var_name.to_string());
|
||||
self.unbound_vars.remove(var_name);
|
||||
self.local_vars.remove(var_name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a variable as unbound
|
||||
pub fn add_unbound_variable(&mut self, var_name: &str) {
|
||||
if var_name != "_" && !self.bound_vars.contains(var_name) {
|
||||
if var_name != "_" {
|
||||
self.bound_vars.remove(var_name);
|
||||
self.current_scope_bound_vars.remove(var_name);
|
||||
self.local_vars.remove(var_name);
|
||||
self.unbound_vars.insert(var_name.to_string());
|
||||
}
|
||||
}
|
||||
@@ -126,23 +162,27 @@ impl ScopeContext {
|
||||
/// Check if we can determine that a variable should be treated as a loop iterator
|
||||
/// (either it's unbound or explicitly marked as such)
|
||||
pub fn should_hoist_as_loop(&self, var_name: &str) -> bool {
|
||||
if var_name == "_" || self.is_unbound(var_name) {
|
||||
true
|
||||
} else {
|
||||
// Treat variables that haven't been bound in this scope as potential loop iterators
|
||||
!self.bound_vars.contains(var_name)
|
||||
if var_name == "_" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a child context inheriting parent bindings, output expressions, and context type
|
||||
pub fn child(&self) -> Self {
|
||||
Self {
|
||||
context_type: self.context_type.clone(),
|
||||
bound_vars: self.bound_vars.clone(),
|
||||
unbound_vars: self.unbound_vars.clone(),
|
||||
key_expr: self.key_expr.clone(),
|
||||
value_expr: self.value_expr.clone(),
|
||||
if self
|
||||
.module_globals
|
||||
.as_ref()
|
||||
.is_some_and(|globals| globals.contains(var_name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.is_unbound(var_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.has_scheduler_scope {
|
||||
return self.local_vars.contains(var_name);
|
||||
}
|
||||
|
||||
!self.bound_vars.contains(var_name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
401
src/compiler/destructuring_planner/assignment.rs
Normal file
401
src/compiler/destructuring_planner/assignment.rs
Normal file
@@ -0,0 +1,401 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Assignment-specific planning utilities.
|
||||
|
||||
use alloc::collections::{BTreeMap, BTreeSet};
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::ast::{AssignOp, Expr, ExprRef};
|
||||
use crate::compiler::destructuring_planner::create_destructuring_plan;
|
||||
use crate::compiler::destructuring_planner::destructuring::create_destructuring_plan_with_tracking;
|
||||
use crate::compiler::destructuring_planner::utils::{
|
||||
collect_plan_var_spans, ensure_literal_match, ensure_structural_compatibility,
|
||||
extract_literal_key, format_literal_key_for_error, plan_only_if_binds,
|
||||
};
|
||||
use crate::compiler::destructuring_planner::{
|
||||
AssignmentPlan, BindingPlan, BindingPlannerError, DestructuringPlan, Result, ScopingMode,
|
||||
VariableBindingContext, WildcardSide,
|
||||
};
|
||||
use crate::lexer::Span;
|
||||
use crate::query::traversal::collect_expr_dependencies;
|
||||
use crate::value::Value;
|
||||
|
||||
/// Convenience function for assignment expressions with specific := and = rules.
|
||||
pub fn create_assignment_binding_plan<T: VariableBindingContext>(
|
||||
op: AssignOp,
|
||||
lhs_expr: &ExprRef,
|
||||
rhs_expr: &ExprRef,
|
||||
context: &T,
|
||||
) -> Result<BindingPlan> {
|
||||
let assignment_plan = match op {
|
||||
AssignOp::ColEq => {
|
||||
// For :=, only LHS can be destructured
|
||||
if let Some(lhs_plan) = plan_only_if_binds(create_destructuring_plan(
|
||||
lhs_expr,
|
||||
context,
|
||||
ScopingMode::AllowShadowing,
|
||||
)) {
|
||||
let mut var_spans = Vec::new();
|
||||
collect_plan_var_spans(&lhs_plan, &mut var_spans);
|
||||
let mut lhs_scope_bindings = BTreeSet::new();
|
||||
for span in var_spans {
|
||||
let name = span.text().to_string();
|
||||
let is_duplicate = !lhs_scope_bindings.insert(name.clone());
|
||||
let has_same_scope_binding = context.has_same_scope_binding(&name);
|
||||
|
||||
if is_duplicate || has_same_scope_binding {
|
||||
return Err(BindingPlannerError::VariableAlreadyDefined {
|
||||
var: name,
|
||||
span,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ensure_structural_compatibility(lhs_expr, rhs_expr)?;
|
||||
ensure_literal_match(&lhs_plan, rhs_expr)?;
|
||||
AssignmentPlan::ColonEquals {
|
||||
lhs_expr: lhs_expr.clone(),
|
||||
rhs_expr: rhs_expr.clone(),
|
||||
lhs_plan,
|
||||
}
|
||||
} else {
|
||||
return Err(BindingPlannerError::ColonEqualsRequiresBindableLeft {
|
||||
span: lhs_expr.span().clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
AssignOp::Eq => {
|
||||
let lhs_struct_plan =
|
||||
create_destructuring_plan(lhs_expr, context, ScopingMode::RespectParent);
|
||||
let rhs_struct_plan =
|
||||
create_destructuring_plan(rhs_expr, context, ScopingMode::RespectParent);
|
||||
|
||||
let lhs_plan = plan_only_if_binds(lhs_struct_plan.clone());
|
||||
let rhs_plan = plan_only_if_binds(rhs_struct_plan.clone());
|
||||
|
||||
let lhs_is_wildcard =
|
||||
matches!(lhs_expr.as_ref(), Expr::Var { span, .. } if span.text() == "_");
|
||||
let rhs_is_wildcard =
|
||||
matches!(rhs_expr.as_ref(), Expr::Var { span, .. } if span.text() == "_");
|
||||
|
||||
if lhs_is_wildcard || rhs_is_wildcard {
|
||||
let wildcard_side = match (lhs_is_wildcard, rhs_is_wildcard) {
|
||||
(true, true) => WildcardSide::Both,
|
||||
(true, false) => WildcardSide::Lhs,
|
||||
(false, true) => WildcardSide::Rhs,
|
||||
(false, false) => unreachable!(),
|
||||
};
|
||||
|
||||
AssignmentPlan::WildcardMatch {
|
||||
lhs_expr: lhs_expr.clone(),
|
||||
rhs_expr: rhs_expr.clone(),
|
||||
wildcard_side,
|
||||
}
|
||||
} else if lhs_plan.is_some() && rhs_plan.is_some() {
|
||||
// Both sides have unbound vars - recursively flatten all nested structures
|
||||
let mut element_pairs = Vec::new();
|
||||
let mut newly_bound = BTreeSet::new();
|
||||
flatten_assignment_pairs(
|
||||
lhs_expr,
|
||||
rhs_expr,
|
||||
context,
|
||||
&mut newly_bound,
|
||||
&mut element_pairs,
|
||||
)?;
|
||||
order_element_pairs(&mut element_pairs, context);
|
||||
|
||||
AssignmentPlan::EqualsBothSides {
|
||||
lhs_expr: lhs_expr.clone(),
|
||||
rhs_expr: rhs_expr.clone(),
|
||||
element_pairs,
|
||||
}
|
||||
} else if lhs_plan.is_none() && rhs_plan.is_none() {
|
||||
AssignmentPlan::EqualityCheck {
|
||||
lhs_expr: lhs_expr.clone(),
|
||||
rhs_expr: rhs_expr.clone(),
|
||||
}
|
||||
} else if let Some(lhs) = lhs_plan {
|
||||
ensure_structural_compatibility(lhs_expr, rhs_expr)?;
|
||||
ensure_literal_match(&lhs, rhs_expr)?;
|
||||
|
||||
AssignmentPlan::EqualsBindLeft {
|
||||
lhs_expr: lhs_expr.clone(),
|
||||
rhs_expr: rhs_expr.clone(),
|
||||
lhs_plan: lhs,
|
||||
}
|
||||
} else if let Some(rhs) = rhs_plan {
|
||||
ensure_structural_compatibility(rhs_expr, lhs_expr)?;
|
||||
ensure_literal_match(&rhs, lhs_expr)?;
|
||||
|
||||
AssignmentPlan::EqualsBindRight {
|
||||
lhs_expr: lhs_expr.clone(),
|
||||
rhs_expr: rhs_expr.clone(),
|
||||
rhs_plan: rhs,
|
||||
}
|
||||
} else if let Some(lhs) = lhs_struct_plan {
|
||||
ensure_structural_compatibility(lhs_expr, rhs_expr)?;
|
||||
ensure_literal_match(&lhs, rhs_expr)?;
|
||||
|
||||
AssignmentPlan::EqualsBindLeft {
|
||||
lhs_expr: lhs_expr.clone(),
|
||||
rhs_expr: rhs_expr.clone(),
|
||||
lhs_plan: lhs,
|
||||
}
|
||||
} else if let Some(rhs) = rhs_struct_plan {
|
||||
ensure_structural_compatibility(rhs_expr, lhs_expr)?;
|
||||
ensure_literal_match(&rhs, lhs_expr)?;
|
||||
|
||||
AssignmentPlan::EqualsBindRight {
|
||||
lhs_expr: lhs_expr.clone(),
|
||||
rhs_expr: rhs_expr.clone(),
|
||||
rhs_plan: rhs,
|
||||
}
|
||||
} else {
|
||||
AssignmentPlan::EqualityCheck {
|
||||
lhs_expr: lhs_expr.clone(),
|
||||
rhs_expr: rhs_expr.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(BindingPlan::Assignment {
|
||||
plan: assignment_plan,
|
||||
})
|
||||
}
|
||||
|
||||
/// Recursively flatten assignment destructuring into (value_expr, pattern_plan) pairs.
|
||||
fn flatten_assignment_pairs<T: VariableBindingContext>(
|
||||
lhs_expr: &ExprRef,
|
||||
rhs_expr: &ExprRef,
|
||||
context: &T,
|
||||
newly_bound: &mut BTreeSet<String>,
|
||||
pairs: &mut Vec<(ExprRef, DestructuringPlan)>,
|
||||
) -> Result<()> {
|
||||
let (lhs_plan, lhs_delta) =
|
||||
preview_binding_plan(lhs_expr, context, ScopingMode::RespectParent, newly_bound);
|
||||
let (rhs_plan, rhs_delta) =
|
||||
preview_binding_plan(rhs_expr, context, ScopingMode::RespectParent, newly_bound);
|
||||
|
||||
if lhs_plan.is_none() && rhs_plan.is_none() {
|
||||
pairs.push((
|
||||
rhs_expr.clone(),
|
||||
DestructuringPlan::EqualityExpr(lhs_expr.clone()),
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let lhs_is_array = matches!(lhs_expr.as_ref(), Expr::Array { .. });
|
||||
let rhs_is_array = matches!(rhs_expr.as_ref(), Expr::Array { .. });
|
||||
let lhs_is_object = matches!(lhs_expr.as_ref(), Expr::Object { .. });
|
||||
let rhs_is_object = matches!(rhs_expr.as_ref(), Expr::Object { .. });
|
||||
|
||||
if (lhs_is_array && rhs_is_object) || (lhs_is_object && rhs_is_array) {
|
||||
return Err(BindingPlannerError::IncompatibleDestructuringPatterns {
|
||||
span: lhs_expr.span().clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if lhs_is_array && rhs_is_array {
|
||||
for (lhs_item, rhs_item) in collect_array_pairs(lhs_expr, rhs_expr)? {
|
||||
flatten_assignment_pairs(&lhs_item, &rhs_item, context, newly_bound, pairs)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if lhs_is_object && rhs_is_object {
|
||||
if let Some(object_pairs) = collect_object_pairs(lhs_expr, rhs_expr)? {
|
||||
for (lhs_value, rhs_value) in object_pairs {
|
||||
flatten_assignment_pairs(&lhs_value, &rhs_value, context, newly_bound, pairs)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
match (lhs_plan, rhs_plan, lhs_expr.as_ref(), rhs_expr.as_ref()) {
|
||||
// Case 1: LHS has pattern, RHS is value - add pair
|
||||
(Some(lhs_pattern), None, _, _) => {
|
||||
newly_bound.extend(lhs_delta);
|
||||
pairs.push((rhs_expr.clone(), lhs_pattern));
|
||||
}
|
||||
// Case 2: RHS has pattern, LHS is value - add pair
|
||||
(None, Some(rhs_pattern), _, _) => {
|
||||
newly_bound.extend(rhs_delta);
|
||||
pairs.push((lhs_expr.clone(), rhs_pattern));
|
||||
}
|
||||
// Case 5: Both have patterns but incompatible structures
|
||||
(Some(_), Some(_), _, _) => {
|
||||
return Err(BindingPlannerError::IncompatibleDestructuringPatterns {
|
||||
span: lhs_expr.span().clone(),
|
||||
});
|
||||
}
|
||||
// Remaining cases are handled by earlier match arms and guard
|
||||
(None, None, _, _) => {
|
||||
unreachable!("handled by equality guard above");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_array_pairs(lhs_expr: &ExprRef, rhs_expr: &ExprRef) -> Result<Vec<(ExprRef, ExprRef)>> {
|
||||
let lhs_items = match lhs_expr.as_ref() {
|
||||
Expr::Array { items, .. } => items,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let rhs_items = match rhs_expr.as_ref() {
|
||||
Expr::Array { items, .. } => items,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if lhs_items.len() != rhs_items.len() {
|
||||
return Err(BindingPlannerError::ArraySizeMismatch {
|
||||
left_size: lhs_items.len(),
|
||||
right_size: rhs_items.len(),
|
||||
span: lhs_expr.span().clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(lhs_items
|
||||
.iter()
|
||||
.cloned()
|
||||
.zip(rhs_items.iter().cloned())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn order_element_pairs<T: VariableBindingContext>(
|
||||
element_pairs: &mut Vec<(ExprRef, DestructuringPlan)>,
|
||||
context: &T,
|
||||
) {
|
||||
if element_pairs.len() <= 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut remaining: Vec<_> = element_pairs
|
||||
.drain(..)
|
||||
.map(|(value_expr, plan)| {
|
||||
let binds = plan.bound_vars().into_iter().collect::<BTreeSet<_>>();
|
||||
let deps = collect_expr_dependencies(&value_expr);
|
||||
(value_expr, plan, deps, binds)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if remaining.iter().any(|(_, _, deps, _)| deps.is_none()) {
|
||||
*element_pairs = remaining
|
||||
.into_iter()
|
||||
.map(|(value_expr, plan, _, _)| (value_expr, plan))
|
||||
.collect();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut scheduled = BTreeSet::new();
|
||||
let mut ordered = Vec::with_capacity(remaining.len());
|
||||
|
||||
while !remaining.is_empty() {
|
||||
let mut progress = false;
|
||||
|
||||
for idx in 0..remaining.len() {
|
||||
let (_, _, deps, _) = &remaining[idx];
|
||||
let deps = deps.as_ref().expect("checked above");
|
||||
let ready = deps.iter().all(|var| {
|
||||
scheduled.contains(var) || !context.is_var_unbound(var, ScopingMode::RespectParent)
|
||||
});
|
||||
|
||||
if ready {
|
||||
let (value_expr, plan, _deps, binds) = remaining.remove(idx);
|
||||
scheduled.extend(binds.into_iter());
|
||||
ordered.push((value_expr, plan));
|
||||
progress = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !progress {
|
||||
ordered.extend(
|
||||
remaining
|
||||
.into_iter()
|
||||
.map(|(value_expr, plan, _, _)| (value_expr, plan)),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
*element_pairs = ordered;
|
||||
}
|
||||
|
||||
fn collect_object_pairs(
|
||||
lhs_expr: &ExprRef,
|
||||
rhs_expr: &ExprRef,
|
||||
) -> Result<Option<Vec<(ExprRef, ExprRef)>>> {
|
||||
let lhs_fields = match lhs_expr.as_ref() {
|
||||
Expr::Object { fields, .. } => fields,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let rhs_fields = match rhs_expr.as_ref() {
|
||||
Expr::Object { fields, .. } => fields,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let mut lhs_map: BTreeMap<Value, ExprRef> = BTreeMap::new();
|
||||
for (_, key_expr, val_expr) in lhs_fields {
|
||||
if let Some(key_value) = extract_literal_key(key_expr) {
|
||||
lhs_map.insert(key_value, val_expr.clone());
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
let mut pairs = Vec::with_capacity(lhs_map.len());
|
||||
let mut rhs_literal_count = 0;
|
||||
let mut missing_literal_key: Option<(String, Span)> = None;
|
||||
|
||||
for (_, key_expr, val_expr) in rhs_fields {
|
||||
if let Some(key_value) = extract_literal_key(key_expr) {
|
||||
rhs_literal_count += 1;
|
||||
|
||||
if let Some(lhs_value_expr) = lhs_map.get(&key_value) {
|
||||
pairs.push((lhs_value_expr.clone(), val_expr.clone()));
|
||||
} else if missing_literal_key.is_none() {
|
||||
missing_literal_key = Some((
|
||||
format_literal_key_for_error(&key_value),
|
||||
key_expr.span().clone(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
if rhs_literal_count != lhs_map.len() {
|
||||
return Err(BindingPlannerError::ObjectFieldCountMismatch {
|
||||
left_count: lhs_map.len(),
|
||||
right_count: rhs_literal_count,
|
||||
span: lhs_expr.span().clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some((key, span)) = missing_literal_key {
|
||||
return Err(BindingPlannerError::ObjectKeyNotFound { key, span });
|
||||
}
|
||||
|
||||
Ok(Some(pairs))
|
||||
}
|
||||
|
||||
fn preview_binding_plan<T: VariableBindingContext>(
|
||||
expr: &ExprRef,
|
||||
context: &T,
|
||||
scoping: ScopingMode,
|
||||
already_bound: &BTreeSet<String>,
|
||||
) -> (Option<DestructuringPlan>, BTreeSet<String>) {
|
||||
let mut scratch = already_bound.clone();
|
||||
let plan = create_destructuring_plan_with_tracking(expr, context, scoping, &mut scratch);
|
||||
let mut delta: BTreeSet<String> = scratch.difference(already_bound).cloned().collect();
|
||||
let plan = plan_only_if_binds(plan);
|
||||
if plan.is_none() {
|
||||
delta.clear();
|
||||
}
|
||||
(plan, delta)
|
||||
}
|
||||
56
src/compiler/destructuring_planner/context.rs
Normal file
56
src/compiler/destructuring_planner/context.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Context traits shared across planner submodules.
|
||||
|
||||
use alloc::collections::BTreeSet;
|
||||
use alloc::string::String;
|
||||
|
||||
/// Scoping mode for variable binding decisions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScopingMode {
|
||||
/// Respect existing bindings from parent scopes (normal scoping).
|
||||
RespectParent,
|
||||
/// Allow shadowing of parent scope bindings (local scoping).
|
||||
AllowShadowing,
|
||||
}
|
||||
|
||||
/// Trait for determining variable binding status within a scope context.
|
||||
pub trait VariableBindingContext {
|
||||
/// Check if a variable is unbound in this context.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `var_name` - The name of the variable to check
|
||||
/// * `scoping` - Whether to respect parent scopes or allow shadowing
|
||||
fn is_var_unbound(&self, var_name: &str, scoping: ScopingMode) -> bool;
|
||||
|
||||
/// Determine whether the current scope already bound this variable.
|
||||
///
|
||||
/// This is used to detect same-scope rebinding conflicts when the planner
|
||||
/// encounters `:=` assignments, while still allowing shadowing in nested scopes.
|
||||
fn has_same_scope_binding(&self, var_name: &str) -> bool;
|
||||
}
|
||||
|
||||
/// Context overlay that tracks newly bound variables on top of an existing context.
|
||||
pub(crate) struct OverlayBindingContext<'a, T: VariableBindingContext> {
|
||||
pub(crate) base: &'a T,
|
||||
pub(crate) newly_bound: &'a BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl<'a, T: VariableBindingContext> VariableBindingContext for OverlayBindingContext<'a, T> {
|
||||
fn is_var_unbound(&self, var_name: &str, scoping: ScopingMode) -> bool {
|
||||
if self.newly_bound.contains(var_name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.base.is_var_unbound(var_name, scoping)
|
||||
}
|
||||
|
||||
fn has_same_scope_binding(&self, var_name: &str) -> bool {
|
||||
if self.newly_bound.contains(var_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.base.has_same_scope_binding(var_name)
|
||||
}
|
||||
}
|
||||
106
src/compiler/destructuring_planner/destructuring.rs
Normal file
106
src/compiler/destructuring_planner/destructuring.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Functions responsible for building destructuring plans.
|
||||
|
||||
use alloc::collections::{BTreeMap, BTreeSet};
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::ast::{Expr, ExprRef};
|
||||
use crate::compiler::destructuring_planner::context::OverlayBindingContext;
|
||||
use crate::compiler::destructuring_planner::utils::extract_literal_key;
|
||||
use crate::compiler::destructuring_planner::{
|
||||
DestructuringPlan, ScopingMode, VariableBindingContext,
|
||||
};
|
||||
|
||||
/// Create a destructuring plan for an expression using a variable binding context.
|
||||
pub fn create_destructuring_plan<T: VariableBindingContext>(
|
||||
expr: &ExprRef,
|
||||
context: &T,
|
||||
scoping: ScopingMode,
|
||||
) -> Option<DestructuringPlan> {
|
||||
let mut newly_bound = BTreeSet::new();
|
||||
create_destructuring_plan_with_tracking(expr, context, scoping, &mut newly_bound)
|
||||
}
|
||||
|
||||
/// Create a destructuring plan while tracking newly bound variables.
|
||||
pub(crate) fn create_destructuring_plan_with_tracking<T: VariableBindingContext>(
|
||||
expr: &ExprRef,
|
||||
context: &T,
|
||||
scoping: ScopingMode,
|
||||
newly_bound: &mut BTreeSet<String>,
|
||||
) -> Option<DestructuringPlan> {
|
||||
let overlay = OverlayBindingContext {
|
||||
base: context,
|
||||
newly_bound,
|
||||
};
|
||||
|
||||
match expr.as_ref() {
|
||||
// Variable binding
|
||||
Expr::Var { span: name, .. } => {
|
||||
if name.text() == "_" {
|
||||
return Some(DestructuringPlan::Ignore);
|
||||
}
|
||||
if overlay.is_var_unbound(name.text(), scoping) {
|
||||
newly_bound.insert(name.text().to_string());
|
||||
Some(DestructuringPlan::Var(name.clone()))
|
||||
} else {
|
||||
// Already bound - treat as equality check
|
||||
Some(DestructuringPlan::EqualityExpr(expr.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
Expr::String { value, .. }
|
||||
| Expr::RawString { value, .. }
|
||||
| Expr::Number { value, .. }
|
||||
| Expr::Bool { value, .. }
|
||||
| Expr::Null { value, .. } => Some(DestructuringPlan::EqualityValue(value.clone())),
|
||||
|
||||
// Array destructuring
|
||||
Expr::Array { items, .. } => {
|
||||
let mut element_plans = Vec::new();
|
||||
for item in items {
|
||||
if let Some(plan) =
|
||||
create_destructuring_plan_with_tracking(item, context, scoping, newly_bound)
|
||||
{
|
||||
element_plans.push(plan);
|
||||
} else {
|
||||
// If any element can't be destructured, fail the whole array
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(DestructuringPlan::Array { element_plans })
|
||||
}
|
||||
|
||||
// Object destructuring
|
||||
Expr::Object { fields, .. } => {
|
||||
let mut field_plans = BTreeMap::new();
|
||||
let mut dynamic_fields = Vec::new();
|
||||
for (_, key_expr, value_expr) in fields {
|
||||
if let Some(value_plan) = create_destructuring_plan_with_tracking(
|
||||
value_expr,
|
||||
context,
|
||||
scoping,
|
||||
newly_bound,
|
||||
) {
|
||||
if let Some(key_value) = extract_literal_key(key_expr) {
|
||||
field_plans.insert(key_value, value_plan);
|
||||
} else {
|
||||
dynamic_fields.push((key_expr.clone(), value_plan));
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(DestructuringPlan::Object {
|
||||
field_plans,
|
||||
dynamic_fields,
|
||||
})
|
||||
}
|
||||
|
||||
// For all others, treat as equality checks
|
||||
_ => Some(DestructuringPlan::EqualityExpr(expr.clone())),
|
||||
}
|
||||
}
|
||||
173
src/compiler/destructuring_planner/error.rs
Normal file
173
src/compiler/destructuring_planner/error.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Error definitions for the destructuring planner.
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use anyhow::Error;
|
||||
use core::error::Error as CoreError;
|
||||
use core::fmt;
|
||||
|
||||
use crate::lexer::Span;
|
||||
|
||||
/// Errors produced while building binding plans.
|
||||
#[derive(Debug)]
|
||||
pub enum BindingPlannerError {
|
||||
/// Assignment operator := requires left-hand side to have bindable variables.
|
||||
ColonEqualsRequiresBindableLeft { span: Span },
|
||||
/// Array size mismatch in assignment destructuring.
|
||||
ArraySizeMismatch {
|
||||
left_size: usize,
|
||||
right_size: usize,
|
||||
span: Span,
|
||||
},
|
||||
/// Array length mismatch detected while planning evaluation (no assignments).
|
||||
ArrayLengthMismatch {
|
||||
expected: usize,
|
||||
actual: usize,
|
||||
span: Span,
|
||||
},
|
||||
/// Object literal keys mismatch detected while planning evaluation (no assignments).
|
||||
ObjectLiteralKeysMismatch {
|
||||
expected: Vec<String>,
|
||||
actual: Vec<String>,
|
||||
span: Span,
|
||||
},
|
||||
/// Object field count mismatch in assignment destructuring.
|
||||
ObjectFieldCountMismatch {
|
||||
left_count: usize,
|
||||
right_count: usize,
|
||||
span: Span,
|
||||
},
|
||||
/// Object key not found in destructuring.
|
||||
ObjectKeyNotFound { key: String, span: Span },
|
||||
/// Variable reuse detected when a new binding is required.
|
||||
VariableAlreadyDefined { var: String, span: Span },
|
||||
/// Incompatible destructuring patterns.
|
||||
IncompatibleDestructuringPatterns { span: Span },
|
||||
/// Failed to create destructuring plan.
|
||||
FailedToCreateDestructuringPlan { plan_type: String, span: Span },
|
||||
}
|
||||
|
||||
/// Result alias used throughout the binding planner.
|
||||
pub type Result<T> = core::result::Result<T, BindingPlannerError>;
|
||||
|
||||
/// Convert planner errors into diagnostic-rich anyhow errors for compiler callers.
|
||||
pub fn map_binding_error(err: BindingPlannerError) -> Error {
|
||||
match err {
|
||||
BindingPlannerError::ColonEqualsRequiresBindableLeft { span } => span
|
||||
.error("assignment operator := requires left-hand side to have bindable variables"),
|
||||
BindingPlannerError::ArraySizeMismatch { span, .. }
|
||||
| BindingPlannerError::ArrayLengthMismatch { span, .. } => {
|
||||
span.error("mismatch in number of array elements")
|
||||
}
|
||||
BindingPlannerError::ObjectLiteralKeysMismatch {
|
||||
expected,
|
||||
actual,
|
||||
span,
|
||||
} => span.error(&format!(
|
||||
"object literal keys mismatch. Expected keys {:?} got {:?}.",
|
||||
expected, actual
|
||||
)),
|
||||
BindingPlannerError::ObjectFieldCountMismatch {
|
||||
left_count,
|
||||
right_count,
|
||||
span,
|
||||
} => span.error(&format!(
|
||||
"object field count mismatch in assignment: left has {left_count} fields, right has {right_count} fields"
|
||||
)),
|
||||
BindingPlannerError::ObjectKeyNotFound { key, span } => span
|
||||
.error(&format!("key \"{key}\" not found in left-hand side object during destructuring")),
|
||||
BindingPlannerError::VariableAlreadyDefined { var, span } => {
|
||||
span.error(&format!("var `{var}` used before definition below"))
|
||||
}
|
||||
BindingPlannerError::IncompatibleDestructuringPatterns { span } => span.error(
|
||||
"incompatible destructuring patterns: both sides must be arrays or objects with matching structure",
|
||||
),
|
||||
BindingPlannerError::FailedToCreateDestructuringPlan { plan_type, span } => {
|
||||
span.error(&format!("failed to create {plan_type} destructuring plan"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BindingPlannerError {
|
||||
pub(crate) fn to_span_message(&self) -> String {
|
||||
match self {
|
||||
BindingPlannerError::ColonEqualsRequiresBindableLeft { span } => span
|
||||
.message(
|
||||
"error",
|
||||
"assignment operator := requires left-hand side to have bindable variables",
|
||||
),
|
||||
BindingPlannerError::ArraySizeMismatch {
|
||||
left_size,
|
||||
right_size,
|
||||
span,
|
||||
} => {
|
||||
let detail = format!(
|
||||
"mismatch in number of array elements (left has {left_size}, right has {right_size})"
|
||||
);
|
||||
span.message("error", detail.as_str())
|
||||
}
|
||||
BindingPlannerError::ArrayLengthMismatch {
|
||||
expected,
|
||||
actual,
|
||||
span,
|
||||
} => {
|
||||
let detail = format!(
|
||||
"array length mismatch. Expected {expected} got {actual}."
|
||||
);
|
||||
span.message("error", detail.as_str())
|
||||
}
|
||||
BindingPlannerError::ObjectLiteralKeysMismatch {
|
||||
expected,
|
||||
actual,
|
||||
span,
|
||||
} => {
|
||||
let detail = format!(
|
||||
"object literal keys mismatch. Expected keys {:?} got {:?}.",
|
||||
expected, actual
|
||||
);
|
||||
span.message("error", detail.as_str())
|
||||
}
|
||||
BindingPlannerError::ObjectFieldCountMismatch {
|
||||
left_count,
|
||||
right_count,
|
||||
span,
|
||||
} => {
|
||||
let detail = format!(
|
||||
"object field count mismatch in assignment: left has {left_count} fields, right has {right_count} fields"
|
||||
);
|
||||
span.message("error", detail.as_str())
|
||||
}
|
||||
BindingPlannerError::ObjectKeyNotFound { key, span } => {
|
||||
let detail = format!(
|
||||
"key \"{key}\" not found in left-hand side object during destructuring"
|
||||
);
|
||||
span.message("error", detail.as_str())
|
||||
}
|
||||
BindingPlannerError::VariableAlreadyDefined { var, span } => {
|
||||
let detail = format!("var `{var}` used before definition below");
|
||||
span.message("error", detail.as_str())
|
||||
}
|
||||
BindingPlannerError::IncompatibleDestructuringPatterns { span } => span
|
||||
.message(
|
||||
"error",
|
||||
"incompatible destructuring patterns: both sides must be arrays or objects with matching structure",
|
||||
),
|
||||
BindingPlannerError::FailedToCreateDestructuringPlan { plan_type, span } => {
|
||||
let detail = format!("failed to create {plan_type} destructuring plan");
|
||||
span.message("error", detail.as_str())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for BindingPlannerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.to_span_message())
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreError for BindingPlannerError {}
|
||||
25
src/compiler/destructuring_planner/mod.rs
Normal file
25
src/compiler/destructuring_planner/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Destructuring and binding planner utilities.
|
||||
//!
|
||||
//! This module hierarchy reorganizes the binding planner into smaller components
|
||||
//! so the compiler can evolve without ambiguity with FFI bindings. Submodules
|
||||
//! will be filled in as code is migrated from the legacy `bindings` module.
|
||||
|
||||
pub mod assignment;
|
||||
pub mod context;
|
||||
pub mod destructuring;
|
||||
pub mod error;
|
||||
pub mod parameters;
|
||||
pub mod plans;
|
||||
pub mod some_in;
|
||||
pub mod utils;
|
||||
|
||||
pub use assignment::create_assignment_binding_plan;
|
||||
pub use context::{ScopingMode, VariableBindingContext};
|
||||
pub use destructuring::create_destructuring_plan;
|
||||
pub use error::{map_binding_error, BindingPlannerError, Result};
|
||||
pub use parameters::{create_loop_index_binding_plan, create_parameter_binding_plan};
|
||||
pub use plans::{AssignmentPlan, BindingPlan, DestructuringPlan, WildcardSide};
|
||||
pub use some_in::create_some_in_binding_plan;
|
||||
59
src/compiler/destructuring_planner/parameters.rs
Normal file
59
src/compiler/destructuring_planner/parameters.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Planner helpers for function parameters and loop indices.
|
||||
|
||||
use alloc::collections::BTreeSet;
|
||||
use alloc::string::ToString;
|
||||
|
||||
use crate::ast::ExprRef;
|
||||
use crate::compiler::destructuring_planner::create_destructuring_plan;
|
||||
use crate::compiler::destructuring_planner::destructuring::create_destructuring_plan_with_tracking;
|
||||
use crate::compiler::destructuring_planner::utils::validate_pattern_bindings;
|
||||
use crate::compiler::destructuring_planner::{
|
||||
BindingPlan, BindingPlannerError, Result, ScopingMode, VariableBindingContext,
|
||||
};
|
||||
|
||||
/// Convenience function for loop index expressions (respects parent scope).
|
||||
pub fn create_loop_index_binding_plan<T: VariableBindingContext>(
|
||||
index_expr: &ExprRef,
|
||||
context: &T,
|
||||
) -> Result<BindingPlan> {
|
||||
let destructuring_plan =
|
||||
create_destructuring_plan(index_expr, context, ScopingMode::RespectParent).ok_or_else(
|
||||
|| BindingPlannerError::FailedToCreateDestructuringPlan {
|
||||
plan_type: "loop index".to_string(),
|
||||
span: index_expr.span().clone(),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(BindingPlan::LoopIndex {
|
||||
index_expr: index_expr.clone(),
|
||||
destructuring_plan,
|
||||
})
|
||||
}
|
||||
|
||||
/// Convenience function for function parameters (always allow shadowing).
|
||||
pub fn create_parameter_binding_plan<T: VariableBindingContext>(
|
||||
param_expr: &ExprRef,
|
||||
context: &T,
|
||||
) -> Result<BindingPlan> {
|
||||
let mut newly_bound = BTreeSet::new();
|
||||
let destructuring_plan = create_destructuring_plan_with_tracking(
|
||||
param_expr,
|
||||
context,
|
||||
ScopingMode::AllowShadowing,
|
||||
&mut newly_bound,
|
||||
)
|
||||
.ok_or_else(|| BindingPlannerError::FailedToCreateDestructuringPlan {
|
||||
plan_type: "parameter".to_string(),
|
||||
span: param_expr.span().clone(),
|
||||
})?;
|
||||
|
||||
validate_pattern_bindings(param_expr, &newly_bound, context)?;
|
||||
|
||||
Ok(BindingPlan::Parameter {
|
||||
param_expr: param_expr.clone(),
|
||||
destructuring_plan,
|
||||
})
|
||||
}
|
||||
241
src/compiler/destructuring_planner/plans.rs
Normal file
241
src/compiler/destructuring_planner/plans.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Core data structures used by the destructuring planner.
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::ast::ExprRef;
|
||||
use crate::lexer::Span;
|
||||
use crate::value::Value;
|
||||
|
||||
/// Strategy for how to destructure an expression.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DestructuringPlan {
|
||||
/// Bind the entire value to a variable.
|
||||
Var(Span),
|
||||
|
||||
/// Ignore the value completely.
|
||||
Ignore,
|
||||
|
||||
/// Check equality with the value of the dynamic expression.
|
||||
EqualityExpr(ExprRef),
|
||||
|
||||
/// Check equality against a literal value captured at planning time.
|
||||
EqualityValue(Value),
|
||||
|
||||
/// Destructure an array.
|
||||
Array {
|
||||
element_plans: Vec<DestructuringPlan>,
|
||||
},
|
||||
|
||||
/// Destructure an object.
|
||||
Object {
|
||||
field_plans: BTreeMap<Value, DestructuringPlan>,
|
||||
dynamic_fields: Vec<(ExprRef, DestructuringPlan)>,
|
||||
},
|
||||
}
|
||||
|
||||
impl DestructuringPlan {
|
||||
fn collect_bound_vars(&self, vars: &mut Vec<String>) {
|
||||
match self {
|
||||
DestructuringPlan::Var(name) => vars.push(name.text().to_string()),
|
||||
DestructuringPlan::Array { element_plans } => {
|
||||
for element in element_plans {
|
||||
element.collect_bound_vars(vars);
|
||||
}
|
||||
}
|
||||
DestructuringPlan::Object {
|
||||
field_plans,
|
||||
dynamic_fields,
|
||||
} => {
|
||||
for plan in field_plans.values() {
|
||||
plan.collect_bound_vars(vars);
|
||||
}
|
||||
for (_, plan) in dynamic_fields {
|
||||
plan.collect_bound_vars(vars);
|
||||
}
|
||||
}
|
||||
DestructuringPlan::Ignore
|
||||
| DestructuringPlan::EqualityExpr(_)
|
||||
| DestructuringPlan::EqualityValue(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn contains_wildcards(&self) -> bool {
|
||||
match self {
|
||||
DestructuringPlan::Ignore => true,
|
||||
DestructuringPlan::Array { element_plans } => element_plans
|
||||
.iter()
|
||||
.any(DestructuringPlan::contains_wildcards),
|
||||
DestructuringPlan::Object {
|
||||
field_plans,
|
||||
dynamic_fields,
|
||||
} => {
|
||||
field_plans
|
||||
.values()
|
||||
.any(DestructuringPlan::contains_wildcards)
|
||||
|| dynamic_fields
|
||||
.iter()
|
||||
.any(|(_, plan)| plan.contains_wildcards())
|
||||
}
|
||||
DestructuringPlan::Var(_)
|
||||
| DestructuringPlan::EqualityExpr(_)
|
||||
| DestructuringPlan::EqualityValue(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bound_vars(&self) -> Vec<String> {
|
||||
let mut vars = Vec::new();
|
||||
self.collect_bound_vars(&mut vars);
|
||||
vars
|
||||
}
|
||||
|
||||
pub(crate) fn introduces_binding(&self) -> bool {
|
||||
match self {
|
||||
DestructuringPlan::Var(_) | DestructuringPlan::Ignore => true,
|
||||
DestructuringPlan::Array { element_plans } => element_plans
|
||||
.iter()
|
||||
.any(DestructuringPlan::introduces_binding),
|
||||
DestructuringPlan::Object {
|
||||
field_plans,
|
||||
dynamic_fields,
|
||||
} => {
|
||||
field_plans
|
||||
.values()
|
||||
.any(DestructuringPlan::introduces_binding)
|
||||
|| dynamic_fields
|
||||
.iter()
|
||||
.any(|(_, plan)| plan.introduces_binding())
|
||||
}
|
||||
DestructuringPlan::EqualityExpr(_) | DestructuringPlan::EqualityValue(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Strategy-based assignment plan with specific rules for := and = operators.
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub enum AssignmentPlan {
|
||||
/// For := (ColEq) - only LHS can have patterns.
|
||||
ColonEquals {
|
||||
lhs_expr: ExprRef,
|
||||
rhs_expr: ExprRef,
|
||||
lhs_plan: DestructuringPlan,
|
||||
},
|
||||
|
||||
/// For = (Eq) - only one side has unbound variables.
|
||||
EqualsBindLeft {
|
||||
lhs_expr: ExprRef,
|
||||
rhs_expr: ExprRef,
|
||||
lhs_plan: DestructuringPlan,
|
||||
},
|
||||
|
||||
/// For = (Eq) - only one side has unbound variables.
|
||||
EqualsBindRight {
|
||||
lhs_expr: ExprRef,
|
||||
rhs_expr: ExprRef,
|
||||
rhs_plan: DestructuringPlan,
|
||||
},
|
||||
|
||||
/// For = (Eq) - both sides have unbound vars, flattened to pairs.
|
||||
/// Each pair is (value_expr, destructuring_plan_for_pattern).
|
||||
EqualsBothSides {
|
||||
lhs_expr: ExprRef,
|
||||
rhs_expr: ExprRef,
|
||||
element_pairs: Vec<(ExprRef, DestructuringPlan)>,
|
||||
},
|
||||
|
||||
/// No variables to bind - simple equality check.
|
||||
EqualityCheck {
|
||||
lhs_expr: ExprRef,
|
||||
rhs_expr: ExprRef,
|
||||
},
|
||||
|
||||
/// No variables to bind and at least one side is a wildcard `_`.
|
||||
WildcardMatch {
|
||||
lhs_expr: ExprRef,
|
||||
rhs_expr: ExprRef,
|
||||
wildcard_side: WildcardSide,
|
||||
},
|
||||
}
|
||||
|
||||
impl AssignmentPlan {
|
||||
pub(crate) fn bound_vars(&self) -> Vec<String> {
|
||||
match self {
|
||||
AssignmentPlan::ColonEquals { lhs_plan, .. }
|
||||
| AssignmentPlan::EqualsBindLeft { lhs_plan, .. } => lhs_plan.bound_vars(),
|
||||
AssignmentPlan::EqualsBindRight { rhs_plan, .. } => rhs_plan.bound_vars(),
|
||||
AssignmentPlan::EqualsBothSides { element_pairs, .. } => {
|
||||
let mut vars = Vec::new();
|
||||
for (_, plan) in element_pairs {
|
||||
vars.extend(plan.bound_vars());
|
||||
}
|
||||
vars
|
||||
}
|
||||
AssignmentPlan::EqualityCheck { .. } | AssignmentPlan::WildcardMatch { .. } => {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates which side of an equality expression contains a wildcard `_`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub enum WildcardSide {
|
||||
Lhs,
|
||||
Rhs,
|
||||
Both,
|
||||
}
|
||||
|
||||
/// High-level plan describing how bindings are produced in various contexts.
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub enum BindingPlan {
|
||||
Assignment {
|
||||
plan: AssignmentPlan,
|
||||
},
|
||||
LoopIndex {
|
||||
index_expr: ExprRef,
|
||||
destructuring_plan: DestructuringPlan,
|
||||
},
|
||||
Parameter {
|
||||
param_expr: ExprRef,
|
||||
destructuring_plan: DestructuringPlan,
|
||||
},
|
||||
SomeIn {
|
||||
collection_expr: ExprRef,
|
||||
key_plan: Option<DestructuringPlan>,
|
||||
value_plan: DestructuringPlan,
|
||||
},
|
||||
}
|
||||
|
||||
impl BindingPlan {
|
||||
/// Return the set of variables newly bound by this plan.
|
||||
pub fn bound_vars(&self) -> Vec<String> {
|
||||
match self {
|
||||
BindingPlan::Assignment { plan } => plan.bound_vars(),
|
||||
BindingPlan::LoopIndex {
|
||||
destructuring_plan, ..
|
||||
} => destructuring_plan.bound_vars(),
|
||||
BindingPlan::Parameter {
|
||||
destructuring_plan, ..
|
||||
} => destructuring_plan.bound_vars(),
|
||||
BindingPlan::SomeIn {
|
||||
key_plan,
|
||||
value_plan,
|
||||
..
|
||||
} => {
|
||||
let mut vars = Vec::new();
|
||||
if let Some(key_destructuring) = key_plan {
|
||||
vars.extend(key_destructuring.bound_vars());
|
||||
}
|
||||
vars.extend(value_plan.bound_vars());
|
||||
vars
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/compiler/destructuring_planner/some_in.rs
Normal file
92
src/compiler/destructuring_planner/some_in.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Planner support for `some .. in` expressions.
|
||||
|
||||
use alloc::collections::BTreeSet;
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::ast::{Expr, ExprRef};
|
||||
use crate::compiler::destructuring_planner::destructuring::create_destructuring_plan_with_tracking;
|
||||
use crate::compiler::destructuring_planner::utils::{
|
||||
check_literal_structure, validate_pattern_bindings, LiteralStructureCheck,
|
||||
};
|
||||
use crate::compiler::destructuring_planner::{
|
||||
BindingPlan, BindingPlannerError, Result, ScopingMode, VariableBindingContext,
|
||||
};
|
||||
|
||||
/// Convenience function for some..in expressions (always allow shadowing for new bindings).
|
||||
pub fn create_some_in_binding_plan<T: VariableBindingContext>(
|
||||
key_expr: &Option<ExprRef>,
|
||||
value_expr: &ExprRef,
|
||||
collection_expr: &ExprRef,
|
||||
context: &T,
|
||||
) -> Result<BindingPlan> {
|
||||
let key_plan = if let Some(key) = key_expr {
|
||||
let mut newly_bound = BTreeSet::new();
|
||||
let plan = create_destructuring_plan_with_tracking(
|
||||
key,
|
||||
context,
|
||||
ScopingMode::RespectParent,
|
||||
&mut newly_bound,
|
||||
);
|
||||
if let Some(plan) = plan {
|
||||
validate_pattern_bindings(key, &newly_bound, context)?;
|
||||
Some(plan)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut newly_bound_value = BTreeSet::new();
|
||||
let value_plan = create_destructuring_plan_with_tracking(
|
||||
value_expr,
|
||||
context,
|
||||
ScopingMode::RespectParent,
|
||||
&mut newly_bound_value,
|
||||
)
|
||||
.ok_or_else(|| BindingPlannerError::FailedToCreateDestructuringPlan {
|
||||
plan_type: String::from("some-in value"),
|
||||
span: value_expr.span().clone(),
|
||||
})?;
|
||||
|
||||
validate_pattern_bindings(value_expr, &newly_bound_value, context)?;
|
||||
|
||||
if let Expr::Array { items, .. } = collection_expr.as_ref() {
|
||||
let mut found_match = false;
|
||||
let mut mismatch_error: Option<BindingPlannerError> = None;
|
||||
let mut saw_unknown = false;
|
||||
|
||||
for collection_item in items {
|
||||
match check_literal_structure(&value_plan, collection_item) {
|
||||
LiteralStructureCheck::Match => {
|
||||
found_match = true;
|
||||
}
|
||||
LiteralStructureCheck::Unknown => {
|
||||
saw_unknown = true;
|
||||
}
|
||||
mismatch => {
|
||||
if let Some(err) = mismatch.into_error() {
|
||||
if mismatch_error.is_none() {
|
||||
mismatch_error = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_match && !saw_unknown {
|
||||
if let Some(err) = mismatch_error {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BindingPlan::SomeIn {
|
||||
collection_expr: collection_expr.clone(),
|
||||
key_plan,
|
||||
value_plan,
|
||||
})
|
||||
}
|
||||
271
src/compiler/destructuring_planner/utils.rs
Normal file
271
src/compiler/destructuring_planner/utils.rs
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Shared helper routines for the destructuring planner modules.
|
||||
|
||||
use alloc::collections::BTreeSet;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::ast::{Expr, ExprRef};
|
||||
use crate::compiler::destructuring_planner::{
|
||||
BindingPlannerError, DestructuringPlan, Result, ScopingMode, VariableBindingContext,
|
||||
};
|
||||
use crate::lexer::Span;
|
||||
use crate::value::Value;
|
||||
|
||||
/// Result of statically comparing a destructuring plan with a literal expression.
|
||||
pub(crate) enum LiteralStructureCheck {
|
||||
Match,
|
||||
Unknown,
|
||||
ArrayMismatch {
|
||||
expected: usize,
|
||||
actual: usize,
|
||||
span: Span,
|
||||
},
|
||||
ObjectMismatch {
|
||||
expected: Vec<String>,
|
||||
actual: Vec<String>,
|
||||
span: Span,
|
||||
},
|
||||
}
|
||||
|
||||
impl LiteralStructureCheck {
|
||||
pub(crate) fn into_error(self) -> Option<BindingPlannerError> {
|
||||
match self {
|
||||
LiteralStructureCheck::Match | LiteralStructureCheck::Unknown => None,
|
||||
LiteralStructureCheck::ArrayMismatch {
|
||||
expected,
|
||||
actual,
|
||||
span,
|
||||
} => Some(BindingPlannerError::ArrayLengthMismatch {
|
||||
expected,
|
||||
actual,
|
||||
span,
|
||||
}),
|
||||
LiteralStructureCheck::ObjectMismatch {
|
||||
expected,
|
||||
actual,
|
||||
span,
|
||||
} => Some(BindingPlannerError::ObjectLiteralKeysMismatch {
|
||||
expected,
|
||||
actual,
|
||||
span,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare a destructuring plan against a literal expression.
|
||||
pub(crate) fn check_literal_structure(
|
||||
plan: &DestructuringPlan,
|
||||
expr: &ExprRef,
|
||||
) -> LiteralStructureCheck {
|
||||
match (plan, expr.as_ref()) {
|
||||
(DestructuringPlan::Array { element_plans }, Expr::Array { items, .. }) => {
|
||||
if items.len() != element_plans.len() {
|
||||
return LiteralStructureCheck::ArrayMismatch {
|
||||
expected: element_plans.len(),
|
||||
actual: items.len(),
|
||||
span: expr.span().clone(),
|
||||
};
|
||||
}
|
||||
|
||||
for (nested_plan, nested_expr) in element_plans.iter().zip(items.iter()) {
|
||||
match check_literal_structure(nested_plan, nested_expr) {
|
||||
LiteralStructureCheck::Match => {}
|
||||
LiteralStructureCheck::Unknown => return LiteralStructureCheck::Unknown,
|
||||
mismatch => return mismatch,
|
||||
}
|
||||
}
|
||||
|
||||
LiteralStructureCheck::Match
|
||||
}
|
||||
(
|
||||
DestructuringPlan::Object {
|
||||
field_plans,
|
||||
dynamic_fields,
|
||||
},
|
||||
Expr::Object { fields, .. },
|
||||
) => {
|
||||
if field_plans.is_empty() && dynamic_fields.is_empty() {
|
||||
return LiteralStructureCheck::Match;
|
||||
}
|
||||
|
||||
if !field_plans.is_empty() {
|
||||
let expected_keys: Vec<String> = field_plans
|
||||
.keys()
|
||||
.map(format_literal_key_for_error)
|
||||
.collect();
|
||||
|
||||
let mut literal_fields: Vec<(Value, &ExprRef)> = Vec::new();
|
||||
let mut actual_keys: Vec<String> = Vec::new();
|
||||
for (_, key_expr, value_expr) in fields {
|
||||
if let Some(key_value) = extract_literal_key(key_expr) {
|
||||
actual_keys.push(format_literal_key_for_error(&key_value));
|
||||
literal_fields.push((key_value, value_expr));
|
||||
}
|
||||
}
|
||||
|
||||
let mut expected_sorted = expected_keys.clone();
|
||||
expected_sorted.sort();
|
||||
let mut actual_sorted = actual_keys.clone();
|
||||
actual_sorted.sort();
|
||||
|
||||
if expected_sorted != actual_sorted {
|
||||
return LiteralStructureCheck::ObjectMismatch {
|
||||
expected: expected_keys,
|
||||
actual: actual_keys,
|
||||
span: expr.span().clone(),
|
||||
};
|
||||
}
|
||||
|
||||
for (key_value, value_expr) in literal_fields {
|
||||
if let Some(field_plan) = field_plans.get(&key_value) {
|
||||
match check_literal_structure(field_plan, value_expr) {
|
||||
LiteralStructureCheck::Match => {}
|
||||
LiteralStructureCheck::Unknown => {
|
||||
return LiteralStructureCheck::Unknown
|
||||
}
|
||||
mismatch => return mismatch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dynamic_fields.is_empty() {
|
||||
return LiteralStructureCheck::Match;
|
||||
}
|
||||
}
|
||||
|
||||
LiteralStructureCheck::Unknown
|
||||
}
|
||||
_ => LiteralStructureCheck::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_literal_match(plan: &DestructuringPlan, expr: &ExprRef) -> Result<()> {
|
||||
match check_literal_structure(plan, expr).into_error() {
|
||||
Some(err) => Err(err),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect_pattern_var_spans(expr: &ExprRef, spans: &mut Vec<Span>) {
|
||||
match expr.as_ref() {
|
||||
Expr::Var { span, .. } => {
|
||||
let name = span.text();
|
||||
if name != "_" && name != "input" && name != "data" {
|
||||
spans.push(span.clone());
|
||||
}
|
||||
}
|
||||
Expr::Array { items, .. } => {
|
||||
for item in items {
|
||||
collect_pattern_var_spans(item, spans);
|
||||
}
|
||||
}
|
||||
Expr::Set { items, .. } => {
|
||||
for item in items {
|
||||
collect_pattern_var_spans(item, spans);
|
||||
}
|
||||
}
|
||||
Expr::Object { fields, .. } => {
|
||||
for (_, _, value_expr) in fields {
|
||||
collect_pattern_var_spans(value_expr, spans);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_pattern_bindings<T: VariableBindingContext>(
|
||||
expr: &ExprRef,
|
||||
newly_bound: &BTreeSet<String>,
|
||||
context: &T,
|
||||
) -> Result<()> {
|
||||
let mut candidate_vars = Vec::new();
|
||||
collect_pattern_var_spans(expr, &mut candidate_vars);
|
||||
|
||||
for span in candidate_vars {
|
||||
let name = span.text().to_string();
|
||||
if newly_bound.contains(&name) {
|
||||
continue;
|
||||
}
|
||||
if !context.is_var_unbound(&name, ScopingMode::RespectParent) {
|
||||
return Err(BindingPlannerError::VariableAlreadyDefined { var: name, span });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn collect_plan_var_spans(plan: &DestructuringPlan, spans: &mut Vec<Span>) {
|
||||
match plan {
|
||||
DestructuringPlan::Var(span) => spans.push(span.clone()),
|
||||
DestructuringPlan::Array { element_plans } => {
|
||||
for nested in element_plans {
|
||||
collect_plan_var_spans(nested, spans);
|
||||
}
|
||||
}
|
||||
DestructuringPlan::Object {
|
||||
field_plans,
|
||||
dynamic_fields,
|
||||
} => {
|
||||
for nested in field_plans.values() {
|
||||
collect_plan_var_spans(nested, spans);
|
||||
}
|
||||
for (_, nested) in dynamic_fields {
|
||||
collect_plan_var_spans(nested, spans);
|
||||
}
|
||||
}
|
||||
DestructuringPlan::Ignore
|
||||
| DestructuringPlan::EqualityExpr(_)
|
||||
| DestructuringPlan::EqualityValue(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_structural_compatibility(
|
||||
lhs_expr: &ExprRef,
|
||||
rhs_expr: &ExprRef,
|
||||
) -> Result<()> {
|
||||
let lhs_is_array = matches!(lhs_expr.as_ref(), Expr::Array { .. });
|
||||
let rhs_is_array = matches!(rhs_expr.as_ref(), Expr::Array { .. });
|
||||
let lhs_is_object = matches!(lhs_expr.as_ref(), Expr::Object { .. });
|
||||
let rhs_is_object = matches!(rhs_expr.as_ref(), Expr::Object { .. });
|
||||
|
||||
if (lhs_is_array && rhs_is_object) || (lhs_is_object && rhs_is_array) {
|
||||
return Err(BindingPlannerError::IncompatibleDestructuringPatterns {
|
||||
span: lhs_expr.span().clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper that discards destructuring plans which do not bind any variables.
|
||||
pub(crate) fn plan_only_if_binds(plan: Option<DestructuringPlan>) -> Option<DestructuringPlan> {
|
||||
plan.and_then(|plan| {
|
||||
if plan.introduces_binding() || plan.contains_wildcards() {
|
||||
Some(plan)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn extract_literal_key(expr: &ExprRef) -> Option<Value> {
|
||||
match expr.as_ref() {
|
||||
Expr::String { value, .. } => Some(value.clone()),
|
||||
Expr::RawString { value, .. } => Some(value.clone()),
|
||||
Expr::Number { value, .. } => Some(value.clone()),
|
||||
Expr::Bool { value, .. } => Some(value.clone()),
|
||||
Expr::Null { .. } => Some(Value::Null),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_literal_key_for_error(value: &Value) -> String {
|
||||
match value {
|
||||
Value::String(s) => s.as_ref().to_string(),
|
||||
_ => value.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,66 @@
|
||||
//! pre-computing loop hoisting information that can be stored in the
|
||||
//! compiled policy and reused by the interpreter.
|
||||
|
||||
use super::destructuring_planner::{
|
||||
map_binding_error, BindingPlan, ScopingMode, VariableBindingContext,
|
||||
};
|
||||
use crate::ast::{Expr, ExprRef, Literal, LiteralStmt, Module, Query, Ref, Rule, RuleHead};
|
||||
use crate::compiler::context::{ContextType, ScopeContext};
|
||||
use crate::lookup::Lookup;
|
||||
use crate::scheduler::compute_module_globals;
|
||||
use crate::*;
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use alloc::collections::BTreeSet;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
/// Implementation of VariableBindingContext for ScopeContext
|
||||
impl VariableBindingContext for ScopeContext {
|
||||
fn is_var_unbound(&self, var_name: &str, scoping: ScopingMode) -> bool {
|
||||
if var_name == "_" {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.unbound_vars.contains(var_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.has_scheduler_scope && self.local_vars.contains(var_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
match scoping {
|
||||
ScopingMode::AllowShadowing => {
|
||||
// Allow shadowing - always consider variables as potentially unbound
|
||||
true
|
||||
}
|
||||
ScopingMode::RespectParent => {
|
||||
// Respect parent scope bindings
|
||||
if self
|
||||
.module_globals
|
||||
.as_ref()
|
||||
.is_some_and(|globals| globals.contains(var_name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.bound_vars.contains(var_name) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_same_scope_binding(&self, var_name: &str) -> bool {
|
||||
if var_name == "_" {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.current_scope_bound_vars.contains(var_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of loop that was hoisted
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LoopType {
|
||||
@@ -54,6 +106,10 @@ pub struct HoistedLoopsLookup {
|
||||
/// For output expressions in comprehensions and rule values
|
||||
expr_loops: Lookup<Vec<HoistedLoop>>,
|
||||
|
||||
/// Maps (module_index, expr_index) -> BindingPlan
|
||||
/// Stores pre-computed binding plans for assignment-style expressions
|
||||
expr_binding_plans: Lookup<BindingPlan>,
|
||||
|
||||
/// Maps (module_index, query_index) -> ScopeContext
|
||||
/// Stores compilation contexts for queries (rules, comprehensions, every)
|
||||
query_contexts: Lookup<ScopeContext>,
|
||||
@@ -69,6 +125,7 @@ impl HoistedLoopsLookup {
|
||||
pub fn ensure_statement_capacity(&mut self, module_idx: u32, stmt_idx: u32) {
|
||||
self.statement_loops.ensure_capacity(module_idx, stmt_idx);
|
||||
self.expr_loops.ensure_capacity(module_idx, 0);
|
||||
self.expr_binding_plans.ensure_capacity(module_idx, 0);
|
||||
self.query_contexts.ensure_capacity(module_idx, 0);
|
||||
}
|
||||
|
||||
@@ -76,6 +133,8 @@ impl HoistedLoopsLookup {
|
||||
pub fn ensure_expr_capacity(&mut self, module_idx: u32, expr_idx: u32) {
|
||||
self.expr_loops.ensure_capacity(module_idx, expr_idx);
|
||||
self.statement_loops.ensure_capacity(module_idx, 0);
|
||||
self.expr_binding_plans
|
||||
.ensure_capacity(module_idx, expr_idx);
|
||||
self.query_contexts.ensure_capacity(module_idx, 0);
|
||||
}
|
||||
|
||||
@@ -84,6 +143,7 @@ impl HoistedLoopsLookup {
|
||||
self.query_contexts.ensure_capacity(module_idx, query_idx);
|
||||
self.statement_loops.ensure_capacity(module_idx, 0);
|
||||
self.expr_loops.ensure_capacity(module_idx, 0);
|
||||
self.expr_binding_plans.ensure_capacity(module_idx, 0);
|
||||
}
|
||||
|
||||
/// Store hoisted loops for a statement
|
||||
@@ -111,11 +171,22 @@ impl HoistedLoopsLookup {
|
||||
self.query_contexts.set(module_idx, query_idx, context);
|
||||
}
|
||||
|
||||
/// Store a binding plan for an expression
|
||||
pub fn set_expr_binding_plan(&mut self, module_idx: u32, expr_idx: u32, plan: BindingPlan) {
|
||||
self.expr_binding_plans.set(module_idx, expr_idx, plan);
|
||||
}
|
||||
|
||||
/// Get the compilation context for a query
|
||||
#[allow(dead_code)]
|
||||
pub fn get_query_context(&self, module_idx: u32, query_idx: u32) -> Option<&ScopeContext> {
|
||||
self.query_contexts.get_checked(module_idx, query_idx)
|
||||
}
|
||||
|
||||
/// Get the binding plan for an expression
|
||||
pub fn get_expr_binding_plan(&self, module_idx: u32, expr_idx: u32) -> Option<&BindingPlan> {
|
||||
self.expr_binding_plans.get_checked(module_idx, expr_idx)
|
||||
}
|
||||
|
||||
/// Merge another loop hoisting table into this one
|
||||
/// This is used to add query module loops to the existing table
|
||||
pub fn merge_query_loops(&mut self, mut other: HoistedLoopsLookup, module_idx: usize) {
|
||||
@@ -127,6 +198,10 @@ impl HoistedLoopsLookup {
|
||||
self.expr_loops.push_module(Vec::new());
|
||||
}
|
||||
|
||||
while self.expr_binding_plans.module_len() < module_idx {
|
||||
self.expr_binding_plans.push_module(Vec::new());
|
||||
}
|
||||
|
||||
while self.query_contexts.module_len() < module_idx {
|
||||
self.query_contexts.push_module(Vec::new());
|
||||
}
|
||||
@@ -141,6 +216,10 @@ impl HoistedLoopsLookup {
|
||||
self.expr_loops.push_module(module);
|
||||
}
|
||||
|
||||
if let Some(module) = other.expr_binding_plans.remove_module(query_module_idx) {
|
||||
self.expr_binding_plans.push_module(module);
|
||||
}
|
||||
|
||||
if let Some(module) = other.query_contexts.remove_module(query_module_idx) {
|
||||
self.query_contexts.push_module(module);
|
||||
}
|
||||
@@ -149,6 +228,7 @@ impl HoistedLoopsLookup {
|
||||
pub fn truncate_modules(&mut self, module_count: usize) {
|
||||
self.statement_loops.truncate_modules(module_count);
|
||||
self.expr_loops.truncate_modules(module_count);
|
||||
self.expr_binding_plans.truncate_modules(module_count);
|
||||
self.query_contexts.truncate_modules(module_count);
|
||||
}
|
||||
|
||||
@@ -163,6 +243,7 @@ impl HoistedLoopsLookup {
|
||||
pub struct LoopHoister {
|
||||
lookup: HoistedLoopsLookup,
|
||||
schedule: Option<crate::Rc<crate::scheduler::Schedule>>,
|
||||
module_globals: Lookup<crate::Rc<BTreeSet<String>>>,
|
||||
}
|
||||
|
||||
impl LoopHoister {
|
||||
@@ -171,6 +252,7 @@ impl LoopHoister {
|
||||
Self {
|
||||
lookup: HoistedLoopsLookup::new(),
|
||||
schedule: None,
|
||||
module_globals: Lookup::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,18 +261,30 @@ impl LoopHoister {
|
||||
Self {
|
||||
lookup: HoistedLoopsLookup::new(),
|
||||
schedule: Some(schedule),
|
||||
module_globals: Lookup::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Populate loop hoisting information for all modules
|
||||
/// Returns the populated lookup table
|
||||
pub fn populate(mut self, modules: &[Ref<Module>]) -> Result<HoistedLoopsLookup> {
|
||||
self.module_globals = compute_module_globals(modules).map_err(|err| anyhow!(err))?;
|
||||
for (module_idx, module) in modules.iter().enumerate() {
|
||||
self.populate_module(module_idx as u32, module)?;
|
||||
}
|
||||
Ok(self.lookup)
|
||||
}
|
||||
|
||||
fn create_scope_context(&self, module_idx: u32) -> ScopeContext {
|
||||
let mut context = ScopeContext::new();
|
||||
|
||||
if let Some(globals) = self.module_globals.get_checked(module_idx, 0) {
|
||||
context.module_globals = Some(globals.clone());
|
||||
}
|
||||
|
||||
context
|
||||
}
|
||||
|
||||
/// Populate loop hoisting information for all modules, with extra capacity
|
||||
/// for additional modules that will be added later (e.g., query modules)
|
||||
///
|
||||
@@ -202,6 +296,7 @@ impl LoopHoister {
|
||||
modules: &[Ref<Module>],
|
||||
extra_capacity: u32,
|
||||
) -> Result<HoistedLoopsLookup> {
|
||||
self.module_globals = compute_module_globals(modules).map_err(|err| anyhow!(err))?;
|
||||
for (module_idx, module) in modules.iter().enumerate() {
|
||||
self.populate_module(module_idx as u32, module)?;
|
||||
}
|
||||
@@ -212,6 +307,9 @@ impl LoopHoister {
|
||||
self.lookup
|
||||
.ensure_statement_capacity(last_module_idx + i, 0);
|
||||
self.lookup.ensure_expr_capacity(last_module_idx + i, 0);
|
||||
self.module_globals.ensure_capacity(last_module_idx + i, 0);
|
||||
self.module_globals
|
||||
.set(last_module_idx + i, 0, crate::Rc::new(BTreeSet::new()));
|
||||
}
|
||||
Ok(self.lookup)
|
||||
}
|
||||
@@ -257,10 +355,18 @@ impl LoopHoister {
|
||||
.ensure_expr_capacity(module_idx, num_expressions - 1);
|
||||
}
|
||||
|
||||
self.module_globals.ensure_capacity(module_idx, 0);
|
||||
let mut reserved_globals = BTreeSet::new();
|
||||
reserved_globals.insert("data".to_string());
|
||||
reserved_globals.insert("input".to_string());
|
||||
self.module_globals
|
||||
.set(module_idx, 0, crate::Rc::new(reserved_globals));
|
||||
|
||||
// Populate the query with default context
|
||||
let context = ScopeContext::new();
|
||||
let context = self.create_scope_context(module_idx);
|
||||
self.lookup.ensure_query_capacity(module_idx, query.qidx);
|
||||
self.populate_query(module_idx, query, &context).map(|_| ())
|
||||
self.populate_query(module_idx, query, &context)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Populate loop information for a single rule
|
||||
@@ -268,11 +374,31 @@ impl LoopHoister {
|
||||
match rule {
|
||||
Rule::Spec { head, bodies, .. } => {
|
||||
// Create a context for this rule
|
||||
let mut context = ScopeContext::new();
|
||||
let mut context = self.create_scope_context(module_idx);
|
||||
|
||||
// Bind function parameters if this is a function rule
|
||||
if let RuleHead::Func { args, .. } = head {
|
||||
for param in args {
|
||||
// Create binding plan for function parameter
|
||||
match super::destructuring_planner::create_parameter_binding_plan(
|
||||
param, &context,
|
||||
) {
|
||||
Ok(binding_plan) => {
|
||||
let expr_idx = param.as_ref().eidx();
|
||||
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
|
||||
|
||||
// Immediately bind variables from the plan to context
|
||||
Self::bind_vars_from_plan_to_context(&binding_plan, &mut context);
|
||||
|
||||
self.lookup.set_expr_binding_plan(
|
||||
module_idx,
|
||||
expr_idx,
|
||||
binding_plan,
|
||||
);
|
||||
}
|
||||
Err(err) => return Err(map_binding_error(err)),
|
||||
}
|
||||
|
||||
// Extract variable name from parameter expression
|
||||
if let Expr::Var { span, .. } = param.as_ref() {
|
||||
context.bind_variable(span.text());
|
||||
@@ -309,11 +435,13 @@ impl LoopHoister {
|
||||
// Process each rule body (definitions)
|
||||
for body in bodies {
|
||||
// Create a context with the output expressions (using Rule context type)
|
||||
let body_context = context.child_with_output_exprs(
|
||||
let mut body_context = context.child_with_output_exprs(
|
||||
ContextType::Rule,
|
||||
key_expr.clone(),
|
||||
value_expr.clone(),
|
||||
);
|
||||
body_context.current_scope_bound_vars =
|
||||
context.current_scope_bound_vars.clone();
|
||||
|
||||
// Store the context for this query
|
||||
let populated_body_context =
|
||||
@@ -348,11 +476,13 @@ impl LoopHoister {
|
||||
|
||||
// Handle rules with head assignments but no bodies (e.g., `y := "string"`)
|
||||
if bodies.is_empty() {
|
||||
let body_context = context.child_with_output_exprs(
|
||||
let mut body_context = context.child_with_output_exprs(
|
||||
ContextType::Rule,
|
||||
key_expr.clone(),
|
||||
value_expr.clone(),
|
||||
);
|
||||
body_context.current_scope_bound_vars =
|
||||
context.current_scope_bound_vars.clone();
|
||||
|
||||
if let Some(ref key) = key_expr {
|
||||
self.populate_output_expr(module_idx, key, &body_context)?;
|
||||
@@ -365,7 +495,7 @@ impl LoopHoister {
|
||||
}
|
||||
Rule::Default { value, .. } => {
|
||||
// For default rules, just process the value expression
|
||||
let context = ScopeContext::new();
|
||||
let context = self.create_scope_context(module_idx);
|
||||
self.populate_output_expr(module_idx, value, &context)?;
|
||||
}
|
||||
}
|
||||
@@ -380,7 +510,8 @@ impl LoopHoister {
|
||||
query: &Query,
|
||||
parent_context: &ScopeContext,
|
||||
) -> Result<ScopeContext> {
|
||||
let mut context = parent_context.child();
|
||||
let mut context = parent_context.clone();
|
||||
context.current_scope_bound_vars = parent_context.current_scope_bound_vars.clone();
|
||||
|
||||
// Get the scheduled order if available
|
||||
let stmt_order: Vec<usize> = if let Some(ref schedule) = self.schedule {
|
||||
@@ -423,93 +554,26 @@ impl LoopHoister {
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse literal expressions to populate nested contexts (comprehensions, every, etc.)
|
||||
self.process_literal_for_contexts(module_idx, &stmt.literal, context)?;
|
||||
let mut loops = Vec::new();
|
||||
self.analyze_literal(module_idx, &stmt.literal, context, &mut loops)?;
|
||||
|
||||
for with_mod in &stmt.with_mods {
|
||||
self.process_expr_for_contexts(module_idx, &with_mod.refr, context)?;
|
||||
self.process_expr_for_contexts(module_idx, &with_mod.r#as, context)?;
|
||||
self.analyze_expr(module_idx, &with_mod.refr, context, &mut loops)?;
|
||||
self.analyze_expr(module_idx, &with_mod.r#as, context, &mut loops)?;
|
||||
}
|
||||
|
||||
// Hoist loops from this statement using populated contexts
|
||||
let loops =
|
||||
self.hoist_loops_from_literal_with_context(module_idx, &stmt.literal, context)?;
|
||||
|
||||
// Always store in lookup table, even if no loops (store empty vec)
|
||||
// This ensures the interpreter can always find an entry
|
||||
self.lookup.ensure_statement_capacity(module_idx, stmt_idx);
|
||||
self.lookup.set_statement_loops(module_idx, stmt_idx, loops);
|
||||
|
||||
// Update context based on variable bindings in this statement
|
||||
self.update_context_from_literal(&stmt.literal, context);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hoist loops from a literal with variable binding context
|
||||
fn hoist_loops_from_literal_with_context(
|
||||
&self,
|
||||
module_idx: u32,
|
||||
literal: &Literal,
|
||||
context: &ScopeContext,
|
||||
) -> Result<Vec<HoistedLoop>> {
|
||||
let mut loops = Vec::new();
|
||||
|
||||
use Literal::*;
|
||||
match literal {
|
||||
SomeIn {
|
||||
key,
|
||||
value,
|
||||
collection,
|
||||
..
|
||||
} => {
|
||||
// Recursively hoist from sub-expressions first
|
||||
if let Some(key) = key {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, key, &mut loops, context)?;
|
||||
}
|
||||
self.hoist_loops_from_expr_with_context(module_idx, value, &mut loops, context)?;
|
||||
self.hoist_loops_from_expr_with_context(
|
||||
module_idx, collection, &mut loops, context,
|
||||
)?;
|
||||
}
|
||||
Expr { expr, .. } => {
|
||||
// Hoist loops from expressions (like array[_] patterns)
|
||||
self.hoist_loops_from_expr_with_context(module_idx, expr, &mut loops, context)?;
|
||||
}
|
||||
Every { domain, query, .. } => {
|
||||
// Hoist from domain expression
|
||||
self.hoist_loops_from_expr_with_context(module_idx, domain, &mut loops, context)?;
|
||||
|
||||
// Process the Every query in a child context
|
||||
let child_context = self
|
||||
.lookup
|
||||
.get_query_context(module_idx, query.qidx)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| context.child());
|
||||
for stmt in &query.stmts {
|
||||
self.hoist_loops_from_literal_with_context(
|
||||
module_idx,
|
||||
&stmt.literal,
|
||||
&child_context,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
NotExpr { expr, .. } => {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, expr, &mut loops, context)?;
|
||||
}
|
||||
_ => {
|
||||
// Other literal types don't have loops to hoist
|
||||
}
|
||||
}
|
||||
|
||||
Ok(loops)
|
||||
}
|
||||
|
||||
/// Traverse literals to populate nested contexts (comprehensions, every, etc.)
|
||||
fn process_literal_for_contexts(
|
||||
fn analyze_literal(
|
||||
&mut self,
|
||||
module_idx: u32,
|
||||
literal: &Literal,
|
||||
context: &ScopeContext,
|
||||
context: &mut ScopeContext,
|
||||
loops: &mut Vec<HoistedLoop>,
|
||||
) -> Result<()> {
|
||||
use Literal::*;
|
||||
|
||||
@@ -520,31 +584,38 @@ impl LoopHoister {
|
||||
collection,
|
||||
..
|
||||
} => {
|
||||
let binding_plan = super::destructuring_planner::create_some_in_binding_plan(
|
||||
key, value, collection, context,
|
||||
)
|
||||
.map_err(map_binding_error)?;
|
||||
|
||||
let expr_idx = collection.as_ref().eidx();
|
||||
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
|
||||
Self::bind_vars_from_plan_to_context(&binding_plan, context);
|
||||
self.lookup
|
||||
.set_expr_binding_plan(module_idx, expr_idx, binding_plan);
|
||||
|
||||
if let Some(key_expr) = key {
|
||||
self.process_expr_for_contexts(module_idx, key_expr, context)?;
|
||||
self.analyze_expr(module_idx, key_expr, context, loops)?;
|
||||
}
|
||||
self.process_expr_for_contexts(module_idx, value, context)?;
|
||||
self.process_expr_for_contexts(module_idx, collection, context)?;
|
||||
self.analyze_expr(module_idx, value, context, loops)?;
|
||||
self.analyze_expr(module_idx, collection, context, loops)?;
|
||||
}
|
||||
Expr { expr, .. } | NotExpr { expr, .. } => {
|
||||
self.process_expr_for_contexts(module_idx, expr, context)?;
|
||||
Expr { expr, .. } => {
|
||||
self.analyze_expr(module_idx, expr, context, loops)?;
|
||||
}
|
||||
Every { domain, query, .. } => {
|
||||
// Process the domain expression for nested contexts
|
||||
self.process_expr_for_contexts(module_idx, domain, context)?;
|
||||
self.analyze_expr(module_idx, domain, context, loops)?;
|
||||
|
||||
// Create a child context for the Every quantifier
|
||||
let every_context = context.child_with_output_exprs(ContextType::Every, None, None);
|
||||
let populated_every_context =
|
||||
let populated_context =
|
||||
self.populate_query(module_idx, query.as_ref(), &every_context)?;
|
||||
self.lookup.ensure_query_capacity(module_idx, query.qidx);
|
||||
self.lookup.set_query_context(
|
||||
module_idx,
|
||||
query.qidx,
|
||||
populated_every_context.clone(),
|
||||
);
|
||||
|
||||
// Nested query already processed for hoisting via populated context
|
||||
self.lookup
|
||||
.set_query_context(module_idx, query.qidx, populated_context);
|
||||
}
|
||||
NotExpr { expr, .. } => {
|
||||
self.analyze_expr(module_idx, expr, context, loops)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -552,25 +623,31 @@ impl LoopHoister {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Traverse expressions to populate nested contexts (comprehensions, function params, etc.)
|
||||
fn process_expr_for_contexts(
|
||||
fn analyze_expr(
|
||||
&mut self,
|
||||
module_idx: u32,
|
||||
expr: &ExprRef,
|
||||
context: &ScopeContext,
|
||||
context: &mut ScopeContext,
|
||||
loops: &mut Vec<HoistedLoop>,
|
||||
) -> Result<()> {
|
||||
use crate::ast::Expr as E;
|
||||
|
||||
match expr.as_ref() {
|
||||
E::String { .. }
|
||||
| E::RawString { .. }
|
||||
| E::Number { .. }
|
||||
| E::Bool { .. }
|
||||
| E::Null { .. }
|
||||
| E::Var { .. } => {}
|
||||
E::Array { items, .. } | E::Set { items, .. } => {
|
||||
for item in items {
|
||||
self.process_expr_for_contexts(module_idx, item, context)?;
|
||||
self.analyze_expr(module_idx, item, context, loops)?;
|
||||
}
|
||||
}
|
||||
E::Object { fields, .. } => {
|
||||
for (_, key_expr, value_expr) in fields {
|
||||
self.process_expr_for_contexts(module_idx, key_expr, context)?;
|
||||
self.process_expr_for_contexts(module_idx, value_expr, context)?;
|
||||
self.analyze_expr(module_idx, key_expr, context, loops)?;
|
||||
self.analyze_expr(module_idx, value_expr, context, loops)?;
|
||||
}
|
||||
}
|
||||
E::ArrayCompr { term, query, .. } | E::SetCompr { term, query, .. } => {
|
||||
@@ -579,17 +656,12 @@ impl LoopHoister {
|
||||
None,
|
||||
Some(term.clone()),
|
||||
);
|
||||
|
||||
let populated_compr_context =
|
||||
let populated_context =
|
||||
self.populate_query(module_idx, query.as_ref(), &compr_context)?;
|
||||
self.lookup.ensure_query_capacity(module_idx, query.qidx);
|
||||
self.lookup.set_query_context(
|
||||
module_idx,
|
||||
query.qidx,
|
||||
populated_compr_context.clone(),
|
||||
);
|
||||
|
||||
self.populate_output_expr(module_idx, term, &populated_compr_context)?;
|
||||
self.lookup
|
||||
.set_query_context(module_idx, query.qidx, populated_context.clone());
|
||||
self.populate_output_expr_with_context(module_idx, term, &populated_context)?;
|
||||
}
|
||||
E::ObjectCompr {
|
||||
key, value, query, ..
|
||||
@@ -599,154 +671,21 @@ impl LoopHoister {
|
||||
Some(key.clone()),
|
||||
Some(value.clone()),
|
||||
);
|
||||
|
||||
let populated_compr_context =
|
||||
let populated_context =
|
||||
self.populate_query(module_idx, query.as_ref(), &compr_context)?;
|
||||
self.lookup.ensure_query_capacity(module_idx, query.qidx);
|
||||
self.lookup.set_query_context(
|
||||
module_idx,
|
||||
query.qidx,
|
||||
populated_compr_context.clone(),
|
||||
);
|
||||
|
||||
self.populate_output_expr(module_idx, key, &populated_compr_context)?;
|
||||
self.populate_output_expr(module_idx, value, &populated_compr_context)?;
|
||||
self.lookup
|
||||
.set_query_context(module_idx, query.qidx, populated_context.clone());
|
||||
self.populate_output_expr_with_context(module_idx, key, &populated_context)?;
|
||||
self.populate_output_expr_with_context(module_idx, value, &populated_context)?;
|
||||
}
|
||||
E::Call { fcn, params, .. } => {
|
||||
self.process_expr_for_contexts(module_idx, fcn, context)?;
|
||||
self.analyze_expr(module_idx, fcn, context, loops)?;
|
||||
for param in params {
|
||||
self.process_expr_for_contexts(module_idx, param, context)?;
|
||||
}
|
||||
}
|
||||
E::UnaryExpr { expr, .. } => {
|
||||
self.process_expr_for_contexts(module_idx, expr, context)?;
|
||||
}
|
||||
E::RefDot { refr, .. } => {
|
||||
self.process_expr_for_contexts(module_idx, refr, context)?;
|
||||
}
|
||||
E::RefBrack { refr, index, .. } => {
|
||||
self.process_expr_for_contexts(module_idx, refr, context)?;
|
||||
self.process_expr_for_contexts(module_idx, index, context)?;
|
||||
}
|
||||
E::BinExpr { lhs, rhs, .. }
|
||||
| E::BoolExpr { lhs, rhs, .. }
|
||||
| E::ArithExpr { lhs, rhs, .. } => {
|
||||
self.process_expr_for_contexts(module_idx, lhs, context)?;
|
||||
self.process_expr_for_contexts(module_idx, rhs, context)?;
|
||||
}
|
||||
E::AssignExpr { lhs, rhs, .. } => {
|
||||
self.process_expr_for_contexts(module_idx, lhs, context)?;
|
||||
self.process_expr_for_contexts(module_idx, rhs, context)?;
|
||||
}
|
||||
E::Membership {
|
||||
key,
|
||||
value,
|
||||
collection,
|
||||
..
|
||||
} => {
|
||||
if let Some(key_expr) = key {
|
||||
self.process_expr_for_contexts(module_idx, key_expr, context)?;
|
||||
}
|
||||
self.process_expr_for_contexts(module_idx, value, context)?;
|
||||
self.process_expr_for_contexts(module_idx, collection, context)?;
|
||||
}
|
||||
#[cfg(feature = "rego-extensions")]
|
||||
E::OrExpr { lhs, rhs, .. } => {
|
||||
self.process_expr_for_contexts(module_idx, lhs, context)?;
|
||||
self.process_expr_for_contexts(module_idx, rhs, context)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hoist loops from expressions with variable binding context
|
||||
fn hoist_loops_from_expr_with_context(
|
||||
&self,
|
||||
module_idx: u32,
|
||||
expr: &ExprRef,
|
||||
loops: &mut Vec<HoistedLoop>,
|
||||
context: &ScopeContext,
|
||||
) -> Result<()> {
|
||||
use Expr::*;
|
||||
match expr.as_ref() {
|
||||
// Primitive types - no loops to hoist
|
||||
String { .. }
|
||||
| RawString { .. }
|
||||
| Number { .. }
|
||||
| Bool { .. }
|
||||
| Null { .. }
|
||||
| Var { .. } => {
|
||||
// No sub-expressions to process
|
||||
}
|
||||
|
||||
// Collection types - hoist from items
|
||||
Array { items, .. } => {
|
||||
for item in items {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, item, loops, context)?;
|
||||
}
|
||||
}
|
||||
Set { items, .. } => {
|
||||
for item in items {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, item, loops, context)?;
|
||||
}
|
||||
}
|
||||
Object { fields, .. } => {
|
||||
for (_, key_expr, value_expr) in fields {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, key_expr, loops, context)?;
|
||||
self.hoist_loops_from_expr_with_context(
|
||||
module_idx, value_expr, loops, context,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Comprehensions - process their queries
|
||||
// Note: Comprehension contexts and output expressions will be handled
|
||||
// by populate_comprehension called from the parent expression processing
|
||||
ArrayCompr { term, query, .. } | SetCompr { term, query, .. } => {
|
||||
let child_context = self
|
||||
.lookup
|
||||
.get_query_context(module_idx, query.qidx)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| context.child());
|
||||
for stmt in &query.stmts {
|
||||
self.hoist_loops_from_literal_with_context(
|
||||
module_idx,
|
||||
&stmt.literal,
|
||||
&child_context,
|
||||
)?;
|
||||
}
|
||||
self.hoist_loops_from_expr_with_context(module_idx, term, loops, &child_context)?;
|
||||
}
|
||||
ObjectCompr {
|
||||
key, value, query, ..
|
||||
} => {
|
||||
let child_context = self
|
||||
.lookup
|
||||
.get_query_context(module_idx, query.qidx)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| context.child());
|
||||
for stmt in &query.stmts {
|
||||
self.hoist_loops_from_literal_with_context(
|
||||
module_idx,
|
||||
&stmt.literal,
|
||||
&child_context,
|
||||
)?;
|
||||
}
|
||||
self.hoist_loops_from_expr_with_context(module_idx, key, loops, &child_context)?;
|
||||
self.hoist_loops_from_expr_with_context(module_idx, value, loops, &child_context)?;
|
||||
}
|
||||
|
||||
// Function calls - check for walk() builtin which generates loops
|
||||
Call { fcn, params, .. } => {
|
||||
// First hoist loops in parameters.
|
||||
for param in params {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, param, loops, context)?;
|
||||
self.analyze_expr(module_idx, param, context, loops)?;
|
||||
}
|
||||
|
||||
// Check if this is a walk() call
|
||||
let is_walk = if let Var {
|
||||
let is_walk = if let E::Var {
|
||||
value: Value::String(name),
|
||||
..
|
||||
} = fcn.as_ref()
|
||||
@@ -759,35 +698,55 @@ impl LoopHoister {
|
||||
if is_walk {
|
||||
loops.push(HoistedLoop {
|
||||
loop_expr: Some(expr.clone()),
|
||||
key: None, // walk doesn't have an index
|
||||
key: None,
|
||||
value: expr.clone(),
|
||||
collection: expr.clone(), // The walk call itself
|
||||
collection: expr.clone(),
|
||||
loop_type: LoopType::Walk,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
// If the last parameter expression contains unbound vars, create a binding plan
|
||||
if let Some(last_param) = params.last() {
|
||||
match super::destructuring_planner::create_parameter_binding_plan(
|
||||
last_param, context,
|
||||
) {
|
||||
Ok(binding_plan) => {
|
||||
let expr_idx = last_param.as_ref().eidx();
|
||||
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
|
||||
|
||||
// For other function calls, hoist loops in parameters
|
||||
// Immediately bind variables from the plan to context
|
||||
Self::bind_vars_from_plan_to_context(&binding_plan, context);
|
||||
|
||||
self.lookup
|
||||
.set_expr_binding_plan(module_idx, expr_idx, binding_plan);
|
||||
}
|
||||
Err(err) => return Err(map_binding_error(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unary expressions - hoist from operand
|
||||
UnaryExpr { expr, .. } => {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, expr, loops, context)?;
|
||||
E::UnaryExpr { expr, .. } => {
|
||||
self.analyze_expr(module_idx, expr, context, loops)?;
|
||||
}
|
||||
|
||||
// Reference expressions - check for array[_] patterns
|
||||
RefDot { refr, .. } => {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, refr, loops, context)?;
|
||||
E::RefDot { refr, .. } => {
|
||||
self.analyze_expr(module_idx, refr, context, loops)?;
|
||||
}
|
||||
RefBrack { refr, index, .. } => {
|
||||
// Recursively hoist from sub-expressions
|
||||
self.hoist_loops_from_expr_with_context(module_idx, refr, loops, context)?;
|
||||
self.hoist_loops_from_expr_with_context(module_idx, index, loops, context)?;
|
||||
E::RefBrack { refr, index, .. } => {
|
||||
self.analyze_expr(module_idx, refr, context, loops)?;
|
||||
self.analyze_expr(module_idx, index, context, loops)?;
|
||||
|
||||
// Check if the index expression contains unbound variables
|
||||
// This handles both simple cases like array[x] and complex cases like array[[x, y]]
|
||||
if Self::expr_contains_unbound_vars(index, context) {
|
||||
// This index contains unbound variables - create a loop to iterate
|
||||
match super::destructuring_planner::create_loop_index_binding_plan(
|
||||
index, context,
|
||||
) {
|
||||
Ok(binding_plan) => {
|
||||
let expr_idx = index.as_ref().eidx();
|
||||
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
|
||||
Self::bind_vars_from_plan_to_context(&binding_plan, context);
|
||||
self.lookup
|
||||
.set_expr_binding_plan(module_idx, expr_idx, binding_plan);
|
||||
}
|
||||
Err(err) => return Err(map_binding_error(err)),
|
||||
}
|
||||
|
||||
loops.push(HoistedLoop {
|
||||
loop_expr: Some(expr.clone()),
|
||||
key: Some(index.clone()),
|
||||
@@ -795,97 +754,59 @@ impl LoopHoister {
|
||||
collection: refr.clone(),
|
||||
loop_type: LoopType::IndexIteration,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
E::BinExpr { lhs, rhs, .. }
|
||||
| E::BoolExpr { lhs, rhs, .. }
|
||||
| E::ArithExpr { lhs, rhs, .. } => {
|
||||
self.analyze_expr(module_idx, lhs, context, loops)?;
|
||||
self.analyze_expr(module_idx, rhs, context, loops)?;
|
||||
}
|
||||
E::AssignExpr { op, lhs, rhs, .. } => {
|
||||
let binding_plan = super::destructuring_planner::create_assignment_binding_plan(
|
||||
op.clone(),
|
||||
lhs,
|
||||
rhs,
|
||||
context,
|
||||
)
|
||||
.map_err(map_binding_error)?;
|
||||
|
||||
// Binary expressions - hoist from both operands
|
||||
BinExpr { lhs, rhs, .. } => {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, lhs, loops, context)?;
|
||||
self.hoist_loops_from_expr_with_context(module_idx, rhs, loops, context)?;
|
||||
}
|
||||
BoolExpr { lhs, rhs, .. } => {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, lhs, loops, context)?;
|
||||
self.hoist_loops_from_expr_with_context(module_idx, rhs, loops, context)?;
|
||||
}
|
||||
ArithExpr { lhs, rhs, .. } => {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, lhs, loops, context)?;
|
||||
self.hoist_loops_from_expr_with_context(module_idx, rhs, loops, context)?;
|
||||
}
|
||||
AssignExpr { lhs, rhs, .. } => {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, lhs, loops, context)?;
|
||||
self.hoist_loops_from_expr_with_context(module_idx, rhs, loops, context)?;
|
||||
}
|
||||
let expr_idx = expr.as_ref().eidx();
|
||||
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
|
||||
Self::bind_vars_from_plan_to_context(&binding_plan, context);
|
||||
self.lookup
|
||||
.set_expr_binding_plan(module_idx, expr_idx, binding_plan);
|
||||
|
||||
// Membership expressions - hoist from key, value, and collection
|
||||
Membership {
|
||||
self.analyze_expr(module_idx, lhs, context, loops)?;
|
||||
self.analyze_expr(module_idx, rhs, context, loops)?;
|
||||
}
|
||||
E::Membership {
|
||||
key,
|
||||
value,
|
||||
collection,
|
||||
..
|
||||
} => {
|
||||
if let Some(key_expr) = key {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, key_expr, loops, context)?;
|
||||
self.analyze_expr(module_idx, key_expr, context, loops)?;
|
||||
}
|
||||
self.hoist_loops_from_expr_with_context(module_idx, value, loops, context)?;
|
||||
self.hoist_loops_from_expr_with_context(module_idx, collection, loops, context)?;
|
||||
self.analyze_expr(module_idx, value, context, loops)?;
|
||||
self.analyze_expr(module_idx, collection, context, loops)?;
|
||||
}
|
||||
|
||||
// Handle conditionally compiled expression types
|
||||
#[cfg(feature = "rego-extensions")]
|
||||
OrExpr { lhs, rhs, .. } => {
|
||||
self.hoist_loops_from_expr_with_context(module_idx, lhs, loops, context)?;
|
||||
self.hoist_loops_from_expr_with_context(module_idx, rhs, loops, context)?;
|
||||
E::OrExpr { lhs, rhs, .. } => {
|
||||
self.analyze_expr(module_idx, lhs, context, loops)?;
|
||||
self.analyze_expr(module_idx, rhs, context, loops)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update context based on variable bindings in a literal
|
||||
fn update_context_from_literal(&self, literal: &Literal, context: &mut ScopeContext) {
|
||||
use crate::ast::Expr as E;
|
||||
use Literal::*;
|
||||
match literal {
|
||||
SomeIn { key, value, .. } => {
|
||||
// Bind the loop variables
|
||||
if let Some(key_expr) = key {
|
||||
if let E::Var { span, .. } = key_expr.as_ref() {
|
||||
context.bind_variable(span.text());
|
||||
}
|
||||
}
|
||||
if let E::Var { span, .. } = value.as_ref() {
|
||||
context.bind_variable(span.text());
|
||||
}
|
||||
}
|
||||
Expr { expr, .. } => {
|
||||
// Look for assignment expressions that bind variables
|
||||
if let E::AssignExpr { lhs, .. } = expr.as_ref() {
|
||||
Self::bind_variables_from_expr(lhs, context);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively bind variables from an expression (for assignments)
|
||||
fn bind_variables_from_expr(expr: &ExprRef, context: &mut ScopeContext) {
|
||||
use crate::ast::Expr as E;
|
||||
match expr.as_ref() {
|
||||
E::Var { span, .. } => {
|
||||
context.bind_variable(span.text());
|
||||
}
|
||||
E::Array { items, .. } => {
|
||||
for item in items {
|
||||
Self::bind_variables_from_expr(item, context);
|
||||
}
|
||||
}
|
||||
E::Object { fields, .. } => {
|
||||
for (_, key_expr, value_expr) in fields {
|
||||
Self::bind_variables_from_expr(key_expr, context);
|
||||
Self::bind_variables_from_expr(value_expr, context);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
/// Bind variables from a binding plan into the context
|
||||
fn bind_vars_from_plan_to_context(binding_plan: &BindingPlan, context: &mut ScopeContext) {
|
||||
let bound_vars = binding_plan.bound_vars();
|
||||
for var in bound_vars {
|
||||
context.bind_variable(&var);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -916,18 +837,23 @@ impl LoopHoister {
|
||||
expr: &ExprRef,
|
||||
context: &ScopeContext,
|
||||
) -> Result<()> {
|
||||
let mut loops = Vec::new();
|
||||
self.hoist_loops_from_expr_with_context(module_idx, expr, &mut loops, context)?;
|
||||
self.populate_output_expr_with_context(module_idx, expr, context)
|
||||
}
|
||||
|
||||
fn populate_output_expr_with_context(
|
||||
&mut self,
|
||||
module_idx: u32,
|
||||
expr: &ExprRef,
|
||||
context: &ScopeContext,
|
||||
) -> Result<()> {
|
||||
let mut loops = Vec::new();
|
||||
let mut expr_context = context.clone();
|
||||
self.analyze_expr(module_idx, expr, &mut expr_context, &mut loops)?;
|
||||
|
||||
// Always store expression loops, even if empty
|
||||
// This ensures the interpreter can always find an entry
|
||||
let expr_idx = expr.as_ref().eidx();
|
||||
self.lookup.ensure_expr_capacity(module_idx, expr_idx);
|
||||
self.lookup.set_expr_loops(module_idx, expr_idx, loops);
|
||||
|
||||
// Traverse child expressions to populate any nested contexts (e.g., comprehensions)
|
||||
self.process_expr_for_contexts(module_idx, expr, context)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,27 +852,24 @@ impl Engine {
|
||||
let module_idx = self.modules.len() as u32;
|
||||
|
||||
use crate::compiler::hoist::LoopHoister;
|
||||
|
||||
let query_schedule_rc = Rc::new(query_schedule.clone());
|
||||
let mut hoister = LoopHoister::new_with_schedule(query_schedule_rc);
|
||||
|
||||
// Run loop hoisting for query snippet
|
||||
let mut hoister = LoopHoister::new_with_schedule(query_schedule_rc.clone());
|
||||
hoister.populate_query_snippet(
|
||||
module_idx,
|
||||
&query_node,
|
||||
query_module.num_statements,
|
||||
query_module.num_expressions,
|
||||
)?;
|
||||
let query_loops = hoister.finalize();
|
||||
let query_lookup = hoister.finalize();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
debug_assert_eq!(
|
||||
query_loops.module_len(),
|
||||
module_idx as usize + 1,
|
||||
"query hoisting table missing expected module slot {}",
|
||||
module_idx
|
||||
);
|
||||
for stmt in &query_node.stmts {
|
||||
debug_assert!(
|
||||
query_loops
|
||||
query_lookup
|
||||
.get_statement_loops(module_idx, stmt.sidx)
|
||||
.is_some(),
|
||||
"missing hoisted loop entry for query statement index {}",
|
||||
@@ -891,7 +888,7 @@ impl Engine {
|
||||
"loop hoisting table should not retain extra modules before merge"
|
||||
);
|
||||
}
|
||||
existing_table.merge_query_loops(query_loops, self.modules.len());
|
||||
existing_table.merge_query_loops(query_lookup, self.modules.len());
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
for stmt in &query_node.stmts {
|
||||
@@ -952,8 +949,11 @@ impl Engine {
|
||||
// Populate loop hoisting table for efficient evaluation
|
||||
// Reserve capacity for 1 extra module (for query modules)
|
||||
use crate::compiler::hoist::LoopHoister;
|
||||
|
||||
// Run loop hoisting pass first
|
||||
let hoister = LoopHoister::new_with_schedule(schedule.clone());
|
||||
let loop_lookup = hoister.populate_with_extra_capacity(&self.modules, 0)?;
|
||||
|
||||
self.interpreter.set_loop_hoisting_table(loop_lookup);
|
||||
|
||||
// Set schedule after hoisting completes
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@ mod lookup;
|
||||
mod number;
|
||||
mod parser;
|
||||
mod policy_info;
|
||||
mod query;
|
||||
#[cfg(feature = "azure_policy")]
|
||||
pub mod registry;
|
||||
mod scheduler;
|
||||
|
||||
@@ -489,4 +489,15 @@ mod test {
|
||||
let n = Number::from(123456f64);
|
||||
assert_eq!(format!("{}", n.format_decimal()), "123456");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn division_matches_high_precision_decimal() {
|
||||
let one = Number::from(1u64);
|
||||
let three = Number::from(3u64);
|
||||
let div = one.divide(&three).unwrap();
|
||||
let from_str: Number = "0.3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333"
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert_eq!(div, from_str);
|
||||
}
|
||||
}
|
||||
|
||||
1
src/query/mod.rs
Normal file
1
src/query/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod traversal;
|
||||
224
src/query/traversal.rs
Normal file
224
src/query/traversal.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use alloc::collections::{BTreeMap, BTreeSet};
|
||||
use alloc::string::{String, ToString};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::ast::Expr::{self, *};
|
||||
use crate::ast::{AssignOp, ExprRef};
|
||||
use crate::lexer::{SourceStr, Span};
|
||||
use crate::value::Value;
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Scope {
|
||||
pub locals: BTreeMap<SourceStr, Span>,
|
||||
pub unscoped: BTreeSet<SourceStr>,
|
||||
pub inputs: BTreeSet<SourceStr>,
|
||||
pub uses_input: bool,
|
||||
}
|
||||
|
||||
pub fn traverse(expr: &ExprRef, f: &mut dyn FnMut(&ExprRef) -> Result<bool>) -> Result<()> {
|
||||
if !f(expr)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match expr.as_ref() {
|
||||
Expr::String { .. }
|
||||
| RawString { .. }
|
||||
| Number { .. }
|
||||
| Bool { .. }
|
||||
| Null { .. }
|
||||
| Var { .. } => (),
|
||||
|
||||
Array { items, .. } | Set { items, .. } => {
|
||||
for item in items {
|
||||
traverse(item, f)?;
|
||||
}
|
||||
}
|
||||
Object { fields, .. } => {
|
||||
for (_, key, value) in fields {
|
||||
traverse(key, f)?;
|
||||
traverse(value, f)?;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayCompr { .. } | SetCompr { .. } | ObjectCompr { .. } => (),
|
||||
|
||||
Call { params, .. } => {
|
||||
for param in params {
|
||||
traverse(param, f)?;
|
||||
}
|
||||
}
|
||||
|
||||
UnaryExpr { expr, .. } => traverse(expr, f)?,
|
||||
|
||||
RefDot { refr, .. } => traverse(refr, f)?,
|
||||
|
||||
RefBrack { refr, index, .. } => {
|
||||
traverse(refr, f)?;
|
||||
traverse(index, f)?;
|
||||
}
|
||||
|
||||
BinExpr { lhs, rhs, .. }
|
||||
| BoolExpr { lhs, rhs, .. }
|
||||
| ArithExpr { lhs, rhs, .. }
|
||||
| AssignExpr { lhs, rhs, .. } => {
|
||||
traverse(lhs, f)?;
|
||||
traverse(rhs, f)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "rego-extensions")]
|
||||
OrExpr { lhs, rhs, .. } => {
|
||||
traverse(lhs, f)?;
|
||||
traverse(rhs, f)?;
|
||||
}
|
||||
|
||||
Membership {
|
||||
key,
|
||||
value,
|
||||
collection,
|
||||
..
|
||||
} => {
|
||||
if let Some(key) = key.as_ref() {
|
||||
traverse(key, f)?;
|
||||
}
|
||||
traverse(value, f)?;
|
||||
traverse(collection, f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn var_exists(var: &Span, parent_scopes: &[Scope]) -> bool {
|
||||
let name = var.source_str();
|
||||
|
||||
for scope in parent_scopes.iter().rev() {
|
||||
if scope.unscoped.contains(&name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(span) = scope.locals.get(&name) {
|
||||
if span.line <= var.line {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn gather_assigned_vars(
|
||||
expr: &ExprRef,
|
||||
can_shadow: bool,
|
||||
parent_scopes: &[Scope],
|
||||
scope: &mut Scope,
|
||||
) -> Result<()> {
|
||||
traverse(expr, &mut |node| match node.as_ref() {
|
||||
Var { span, .. } if matches!(span.text(), "_" | "input" | "data") => {
|
||||
if span.text() == "input" {
|
||||
scope.uses_input = true;
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
Var { span, .. } if can_shadow => {
|
||||
scope.locals.insert(span.source_str(), span.clone());
|
||||
Ok(false)
|
||||
}
|
||||
Var { span, .. } if var_exists(span, parent_scopes) => {
|
||||
scope.inputs.insert(span.source_str());
|
||||
Ok(false)
|
||||
}
|
||||
Var { span, .. } => {
|
||||
scope.unscoped.insert(span.source_str());
|
||||
Ok(false)
|
||||
}
|
||||
Array { .. } | Object { .. } => Ok(true),
|
||||
_ => Ok(false),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn gather_input_vars(expr: &ExprRef, parent_scopes: &[Scope], scope: &mut Scope) -> Result<()> {
|
||||
traverse(expr, &mut |node| match node.as_ref() {
|
||||
Var { span, .. } => {
|
||||
let name = span.source_str();
|
||||
if name.text() == "input" {
|
||||
scope.uses_input = true;
|
||||
} else if !scope.unscoped.contains(&name) && var_exists(span, parent_scopes) {
|
||||
scope.inputs.insert(name);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
_ => Ok(true),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn gather_loop_vars(expr: &ExprRef, parent_scopes: &[Scope], scope: &mut Scope) -> Result<()> {
|
||||
traverse(expr, &mut |node| match node.as_ref() {
|
||||
Var { span, .. } if span.text() == "input" => {
|
||||
scope.uses_input = true;
|
||||
Ok(false)
|
||||
}
|
||||
RefBrack { index, .. } => {
|
||||
gather_assigned_vars(index, false, parent_scopes, scope)?;
|
||||
Ok(true)
|
||||
}
|
||||
_ => Ok(true),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn gather_vars(
|
||||
expr: &ExprRef,
|
||||
can_shadow: bool,
|
||||
parent_scopes: &[Scope],
|
||||
scope: &mut Scope,
|
||||
) -> Result<()> {
|
||||
if let AssignExpr { op, lhs, rhs, .. } = expr.as_ref() {
|
||||
gather_assigned_vars(lhs, *op == AssignOp::ColEq, parent_scopes, scope)?;
|
||||
gather_assigned_vars(rhs, false, parent_scopes, scope)?;
|
||||
} else {
|
||||
gather_assigned_vars(expr, can_shadow, parent_scopes, scope)?;
|
||||
}
|
||||
|
||||
gather_input_vars(expr, parent_scopes, scope)?;
|
||||
gather_loop_vars(expr, parent_scopes, scope)
|
||||
}
|
||||
|
||||
pub fn collect_expr_dependencies(expr: &ExprRef) -> Option<BTreeSet<String>> {
|
||||
let mut deps = BTreeSet::new();
|
||||
let mut valid = true;
|
||||
|
||||
if traverse(expr, &mut |node| match node.as_ref() {
|
||||
Var { value, .. } => {
|
||||
if let Value::String(name) = value {
|
||||
let var = name.as_ref();
|
||||
if var != "_" {
|
||||
deps.insert(var.to_string());
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
ArrayCompr { .. } | SetCompr { .. } | ObjectCompr { .. } => {
|
||||
valid = false;
|
||||
Ok(false)
|
||||
}
|
||||
#[cfg(feature = "rego-extensions")]
|
||||
OrExpr { .. } => {
|
||||
valid = false;
|
||||
Ok(false)
|
||||
}
|
||||
_ => Ok(true),
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if valid {
|
||||
Some(deps)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
277
src/scheduler.rs
277
src/scheduler.rs
@@ -1,10 +1,14 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use crate::ast::Expr::{Set, *};
|
||||
use crate::ast::Expr::*;
|
||||
use crate::ast::*;
|
||||
use crate::lexer::*;
|
||||
use crate::lookup::*;
|
||||
pub use crate::query::traversal::Scope;
|
||||
use crate::query::traversal::{
|
||||
gather_assigned_vars, gather_input_vars, gather_loop_vars, gather_vars, traverse,
|
||||
};
|
||||
use crate::utils::*;
|
||||
use crate::*;
|
||||
|
||||
@@ -208,205 +212,12 @@ pub fn schedule<Str: Clone + cmp::Ord + fmt::Debug>(
|
||||
Ok(SortResult::Order(order))
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Scope {
|
||||
pub locals: BTreeMap<SourceStr, Span>,
|
||||
pub unscoped: BTreeSet<SourceStr>,
|
||||
pub inputs: BTreeSet<SourceStr>,
|
||||
pub uses_input: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct QuerySchedule {
|
||||
pub scope: Scope,
|
||||
pub order: Vec<u16>,
|
||||
}
|
||||
|
||||
pub fn traverse(expr: &Ref<Expr>, f: &mut dyn FnMut(&Ref<Expr>) -> Result<bool>) -> Result<()> {
|
||||
if !f(expr)? {
|
||||
return Ok(());
|
||||
}
|
||||
match expr.as_ref() {
|
||||
Expr::String { .. }
|
||||
| RawString { .. }
|
||||
| Number { .. }
|
||||
| Bool { .. }
|
||||
| Null { .. }
|
||||
| Var { .. } => (),
|
||||
|
||||
Array { items, .. } | Set { items, .. } => {
|
||||
for i in items {
|
||||
traverse(i, f)?;
|
||||
}
|
||||
}
|
||||
Object { fields, .. } => {
|
||||
for (_, k, v) in fields {
|
||||
traverse(k, f)?;
|
||||
traverse(v, f)?;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayCompr { .. } | SetCompr { .. } | ObjectCompr { .. } => (),
|
||||
|
||||
Call { params, .. } => {
|
||||
for p in params {
|
||||
traverse(p, f)?;
|
||||
}
|
||||
}
|
||||
|
||||
UnaryExpr { expr, .. } => traverse(expr, f)?,
|
||||
|
||||
RefDot { refr, .. } => traverse(refr, f)?,
|
||||
|
||||
RefBrack { refr, index, .. } => {
|
||||
traverse(refr, f)?;
|
||||
traverse(index, f)?;
|
||||
}
|
||||
|
||||
BinExpr { lhs, rhs, .. }
|
||||
| BoolExpr { lhs, rhs, .. }
|
||||
| ArithExpr { lhs, rhs, .. }
|
||||
| AssignExpr { lhs, rhs, .. } => {
|
||||
traverse(lhs, f)?;
|
||||
traverse(rhs, f)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "rego-extensions")]
|
||||
OrExpr { lhs, rhs, .. } => {
|
||||
traverse(lhs, f)?;
|
||||
traverse(rhs, f)?;
|
||||
}
|
||||
|
||||
Membership {
|
||||
key,
|
||||
value,
|
||||
collection,
|
||||
..
|
||||
} => {
|
||||
if let Some(key) = key.as_ref() {
|
||||
traverse(key, f)?;
|
||||
}
|
||||
traverse(value, f)?;
|
||||
traverse(collection, f)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn var_exists(var: &Span, parent_scopes: &[Scope]) -> bool {
|
||||
let name = var.source_str();
|
||||
|
||||
for pscope in parent_scopes.iter().rev() {
|
||||
if pscope.unscoped.contains(&name) {
|
||||
return true;
|
||||
}
|
||||
// Check parent scope vars defined using :=.
|
||||
if let Some(s) = pscope.locals.get(&name) {
|
||||
// Note: Since a rule cannot span multiple files, it is safe to check only
|
||||
// the line numbers.
|
||||
if s.line <= var.line {
|
||||
// The variable was defined in parent scope prior to current comprehension.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn gather_assigned_vars(
|
||||
expr: &Ref<Expr>,
|
||||
can_shadow: bool,
|
||||
parent_scopes: &[Scope],
|
||||
scope: &mut Scope,
|
||||
) -> Result<()> {
|
||||
traverse(expr, &mut |e| match e.as_ref() {
|
||||
// Ignore _, input, data.
|
||||
Var { span: v, .. } if matches!(v.text(), "_" | "input" | "data") => {
|
||||
if v.text() == "input" {
|
||||
scope.uses_input = true;
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// Record local var that can shadow input var.
|
||||
Var { span: v, .. } if can_shadow => {
|
||||
scope.locals.insert(v.source_str(), v.clone());
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// Record input vars.
|
||||
Var { span: v, .. } if var_exists(v, parent_scopes) => {
|
||||
scope.inputs.insert(v.source_str());
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// Record local var.
|
||||
Var { span: v, .. } => {
|
||||
scope.unscoped.insert(v.source_str());
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// TODO: key vs value for object binding
|
||||
Array { .. } | Object { .. } => Ok(true),
|
||||
_ => Ok(false),
|
||||
})
|
||||
}
|
||||
|
||||
fn gather_input_vars(expr: &Ref<Expr>, parent_scopes: &[Scope], scope: &mut Scope) -> Result<()> {
|
||||
traverse(expr, &mut |e| match e.as_ref() {
|
||||
Var { span: v, .. } => {
|
||||
let name = v.source_str();
|
||||
if name.text() == "input" {
|
||||
scope.uses_input = true;
|
||||
} else if !scope.unscoped.contains(&name) && var_exists(v, parent_scopes) {
|
||||
scope.inputs.insert(name);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
_ => Ok(true),
|
||||
})
|
||||
}
|
||||
|
||||
fn gather_loop_vars(expr: &Ref<Expr>, parent_scopes: &[Scope], scope: &mut Scope) -> Result<()> {
|
||||
traverse(expr, &mut |e| match e.as_ref() {
|
||||
Var { span: v, .. } if v.text() == "input" => {
|
||||
scope.uses_input = true;
|
||||
Ok(false)
|
||||
}
|
||||
RefBrack { index, .. } => {
|
||||
gather_assigned_vars(index, false, parent_scopes, scope)?;
|
||||
Ok(true)
|
||||
}
|
||||
_ => Ok(true),
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: start opa discussion
|
||||
// k = "k"
|
||||
// t = {"k": 5}
|
||||
// {k:y} = t
|
||||
// Try inlining value of t
|
||||
fn gather_vars(
|
||||
expr: &Ref<Expr>,
|
||||
can_shadow: bool,
|
||||
parent_scopes: &[Scope],
|
||||
scope: &mut Scope,
|
||||
) -> Result<()> {
|
||||
// Process assignment expressions to gather vars that are defined/assigned
|
||||
// in current scope.
|
||||
if let AssignExpr { op, lhs, rhs, .. } = expr.as_ref() {
|
||||
gather_assigned_vars(lhs, *op == AssignOp::ColEq, parent_scopes, scope)?;
|
||||
gather_assigned_vars(rhs, false, parent_scopes, scope)?;
|
||||
} else {
|
||||
gather_assigned_vars(expr, can_shadow, parent_scopes, scope)?;
|
||||
}
|
||||
|
||||
// Process all expressions to gather loop index vars and inputs.
|
||||
// TODO: := assignment and use in same statement.
|
||||
gather_input_vars(expr, parent_scopes, scope)?;
|
||||
gather_loop_vars(expr, parent_scopes, scope)
|
||||
}
|
||||
|
||||
pub struct Analyzer {
|
||||
packages: BTreeMap<String, Scope>,
|
||||
scopes: Vec<Scope>,
|
||||
@@ -1011,7 +822,7 @@ impl Analyzer {
|
||||
expr: &Ref<Expr>,
|
||||
scope: &Scope,
|
||||
_first_use: &BTreeMap<SourceStr, Span>,
|
||||
vars: &mut Vec<SourceStr>,
|
||||
vars: &mut Vec<(SourceStr, Span)>,
|
||||
non_vars: &mut Vec<Ref<Expr>>,
|
||||
) -> Result<()> {
|
||||
traverse(expr, &mut |e| match e.as_ref() {
|
||||
@@ -1021,7 +832,7 @@ impl Analyzer {
|
||||
Ok(false)
|
||||
}
|
||||
Var { span: v, .. } if scope.locals.contains_key(&v.source_str()) => {
|
||||
vars.push(v.source_str());
|
||||
vars.push((v.source_str(), v.clone()));
|
||||
Ok(false)
|
||||
}
|
||||
// TODO: Object key/value
|
||||
@@ -1114,13 +925,17 @@ impl Analyzer {
|
||||
)?;
|
||||
|
||||
// Add dependency between some-vars and vars used in collection.
|
||||
for var in &some_vars {
|
||||
for (var, _) in &some_vars {
|
||||
definitions.push(Definition {
|
||||
var: var.clone(),
|
||||
used_vars: col_used_vars.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
for (var, span) in &some_vars {
|
||||
first_use.entry(var.clone()).or_insert(span.clone());
|
||||
}
|
||||
|
||||
let mut used_vars = vec![];
|
||||
for e in non_vars {
|
||||
let mut definitions = vec![];
|
||||
@@ -1296,3 +1111,71 @@ impl Analyzer {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute module globals for each module.
|
||||
///
|
||||
/// For each module, the globals are:
|
||||
/// 1) The set of rule names defined in the package that the module defines
|
||||
/// 2) Additionally, the set of aliases imported by the module
|
||||
pub fn compute_module_globals(
|
||||
modules: &[Ref<Module>],
|
||||
) -> Result<Lookup<crate::Rc<BTreeSet<String>>>> {
|
||||
let mut result = Lookup::new();
|
||||
let mut packages: BTreeMap<String, crate::Rc<BTreeSet<String>>> = BTreeMap::new();
|
||||
|
||||
// First pass: collect all rule names by package
|
||||
for m in modules {
|
||||
let path = get_path_string(&m.package.refr, Some("data"))?;
|
||||
let package_globals: &mut crate::Rc<BTreeSet<String>> = packages.entry(path).or_default();
|
||||
|
||||
for r in &m.policy {
|
||||
let var = match r.as_ref() {
|
||||
Rule::Default { refr, .. }
|
||||
| Rule::Spec {
|
||||
head:
|
||||
RuleHead::Compr { refr, .. }
|
||||
| RuleHead::Set { refr, .. }
|
||||
| RuleHead::Func { refr, .. },
|
||||
..
|
||||
} => get_root_var(refr)?,
|
||||
};
|
||||
crate::Rc::make_mut(package_globals).insert(var.text().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: for each module, combine package globals with module-specific imports
|
||||
for (module_idx, m) in modules.iter().enumerate() {
|
||||
let path = get_path_string(&m.package.refr, Some("data"))?;
|
||||
let mut module_globals = packages.get(&path).cloned().unwrap_or_default();
|
||||
|
||||
// Add import aliases specific to this module
|
||||
for import in &m.imports {
|
||||
if let Some(var) = &import.r#as {
|
||||
crate::Rc::make_mut(&mut module_globals).insert(var.text().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure reserved root documents are always treated as globals.
|
||||
for &reserved in ["input", "data"].iter() {
|
||||
crate::Rc::make_mut(&mut module_globals).insert(reserved.to_string());
|
||||
}
|
||||
|
||||
// Reserved documents are always available in every module.
|
||||
let reserved_docs = ["input", "data"];
|
||||
for doc in reserved_docs {
|
||||
crate::Rc::make_mut(&mut module_globals).insert(doc.to_string());
|
||||
}
|
||||
|
||||
// Seed with reserved document roots that are always globally accessible.
|
||||
{
|
||||
let globals = crate::Rc::make_mut(&mut module_globals);
|
||||
globals.insert("input".to_string());
|
||||
globals.insert("data".to_string());
|
||||
}
|
||||
|
||||
result.ensure_capacity(module_idx as u32, 0);
|
||||
result.set(module_idx as u32, 0, module_globals);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -253,9 +253,10 @@ pub fn eval_file(
|
||||
query: &str,
|
||||
enable_tracing: bool,
|
||||
strict: bool,
|
||||
v0: bool,
|
||||
) -> Result<(Vec<Value>, Vec<String>)> {
|
||||
let mut engine: Engine = Engine::new();
|
||||
engine.set_rego_v0(true);
|
||||
engine.set_rego_v0(v0);
|
||||
engine.set_strict_builtin_errors(strict);
|
||||
engine.set_gather_prints(true);
|
||||
|
||||
@@ -333,9 +334,10 @@ pub fn eval_file_with_rule_evaluation(
|
||||
query: &str,
|
||||
_enable_tracing: bool,
|
||||
strict: bool,
|
||||
v0: bool,
|
||||
) -> Result<(Vec<Value>, Vec<String>)> {
|
||||
let mut engine: Engine = Engine::new();
|
||||
engine.set_rego_v0(true);
|
||||
engine.set_rego_v0(v0);
|
||||
engine.set_strict_builtin_errors(strict);
|
||||
engine.set_gather_prints(true);
|
||||
|
||||
@@ -484,9 +486,19 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "graph"))]
|
||||
{
|
||||
// Skip tests that depend on graph builtin that need graph feature.
|
||||
if file.contains("walk.yaml") {
|
||||
std::println!("skipped {file} without graph feature.");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
std::println!("running {file}");
|
||||
|
||||
let v0 = !file.contains("bindings.yaml");
|
||||
|
||||
for case in test.cases {
|
||||
std::print!("case {} ", case.note);
|
||||
if case.skip == Some(true) {
|
||||
@@ -516,6 +528,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
case.query.as_str(),
|
||||
enable_tracing,
|
||||
case.strict,
|
||||
v0,
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "azure_policy"))]
|
||||
@@ -530,6 +543,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
case.query.as_str(),
|
||||
enable_tracing,
|
||||
case.strict,
|
||||
v0,
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
432
tests/interpreter/cases/binding/bindings.yaml
Normal file
432
tests/interpreter/cases/binding/bindings.yaml
Normal file
@@ -0,0 +1,432 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
cases:
|
||||
- note: assignment-colonequals-nested-pattern
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := {"outer": outer, "inner": inner, "tag": tag} if {
|
||||
[outer, {"meta": {"inner": inner, "tag": tag}}] := ["alpha", {"meta": {"inner": "omega", "tag": "v1"}}]
|
||||
}
|
||||
query: data.test.result
|
||||
want_result:
|
||||
outer: "alpha"
|
||||
inner: "omega"
|
||||
tag: "v1"
|
||||
|
||||
- note: assignment-colonequals-cannot-rebind
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if {
|
||||
value := "initial"
|
||||
value := "shadowed"
|
||||
}
|
||||
query: data.test.result
|
||||
error: "ar `value` used before definition below"
|
||||
|
||||
- note: assignment-colonequals-requires-bindable-left
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if {
|
||||
1 := value
|
||||
value = 1
|
||||
}
|
||||
query: data.test.result
|
||||
error: "assignment operator := requires left-hand side to have bindable variables"
|
||||
|
||||
- note: equals-binds-left-nested-literal
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords
|
||||
|
||||
result := {"first": first, "second": second, "deep": deep} if {
|
||||
[first, {"details": [second, deep]}] = ["foo", {"details": ["bar", "baz"]}]
|
||||
}
|
||||
query: data.test.result
|
||||
want_result:
|
||||
first: "foo"
|
||||
second: "bar"
|
||||
deep: "baz"
|
||||
|
||||
- note: equals-binds-right-nested-literal
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := {"first": first, "second": second, "deep": deep} if {
|
||||
payload := ["foo", {"details": ["bar", "baz"]}]
|
||||
payload = [first, {"details": [second, deep]}]
|
||||
}
|
||||
query: data.test.result
|
||||
want_result:
|
||||
first: "foo"
|
||||
second: "bar"
|
||||
deep: "baz"
|
||||
|
||||
- note: equals-both-sides-nested-dependent-order
|
||||
data:
|
||||
transitions:
|
||||
- [{"id": 1, "next": {"target": 2}}, {"id": 2, "payload": {"value": "beta"}}]
|
||||
- [{"id": 2, "next": {"target": 3}}, {"id": 3, "payload": {"value": "gamma"}}]
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := [[left_id, right_id, value] |
|
||||
some left, right, left_id, right_id, value
|
||||
data.transitions[_] = [left, right]
|
||||
[{"id": left_id, "next": {"target": right_id}}, {"id": right_id, "payload": {"value": value}}] = [left, right]
|
||||
]
|
||||
query: data.test.result
|
||||
want_result: [[1, 2, "beta"], [2, 3, "gamma"]]
|
||||
|
||||
- note: equals-non-shadowing-success
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
default result = false
|
||||
|
||||
result = true if {
|
||||
user_id := "user-1"
|
||||
[user_id, role] = ["user-1", "admin"]
|
||||
role = "admin"
|
||||
}
|
||||
query: data.test.result
|
||||
want_result: true
|
||||
|
||||
- note: equals-non-shadowing-mismatch
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
default result = false
|
||||
|
||||
result = true if {
|
||||
user_id := "user-1"
|
||||
[user_id, role] = ["user-2", "admin"]
|
||||
}
|
||||
query: data.test.result
|
||||
want_result: false
|
||||
|
||||
- note: colon-equals-shadowing-allowed
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
x := 10
|
||||
|
||||
y if {
|
||||
x := 5
|
||||
}
|
||||
query: data.test.y
|
||||
want_result: true
|
||||
|
||||
- note: equals-wildcard-both-sides
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if {
|
||||
[_, _] = [1, 2]
|
||||
}
|
||||
query: data.test.result
|
||||
want_result: true
|
||||
|
||||
- note: equals-incompatible-patterns-error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if {
|
||||
[x] = {"key": 1}
|
||||
}
|
||||
query: data.test.result
|
||||
error: "incompatible destructuring patterns: both sides must be arrays or objects with matching structure"
|
||||
|
||||
- note: equals-array-size-mismatch-error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if {
|
||||
some a, b, c
|
||||
[x, y] = [a, b, c]
|
||||
}
|
||||
query: data.test.result
|
||||
error: "mismatch in number of array elements"
|
||||
|
||||
- note: equals-array-literal-length-mismatch-error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if {
|
||||
[x, y] = [1, 2, 3]
|
||||
}
|
||||
query: data.test.result
|
||||
error: "mismatch in number of array elements"
|
||||
|
||||
- note: equals-object-literal-keys-mismatch-error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if{
|
||||
{"a": first, "b": second} = {"a": 1, "c": 2}
|
||||
}
|
||||
query: data.test.result
|
||||
error: "object literal keys mismatch. Expected keys [\"a\", \"b\"] got [\"a\", \"c\"]."
|
||||
|
||||
- note: equals-object-key-not-found-error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if {
|
||||
some value
|
||||
{"a": first, "b": second} = {"a": 1, "c": value}
|
||||
}
|
||||
query: data.test.result
|
||||
error: "key \"c\" not found in left-hand side object during destructuring"
|
||||
|
||||
- note: equals-object-field-count-mismatch-error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if {
|
||||
some a, b
|
||||
{"a": first} = {"a": a, "b": b}
|
||||
}
|
||||
query: data.test.result
|
||||
error: "object field count mismatch in assignment: left has 1 fields, right has 2 fields"
|
||||
|
||||
- note: dynamic-object-field-binding
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := {"target": target, "captured": captured} if {
|
||||
source := {"target": {"value": {"inner": 1}}, "alt": {"value": {"inner": 2}}}
|
||||
{"target": {"value": {"inner": target}}, chosen: {"value": {"inner": captured}}} = source
|
||||
chosen = "alt"
|
||||
}
|
||||
query: data.test.result
|
||||
want_result:
|
||||
target: 1
|
||||
captured: 2
|
||||
|
||||
- note: some-in-nested-binding
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := {
|
||||
[letter, code] |
|
||||
some letter, detail in {"a": {"info": {"code": 1}}, "b": {"info": {"code": 2}}}
|
||||
detail.info.code = code
|
||||
}
|
||||
query: data.test.result
|
||||
want_result:
|
||||
set!:
|
||||
- ["a", 1]
|
||||
- ["b", 2]
|
||||
|
||||
- note: some-in-array-length-mismatch-error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if {
|
||||
some [x, y] in [[1, 2, 3]]
|
||||
}
|
||||
query: data.test.result
|
||||
error: "mismatch in number of array elements"
|
||||
|
||||
- note: some-in-shadowing
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := {"before": before, "after": value} if {
|
||||
value := 0
|
||||
before := value
|
||||
some value in [1, 2]
|
||||
value == 2
|
||||
}
|
||||
query: data.test.result
|
||||
error: "var `value` used before definition below"
|
||||
|
||||
- note: some-in-shadowing-equals
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if {
|
||||
value = 0
|
||||
some value in [1, 2]
|
||||
}
|
||||
query: data.test.result
|
||||
error: "var `value` used before definition below"
|
||||
|
||||
- note: some-in-after-shadows
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := true if {
|
||||
some value in [1, 2]
|
||||
value := 1
|
||||
}
|
||||
query: data.test.result
|
||||
error: "var `value` used before definition below"
|
||||
|
||||
- note: comprehension-nested-binding
|
||||
data:
|
||||
records:
|
||||
- {"type": "user", "profile": {"name": "alice", "roles": ["admin", "user"]}}
|
||||
- {"type": "user", "profile": {"name": "bob", "roles": ["user"]}}
|
||||
- {"type": "service", "profile": {"name": "svc"}}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := [[name, role] |
|
||||
some record, role
|
||||
data.records[_] = record
|
||||
record = {"type": "user", "profile": {"name": name, "roles": roles}}
|
||||
roles[_] = role
|
||||
role = "admin"
|
||||
]
|
||||
query: data.test.result
|
||||
want_result: [["alice", "admin"]]
|
||||
|
||||
- note: comprehension-scalar-filter
|
||||
data:
|
||||
scores:
|
||||
- {"value": 10}
|
||||
- {"value": 20}
|
||||
- {"value": 30}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := [score |
|
||||
some entry, score
|
||||
data.scores[_] = entry
|
||||
entry = {"value": score}
|
||||
score >= 20
|
||||
]
|
||||
query: data.test.result
|
||||
want_result: [20, 30]
|
||||
|
||||
|
||||
- note: parameter-nested-destructuring
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
pair([[name, {"primary": role}], {"meta": {"active": active}}]) := [
|
||||
[name, {"primary": role}],
|
||||
{"meta": {"active": active}},
|
||||
] if {
|
||||
name = "alice"
|
||||
role = "admin"
|
||||
active = true
|
||||
}
|
||||
|
||||
result := {"name": name, "role": role, "active": active} if {
|
||||
[[name, {"primary": role}], {"meta": {"active": active}}] := pair([
|
||||
["alice", {"primary": "admin"}],
|
||||
{"meta": {"active": true}},
|
||||
])
|
||||
}
|
||||
query: data.test.result
|
||||
want_result:
|
||||
name: "alice"
|
||||
role: "admin"
|
||||
active: true
|
||||
|
||||
- note: parameter-nested-destructuring-colonequals-error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
pair([[name, {"primary": role}], {"meta": {"active": active}}]) if {
|
||||
name := "alice"
|
||||
role := "admin"
|
||||
active := true
|
||||
}
|
||||
query: data.test.pair
|
||||
error: "var `name` used before definition below"
|
||||
|
||||
- note: colon-equals-parameter-rebinding-error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
pair([[name, {"primary": role}], {"meta": {"active": active}}]) if {
|
||||
name := "alice"
|
||||
role := "admin"
|
||||
active := true
|
||||
}
|
||||
|
||||
result := true if {
|
||||
pair([
|
||||
["alice", {"primary": "admin"}],
|
||||
{"meta": {"active": true}},
|
||||
])
|
||||
}
|
||||
query: data.test.result
|
||||
error: "var `name` used before definition below"
|
||||
|
||||
- note: parameter-shadowing
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
global_name := "global"
|
||||
|
||||
capture([name, {"role": role}]) if {
|
||||
name := "local"
|
||||
role := "admin"
|
||||
}
|
||||
|
||||
result := {"global": global_name, "param": name, "role": role} if {
|
||||
capture([name, {"role": role}])
|
||||
}
|
||||
query: data.test.result
|
||||
error: "var `name` used before definition below"
|
||||
20
tests/interpreter/cases/binding/walk.yaml
Normal file
20
tests/interpreter/cases/binding/walk.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
cases:
|
||||
- note: walk-loop-index-destructuring
|
||||
data:
|
||||
doc:
|
||||
team:
|
||||
ops:
|
||||
members: ["alice"]
|
||||
dev:
|
||||
members: ["bob"]
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
result := [[section, role, members] |
|
||||
walk(data.doc, [[section, role], {"members": members}])
|
||||
]
|
||||
query: data.test.result
|
||||
want_result: [["team", "dev", ["bob"]], ["team", "ops", ["alice"]]]
|
||||
@@ -27,7 +27,6 @@ cases:
|
||||
want_result:
|
||||
d: 5.1
|
||||
e: 3.25
|
||||
z: true
|
||||
|
||||
- note: non-numeric
|
||||
data: {}
|
||||
|
||||
@@ -76,7 +76,7 @@ cases:
|
||||
import future.keywords
|
||||
x { some [1] in [[1, 2]] }
|
||||
query: data.test
|
||||
error: "array length mismatch. Expected 1 got 2."
|
||||
error: "mismatch in number of array elements"
|
||||
|
||||
- note: array-length-mismatch-skipped
|
||||
data: {}
|
||||
|
||||
Reference in New Issue
Block a user