feat!: Indexes for nodes in the AST (#414)

Indexes allow associating extra data with nodes in the AST
using an array and then quickly looking up the array to fetch
the extra data.

- Index eidx for expressions
- Index sidx for statements
- Index qidx for queries.

AST nodes are not cloneable. Therefore once a module is created,
it is not possible to accidentally create two nodes with the same
index inadvertently via clone.

Also added IndexChecker in debug builds. When a module is parsed,
it will assert that indexes have been constructed correctly.

AST Cleanup
- Make literal expressions (null, val, number, string etc) also structs
  to match all other expressions
- Merge True and False nodes into a single Bool node.

Also update dependencies.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2025-06-20 15:09:07 -05:00
committed by GitHub
parent 620f8a4547
commit 444b2970a1
35 changed files with 2015 additions and 586 deletions
+96 -10
View File
@@ -106,29 +106,60 @@ pub type Ref<T> = NodeRef<T>;
#[cfg_attr(feature = "ast", derive(serde::Serialize))]
pub enum Expr {
// Simple items that only have a span as content.
String((Span, Value)),
RawString((Span, Value)),
Number((Span, Value)),
True(Span),
False(Span),
Null(Span),
Var((Span, Value)),
String {
span: Span,
value: Value,
eidx: u32,
},
RawString {
span: Span,
value: Value,
eidx: u32,
},
Number {
span: Span,
value: Value,
eidx: u32,
},
Bool {
span: Span,
value: Value,
eidx: u32,
},
Null {
span: Span,
value: Value,
eidx: u32,
},
Var {
span: Span,
value: Value,
eidx: u32,
},
// array
Array {
span: Span,
items: Vec<Ref<Expr>>,
eidx: u32,
},
// set
Set {
span: Span,
items: Vec<Ref<Expr>>,
eidx: u32,
},
Object {
span: Span,
fields: Vec<(Span, Ref<Expr>, Ref<Expr>)>,
eidx: u32,
},
// Comprehensions
@@ -136,12 +167,14 @@ pub enum Expr {
span: Span,
term: Ref<Expr>,
query: Ref<Query>,
eidx: u32,
},
SetCompr {
span: Span,
term: Ref<Expr>,
query: Ref<Query>,
eidx: u32,
},
ObjectCompr {
@@ -149,17 +182,20 @@ pub enum Expr {
key: Ref<Expr>,
value: Ref<Expr>,
query: Ref<Query>,
eidx: u32,
},
Call {
span: Span,
fcn: Ref<Expr>,
params: Vec<Ref<Expr>>,
eidx: u32,
},
UnaryExpr {
span: Span,
expr: Ref<Expr>,
eidx: u32,
},
// ref
@@ -167,12 +203,14 @@ pub enum Expr {
span: Span,
refr: Ref<Expr>,
field: (Span, Value),
eidx: u32,
},
RefBrack {
span: Span,
refr: Ref<Expr>,
index: Ref<Expr>,
eidx: u32,
},
// Infix expressions
@@ -181,12 +219,15 @@ pub enum Expr {
op: BinOp,
lhs: Ref<Expr>,
rhs: Ref<Expr>,
eidx: u32,
},
BoolExpr {
span: Span,
op: BoolOp,
lhs: Ref<Expr>,
rhs: Ref<Expr>,
eidx: u32,
},
ArithExpr {
@@ -194,6 +235,7 @@ pub enum Expr {
op: ArithOp,
lhs: Ref<Expr>,
rhs: Ref<Expr>,
eidx: u32,
},
AssignExpr {
@@ -201,6 +243,7 @@ pub enum Expr {
op: AssignOp,
lhs: Ref<Expr>,
rhs: Ref<Expr>,
eidx: u32,
},
Membership {
@@ -208,6 +251,7 @@ pub enum Expr {
key: Option<Ref<Expr>>,
value: Ref<Expr>,
collection: Ref<Expr>,
eidx: u32,
},
#[cfg(feature = "rego-extensions")]
@@ -215,6 +259,7 @@ pub enum Expr {
span: Span,
lhs: Ref<Expr>,
rhs: Ref<Expr>,
eidx: u32,
},
}
@@ -222,9 +267,13 @@ impl Expr {
pub fn span(&self) -> &Span {
use Expr::*;
match self {
String(s) | RawString(s) | Number(s) | Var(s) => &s.0,
True(s) | False(s) | Null(s) => s,
Array { span, .. }
String { span, .. }
| RawString { span, .. }
| Number { span, .. }
| Bool { span, .. }
| Null { span, .. }
| Var { span, .. }
| Array { span, .. }
| Set { span, .. }
| Object { span, .. }
| ArrayCompr { span, .. }
@@ -243,6 +292,35 @@ impl Expr {
OrExpr { span, .. } => span,
}
}
pub fn eidx(&self) -> u32 {
use Expr::*;
match self {
String { eidx, .. }
| RawString { eidx, .. }
| Number { eidx, .. }
| Bool { eidx, .. }
| Null { eidx, .. }
| Var { eidx, .. }
| Array { eidx, .. }
| Set { eidx, .. }
| Object { eidx, .. }
| ArrayCompr { eidx, .. }
| SetCompr { eidx, .. }
| ObjectCompr { eidx, .. }
| Call { eidx, .. }
| UnaryExpr { eidx, .. }
| RefDot { eidx, .. }
| RefBrack { eidx, .. }
| BinExpr { eidx, .. }
| BoolExpr { eidx, .. }
| ArithExpr { eidx, .. }
| AssignExpr { eidx, .. }
| Membership { eidx, .. } => *eidx,
#[cfg(feature = "rego-extensions")]
OrExpr { eidx, .. } => *eidx,
}
}
}
#[derive(Debug)]
@@ -290,6 +368,7 @@ pub struct LiteralStmt {
pub literal: Literal,
#[cfg_attr(feature = "ast", serde(skip_serializing_if = "Vec::is_empty"))]
pub with_mods: Vec<WithModifier>,
pub sidx: u32,
}
#[derive(Debug)]
@@ -297,6 +376,7 @@ pub struct LiteralStmt {
pub struct Query {
pub span: Span,
pub stmts: Vec<LiteralStmt>,
pub qidx: u32,
}
#[derive(Debug)]
@@ -385,6 +465,12 @@ pub struct Module {
#[cfg_attr(feature = "ast", serde(rename(serialize = "rules")))]
pub policy: Vec<Ref<Rule>>,
pub rego_v1: bool,
// Number of expressions in the module.
pub num_expressions: u32,
// Number of statements in the module.
pub num_statements: u32,
// Number of queries in the module.
pub num_queries: u32,
}
pub type ExprRef = Ref<Expr>;
+281
View File
@@ -0,0 +1,281 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![cfg(debug_assertions)]
use crate::ast::*;
use alloc::collections::BTreeSet;
use alloc::format;
use anyhow::{bail, Result};
// Ensures that indexes are unique and continuous, starting from 0.
#[derive(Default)]
pub struct IndexChecker {
eidx: BTreeSet<u32>,
sidx: BTreeSet<u32>,
qidx: BTreeSet<u32>,
}
#[cfg(debug_assertions)]
impl IndexChecker {
fn check_query(&mut self, query: &Query) -> Result<()> {
let qidx = query.qidx;
if !self.qidx.insert(qidx) {
bail!(query
.span
.error(format!("query with qidx {qidx} already exists").as_str()));
}
for stmt in &query.stmts {
if !self.sidx.insert(stmt.sidx) {
bail!(stmt
.span
.error(format!("statement with sidx {} already exists", stmt.sidx).as_str()));
}
match &stmt.literal {
Literal::Every { domain, query, .. } => {
self.check_eidx(domain)?;
self.check_query(query.as_ref())?;
}
Literal::SomeVars { .. } => (),
Literal::Expr { expr, .. } => self.check_eidx(expr.as_ref())?,
Literal::SomeIn {
key,
value,
collection,
..
} => {
if let Some(key) = key {
self.check_eidx(key.as_ref())?;
}
self.check_eidx(value.as_ref())?;
self.check_eidx(collection.as_ref())?;
}
Literal::NotExpr { expr, .. } => {
self.check_eidx(expr.as_ref())?;
}
}
for with_mod in &stmt.with_mods {
self.check_eidx(with_mod.refr.as_ref())?;
self.check_eidx(with_mod.r#as.as_ref())?;
}
}
Ok(())
}
fn check_eidx(&mut self, expr: &Expr) -> Result<()> {
use Expr::*;
let eidx = expr.eidx();
if !self.eidx.insert(eidx) {
bail!(expr
.span()
.error(format!("expression with eidx {eidx} already exists").as_str()));
}
match expr {
String { .. }
| RawString { .. }
| Number { .. }
| Bool { .. }
| Null { .. }
| Var { .. } => (),
Array { items, .. } => {
for elem in items {
self.check_eidx(elem.as_ref())?;
}
}
Set { items, .. } => {
for elem in items {
self.check_eidx(elem.as_ref())?;
}
}
Object { fields, .. } => {
for pair in fields {
self.check_eidx(pair.1.as_ref())?;
self.check_eidx(pair.2.as_ref())?;
}
}
ArrayCompr { term, query, .. } => {
self.check_eidx(term.as_ref())?;
self.check_query(query.as_ref())?;
}
SetCompr { term, query, .. } => {
self.check_eidx(term.as_ref())?;
self.check_query(query.as_ref())?;
}
ObjectCompr {
key, value, query, ..
} => {
self.check_eidx(key.as_ref())?;
self.check_eidx(value.as_ref())?;
self.check_query(query.as_ref())?;
}
Call { fcn, params, .. } => {
self.check_eidx(fcn.as_ref())?;
for param in params {
self.check_eidx(param.as_ref())?;
}
}
UnaryExpr { expr, .. } => {
self.check_eidx(expr.as_ref())?;
}
RefBrack { refr, index, .. } => {
self.check_eidx(refr.as_ref())?;
self.check_eidx(index.as_ref())?;
}
RefDot { refr, .. } => {
self.check_eidx(refr.as_ref())?;
}
BinExpr { lhs, rhs, .. }
| BoolExpr { lhs, rhs, .. }
| ArithExpr { lhs, rhs, .. }
| AssignExpr { lhs, rhs, .. } => {
self.check_eidx(lhs.as_ref())?;
self.check_eidx(rhs.as_ref())?;
}
Membership {
key,
value,
collection,
..
} => {
if let Some(key) = key {
self.check_eidx(key.as_ref())?;
}
self.check_eidx(value.as_ref())?;
self.check_eidx(collection.as_ref())?;
}
#[cfg(feature = "rego-extensions")]
OrExpr { lhs, rhs, .. } => {
self.check_eidx(lhs.as_ref())?;
self.check_eidx(rhs.as_ref())?;
}
}
Ok(())
}
fn check_rule_assign(&mut self, assign: &RuleAssign) -> Result<()> {
self.check_eidx(&assign.value)
}
fn check_rule_body(&mut self, body: &RuleBody) -> Result<()> {
if let Some(assign) = &body.assign {
self.check_rule_assign(assign)?;
}
self.check_query(&body.query)
}
fn check_rule_heade(&mut self, head: &RuleHead) -> Result<()> {
match head {
RuleHead::Compr { refr, assign, .. } => {
self.check_eidx(refr.as_ref())?;
if let Some(assign) = assign {
self.check_rule_assign(assign)?;
}
}
RuleHead::Func {
refr, args, assign, ..
} => {
self.check_eidx(refr.as_ref())?;
if let Some(assign) = assign {
self.check_rule_assign(assign)?;
}
for arg in args {
self.check_eidx(arg.as_ref())?;
}
}
RuleHead::Set { refr, key, .. } => {
self.check_eidx(refr.as_ref())?;
if let Some(key) = key {
self.check_eidx(key.as_ref())?;
}
}
}
Ok(())
}
fn check_gathered_indexes(
&self,
num_idx: u32,
idx_set: &BTreeSet<u32>,
idx_type: &str,
) -> Result<()> {
if num_idx == 0 {
if !idx_set.is_empty() {
bail!("no {idx_type} indexes should be collected when num_{idx_type}s is 0");
}
return Ok(());
}
if idx_set
.first()
.unwrap_or_else(|| panic!("no {idx_type} indexes collected"))
!= &0
{
bail!("start {idx_type} index must be 0");
}
let last_idx = idx_set
.last()
.unwrap_or_else(|| panic!("no {idx_type} indexes collected"));
if last_idx != &(num_idx - 1) {
bail!(
"last {idx_type} index must be {} got {last_idx} instead",
num_idx - 1
);
}
Ok(())
}
pub fn check_module(&mut self, module: &Module) -> Result<()> {
self.check_eidx(module.package.refr.as_ref())?;
for import in &module.imports {
self.check_eidx(import.refr.as_ref())?;
}
for rule in &module.policy {
match rule.as_ref() {
Rule::Spec { head, bodies, .. } => {
self.check_rule_heade(head)?;
for body in bodies {
self.check_rule_body(body)?;
}
}
Rule::Default {
refr, args, value, ..
} => {
self.check_eidx(refr.as_ref())?;
for arg in args {
self.check_eidx(arg.as_ref())?;
}
self.check_eidx(value.as_ref())?;
}
}
}
if module.num_expressions == 0 {
bail!("module must have at least one expression");
}
self.check_gathered_indexes(module.num_expressions, &self.eidx, "expression")?;
self.check_gathered_indexes(module.num_statements, &self.sidx, "statement")?;
self.check_gathered_indexes(module.num_queries, &self.qidx, "query")?;
Ok(())
}
}
+92 -70
View File
@@ -325,9 +325,9 @@ impl Interpreter {
}
match expr.as_ref() {
// Stop path collection upon encountering the leading variable.
Expr::Var(v) => {
Expr::Var { span, .. } => {
path.reverse();
return self.lookup_var(&v.0, &path[..], false);
return self.lookup_var(span, &path[..], false);
}
// Accumulate chained . field accesses.
Expr::RefDot { refr, field, .. } => {
@@ -336,9 +336,9 @@ impl Interpreter {
}
Expr::RefBrack { refr, index, .. } => match index.as_ref() {
// refr["field"] is the same as refr.field
Expr::String(s) => {
Expr::String { span, .. } => {
expr = refr;
path.push(s.0.text());
path.push(span.text());
}
// Handle other forms of refr.
// Note, we have the choice to evaluate a non-string index
@@ -410,7 +410,9 @@ impl Interpreter {
fn hoist_loops_impl(&self, expr: &ExprRef, loops: &mut Vec<LoopExpr>) {
use Expr::*;
match expr.as_ref() {
RefBrack { refr, index, span } => {
RefBrack {
refr, index, span, ..
} => {
// First hoist any loops in refr
self.hoist_loops_impl(refr, loops);
@@ -420,8 +422,8 @@ impl Interpreter {
// Then hoist the current bracket operation.
let mut indices = Vec::with_capacity(1);
let _ = traverse(index, &mut |e| match e.as_ref() {
Var(ident) if self.is_loop_index_var(&ident.0.source_str()) => {
indices.push(ident.0.source_str());
Var { span: ident, .. } if self.is_loop_index_var(&ident.source_str()) => {
indices.push(ident.source_str());
Ok(false)
}
Array { .. } | Object { .. } => Ok(true),
@@ -438,7 +440,12 @@ impl Interpreter {
}
// Primitives
String(_) | RawString(_) | Number(_) | True(_) | False(_) | Null(_) | Var(_) => (),
String { .. }
| RawString { .. }
| Number { .. }
| Bool { .. }
| Null { .. }
| Var { .. } => (),
// Recurse into expressions in other variants.
Array { items, .. } | Set { items, .. } | Call { params: items, .. } => {
@@ -595,17 +602,17 @@ impl Interpreter {
let (name, value) = match op {
AssignOp::Eq => {
match (lhs.as_ref(), rhs.as_ref()) {
(_, Expr::Var(var))
if var.0.source_str().text() != "input"
&& self.lookup_var(&var.0, &[], true)? == Value::Undefined =>
(_, Expr::Var { span: var, .. })
if var.source_str().text() != "input"
&& self.lookup_var(var, &[], true)? == Value::Undefined =>
{
(var.0.source_str(), self.eval_expr(lhs)?)
(var.source_str(), self.eval_expr(lhs)?)
}
(Expr::Var(var), _)
if var.0.source_str().text() != "input"
&& self.lookup_var(&var.0, &[], true)? == Value::Undefined =>
(Expr::Var { span: var, .. }, _)
if var.source_str().text() != "input"
&& self.lookup_var(var, &[], true)? == Value::Undefined =>
{
(var.0.source_str(), self.eval_expr(rhs)?)
(var.source_str(), self.eval_expr(rhs)?)
}
(
Expr::Array {
@@ -614,6 +621,7 @@ impl Interpreter {
Expr::Array {
items: rhs_items,
span: rhs_span,
..
},
) => {
if lhs_items.len() != rhs_items.len() {
@@ -635,6 +643,7 @@ impl Interpreter {
Expr::Object {
fields: rhs_fields,
span: rhs_span,
..
},
) => {
if lhs_fields.len() != rhs_fields.len() {
@@ -706,8 +715,8 @@ impl Interpreter {
return Ok(rhs_value);
}
let name = if let Expr::Var(s) = lhs.as_ref() {
s.0.source_str()
let name = if let Expr::Var { span: s, .. } = lhs.as_ref() {
s.source_str()
} else {
let mut cache = BTreeMap::new();
let mut type_match = BTreeSet::new();
@@ -838,16 +847,16 @@ impl Interpreter {
let raise_error = is_last && type_match.get(expr).is_none();
match (expr.as_ref(), value) {
(Expr::Var(ident), _) if ident.0.text() == "_" => Ok(true),
(Expr::Var(ident), _)
(Expr::Var { span: ident, .. }, _) if ident.text() == "_" => Ok(true),
(Expr::Var { span: ident, .. }, _)
if check_existing_value
&& self.lookup_local_var(&ident.0.source_str()) == Some(value.clone()) =>
&& self.lookup_local_var(&ident.source_str()) == Some(value.clone()) =>
{
Ok(false)
}
(Expr::Var(ident), _) => {
self.add_variable(&ident.0.source_str(), value.clone())?;
(Expr::Var { span: ident, .. }, _) => {
self.add_variable(&ident.source_str(), value.clone())?;
Ok(true)
}
@@ -1076,7 +1085,9 @@ impl Interpreter {
Ok(match &stmt.literal {
Literal::Expr { span, expr, .. } => {
let value = match expr.as_ref() {
Expr::Call { span, fcn, params } => self.eval_call(
Expr::Call {
span, fcn, params, ..
} => self.eval_call(
span,
expr,
fcn,
@@ -1116,7 +1127,9 @@ impl Interpreter {
Literal::NotExpr { span, expr, .. } => {
let value = match expr.as_ref() {
// Extra parameter is allowed; but a return argument is not allowed.
Expr::Call { span, fcn, params } => self.eval_call(
Expr::Call {
span, fcn, params, ..
} => self.eval_call(
span,
expr,
fcn,
@@ -1368,7 +1381,9 @@ impl Interpreter {
let (saved_state, _) = self.apply_with_modifiers(stmts[0])?;
let loop_expr_value = loop_expr.value();
let loop_expr_value = if let Expr::Call { span, fcn, params } = loop_expr_value.as_ref()
let loop_expr_value = if let Expr::Call {
span, fcn, params, ..
} = loop_expr_value.as_ref()
{
// Handle walk(obj, output_param)
let extra_arg = get_extra_arg(
@@ -1395,8 +1410,11 @@ impl Interpreter {
// (this can happen if the same index is used for two different collections),
// then evaluate statements only if the index applies to this collection.
let loop_expr_index = loop_expr.index();
if let Some(Expr::Var(index_var)) = loop_expr_index.as_ref().map(|r| r.as_ref()) {
if let Some(idx) = self.lookup_local_var(&index_var.0.source_str()) {
if let Some(Expr::Var {
span: index_var, ..
}) = loop_expr_index.as_ref().map(|r| r.as_ref())
{
if let Some(idx) = self.lookup_local_var(&index_var.source_str()) {
if loop_expr_value[&idx] != Value::Undefined {
result = self.eval_stmts_in_loop(stmts, &loops[1..])? || result;
return Ok(result);
@@ -1517,8 +1535,8 @@ impl Interpreter {
let mut expr = refr;
loop {
match expr.as_ref() {
Expr::Var(v) => {
comps.push(Value::String(v.0.text().into()));
Expr::Var { span: v, .. } => {
comps.push(Value::String(v.text().into()));
break;
}
Expr::RefBrack { refr, index, .. } => {
@@ -1613,7 +1631,7 @@ impl Interpreter {
fn is_constant_ref(&self, mut expr: &Ref<Expr>) -> Result<bool> {
loop {
match expr.as_ref() {
Expr::Var(_) => break,
Expr::Var { .. } => break,
Expr::RefDot { refr, .. } => expr = refr,
Expr::RefBrack { refr, index, .. } if self.is_simple_literal(index)? => expr = refr,
_ => return Ok(false),
@@ -1625,12 +1643,11 @@ impl Interpreter {
fn is_simple_literal(&self, expr: &Ref<Expr>) -> Result<bool> {
Ok(matches!(
expr.as_ref(),
Expr::String(_)
| Expr::RawString(_)
| Expr::True(_)
| Expr::False(_)
| Expr::Null(_)
| Expr::Number(_)
Expr::String { .. }
| Expr::RawString { .. }
| Expr::Bool { .. }
| Expr::Null { .. }
| Expr::Number { .. }
))
}
@@ -2529,13 +2546,13 @@ impl Interpreter {
// TODO: global var check; interop with `some var`
if let Some(ea) = extra_arg {
match ea.as_ref() {
Expr::Var(var)
if allow_return_arg && self.lookup_local_var(&var.0.source_str()).is_none() =>
Expr::Var { span: var, .. }
if allow_return_arg && self.lookup_local_var(&var.source_str()).is_none() =>
{
let value =
self.eval_call_impl(span, expr, fcn, &params[..params.len() - 1])?;
if var.0.text() != "_" {
self.add_variable(&var.0.source_str(), value)?;
if var.text() != "_" {
self.add_variable(&var.source_str(), value)?;
}
Ok(Value::Bool(true))
}
@@ -2799,15 +2816,14 @@ impl Interpreter {
}
match expr.as_ref() {
Expr::Null(_) => Ok(Value::Null),
Expr::True(_) => Ok(Value::Bool(true)),
Expr::False(_) => Ok(Value::Bool(false)),
Expr::Number((_, v)) => Ok(v.clone()),
Expr::Null { value: v, .. }
| Expr::Bool { value: v, .. }
| Expr::Number { value: v, .. } => Ok(v.clone()),
// TODO: Handle string vs rawstring
Expr::String((_, v)) => Ok(v.clone()),
Expr::RawString((_, v)) => Ok(v.clone()),
Expr::String { value: v, .. } => Ok(v.clone()),
Expr::RawString { value: v, .. } => Ok(v.clone()),
// TODO: Handle undefined variables
Expr::Var(_) => self.eval_chained_ref_dot_or_brack(expr),
Expr::Var { .. } => self.eval_chained_ref_dot_or_brack(expr),
Expr::RefDot { .. } => self.eval_chained_ref_dot_or_brack(expr),
Expr::RefBrack { .. } => self.eval_chained_ref_dot_or_brack(expr),
@@ -2843,8 +2859,10 @@ impl Interpreter {
key, value, query, ..
} => self.eval_object_compr(key, value, query),
Expr::SetCompr { term, query, .. } => self.eval_set_compr(term, query),
Expr::UnaryExpr { span, expr: uexpr } => match uexpr.as_ref() {
Expr::Number(_) if !uexpr.span().text().starts_with('-') => {
Expr::UnaryExpr {
span, expr: uexpr, ..
} => match uexpr.as_ref() {
Expr::Number { .. } if !uexpr.span().text().starts_with('-') => {
builtins::numbers::arithmetic_operation(
span,
&ArithOp::Sub,
@@ -2859,9 +2877,9 @@ impl Interpreter {
.span()
.error("unary - can only be used with numeric literals")),
},
Expr::Call { span, fcn, params } => {
self.eval_call(span, expr, fcn, params, None, false)
}
Expr::Call {
span, fcn, params, ..
} => self.eval_call(span, expr, fcn, params, None, false),
}
}
@@ -3041,14 +3059,16 @@ impl Interpreter {
comps.push(field.0.text());
expr = Some(refr);
}
Expr::RefBrack { refr, index, .. } if matches!(index.as_ref(), Expr::String(_)) => {
if let Expr::String(s) = index.as_ref() {
comps.push(s.0.text());
Expr::RefBrack { refr, index, .. }
if matches!(index.as_ref(), Expr::String { .. }) =>
{
if let Expr::String { span: s, .. } = index.as_ref() {
comps.push(s.text());
expr = Some(refr);
}
}
Expr::Var(v) => {
comps.push(v.0.text());
Expr::Var { span: v, .. } => {
comps.push(v.text());
expr = None;
}
_ => bail!(e.span().error("invalid ref expression")),
@@ -3088,10 +3108,12 @@ impl Interpreter {
use Expr::*;
let (kind, span) = match expr.as_ref() {
// Scalars are supported
String(_) | RawString(_) | Number(_) | True(_) | False(_) | Null(_) => return Ok(()),
String { .. } | RawString { .. } | Number { .. } | Bool { .. } | Null { .. } => {
return Ok(())
}
// Uminus of number is treated as a single expression,
UnaryExpr { expr, .. } if matches!(expr.as_ref(), Number(_)) => return Ok(()),
UnaryExpr { expr, .. } if matches!(expr.as_ref(), Number { .. }) => return Ok(()),
// Comprehensions are supported since they won't evaluate to undefined.
ArrayCompr { .. } | SetCompr { .. } | ObjectCompr { .. } => return Ok(()),
@@ -3114,7 +3136,7 @@ impl Interpreter {
}
// The following may evaluate to undefined.
Var((span, _)) => ("var", span),
Var { span, .. } => ("var", span),
Call { span, .. } => ("call", span),
UnaryExpr { span, .. } => ("unaryexpr", span),
RefDot { span, .. } => ("ref", span),
@@ -3169,7 +3191,7 @@ impl Interpreter {
let (refr, index) = match refr.as_ref() {
Expr::RefBrack { refr, index, .. } => (refr, Some(index.clone())),
Expr::RefDot { .. } => (refr, None),
Expr::Var(_) => (refr, None),
Expr::Var { .. } => (refr, None),
_ => bail!(refr.span().error(&format!(
"invalid token {:?} with the default keyword",
refr
@@ -3489,13 +3511,13 @@ impl Interpreter {
let mut components: Vec<Rc<str>> = vec![];
loop {
refr = match refr.as_ref() {
Expr::Var(v) => {
components.push(v.0.text().into());
Expr::Var { span: v, .. } => {
components.push(v.text().into());
break;
}
Expr::RefBrack { refr, index, .. } => {
if let Expr::String(s) = index.as_ref() {
components.push(s.0.text().into());
if let Expr::String { span: s, .. } = index.as_ref() {
components.push(s.text().into());
} else {
components.clear();
}
@@ -3625,10 +3647,10 @@ impl Interpreter {
_ => match import.refr.as_ref() {
Expr::RefDot { field, .. } => field.0.text(),
Expr::RefBrack { index, .. } => match index.as_ref() {
Expr::String(s) => s.0.text(),
Expr::String { span: s, .. } => s.text(),
_ => "",
},
Expr::Var(v) if v.0.text() == "input" => {
Expr::Var { span: v, .. } if v.text() == "input" => {
// Warn redundant import of input. Ignore it.
#[cfg(feature = "std")]
std::eprintln!(
@@ -3667,7 +3689,7 @@ impl Interpreter {
// TODO: refactor.
let refr = match refr.as_ref() {
Expr::RefBrack { index, .. }
if matches!(index.as_ref(), Expr::String(_)) =>
if matches!(index.as_ref(), Expr::String { .. }) =>
{
refr
}
@@ -3681,7 +3703,7 @@ impl Interpreter {
Expr::RefBrack { refr, index, .. } => {
if !matches!(
index.as_ref(),
Expr::True(_) | Expr::False(_) | Expr::Number(_) | Expr::String(_)
Expr::Bool { .. } | Expr::Number { .. } | Expr::String { .. }
) {
// OPA's behavior is ignoring the non-scalar index
bail!(index.span().error("index is not a scalar value"));
+1
View File
@@ -22,6 +22,7 @@ extern crate std;
mod ast;
mod builtins;
mod engine;
mod indexchecker;
mod interpreter;
mod lexer;
mod number;
+166 -36
View File
@@ -21,6 +21,13 @@ pub struct Parser<'source> {
end: u32,
future_keywords: BTreeMap<String, Option<Span>>,
rego_v1: bool,
// The index of the last expression that was parsed.
eidx: u32,
// The index of the last statement that was parsed.
sidx: u32,
// The index of the last query that was parsed.
qidx: u32,
}
const FUTURE_KEYWORDS: [&str; 4] = ["contains", "every", "if", "in"];
@@ -37,9 +44,30 @@ impl<'source> Parser<'source> {
end: 0,
future_keywords: BTreeMap::new(),
rego_v1: false,
eidx: 0,
sidx: 0,
qidx: 0,
})
}
fn next_eidx(&mut self) -> u32 {
let eidx = self.eidx;
self.eidx += 1;
eidx
}
fn next_sidx(&mut self) -> u32 {
let sidx = self.sidx;
self.sidx += 1;
sidx
}
fn next_qidx(&mut self) -> u32 {
let qidx = self.qidx;
self.qidx += 1;
qidx
}
pub fn enable_rego_v1(&mut self) -> Result<()> {
self.turn_on_rego_v1(&None)
}
@@ -129,13 +157,13 @@ impl<'source> Parser<'source> {
Self::get_path_ref_components_into(refr, comps)?;
Self::get_path_ref_components_into(index, comps)?;
}
Expr::Var(v) => comps.push(v.0.clone()),
Expr::String(s) => comps.push(s.0.clone()),
Expr::True(s) | Expr::False(s) | Expr::Null(s) => comps.push(s.clone()),
Expr::Number(s) => {
Expr::Var { span: v, .. } => comps.push(v.clone()),
Expr::String { span: s, .. } => comps.push(s.clone()),
Expr::Bool { span: s, .. } | Expr::Null { span: s, .. } => comps.push(s.clone()),
Expr::Number { span, value, .. } => {
// Ensure that the span will be the serialized representation.
if *s.0.text() == s.1.to_json_str()? {
comps.push(s.0.clone());
if span.text() == value.to_json_str()? {
comps.push(span.clone());
} else {
bail!(refr.span().error("not a valid ref"));
}
@@ -262,9 +290,13 @@ impl<'source> Parser<'source> {
}
}
fn read_number(span: Span) -> Result<Expr> {
fn read_number(&mut self, span: Span) -> Result<Expr> {
match Number::from_str(span.text()) {
Ok(v) => Ok(Expr::Number((span, Value::Number(v)))),
Ok(v) => Ok(Expr::Number {
span,
value: Value::Number(v),
eidx: self.next_eidx(),
}),
Err(_) => bail!(span.error("could not parse number")),
}
}
@@ -272,27 +304,51 @@ impl<'source> Parser<'source> {
fn parse_scalar_or_var(&mut self) -> Result<Expr> {
let span = self.tok.1.clone();
let node = match &self.tok.0 {
TokenKind::Number => Self::read_number(span)?,
TokenKind::Number => self.read_number(span)?,
TokenKind::String => {
let v = match serde_json::from_str::<Value>(format!("\"{}\"", span.text()).as_str())
{
Ok(v) => v,
Err(e) => bail!(span.error(format!("invalid string literal. {e}").as_str())),
};
Expr::String((span, v))
Expr::String {
span,
value: v,
eidx: self.next_eidx(),
}
}
TokenKind::RawString => {
let v = Value::from(span.text().to_string());
Expr::RawString((span, v))
Expr::RawString {
span,
value: v,
eidx: self.next_eidx(),
}
}
TokenKind::Ident => match self.token_text() {
"null" => Expr::Null(span),
"true" => Expr::True(span),
"false" => Expr::False(span),
"null" => Expr::Null {
span,
value: Value::Null,
eidx: self.next_eidx(),
},
"true" => Expr::Bool {
span,
value: Value::from(true),
eidx: self.next_eidx(),
},
"false" => Expr::Bool {
span,
value: Value::from(false),
eidx: self.next_eidx(),
},
_ => {
let ident = self.parse_var()?;
let v = Value::from(ident.text());
return Ok(Expr::Var((ident, v)));
let value = Value::from(ident.text());
return Ok(Expr::Var {
span: ident,
value,
eidx: self.next_eidx(),
});
}
},
_ => {
@@ -353,6 +409,7 @@ impl<'source> Parser<'source> {
span,
term: Ref::new(term),
query: Ref::new(query),
eidx: self.next_eidx(),
})
}
Err(_) if self.end == pos => {
@@ -372,7 +429,11 @@ impl<'source> Parser<'source> {
}
self.expect("]", "while parsing array")?;
span.end = self.end;
Ok(Expr::Array { span, items })
Ok(Expr::Array {
span,
items,
eidx: self.next_eidx(),
})
}
Err(err) => Err(err),
}
@@ -390,6 +451,7 @@ impl<'source> Parser<'source> {
span,
term: Ref::new(term),
query: Ref::new(query),
eidx: self.next_eidx(),
});
}
Err(err) if self.end != pos => {
@@ -408,6 +470,7 @@ impl<'source> Parser<'source> {
return Ok(Expr::Object {
span,
fields: vec![],
eidx: self.next_eidx(),
});
}
@@ -427,7 +490,11 @@ impl<'source> Parser<'source> {
}
self.expect("}", "while parsing set")?;
span.end = self.end;
return Ok(Expr::Set { span, items });
return Ok(Expr::Set {
span,
items,
eidx: self.next_eidx(),
});
}
// Parse as object.
@@ -442,6 +509,7 @@ impl<'source> Parser<'source> {
key: Ref::new(first),
value: Ref::new(term),
query: Ref::new(query),
eidx: self.next_eidx(),
});
}
Err(err) if self.end != pos => {
@@ -483,6 +551,7 @@ impl<'source> Parser<'source> {
Ok(Expr::Object {
span,
fields: items,
eidx: self.next_eidx(),
})
}
@@ -494,6 +563,7 @@ impl<'source> Parser<'source> {
Ok(Expr::Set {
span,
items: vec![],
eidx: self.next_eidx(),
})
}
@@ -513,6 +583,7 @@ impl<'source> Parser<'source> {
Ok(Expr::UnaryExpr {
span,
expr: Ref::new(expr),
eidx: self.next_eidx(),
})
}
@@ -531,18 +602,18 @@ impl<'source> Parser<'source> {
let mut expr = &term;
while possible_fcn {
match expr {
Expr::Var(_) => break,
Expr::Var { .. } => break,
Expr::RefDot { refr, .. } => expr = refr,
Expr::RefBrack { refr, index, .. } => {
expr = refr;
possible_fcn = matches!(index.as_ref(), Expr::String(_));
possible_fcn = matches!(index.as_ref(), Expr::String { .. });
}
_ => {
possible_fcn = false;
}
}
}
matches!(&term, Expr::Var(_));
matches!(&term, Expr::Var { .. });
loop {
let mut span = self.tok.1.clone();
@@ -586,6 +657,7 @@ impl<'source> Parser<'source> {
span,
refr: Ref::new(term),
field: (field, fieldv),
eidx: self.next_eidx(),
};
}
"[" => {
@@ -593,7 +665,7 @@ impl<'source> Parser<'source> {
let index = self.parse_in_expr()?;
// If the index is a string, the ref could be path to a function.
possible_fcn = possible_fcn && matches!(&index, Expr::String(_));
possible_fcn = possible_fcn && matches!(&index, Expr::String { .. });
self.expect("]", "while parsing bracketed reference")?;
span.end = self.end;
@@ -602,6 +674,7 @@ impl<'source> Parser<'source> {
span,
refr: Ref::new(term),
index: Ref::new(index),
eidx: self.next_eidx(),
};
}
"(" if possible_fcn => {
@@ -624,6 +697,7 @@ impl<'source> Parser<'source> {
span,
fcn: Ref::new(term),
params: args,
eidx: self.next_eidx(),
};
// The expression can no longer be a function after the call.
@@ -661,6 +735,7 @@ impl<'source> Parser<'source> {
op,
lhs: Ref::new(expr),
rhs: Ref::new(right),
eidx: self.next_eidx(),
};
}
}
@@ -685,7 +760,7 @@ impl<'source> Parser<'source> {
rhs_span.col += 1;
self.next_token()?;
Self::read_number(rhs_span)?
self.read_number(rhs_span)?
} else {
self.next_token()?;
self.parse_mul_div_mod_expr()?
@@ -696,6 +771,7 @@ impl<'source> Parser<'source> {
op,
lhs: Ref::new(expr),
rhs: Ref::new(right),
eidx: self.next_eidx(),
};
}
}
@@ -715,6 +791,7 @@ impl<'source> Parser<'source> {
op: BinOp::Intersection,
lhs: Ref::new(expr),
rhs: Ref::new(right),
eidx: self.next_eidx(),
};
}
Ok(expr)
@@ -735,6 +812,7 @@ impl<'source> Parser<'source> {
op: BinOp::Union,
lhs: Ref::new(expr),
rhs: Ref::new(right),
eidx: self.next_eidx(),
};
}
Ok(expr)
@@ -763,6 +841,7 @@ impl<'source> Parser<'source> {
op,
lhs: Ref::new(expr),
rhs: Ref::new(right),
eidx: self.next_eidx(),
};
}
Ok(expr)
@@ -789,6 +868,7 @@ impl<'source> Parser<'source> {
key,
value,
collection: Ref::new(expr3),
eidx: self.next_eidx(),
};
expr2 = None;
@@ -832,6 +912,7 @@ impl<'source> Parser<'source> {
span,
lhs: Ref::new(expr),
rhs: Ref::new(rhs),
eidx: self.next_eidx(),
};
}
Ok(expr)
@@ -864,11 +945,11 @@ impl<'source> Parser<'source> {
let op = match self.token_text() {
"=" => AssignOp::Eq,
":=" if self.rego_v1 => {
if let Expr::Var(v) = &expr {
if v.0.text() == "input" {
if let Expr::Var { span: v, .. } = &expr {
if v.text() == "input" {
bail!(span.error("input cannot be shadowed"));
}
if v.0.text() == "data" {
if v.text() == "data" {
bail!(span.error("data cannot be shadowed"));
}
}
@@ -889,6 +970,7 @@ impl<'source> Parser<'source> {
op,
lhs: Ref::new(expr),
rhs: Ref::new(right),
eidx: self.next_eidx(),
})
}
@@ -973,7 +1055,7 @@ impl<'source> Parser<'source> {
for (idx, ref_expr) in refs.iter().enumerate() {
let span = &vars[idx];
match ref_expr.as_ref() {
Expr::Var(_) => (),
Expr::Var { .. } => (),
_ => {
return Err(anyhow!(
"{}:{}:{} error: encountered `{}` while expecting identifier",
@@ -987,6 +1069,8 @@ impl<'source> Parser<'source> {
}
span.end = self.end;
// Since exprs are discarded, adjust the expression index counter.
self.eidx -= vars.len() as u32;
return Ok(Literal::SomeVars { span, vars });
}
@@ -1053,6 +1137,7 @@ impl<'source> Parser<'source> {
span,
literal,
with_mods,
sidx: self.next_sidx(),
})
}
@@ -1130,6 +1215,7 @@ impl<'source> Parser<'source> {
Ok(Query {
span,
stmts: literals,
qidx: self.next_qidx(),
})
}
@@ -1166,7 +1252,12 @@ impl<'source> Parser<'source> {
let start = self.tok.1.start;
let var = self.parse_var()?;
let mut refr = Expr::Var(Self::span_and_value(var));
let (span, value) = Self::span_and_value(var);
let mut refr = Expr::Var {
span,
value,
eidx: self.next_eidx(),
};
loop {
let mut span = self.tok.1.clone();
let sep_pos = span.start;
@@ -1203,12 +1294,20 @@ impl<'source> Parser<'source> {
span,
refr: Ref::new(refr),
field: Self::span_and_value(field),
eidx: self.next_eidx(),
};
}
"[" => {
self.next_token()?;
let index = match &self.tok.0 {
TokenKind::String => Expr::String(Self::span_and_value(self.tok.1.clone())),
TokenKind::String => {
let (span, value) = Self::span_and_value(self.tok.1.clone());
Expr::String {
span,
value,
eidx: self.next_eidx(),
}
}
_ => {
return Err(self.source.error(
self.tok.1.line,
@@ -1224,6 +1323,7 @@ impl<'source> Parser<'source> {
span,
refr: Ref::new(refr),
index: Ref::new(index),
eidx: self.next_eidx(),
};
}
_ => break,
@@ -1247,7 +1347,12 @@ impl<'source> Parser<'source> {
bail!(span.error("data cannot be shadowed"));
}
}
Expr::Var(Self::span_and_value(v))
let (span, value) = Self::span_and_value(v);
Expr::Var {
span,
value,
eidx: self.next_eidx(),
}
} else {
return Err(self.source.error(
span.line,
@@ -1292,6 +1397,7 @@ impl<'source> Parser<'source> {
span,
refr: Ref::new(term),
field: Self::span_and_value(field),
eidx: self.next_eidx(),
};
}
"[" => {
@@ -1303,6 +1409,7 @@ impl<'source> Parser<'source> {
span,
refr: Ref::new(term),
index: Ref::new(index),
eidx: self.next_eidx(),
};
}
_ => break,
@@ -1362,15 +1469,17 @@ impl<'source> Parser<'source> {
if assign.is_none() && is_set_follower {
match rule_ref.as_ref() {
Expr::RefBrack { refr, index, .. }
if matches!(refr.as_ref(), Expr::Var(_)) =>
if matches!(refr.as_ref(), Expr::Var { .. }) =>
{
// Adjust the expression counter since we are discarding the RefBrack expression.
self.eidx -= 1;
return Ok(RuleHead::Set {
span,
refr: refr.clone(),
key: Some(index.clone()),
});
}
Expr::RefDot { refr, .. } if matches!(refr.as_ref(), Expr::Var(_)) => {
Expr::RefDot { refr, .. } if matches!(refr.as_ref(), Expr::Var { .. }) => {
return Ok(RuleHead::Set {
span,
refr: rule_ref,
@@ -1416,7 +1525,11 @@ impl<'source> Parser<'source> {
*self = state;
let stmts = vec![self.parse_literal_stmt()?];
span.end = self.end;
Ok(Query { span, stmts })
Ok(Query {
span,
stmts,
qidx: self.next_qidx(),
})
}
pub fn parse_rule_bodies(&mut self) -> Result<Vec<RuleBody>> {
@@ -1539,6 +1652,7 @@ impl<'source> Parser<'source> {
let query = Ref::new(Query {
span: query_span,
stmts: vec![],
qidx: self.next_qidx(),
});
span.end = self.end;
bodies.push(RuleBody {
@@ -1597,7 +1711,14 @@ impl<'source> Parser<'source> {
refr: rule_ref,
args: args
.into_iter()
.map(|a| Ref::new(Expr::Var(Self::span_and_value(a))))
.map(|a| {
let (span, value) = Self::span_and_value(a);
Ref::new(Expr::Var {
span,
value,
eidx: self.next_eidx(),
})
})
.collect(),
op,
value,
@@ -1769,12 +1890,21 @@ impl<'source> Parser<'source> {
policy.push(Ref::new(self.parse_rule()?));
}
Ok(Module {
let m = Module {
package,
imports,
policy,
rego_v1: self.rego_v1,
})
num_expressions: self.eidx,
num_statements: self.sidx,
num_queries: self.qidx,
};
#[cfg(debug_assertions)]
{
indexchecker::IndexChecker::default().check_module(&m)?;
}
Ok(m)
}
pub fn parse_user_query(&mut self) -> Result<Ref<Query>> {
+30 -27
View File
@@ -219,7 +219,12 @@ pub fn traverse(expr: &Ref<Expr>, f: &mut dyn FnMut(&Ref<Expr>) -> Result<bool>)
return Ok(());
}
match expr.as_ref() {
String(_) | RawString(_) | Number(_) | True(_) | False(_) | Null(_) | Var(_) => (),
Expr::String { .. }
| RawString { .. }
| Number { .. }
| Bool { .. }
| Null { .. }
| Var { .. } => (),
Array { items, .. } | Set { items, .. } => {
for i in items {
@@ -308,23 +313,23 @@ fn gather_assigned_vars(
) -> Result<()> {
traverse(expr, &mut |e| match e.as_ref() {
// Ignore _, input, data.
Var(v) if matches!(v.0.text(), "_" | "input" | "data") => Ok(false),
Var { span: v, .. } if matches!(v.text(), "_" | "input" | "data") => Ok(false),
// Record local var that can shadow input var.
Var(v) if can_shadow => {
scope.locals.insert(v.0.source_str(), v.0.clone());
Var { span: v, .. } if can_shadow => {
scope.locals.insert(v.source_str(), v.clone());
Ok(false)
}
// Record input vars.
Var(v) if var_exists(&v.0, parent_scopes) => {
scope.inputs.insert(v.0.source_str());
Var { span: v, .. } if var_exists(v, parent_scopes) => {
scope.inputs.insert(v.source_str());
Ok(false)
}
// Record local var.
Var(v) => {
scope.unscoped.insert(v.0.source_str());
Var { span: v, .. } => {
scope.unscoped.insert(v.source_str());
Ok(false)
}
@@ -336,10 +341,10 @@ fn gather_assigned_vars(
fn gather_input_vars(expr: &Ref<Expr>, parent_scopes: &[Scope], scope: &mut Scope) -> Result<()> {
traverse(expr, &mut |e| match e.as_ref() {
Var(v)
if !scope.unscoped.contains(&v.0.source_str()) && var_exists(&v.0, parent_scopes) =>
Var { span: v, .. }
if !scope.unscoped.contains(&v.source_str()) && var_exists(v, parent_scopes) =>
{
scope.inputs.insert(v.0.source_str());
scope.inputs.insert(v.source_str());
Ok(false)
}
_ => Ok(true),
@@ -555,8 +560,8 @@ impl Analyzer {
RuleHead::Func { args, assign, .. } => {
for a in args.iter() {
traverse(a, &mut |e| {
if let Var(v) = e.as_ref() {
scope.unscoped.insert(v.0.source_str());
if let Var { span: v, .. } = e.as_ref() {
scope.unscoped.insert(v.source_str());
}
Ok(true)
})?;
@@ -647,10 +652,10 @@ impl Analyzer {
let mut used_vars = vec![];
let mut comprs = vec![];
traverse(expr, &mut |e| match e.as_ref() {
Var(v) if !matches!(v.0.text(), "_" | "input" | "data") => {
let name = v.0.source_str();
Var { span: v, .. } if !matches!(v.text(), "_" | "input" | "data") => {
let name = v.source_str();
let is_extra_arg = match assigned_vars {
Some(vars) => vars.contains(&v.0.source_str()),
Some(vars) => vars.contains(&v.source_str()),
_ => false,
};
@@ -659,20 +664,18 @@ impl Analyzer {
{
if !is_extra_arg {
used_vars.push(name.clone());
first_use.entry(name).or_insert(v.0.clone());
first_use.entry(name).or_insert(v.clone());
}
} else if !scope.inputs.contains(&name) {
bail!(v
.0
.error(format!("use of undefined variable `{name}` is unsafe").as_str()));
bail!(v.error(format!("use of undefined variable `{name}` is unsafe").as_str()));
}
Ok(false)
}
RefBrack { refr, index, .. } => {
traverse(index, &mut |e| match e.as_ref() {
Var(v) => {
let var = v.0.source_str();
Var { span: v, .. } => {
let var = v.source_str();
if scope.locals.contains_key(&var) || scope.unscoped.contains(&var) {
let (rb_used_vars, rb_comprs) =
Self::gather_used_vars_comprs_index_vars(
@@ -765,11 +768,11 @@ impl Analyzer {
) -> Result<Vec<SourceStr>> {
let mut vars = vec![];
traverse(expr, &mut |e| match e.as_ref() {
Var(v) => {
let var = v.0.source_str();
Var { span: v, .. } => {
let var = v.source_str();
if scope.locals.contains_key(&var) {
if check_first_use {
Self::check_first_use(&v.0, first_use)?;
Self::check_first_use(v, first_use)?;
}
vars.push(var);
} else if scope.unscoped.contains(&var) {
@@ -955,8 +958,8 @@ impl Analyzer {
non_vars: &mut Vec<Ref<Expr>>,
) -> Result<()> {
traverse(expr, &mut |e| match e.as_ref() {
Var(v) if scope.locals.contains_key(&v.0.source_str()) => {
vars.push(v.0.source_str());
Var { span: v, .. } if scope.locals.contains_key(&v.source_str()) => {
vars.push(v.source_str());
Ok(false)
}
// TODO: Object key/value
+5 -5
View File
@@ -19,13 +19,13 @@ pub fn get_path_string(refr: &Expr, document: Option<&str>) -> Result<String> {
expr = Some(refr);
}
Some(Expr::RefBrack { refr, index, .. }) => {
if let Expr::String(s) = index.as_ref() {
comps.push(s.0.text());
if let Expr::String { span: s, .. } = index.as_ref() {
comps.push(s.text());
}
expr = Some(refr);
}
Some(Expr::Var(v)) => {
comps.push(v.0.text());
Some(Expr::Var { span: v, .. }) => {
comps.push(v.text());
expr = None;
}
_ => bail!("internal error: not a simple ref {expr:?}"),
@@ -112,7 +112,7 @@ pub fn get_root_var(mut expr: &Expr) -> Result<SourceStr> {
let empty = expr.span().source_str().clone_empty();
loop {
match expr {
Expr::Var(v) => return Ok(v.0.source_str()),
Expr::Var { span: v, .. } => return Ok(v.source_str()),
Expr::RefDot { refr, .. } | Expr::RefBrack { refr, .. } => expr = refr,
_ => return Ok(empty),
}