use super::types::StatementType;
use super::{Data, Statement, StatementData, Value};
use crate::{Result, error};
use indexmap::{IndexMap, IndexSet};
use snafu::{OptionExt, ensure};
use uuid::Uuid;
const MAX_RECURSION_DEPTH: usize = 100;
pub struct Scope {
root: Statement,
symbol_table: IndexMap<String, Value>,
path_lookup: IndexMap<Uuid, String>,
recursion_depth: usize,
}
impl Scope {
pub fn new(node: &Statement) -> Self {
let mut scope = Self {
root: node.clone(),
symbol_table: IndexMap::new(),
path_lookup: IndexMap::new(),
recursion_depth: 0,
};
Self::build_symbol_table(&mut scope, node, Vec::new());
scope
}
fn build_symbol_table(scope: &mut Scope, node: &Statement, path: Vec<String>) {
let mut new_path = path;
new_path.push(node.id.clone());
match &node.data {
StatementData::Group(children) | StatementData::Labeled(_, children) => {
for child in children.values() {
Self::build_symbol_table(scope, child, new_path.clone());
}
}
StatementData::Single(value) => {
Self::walk_value(scope, value, new_path);
}
}
}
fn walk_value(scope: &mut Scope, node: &Value, path: Vec<String>) {
Self::add_symbol(scope, path.clone(), node);
match &node.data {
Data::Table(contents) => {
for (key, value) in contents {
let mut new_path = path.clone();
new_path.push(key.clone());
Self::walk_value(scope, value, new_path);
}
}
Data::Array(contents) => {
for (index, child) in contents.iter().enumerate() {
let mut array_path = path.clone();
array_path.push(index.to_string());
Self::walk_value(scope, child, array_path);
}
}
_ => {}
}
}
fn add_symbol(scope: &mut Scope, path: Vec<String>, node: &Value) {
let key = path.join(".");
scope.symbol_table.insert(key.clone(), node.clone());
scope.path_lookup.insert(node.uid, key);
}
pub fn apply(&mut self) -> Result<Statement> {
let mut visit_log = IndexSet::new();
let root = self.root.clone();
self.recursion_depth = 0;
self.resolve_statement(&root, &mut visit_log)
}
fn resolve_path(&self, current: &Value, input: String) -> Result<String> {
let operating_path: Vec<String> = if input.starts_with("self") || input.starts_with("super")
{
let current_path = self
.path_lookup
.get(¤t.uid)
.context(error::NoMacroSnafu {
location: current.meta.location.clone(),
path: "unknown".to_string(),
})?;
if input.starts_with("super") {
let mut current_segments: Vec<&str> = current_path.split('.').collect();
let new_segments: Vec<&str> = input.split('.').collect();
current_segments.pop();
for segment in new_segments.iter().skip(1) {
if *segment == "super" {
current_segments.pop();
} else {
current_segments.push(segment);
}
}
current_segments.iter().map(|x| x.to_string()).collect()
} else {
let replaced = input.replace("self", current_path);
replaced.split('.').map(|x| x.to_string()).collect()
}
} else {
input.split('.').map(|x| x.to_string()).collect()
};
let mut final_path: Vec<String> = Vec::new();
for entry in operating_path {
match entry.as_str() {
"this" | "self" => continue, "super" => {
final_path.pop(); }
_ => {
final_path.push(entry);
}
}
}
Ok(final_path.join("."))
}
fn resolve_macro(
&mut self,
at: &Value,
input: String,
visit_log: &mut IndexSet<Uuid>,
) -> Result<Value> {
ensure!(
self.recursion_depth < MAX_RECURSION_DEPTH,
error::RecursionLimitSnafu {
location: at.meta.location.clone(),
limit: MAX_RECURSION_DEPTH,
}
);
self.recursion_depth += 1;
let result = self.resolve_macro_internal(at, input, visit_log);
self.recursion_depth -= 1;
result
}
fn resolve_macro_internal(
&mut self,
at: &Value,
input: String,
visit_log: &mut IndexSet<Uuid>,
) -> Result<Value> {
let path = self.resolve_path(at, input.clone())?;
if let Some(data) = self.symbol_table.get(&path) {
let mut resolved_value = Value {
uid: at.uid,
data: data.data.clone(),
meta: at.meta.clone(),
};
if matches!(resolved_value.data, Data::Macro(_)) {
resolved_value = self.resolve_value(&resolved_value, visit_log)?;
}
Ok(resolved_value)
} else {
self.resolve_macro_string(at, input, visit_log)
}
}
fn resolve_macro_string(
&mut self,
at: &Value,
input: String,
visit_log: &mut IndexSet<Uuid>,
) -> Result<Value> {
if !input.contains('{') {
return error::NoMacroSnafu {
location: at.meta.location.clone(),
path: input,
}
.fail();
}
let mut result = String::new();
let chars = input.chars().peekable();
let mut brace_depth = 0;
let mut current_macro = String::new();
for ch in chars {
match ch {
'{' if brace_depth == 0 => {
brace_depth = 1;
current_macro.clear();
}
'{' if brace_depth > 0 => {
brace_depth += 1;
current_macro.push(ch);
}
'}' if brace_depth == 1 => {
let path = self.resolve_path(at, current_macro.clone())?;
let resolved_value =
self.symbol_table.get(&path).context(error::NoMacroSnafu {
location: at.meta.location.clone(),
path: current_macro.clone(),
})?;
let mut final_value = resolved_value.clone();
if matches!(final_value.data, Data::Macro(_)) {
final_value = self.resolve_value(&final_value, visit_log)?;
}
result.push_str(&final_value.to_macro_string());
brace_depth = 0;
current_macro.clear();
}
'}' if brace_depth > 1 => {
brace_depth -= 1;
current_macro.push(ch);
}
_ if brace_depth > 0 => {
current_macro.push(ch);
}
_ => {
result.push(ch);
}
}
}
if brace_depth > 0 {
return error::NoMacroSnafu {
location: at.meta.location.clone(),
path: format!("Unclosed macro reference: {{{}", current_macro),
}
.fail();
}
Ok(Value {
uid: at.uid,
data: Data::String(result),
meta: at.meta.clone(),
})
}
fn resolve_statement(
&mut self,
at: &Statement,
visit_log: &mut IndexSet<Uuid>,
) -> Result<Statement> {
let uid = at.uid;
ensure!(
!visit_log.contains(&uid),
error::LoopSnafu {
location: at.meta.location.clone()
}
);
let result = match &at.type_ {
StatementType::Module(_) => {
let mut new_children = IndexMap::new();
for (key, value) in at.get_grouped().unwrap() {
new_children.insert(key.clone(), self.resolve_statement(value, visit_log)?);
}
Statement::new_module(&at.id, new_children, at.meta.clone())
}
StatementType::Section(_) => {
let mut new_children = IndexMap::new();
for (key, value) in at.get_grouped().unwrap() {
new_children.insert(key.clone(), self.resolve_statement(value, visit_log)?);
}
Statement::new_section(&at.id, new_children, at.meta.clone())
}
StatementType::Block { .. } => {
let mut new_children = IndexMap::new();
let mut new_labels = Vec::new();
let (labels, children) = at.get_labeled().unwrap();
for label in labels {
new_labels.push(self.resolve_value(label, visit_log)?);
}
for (key, value) in children.iter() {
new_children.insert(key.clone(), self.resolve_statement(value, visit_log)?);
}
Statement::new_block(&at.id, new_labels, new_children, at.meta.clone())
}
StatementType::Control(expected) => {
let new_value = self.resolve_value(at.get_value().unwrap(), visit_log)?;
ensure!(
expected.can_assign(&new_value.type_of()),
error::ImplicitConvertSnafu {
left: expected.clone(),
right: new_value.type_of()
}
);
Statement::new_control(&at.id, Some(expected.clone()), new_value, at.meta.clone())?
}
StatementType::Assignment(expected) => {
let is_macro = at.get_value().unwrap().as_macro().is_some();
let new_value = self.resolve_value(at.get_value().unwrap(), visit_log)?;
let expected = if is_macro {
new_value.type_of()
} else {
expected.clone()
};
ensure!(
expected.can_assign(&new_value.type_of()),
error::ImplicitConvertSnafu {
left: expected.clone(),
right: new_value.type_of()
}
);
Statement::new_assign(&at.id, Some(expected.clone()), new_value, at.meta.clone())?
}
};
visit_log.insert(uid);
Ok(result)
}
fn resolve_value(&mut self, at: &Value, visit_log: &mut IndexSet<Uuid>) -> Result<Value> {
let uid = at.uid;
ensure!(
!visit_log.contains(&uid),
error::LoopSnafu {
location: at.meta.location.clone()
}
);
let result = match &at.data {
Data::Macro(value) => self.resolve_macro(at, value.clone(), visit_log)?,
Data::Table(children) => {
let mut new_children = IndexMap::new();
for (key, value) in children.iter() {
new_children.insert(key.clone(), self.resolve_value(value, visit_log)?);
}
Value {
uid,
data: Data::Table(new_children),
meta: at.meta.clone(),
}
}
Data::Array(children) => {
let mut new_children = Vec::new();
for value in children.iter() {
new_children.push(self.resolve_value(value, visit_log)?);
}
Value {
uid,
data: Data::Array(new_children),
meta: at.meta.clone(),
}
}
_ => at.clone(),
};
visit_log.insert(uid);
Ok(result)
}
pub fn symbol_table(&self) -> &IndexMap<String, Value> {
&self.symbol_table
}
pub fn path_lookup(&self) -> &IndexMap<Uuid, String> {
&self.path_lookup
}
pub fn lookup(&self, path: &str) -> Option<&Value> {
self.symbol_table.get(path)
}
pub fn available_paths(&self) -> Vec<&String> {
self.symbol_table.keys().collect()
}
pub fn validate_macros(&self) -> Result<()> {
for (_path, value) in &self.symbol_table {
if let Data::Macro(macro_ref) = &value.data {
let resolved_path = self.resolve_path(value, macro_ref.clone())?;
if !self.symbol_table.contains_key(&resolved_path) {
return error::NoMacroSnafu {
location: value.meta.location.clone(),
path: macro_ref.clone(),
}
.fail();
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::types::{Location, Metadata};
#[test]
fn test_scope_creation() {
let meta = Metadata::new(Location::new(0, 0));
let mut children = IndexMap::new();
let value = Value::new_string("test".to_string(), meta.clone());
let stmt = Statement::new_assign("test_var", None, value, meta.clone()).unwrap();
children.insert("test_var".to_string(), stmt);
let module = Statement::new_module("root", children, meta);
let scope = Scope::new(&module);
assert!(!scope.symbol_table.is_empty());
assert!(scope.lookup("root.test_var").is_some());
}
#[test]
fn test_macro_resolution() {
let meta = Metadata::new(Location::new(0, 0));
let mut children = IndexMap::new();
let target_value = Value::new_string("hello".to_string(), meta.clone());
let target_stmt =
Statement::new_assign("target", None, target_value, meta.clone()).unwrap();
children.insert("target".to_string(), target_stmt);
let macro_value = Value::new_macro("root.target".to_string(), meta.clone());
let macro_stmt =
Statement::new_assign("macro_ref", None, macro_value, meta.clone()).unwrap();
children.insert("macro_ref".to_string(), macro_stmt);
let module = Statement::new_module("root", children, meta);
let mut scope = Scope::new(&module);
let resolved = scope.apply().unwrap();
let resolved_macro = resolved.find_by_path("macro_ref").unwrap();
if let Some(resolved_value) = resolved_macro.get_value() {
assert_eq!(resolved_value.as_string(), Some(&"hello".to_string()));
}
}
}