feat: Add pipe parameter validation and fix VM call frame

Adds a check within the `PipeHook` to ensure the number of input streams
provided to a `pipe` operation matches the number of parameters expected
by the associated lambda function. This prevents runtime errors and
provides a clearer compile-time diagnostic.

Additionally, this commit corrects the order in which a `CallFrame` is
pushed onto the VM's frame stack. By pushing the frame earlier, it
ensures that slow-path `unpack` operations have access to a valid frame,
preventing "No call frame" errors.
This commit is contained in:
2026-03-30 09:43:58 +02:00
parent e82d48d1cb
commit 9b13546609
4 changed files with 85 additions and 10 deletions
+39 -1
View File
@@ -1,4 +1,5 @@
use crate::ast::compiler::call_hooks::RtlCompilerHook;
use crate::ast::compiler::call_hooks::{InferenceAccess, RtlCompilerHook};
use crate::ast::diagnostics::Diagnostics;
use crate::ast::nodes::{Node, NodeKind, TypedNode, TypedPhase};
use crate::ast::rtl::series::{RingBuffer, ScalarValue, SeriesMember};
use crate::ast::types::{NativeFunction, NodeIdentity, PipeFn, SourceLocation, StreamStorage, Value};
@@ -403,6 +404,43 @@ pub struct PipeHook {
}
impl RtlCompilerHook for PipeHook {
fn post_call(
&self,
args: &TypedNode,
ret_ty: StaticType,
_ctx: &dyn InferenceAccess,
diag: &mut Diagnostics,
) -> StaticType {
// Validate: input stream count must match lambda parameter count.
if let NodeKind::Tuple { elements } = &args.kind
&& elements.len() == 2
{
let expected = match &elements[0].ty {
StaticType::Stream(_) | StaticType::Series(_) => 1,
StaticType::Vector(_, count) => *count,
StaticType::Tuple(elems) => elems.len(),
_ => return ret_ty,
};
if let NodeKind::Lambda { params, .. } = &elements[1].kind {
let actual = match &params.kind {
NodeKind::Tuple { elements } => elements.len(),
_ => 1,
};
if actual != expected {
diag.push_error(
format!(
"pipe: lambda expects {} parameter(s) but {} input stream(s) provided",
actual, expected
),
Some(elements[1].identity.clone()),
);
}
}
}
ret_ty
}
fn finalize(
&self,
_callee: Rc<TypedNode>,
+7 -5
View File
@@ -239,6 +239,13 @@ impl VM {
self.stack.clear();
self.frames.clear();
// Push the call frame early so that `unpack` (slow path) can use
// `set_value` / `get_value`, which require a valid call frame.
self.frames.push(CallFrame {
stack_base: 0,
closure: Some(closure_rc.clone()),
});
let closure = closure_rc.as_ref();
if let Some(count) = closure.positional_count
&& args.len() == count as usize
@@ -254,11 +261,6 @@ impl VM {
self.stack.resize(closure.stack_size as usize, Value::Void);
}
self.frames.push(CallFrame {
stack_base: 0,
closure: Some(closure_rc.clone()),
});
let result = self.eval_internal(observer, &closure.exec_node);
self.frames.pop();
self.resolve_tail_calls(observer, result)