use microcad_lang_base::{Identifier, SrcReferrer};
use crate::{
eval::*,
lower::ir,
model::*,
symbol::{Symbol, SymbolDef},
};
#[derive(Default)]
pub struct Stack(Vec<StackFrame>);
impl Stack {
pub fn put_local(&mut self, id: Option<Identifier>, symbol: Symbol) -> EvalResult<()> {
let id = if let Some(id) = id { id } else { symbol.id() };
let name = symbol.full_name();
for (pos, frame) in self.0.iter_mut().rev().enumerate() {
match frame {
StackFrame::Source(_, locals)
| StackFrame::Workbench(_, _, locals)
| StackFrame::Init(locals)
| StackFrame::Body(locals)
| StackFrame::Function(_, locals) => {
let op = if locals.insert(id.clone(), symbol).is_some() {
"Added"
} else {
"Set"
};
if name.is_qualified() {
log::debug!("{op} {name:?} as {id:?} to local stack");
} else {
log::debug!("{op} {id:?} to local stack");
}
log::trace!("Local Stack:\n{self:?}");
return Ok(());
}
StackFrame::Call {
symbol: _,
args: _,
src_ref: _,
} => {
if pos > 0 {
return Err(EvalError::WrongStackFrame(id, "call").into());
}
}
}
}
Err(EvalError::LocalStackEmpty(id).into())
}
fn current_workbench_id(&self) -> Option<&Identifier> {
self.0.iter().rev().find_map(|frame| {
if let StackFrame::Workbench(_, id, _) = frame {
Some(id)
} else {
None
}
})
}
pub fn current_module_name(&self) -> ir::QualifiedName {
if self.0.is_empty() {
ir::QualifiedName::default()
} else {
let mut module_name = ir::QualifiedName::default();
for (n, frame) in self.0.iter().rev().enumerate() {
match frame {
StackFrame::Source(id, ..) => {
module_name.insert(0, id.clone());
}
StackFrame::Call { symbol, .. } => {
if n > 0 {
module_name =
symbol.full_name().remove_last().with_prefix(&module_name);
break;
}
}
_ => (),
}
}
module_name
}
}
pub fn current_call_name(&self) -> Option<ir::QualifiedName> {
self.0
.iter()
.rev()
.enumerate()
.take_while(|(n, frame)| {
if let StackFrame::Call { symbol, .. } = frame {
if *n > 0 {
let parent = symbol.get_parent().expect("call from nowhere");
if parent.is_module() {
return false;
}
}
};
true
})
.find_map(|(n, frame)| match frame {
StackFrame::Workbench(_, id, _) | StackFrame::Function(id, _) => Some(
ir::QualifiedName::new(vec![id.clone()], id.src_ref())
.with_prefix(&self.current_module_name()),
),
StackFrame::Call { symbol, .. } => {
if n > 0 {
let parent = symbol.get_parent().expect("call from nowhere");
if parent.is_workbench() {
Some(parent.full_name())
} else {
unreachable!("call must com from either module or workbench")
}
} else {
None
}
}
_ => None,
})
}
pub fn current_frame(&self) -> Option<&StackFrame> {
self.0.last()
}
pub fn pretty_print_call_trace(
&self,
f: &mut dyn std::fmt::Write,
source_by_hash: &impl super::GetSourceByHash,
) -> std::fmt::Result {
let mut none: bool = true;
for (idx, frame) in self
.0
.iter()
.filter(|frame| {
matches!(
frame,
StackFrame::Call {
symbol: _,
args: _,
src_ref: _
}
)
})
.enumerate()
{
none = false;
frame.print_stack(f, source_by_hash, idx)?;
}
if none {
writeln!(f, "EMPTY STACK")?
}
Ok(())
}
pub(crate) fn current_symbol(&self) -> Option<Symbol> {
self.0.iter().rev().find_map(|frame| frame.symbol())
}
}
impl Lookup<Box<EvalError>> for Stack {
fn lookup(&self, name: &ir::QualifiedName, _: LookupTarget) -> EvalResult<Symbol> {
use crate::lower::SingleIdentifier;
log::trace!(
"{lookup} for local symbol '{name:?}'",
lookup = microcad_lang_base::mark!(LOOKUP)
);
let symbol = if let Some(id) = name.single_identifier() {
self.fetch_symbol(id)
} else {
let (id, _) = name.split_first();
let local = match self.fetch_symbol(&id) {
Ok(local) => local,
Err(err) => {
log::trace!(
"{not_found} local symbol: {name:?}",
not_found = microcad_lang_base::mark!(NOT_FOUND),
);
return Err(err);
}
};
return Ok(local);
};
match symbol {
Ok(symbol) => {
log::trace!(
"{found} local symbol: {symbol:?}",
found = microcad_lang_base::mark!(FOUND),
);
Ok(symbol)
}
Err(err) => {
log::trace!(
"{not_found} local symbol: {name:?}",
not_found = microcad_lang_base::mark!(NOT_FOUND),
);
Err(err)
}
}
}
fn ambiguity_error(ambiguous: ir::QualifiedName, others: ir::QualifiedNames) -> Box<EvalError> {
EvalError::AmbiguousSymbol(ambiguous, others).into()
}
}
impl Locals for Stack {
fn open(&mut self, frame: StackFrame) {
if let Some(id) = frame.id() {
log::trace!("Opening {} stack frame '{id}'", frame.kind_str());
} else {
log::trace!("Opening {} stack frame", frame.kind_str());
}
self.0.push(frame);
}
fn close(&mut self) -> StackFrame {
log::trace!("Stack before closing:\n{self:?}");
let frame = self.0.pop().expect("stack underflow");
log::trace!("Closing {} stack frame", frame.kind_str());
frame
}
fn set_local_value(&mut self, id: Identifier, value: Value) -> EvalResult<()> {
self.put_local(
Some(id.clone()),
Symbol::new(SymbolDef::Value(id, value), None),
)
}
fn get_local_value(&self, id: &Identifier) -> EvalResult<Value> {
match self.fetch_symbol(id) {
Ok(symbol) => symbol.with_def(|def| match def {
SymbolDef::Value(.., value) => Ok(value.clone()),
_ => Err(EvalError::LocalNotFound(id.clone()).into()),
}),
Err(_) => Err(EvalError::LocalNotFound(id.clone()).into()),
}
}
fn get_model(&self) -> EvalResult<Model> {
match self
.0
.iter()
.rev()
.take_while(|frame| !matches!(frame, StackFrame::Call { .. }))
.find(|frame| matches!(frame, StackFrame::Workbench(_, _, _)))
{
Some(StackFrame::Workbench(model, _, _)) => Ok(model.clone()),
_ => Err(EvalError::NoModelInWorkbench.into()),
}
}
fn fetch_symbol(&self, id: &Identifier) -> EvalResult<Symbol> {
for (n, frame) in self.0.iter().rev().enumerate() {
match frame {
StackFrame::Source(_, locals)
| StackFrame::Body(locals)
| StackFrame::Workbench(_, _, locals)
| StackFrame::Init(locals)
| StackFrame::Function(_, locals) => {
if let Some(local) = locals.get(id) {
log::trace!("fetched {id:?} from locals");
return Ok(local.clone());
}
}
StackFrame::Call {
symbol: _,
args: _,
src_ref: _,
} => {
if n > 0 {
break;
}
}
}
}
Err(EvalError::LocalNotFound(id.clone()).into())
}
fn current_name(&self) -> ir::QualifiedName {
if let Some(id) = self.current_workbench_id() {
let name = ir::QualifiedName::new(vec![id.clone()], id.src_ref());
name.with_prefix(&self.current_module_name())
} else {
self.current_module_name()
}
}
}
impl std::fmt::Debug for Stack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.0.is_empty() {
writeln!(f, "EMPTY STACK")
} else {
for (n, locals) in self.0.iter().enumerate() {
locals.print_locals(f, n, 0)?;
}
Ok(())
}
}
}
#[test]
#[allow(clippy::unwrap_used)]
fn local_stack() {
use crate::symbol::SymbolMap;
let mut stack = Stack::default();
let make_int = |id, value| Symbol::new(SymbolDef::Value(id, Value::Integer(value)), None);
let fetch_int = |stack: &Stack, id: &str| -> Option<i64> {
match stack.fetch_symbol(&id.into()) {
Ok(node) => node.with_def(|def| match def {
SymbolDef::Value(.., Value::Integer(value)) => Some(*value),
_ => todo!("error"),
}),
_ => None,
}
};
let root_name: Identifier = "test".into();
let root_id = ir::QualifiedName::from(root_name);
stack.open(StackFrame::Source("test".into(), SymbolMap::default()));
assert!(stack.current_module_name() == root_id);
assert!(stack.put_local(None, make_int("a".into(), 1)).is_ok());
println!("{stack:?}");
assert!(fetch_int(&stack, "a").unwrap() == 1);
assert!(fetch_int(&stack, "b").is_none());
assert!(fetch_int(&stack, "c").is_none());
stack.open(StackFrame::Body(SymbolMap::default()));
assert!(stack.current_module_name() == root_id);
assert!(fetch_int(&stack, "a").unwrap() == 1);
assert!(fetch_int(&stack, "b").is_none());
assert!(fetch_int(&stack, "c").is_none());
assert!(stack.put_local(None, make_int("b".into(), 2)).is_ok());
assert!(fetch_int(&stack, "a").unwrap() == 1);
assert!(fetch_int(&stack, "b").unwrap() == 2);
assert!(fetch_int(&stack, "c").is_none());
assert!(
stack
.put_local(Some("x".into()), make_int("x".into(), 3))
.is_ok()
);
assert!(fetch_int(&stack, "a").unwrap() == 1);
assert!(fetch_int(&stack, "b").unwrap() == 2);
assert!(fetch_int(&stack, "x").unwrap() == 3);
stack.close();
assert!(stack.current_module_name() == root_id);
assert!(fetch_int(&stack, "a").unwrap() == 1);
assert!(fetch_int(&stack, "b").is_none());
assert!(fetch_int(&stack, "c").is_none());
stack.close();
assert!(stack.current_module_name().is_empty());
}