Refactor tail call resolution logic

The tail call resolution logic was duplicated in `Environment::call` and
`VM::run`. This commit extracts the tail call resolution logic into a
single method `VM::resolve_tail_calls` and uses it in both places.

Additionally, this commit adds support for series indexing as a form of
tail call, allowing for direct access to series elements through the
`series(index)` syntax. This is useful for back-referencing in
time-series data.

A new example `err.myc` is added to demonstrate basic series usage and
error handling.
This commit is contained in:
Michael Schimmel
2026-03-06 12:35:00 +01:00
parent b9d88f019e
commit 13dc6beb52
4 changed files with 52 additions and 41 deletions
+6
View File
@@ -0,0 +1,6 @@
;; Output: 0
(do
(def history (series :float))
(push history 0.0)
(history 0)
)
+1
View File
@@ -10,6 +10,7 @@
(def sum (+ val (history 0)))
(push history sum)
(/ sum length)
(history 0)
))
)))
+8 -19
View File
@@ -501,7 +501,10 @@ impl Environment {
Ok(v) => v,
Err(e) => panic!("Myc Runtime Error: {}", e),
};
vm.resolve_tail_calls(res)
match vm.resolve_tail_calls(&mut crate::ast::vm::NoOpObserver, Ok(res)) {
Ok(v) => v,
Err(e) => panic!("Myc Runtime Error: {}", e),
}
}),
});
}
@@ -528,7 +531,10 @@ impl Environment {
Err(e) => panic!("Myc Runtime Error (Closure): {}", e),
};
}
vm.resolve_tail_calls(final_res)
match vm.resolve_tail_calls(&mut crate::ast::vm::NoOpObserver, Ok(final_res)) {
Ok(v) => v,
Err(e) => panic!("Myc Runtime Error: {}", e),
}
}),
})
}
@@ -645,23 +651,6 @@ impl Environment {
result = vm.run_with_observer(&mut observer, &closure.exec_node);
}
while let Ok(Value::TailCallRequest(payload)) = result {
let (next_obj, next_args) = *payload;
if next_obj
.as_any()
.downcast_ref::<crate::ast::vm::Closure>()
.is_some()
{
result = vm.run_with_args_observed(&mut observer, next_obj, &next_args);
} else {
result = Err(format!(
"Tail call target is not a closure: {}",
next_obj.type_name()
));
break;
}
}
self.run_pipeline();
Ok((result, observer.logs))
+37 -22
View File
@@ -144,14 +144,23 @@ impl VM {
stack_base: 0,
closure: None,
});
let mut result = self.eval(root);
let result = self.eval(root);
self.frames.pop();
self.resolve_tail_calls(&mut NoOpObserver, result)
}
pub fn resolve_tail_calls<O: VMObserver>(
&mut self,
observer: &mut O,
mut result: Result<Value, String>,
) -> Result<Value, String> {
loop {
match result {
Ok(Value::TailCallRequest(payload)) => {
let (next_obj, next_args) = *payload;
if let Some(closure) = next_obj.as_any().downcast_ref::<Closure>() {
self.stack.clear();
// frames should be empty here since we popped before entering the loop
self.frames.push(CallFrame {
stack_base: 0,
closure: Some(next_obj.clone()),
@@ -163,11 +172,35 @@ impl VM {
} else {
self.unpack(&closure.parameter_node, &next_args, &mut 0)?;
}
result = self.eval(&closure.exec_node);
result = self.eval_observed(observer, &closure.exec_node);
self.frames.pop();
} else if let Some(series) = next_obj.as_series() {
if next_args.len() != 1 {
return Err(format!(
"{} indexer expects exactly 1 argument (the lookback index), got {}",
next_obj.type_name(),
next_args.len()
));
}
if let Value::Int(idx) = &next_args[0] {
if *idx < 0 {
return Err(format!(
"{} lookback index cannot be negative: {}",
next_obj.type_name(),
idx
));
}
result = Ok(series.get_item(*idx as usize).unwrap_or(Value::Void));
} else {
return Err(format!(
"Tail call target is not a closure: {}",
"{} index must be an integer, got {}",
next_obj.type_name(),
next_args[0]
));
}
} else {
return Err(format!(
"Tail call target is not callable: {}",
next_obj.type_name()
));
}
@@ -235,7 +268,7 @@ impl VM {
});
let result = self.eval_observed(observer, root);
self.frames.pop();
result
self.resolve_tail_calls(observer, result)
}
#[inline(always)]
@@ -745,24 +778,6 @@ impl VM {
}
}
pub fn resolve_tail_calls(&mut self, mut result: Value) -> Value {
while let Value::TailCallRequest(payload) = result {
let (next_obj, next_args) = *payload;
if next_obj.as_any().downcast_ref::<Closure>().is_some() {
result = match self.run_with_args(next_obj, &next_args) {
Ok(v) => v,
Err(e) => panic!("Myc Runtime Error (TailCall): {}", e),
};
} else {
panic!(
"Tail call target is not a closure: {}",
next_obj.type_name()
);
}
}
result
}
fn eval_args_to_stack<O: VMObserver>(
&mut self,
obs: &mut O,