Files
regorus/src/scheduler.rs
Anand Krishnamoorthi 11940ddb04 Introduce Object storage abstraction (#735)
Add an opaque Object type for the key→value storage backing
Value::Object. It exposes a small set of methods (get, insert, remove,
iter, iter_sorted, cursor, serde) and keeps the backing store private,
so future representations -- inline small-map, hash-backed, lazy,
arena, FFI-callback -- can plug in without touching the call sites
that name this type.

Nothing in the engine uses Object yet. Value::Object still wraps
Rc<BTreeMap<Value, Value>>; the payload swap and call-site migration
come in the next PR. Object stands on its own unit tests in the
meantime.

docs/value/object.md walks through the design, the precedents it
follows (serde_json::Map, toml::Table, simdjson DOM), and the
concrete workloads the abstraction is meant to unlock.

A matching Set abstraction follows in a separate PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-04 12:31:15 -05:00

1219 lines
43 KiB
Rust

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
clippy::unwrap_used,
clippy::shadow_unrelated,
clippy::unused_self,
clippy::option_if_let_else,
clippy::semicolon_if_nothing_returned,
clippy::print_stderr,
clippy::use_debug,
clippy::as_conversions,
clippy::pattern_type_mismatch
)] // scheduler logic indexes arrays and unwraps queues intentionally
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::*;
use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
use alloc::string::String;
use core::cmp;
use core::fmt;
use anyhow::{anyhow, bail, Result};
#[derive(Debug)]
pub struct Definition<Str: Clone + cmp::Ord> {
// The variable being defined.
// This can be an empty string to indicate that
// no variable is being defined.
pub var: Str,
// Other variables in the same scope used to compute
// the value of this variable.
pub used_vars: Vec<Str>,
}
#[derive(Debug)]
pub struct StmtInfo<Str: Clone + cmp::Ord> {
// A statement can define multiple variables.
// A variable can also be defined by multiple statement.
pub definitions: Vec<Definition<Str>>,
}
#[derive(Debug)]
pub enum SortResult {
// The order in which statements must be executed.
Order(Vec<u16>),
// List of statements comprising a cycle for a given var.
#[allow(unused)]
Cycle(String, Vec<usize>),
}
pub fn schedule<Str: Clone + cmp::Ord + fmt::Debug>(
infos: &mut [StmtInfo<Str>],
empty: &Str,
) -> Result<SortResult> {
let num_statements = infos.len();
// Mapping from each var to the list of statements that define it.
let mut defining_stmts: BTreeMap<Str, Vec<usize>> = BTreeMap::new();
// For each statement, interate through its definitions and add the
// statement (index) to the var's defining-statements list.
for (idx, info) in infos.iter().enumerate() {
for defn in &info.definitions {
let varc = defn.var.clone();
defining_stmts.entry(varc).or_default().push(idx);
}
}
// Order of execution for statements.
let mut order = Vec::with_capacity(infos.len());
// Keep track of whether a var has been defined or not.
let mut defined_vars = BTreeSet::new();
// Keep track of whether a statement has been scheduled or not.
let mut scheduled = vec![false; infos.len()];
// List of vars to be processed.
let mut vars_to_process: Vec<Str> = defining_stmts.keys().cloned().collect();
let mut tmp = vec![];
let mut queue = VecDeque::new();
let mut schedule_stmt = |stmt_idx: usize| {
// Check if the statement has already been scheduled.
if scheduled[stmt_idx] {
return None;
}
let definitions = &infos[stmt_idx].definitions;
let can_be_scheduled = if definitions.len() == 1 {
// Handle the more common case of single definition statements optimally.
// Check if all the vars used by the definition are previously assigned.
definitions[0]
.used_vars
.iter()
.all(|uv| defined_vars.contains(uv))
} else {
// Set of vars that can be defined in this statement.
let mut defined_in_stmt = BTreeSet::new();
// Add each definition to processing queue.
queue.clear();
for defn in definitions {
queue.push_back(defn);
}
while !queue.is_empty() {
let n = queue.len();
for _ in 0..n {
let defn = queue.pop_front().unwrap();
// Check if the vars used by this definition are
// 1) defined via prior assignments (or)
// 2) defined in current statement
if defn
.used_vars
.iter()
.all(|uv| defined_vars.contains(uv) || defined_in_stmt.contains(uv))
{
defined_in_stmt.insert(defn.var.clone());
} else {
// The definiton must be processed again.
queue.push_back(defn);
}
}
// If no definition became defined, then there is a cycle between
// the definitions in this statement. The cycle cannot be broken yet.
if n == queue.len() {
break;
}
}
// If the vars used by all the definitions are already defined or
// can be defined by scheduling this statement, return true.
queue.is_empty()
};
// Schedule the var if possible.
if can_be_scheduled {
order.push(stmt_idx as u16);
scheduled[stmt_idx] = true;
// For each definition in the statement, mark its var as defined.
for defn in &infos[stmt_idx].definitions {
defined_vars.insert(defn.var.clone());
}
Some(true)
} else {
Some(false)
}
};
let mut process_var = |var| {
let mut stmt_scheduled = false;
let mut reprocess_var = false;
// Loop through each statement that defines the var.
for stmt_idx in defining_stmts.entry(var).or_default().iter().cloned() {
match schedule_stmt(stmt_idx) {
Some(true) => {
stmt_scheduled = true;
}
Some(false) => {
reprocess_var = true;
}
None => {
// Statement has already been scheduled.
}
}
}
(stmt_scheduled, reprocess_var)
};
process_var(empty.clone());
let mut done = false;
while !done {
done = true;
// Swap with temporary vec.
core::mem::swap(&mut vars_to_process, &mut tmp);
// Loop through each unscheduled var.
for var in tmp.iter().cloned() {
let (stmt_scheduled, reprocess_var) = process_var(var.clone());
if stmt_scheduled {
done = false;
// If a statement has been scheduled, it means that the
// var has been defined. Process "" (statements that don't define any var)
// to see if any statements that depend on var can be scheduled.
// Doing so allows statements like `x > 10` to be scheduled immediately after x has been defined.
// TODO: Also schedule statements like `y = x > 10` immediately.
process_var(empty.clone());
}
if reprocess_var {
vars_to_process.push(var);
}
}
}
if order.len() != num_statements {
#[cfg(feature = "std")]
std::eprintln!("could not schedule all statements {order:?}");
return Ok(SortResult::Order(
(0..num_statements).map(|i| i as u16).collect(),
));
}
// TODO: determine cycles.
Ok(SortResult::Order(order))
}
#[derive(Clone, Default, Debug)]
pub struct QuerySchedule {
pub scope: Scope,
pub order: Vec<u16>,
}
pub struct Analyzer {
packages: BTreeMap<String, Scope>,
scopes: Vec<Scope>,
schedule_table: Lookup<QuerySchedule>,
functions: FunctionTable,
current_module_path: String,
current_module_index: u32,
}
#[derive(Debug, Clone)]
pub struct Schedule {
pub queries: Lookup<QuerySchedule>,
}
impl Default for Analyzer {
fn default() -> Self {
Self::new()
}
}
impl Analyzer {
pub fn new() -> Analyzer {
Analyzer {
packages: BTreeMap::new(),
schedule_table: Lookup::new(),
scopes: vec![],
functions: FunctionTable::new(),
current_module_path: String::default(),
current_module_index: 0,
}
}
pub fn analyze(mut self, modules: &[Ref<Module>]) -> Result<Schedule> {
self.add_rules_and_aliases(modules)?;
self.functions = gather_functions(modules)?;
// Pre-allocate capacity for all modules based on their num_queries
for (module_index, m) in modules.iter().enumerate() {
let module_idx = module_index as u32;
if m.num_queries > 0 {
// Reserve capacity for all queries in this module (0 to num_queries-1)
self.schedule_table
.ensure_capacity(module_idx, m.num_queries - 1);
}
}
for (module_index, m) in modules.iter().enumerate() {
self.current_module_index = module_index as u32;
self.analyze_module(m)?;
}
Ok(Schedule {
queries: self.schedule_table,
})
}
pub fn analyze_query_snippet(
mut self,
modules: &[Ref<Module>],
query: &Ref<Query>,
) -> Result<Schedule> {
self.add_rules_and_aliases(modules)?;
// Pre-allocate capacity for all modules based on their num_queries
for (module_index, m) in modules.iter().enumerate() {
let module_idx = module_index as u32;
if m.num_queries > 0 {
// Reserve capacity for all queries in this module (0 to num_queries-1)
self.schedule_table
.ensure_capacity(module_idx, m.num_queries - 1);
}
}
// Query snippets are treated as if they're part of a module appended at the end
let snippet_module_index = modules.len() as u32;
self.schedule_table
.ensure_capacity(snippet_module_index, query.qidx);
self.current_module_index = snippet_module_index;
self.analyze_query(None, None, query, Scope::default())?;
Ok(Schedule {
queries: self.schedule_table,
})
}
fn add_rules_and_aliases(&mut self, modules: &[Ref<Module>]) -> Result<()> {
for m in modules {
let path = get_path_string(&m.package.refr, Some("data"))?;
let scope: &mut Scope = self.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)?,
};
scope.unscoped.insert(var);
}
for import in &m.imports {
// Ensure default alias imports (e.g. import data.pkg.mod) are visible to this scope.
if let Some(alias_span) = import_alias_span(import) {
scope.unscoped.insert(alias_span.source_str());
}
}
}
Ok(())
}
fn analyze_module(&mut self, m: &Module) -> Result<()> {
let path = get_path_string(&m.package.refr, Some("data"))?;
let scope = match self.packages.get(&path) {
Some(s) => s,
_ => bail!("internal error: package scope missing"),
};
self.current_module_path = path;
self.scopes.push(scope.clone());
for r in &m.policy {
self.analyze_rule(r)?;
}
self.scopes.pop();
Ok(())
}
fn analyze_rule(&mut self, r: &Ref<Rule>) -> Result<()> {
match r.as_ref() {
Rule::Spec { head, bodies, .. } => {
let (key, value, scope) = self.analyze_rule_head(head)?;
// Push arg scope if any.
// Args are maintained in a separate scope so that they aren't used for
// scheduling.
self.scopes.push(scope);
for b in bodies {
self.analyze_query(key.clone(), value.clone(), &b.query, Scope::default())?;
}
if bodies.is_empty() {
if let Some(value) = value {
self.analyze_value_expr(&value)?;
}
}
self.scopes.pop();
Ok(())
}
Rule::Default { value, .. } => self.analyze_value_expr(value),
}
}
fn analyze_value_expr(&mut self, expr: &Ref<Expr>) -> Result<()> {
let mut comprs = vec![];
traverse(expr, &mut |e| match e.as_ref() {
ArrayCompr { .. } | SetCompr { .. } | ObjectCompr { .. } => {
comprs.push(e.clone());
Ok(false)
}
_ => Ok(true),
})?;
for compr in comprs {
match compr.as_ref() {
Expr::ArrayCompr { query, term, .. } | Expr::SetCompr { query, term, .. } => {
self.analyze_query(None, Some(term.clone()), query, Scope::default())?;
}
Expr::ObjectCompr {
query, key, value, ..
} => self.analyze_query(
Some(key.clone()),
Some(value.clone()),
query,
Scope::default(),
)?,
_ => (),
}
}
Ok(())
}
fn analyze_rule_head(
&mut self,
head: &RuleHead,
) -> Result<(Option<ExprRef>, Option<ExprRef>, Scope)> {
let mut scope = Scope::default();
Ok(match head {
RuleHead::Compr { assign, .. } => {
(None, assign.as_ref().map(|a| a.value.clone()), scope)
}
RuleHead::Set { key, .. } => (key.clone(), None, scope),
RuleHead::Func { args, assign, .. } => {
for a in args.iter() {
traverse(a, &mut |e| {
if let Var { span: v, .. } = e.as_ref() {
scope.unscoped.insert(v.source_str());
}
Ok(true)
})?;
}
(None, assign.as_ref().map(|a| a.value.clone()), scope)
}
})
}
fn gather_local_vars(
&mut self,
key: Option<Ref<Expr>>,
value: Option<Ref<Expr>>,
query: &Query,
scope: &mut Scope,
) -> Result<()> {
// First process assign, some expressions and gather local vars.
for stmt in &query.stmts {
for wm in &stmt.with_mods {
gather_input_vars(&wm.r#as, &self.scopes, scope)?;
gather_loop_vars(&wm.r#as, &self.scopes, scope)?;
}
match &stmt.literal {
Literal::SomeVars { vars, .. } => vars.iter().for_each(|v| {
scope.locals.insert(v.source_str(), v.clone());
}),
Literal::SomeIn {
key,
value,
collection,
..
} => {
if let Some(key) = key {
gather_vars(key, true, &self.scopes, scope)?;
}
gather_vars(value, true, &self.scopes, scope)?;
gather_input_vars(collection, &self.scopes, scope)?;
gather_loop_vars(collection, &self.scopes, scope)?;
}
Literal::Expr { expr, .. } | Literal::NotExpr { expr, .. } => {
if let AssignExpr { .. } = expr.as_ref() {
gather_vars(expr, false, &self.scopes, scope)?;
} else {
gather_input_vars(expr, &self.scopes, scope)?;
gather_loop_vars(expr, &self.scopes, scope)?;
let extra_arg = get_extra_arg(
expr,
Some(self.current_module_path.as_str()),
&self.functions,
);
if let Some(ea) = extra_arg {
gather_vars(&ea, false, &self.scopes, scope)?;
}
}
}
Literal::Every { domain, .. } => {
// key, value defined in every stmt is visible only in its body.
gather_input_vars(domain, &self.scopes, scope)?;
gather_loop_vars(domain, &self.scopes, scope)?;
}
}
}
if let Some(key) = &key {
gather_vars(key, false, &self.scopes, scope)?;
}
if let Some(value) = &value {
gather_vars(value, false, &self.scopes, scope)?;
}
// Remove input vars that are shadowed.
for v in scope.locals.keys() {
scope.inputs.remove(v);
scope.unscoped.remove(v);
}
Ok(())
}
fn gather_used_vars_comprs_index_vars(
expr: &Ref<Expr>,
scope: &mut Scope,
first_use: &mut BTreeMap<SourceStr, Span>,
definitions: &mut Vec<Definition<SourceStr>>,
assigned_vars: &Option<&BTreeSet<SourceStr>>,
) -> Result<(Vec<SourceStr>, Vec<Ref<Expr>>)> {
let mut used_vars = vec![];
let mut comprs = vec![];
traverse(expr, &mut |e| match e.as_ref() {
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.source_str()),
_ => false,
};
if scope.locals.contains_key(&name) || scope.unscoped.contains(&name)
/*|| scope.inputs.contains(name) */
{
if !is_extra_arg {
used_vars.push(name.clone());
first_use.entry(name).or_insert(v.clone());
}
} else if !scope.inputs.contains(&name) {
bail!(v.error(format!("use of undefined variable `{name}` is unsafe").as_str()));
}
Ok(false)
}
Var { span: v, .. } if v.text() == "input" => {
scope.uses_input = true;
Ok(false)
}
RefBrack { refr, index, .. } => {
traverse(index, &mut |e| match e.as_ref() {
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(
refr,
scope,
first_use,
definitions,
assigned_vars,
)?;
definitions.push(Definition {
var: var.clone(),
used_vars: rb_used_vars.clone(),
});
used_vars.extend(rb_used_vars);
used_vars.push(var);
comprs.extend(rb_comprs);
}
Ok(false)
}
Expr::Array { .. } | Expr::Object { .. } => Ok(true),
_ => Ok(false),
})?;
Ok(true)
}
ArrayCompr { .. } | SetCompr { .. } | ObjectCompr { .. } => {
comprs.push(e.clone());
Ok(false)
}
_ => Ok(true),
})?;
Ok((used_vars, comprs))
}
fn process_comprs(
&mut self,
comprs: &[Ref<Expr>],
scope: &mut Scope,
first_use: &mut BTreeMap<SourceStr, Span>,
used_vars: &mut Vec<SourceStr>,
) -> Result<()> {
self.scopes.push(scope.clone());
for compr in comprs {
let compr_scope = match compr.as_ref() {
Expr::ArrayCompr { query, term, .. } | Expr::SetCompr { query, term, .. } => {
self.analyze_query(None, Some(term.clone()), query, Scope::default())?;
self.schedule_table
.get_checked(self.current_module_index, query.qidx)
.map_err(|err| anyhow!("schedule_table out of bounds: {err}"))?
.map(|qs| &qs.scope)
}
Expr::ObjectCompr {
query, key, value, ..
} => {
self.analyze_query(
Some(key.clone()),
Some(value.clone()),
query,
Scope::default(),
)?;
self.schedule_table
.get_checked(self.current_module_index, query.qidx)
.map_err(|err| anyhow!("schedule_table out of bounds: {err}"))?
.map(|qs| &qs.scope)
}
_ => break,
};
// Record vars used by the comprehension scope.
if let Some(compr_scope) = compr_scope {
// Propagate input usage from nested scope to parent
if compr_scope.uses_input {
scope.uses_input = true;
}
for iv in &compr_scope.inputs {
if scope.locals.contains_key(iv) || scope.unscoped.contains(iv) {
// Record possible first use of current scope's local var.
first_use.entry(iv.clone()).or_insert(compr.span().clone());
used_vars.push(iv.clone());
} else {
// If the var is not a local var, then add it to the set of input vars.
scope.inputs.insert(iv.clone());
}
}
}
}
self.scopes.pop();
Ok(())
}
fn gather_assigned_vars(
&self,
expr: &Ref<Expr>,
scope: &Scope,
check_first_use: bool,
first_use: &BTreeMap<SourceStr, Span>,
) -> Result<Vec<SourceStr>> {
let mut vars = vec![];
traverse(expr, &mut |e| match e.as_ref() {
Var { span: v, .. } => {
let var = v.source_str();
if scope.locals.contains_key(&var) {
if check_first_use {
Self::check_first_use(v, first_use)?;
}
vars.push(var);
} else if scope.unscoped.contains(&var) {
vars.push(var);
}
Ok(false)
}
// TODO: key vs value for object binding
Expr::Array { .. } | Expr::Object { .. } => Ok(true),
_ => Ok(false),
})?;
Ok(vars)
}
#[allow(clippy::too_many_arguments)]
fn process_assign_expr(
&mut self,
op: &AssignOp,
lhs: &Ref<Expr>,
rhs: &Ref<Expr>,
scope: &mut Scope,
first_use: &mut BTreeMap<SourceStr, Span>,
definitions: &mut Vec<Definition<SourceStr>>,
mut with_mods_used_vars: Vec<SourceStr>,
mut with_mods_comprs: Vec<Ref<Expr>>,
) -> Result<()> {
let empty_str = lhs.span().source_str().clone_empty();
match (lhs.as_ref(), rhs.as_ref()) {
(
Array {
items: lhs_items, ..
},
Array {
items: rhs_items, ..
},
) => {
if lhs_items.len() != rhs_items.len() {
let span = rhs.span();
bail!(span.error("mismatch in number of array elements"));
}
for (idx, lhs_elem) in lhs_items.iter().enumerate() {
self.process_assign_expr(
op,
lhs_elem,
&rhs_items[idx],
scope,
first_use,
definitions,
with_mods_used_vars.clone(),
with_mods_comprs.clone(),
)?;
}
return Ok(());
}
// TODO: object
_ => {
{
let (mut used_vars, mut comprs) = Self::gather_used_vars_comprs_index_vars(
rhs,
scope,
first_use,
definitions,
&None,
)?;
used_vars.append(&mut with_mods_used_vars.clone());
comprs.append(&mut with_mods_comprs.clone());
self.process_comprs(&comprs[..], scope, first_use, &mut used_vars)?;
let check_first_use = *op == AssignOp::ColEq;
let assigned_vars =
self.gather_assigned_vars(lhs, scope, check_first_use, first_use)?;
for var in &assigned_vars {
let used_vars = used_vars.iter().filter(|v| v != &var).cloned().collect();
definitions.push(Definition {
var: var.clone(),
used_vars,
});
}
if assigned_vars.is_empty() {
definitions.push(Definition {
var: empty_str.clone(),
used_vars: used_vars.clone(),
});
}
}
{
let (mut used_vars, mut comprs) = Self::gather_used_vars_comprs_index_vars(
lhs,
scope,
first_use,
definitions,
&None,
)?;
used_vars.append(&mut with_mods_used_vars);
comprs.append(&mut with_mods_comprs);
let check_first_use = false;
self.process_comprs(&comprs[..], scope, first_use, &mut used_vars)?;
let assigned_vars =
self.gather_assigned_vars(rhs, scope, check_first_use, first_use)?;
for var in &assigned_vars {
let used_vars = used_vars.iter().filter(|v| v != &var).cloned().collect();
definitions.push(Definition {
var: var.clone(),
used_vars,
});
}
if assigned_vars.is_empty() {
definitions.push(Definition {
var: empty_str.clone(),
used_vars: used_vars.clone(),
});
}
}
}
}
Ok(())
}
fn process_expr(
&mut self,
expr: &Ref<Expr>,
scope: &mut Scope,
first_use: &mut BTreeMap<SourceStr, Span>,
definitions: &mut Vec<Definition<SourceStr>>,
mut with_mods_used_vars: Vec<SourceStr>,
mut with_mods_comprs: Vec<Ref<Expr>>,
) -> Result<()> {
match expr.as_ref() {
AssignExpr { op, lhs, rhs, .. } => self.process_assign_expr(
op,
lhs,
rhs,
scope,
first_use,
definitions,
with_mods_used_vars,
with_mods_comprs,
),
_ => {
let (mut used_vars, mut comprs) = Self::gather_used_vars_comprs_index_vars(
expr,
scope,
first_use,
definitions,
&None,
)?;
comprs.append(&mut with_mods_comprs);
used_vars.append(&mut with_mods_used_vars);
self.process_comprs(&comprs[..], scope, first_use, &mut used_vars)?;
definitions.push(Definition {
var: expr.span().source_str().clone_empty(),
used_vars,
});
Ok(())
}
}
}
fn check_first_use(var: &Span, first_use: &BTreeMap<SourceStr, Span>) -> Result<()> {
let name = var.source_str();
if let Some(r#use) = first_use.get(&name) {
if r#use.line < var.line || (r#use.line == var.line && r#use.col < var.col) {
bail!(r#use.error(
format!(
"var `{name}` used before definition below.{}",
var.message("definition", "")
)
.as_str()
));
}
}
Ok(())
}
fn gather_some_vars(
expr: &Ref<Expr>,
scope: &Scope,
_first_use: &BTreeMap<SourceStr, Span>,
vars: &mut Vec<(SourceStr, Span)>,
non_vars: &mut Vec<Ref<Expr>>,
) -> Result<()> {
traverse(expr, &mut |e| match e.as_ref() {
Var { span: v, .. } if v.text() == "input" => {
// Note: We can't modify scope here since it's not mutable,
// but input usage will be tracked elsewhere
Ok(false)
}
Var { span: v, .. } if scope.locals.contains_key(&v.source_str()) => {
vars.push((v.source_str(), v.clone()));
Ok(false)
}
// TODO: Object key/value
Expr::Array { .. } | Expr::Object { .. } => Ok(true),
_ => {
non_vars.push(e.clone());
Ok(false)
}
})
}
fn analyze_query(
&mut self,
key: Option<Ref<Expr>>,
value: Option<Ref<Expr>>,
query: &Ref<Query>,
mut scope: Scope,
) -> Result<()> {
let empty_str = query.span.source_str().clone_empty();
self.gather_local_vars(key, value, query, &mut scope)?;
let mut infos = vec![];
let mut first_use = BTreeMap::new();
for stmt in &query.stmts {
let mut definitions = vec![];
let mut with_mods_used_vars = vec![];
let mut with_mods_comprs = vec![];
for wm in &stmt.with_mods {
let (mut used_vars, mut comprs) = Self::gather_used_vars_comprs_index_vars(
&wm.r#as,
&mut scope,
&mut first_use,
&mut definitions,
&None,
)?;
with_mods_used_vars.append(&mut used_vars);
with_mods_comprs.append(&mut comprs);
}
match &stmt.literal {
Literal::SomeVars { vars, .. } => {
for v in vars {
Self::check_first_use(v, &first_use)?;
}
}
Literal::SomeIn {
key,
value,
collection,
..
} => {
let mut some_vars = vec![];
let mut non_vars = vec![];
if let Some(key) = key {
Self::gather_some_vars(
key,
&scope,
&first_use,
&mut some_vars,
&mut non_vars,
)?;
}
Self::gather_some_vars(
value,
&scope,
&first_use,
&mut some_vars,
&mut non_vars,
)?;
let mut col_definitions = vec![];
let (mut col_used_vars, mut col_comprs) =
Self::gather_used_vars_comprs_index_vars(
collection,
&mut scope,
&mut first_use,
&mut col_definitions,
&None,
)?;
col_used_vars.append(&mut with_mods_used_vars);
col_comprs.append(&mut with_mods_comprs.clone());
definitions.append(&mut col_definitions);
self.process_comprs(
&col_comprs[..],
&mut scope,
&mut first_use,
&mut col_used_vars,
)?;
// Add dependency between some-vars and vars used in collection.
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![];
let (mut uv, mut comprs) = Self::gather_used_vars_comprs_index_vars(
&e,
&mut scope,
&mut first_use,
&mut definitions,
&None,
)?;
uv.append(&mut with_mods_used_vars.clone());
comprs.append(&mut with_mods_comprs.clone());
if !definitions.is_empty() {
bail!("internal error: non empty definitions");
}
used_vars.extend(uv);
self.process_comprs(
&comprs[..],
&mut scope,
&mut first_use,
&mut used_vars,
)?;
}
definitions.push(Definition {
var: empty_str.clone(),
used_vars,
});
// TODO: vars in compr
}
Literal::Expr { expr, .. } => {
let extra_arg = get_extra_arg(
expr,
Some(self.current_module_path.as_str()),
&self.functions,
);
if let Some(ref ea) = extra_arg {
// Gather vars that are being bound
let mut extras_scope = Scope::default();
gather_assigned_vars(ea, false, &self.scopes, &mut extras_scope)?;
for var in &extras_scope.unscoped {
scope.unscoped.insert(var.clone());
}
// Gather vars being used.
let (mut used_vars, mut comprs) = Self::gather_used_vars_comprs_index_vars(
expr,
&mut scope,
&mut first_use,
&mut definitions,
&Some(&extras_scope.unscoped),
)?;
used_vars.append(&mut with_mods_used_vars);
comprs.append(&mut with_mods_comprs);
self.process_comprs(
&comprs[..],
&mut scope,
&mut first_use,
&mut used_vars,
)?;
if !extras_scope.unscoped.is_empty() {
for var in extras_scope.unscoped {
definitions.push(Definition {
var,
used_vars: used_vars.clone(),
});
}
} else {
definitions.push(Definition {
var: empty_str.clone(),
used_vars,
});
}
} else {
self.process_expr(
expr,
&mut scope,
&mut first_use,
&mut definitions,
with_mods_used_vars,
with_mods_comprs,
)?;
}
}
Literal::NotExpr { expr, .. } => {
self.process_expr(
expr,
&mut scope,
&mut first_use,
&mut definitions,
with_mods_used_vars,
with_mods_comprs,
)?;
}
Literal::Every {
key,
value,
domain,
query,
..
} => {
// Create dependencies for vars used in domain.
let (mut uv, mut comprs) = Self::gather_used_vars_comprs_index_vars(
domain,
&mut scope,
&mut first_use,
&mut definitions,
&None,
)?;
uv.append(&mut with_mods_used_vars);
comprs.append(&mut with_mods_comprs);
self.process_comprs(&comprs[..], &mut scope, &mut first_use, &mut uv)?;
definitions.push(Definition {
var: empty_str.clone(),
used_vars: uv,
});
self.scopes.push(scope.clone());
let mut e_scope = Scope::default();
if let Some(key) = key {
e_scope.locals.insert(key.source_str(), key.clone());
}
e_scope.locals.insert(value.source_str(), value.clone());
self.scopes.push(e_scope);
// TODO: mark first use of key, value so that they cannot be := assigned
// within query.
self.analyze_query(None, None, query, Scope::default())?;
// TODO: propagate used vars from query
self.scopes.pop();
self.scopes.pop();
}
}
// If no definitions exist (e.g when only inputs are used), create a definition
// binding the "" var so that these statements get scheduled first.
if definitions.is_empty() {
definitions.push(Definition {
var: empty_str.clone(),
used_vars: vec![],
});
}
infos.push(StmtInfo { definitions });
}
let res = schedule(&mut infos[..], &query.span.source_str().clone_empty());
let order = match res {
Ok(SortResult::Order(ord)) => ord,
Err(err) => {
bail!(query.span.error(&err.to_string()))
}
_ => Vec::new(),
};
let query_schedule = QuerySchedule {
scope: scope.clone(),
order,
};
self.schedule_table
.set_checked(self.current_module_index, query.qidx, query_schedule)
.map_err(|err| anyhow!("schedule_table out of bounds: {err}"))?;
// Propagate input usage to parent scopes
if scope.uses_input && !self.scopes.is_empty() {
if let Some(parent_scope) = self.scopes.last_mut() {
parent_scope.uses_input = true;
}
}
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(alias_span) = import_alias_span(import) {
crate::Rc::make_mut(&mut module_globals).insert(alias_span.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_checked(module_idx as u32, 0, module_globals)
.map_err(|err| anyhow!("module globals out of bounds: {err}"))?;
}
Ok(result)
}
// Extract the binding name an import contributes, even without an explicit `as` clause.
fn import_alias_span(import: &Import) -> Option<Span> {
if let Some(alias) = &import.r#as {
return Some(alias.clone());
}
match import.refr.as_ref() {
RefDot { field, .. } => Some(field.0.clone()),
RefBrack { index, .. } => match index.as_ref() {
Expr::String { span, .. } => Some(span.clone()),
_ => None,
},
Var { span, .. } => Some(span.clone()),
_ => None,
}
}