9b13546609
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.
148 lines
5.3 KiB
Rust
148 lines
5.3 KiB
Rust
use myc::ast::environment::Environment;
|
|
use myc::ast::types::Value;
|
|
|
|
/// Verifies that bidirectional type inference correctly propagates stream inner types
|
|
/// into lambda parameters for `pipe` calls with a single stream in a vector `[src]`.
|
|
/// The lambda parameter should be inferred as the stream's Record type, not `Any`.
|
|
#[test]
|
|
fn test_pipe_infers_lambda_param_from_vector_input() {
|
|
let env = Environment::new();
|
|
let source = "(do
|
|
(def src (create-random-ohlc 42 5))
|
|
(pipe [src] (fn [tick] (.close tick)))
|
|
)";
|
|
let dump = env.dump_ast(source).expect("dump_ast failed");
|
|
|
|
// The lambda parameter `tick` should be inferred as the OHLC Record type,
|
|
// NOT as `Any`. This validates the Vector → Stream<T> → T hint resolution.
|
|
assert!(
|
|
!dump.contains("params: Tuple([Any])"),
|
|
"Lambda params should NOT be Any — bidirectional inference failed.\nDump:\n{dump}"
|
|
);
|
|
// The pipe call should return Stream(Float), not Stream(Any)
|
|
assert!(
|
|
dump.contains("Stream(Float)"),
|
|
"Pipe should produce Stream(Float), not Stream(Any).\nDump:\n{dump}"
|
|
);
|
|
|
|
// Also verify it runs correctly
|
|
let res = env.run_script(source);
|
|
assert!(res.is_ok(), "Script failed: {:?}", res.err());
|
|
}
|
|
|
|
/// Verifies bidirectional inference for pipe with multiple stream inputs in a tuple.
|
|
#[test]
|
|
fn test_pipe_infers_lambda_params_from_tuple_inputs() {
|
|
let env = Environment::new();
|
|
let source = "(do
|
|
(def src1 (create-random-ohlc 42 5))
|
|
(def src2 (create-random-ohlc 99 5))
|
|
(pipe [src1 src2] (fn [t1 t2] (+ (.close t1) (.close t2))))
|
|
)";
|
|
let res = env.run_script(source);
|
|
assert!(res.is_ok(), "Script failed: {:?}", res.err());
|
|
}
|
|
|
|
/// Verifies that PipeHook::finalize replaces the `pipe` identifier with a
|
|
/// pre-configured factory Constant — mirroring the SeriesHook pattern.
|
|
/// FAILS before PipeHook is implemented (callee is still `Identifier: pipe`).
|
|
#[test]
|
|
fn test_pipe_callee_is_factory_after_finalize() {
|
|
let env = Environment::new();
|
|
let dump = env.dump_ast("(do
|
|
(def ohlc (create-random-ohlc 42 5))
|
|
(pipe ohlc (fn [bar] (.close bar)))
|
|
)").expect("dump_ast failed");
|
|
|
|
assert!(
|
|
!dump.contains("Identifier: pipe"),
|
|
"PipeHook::finalize should replace `pipe` with a pre-configured factory Constant.\nDump:\n{dump}"
|
|
);
|
|
}
|
|
|
|
/// Field accessor on unknown field in pipe lambda must be a compile-time error.
|
|
/// The lambda param is inferred as the concrete Record type (not Any),
|
|
/// so the compiler can validate field names.
|
|
#[test]
|
|
fn test_pipe_field_typo_is_compile_error() {
|
|
let env = Environment::new();
|
|
let valid = "(do (def ohlc (create-random-ohlc 42 5)) (pipe ohlc (fn [bar] (.close bar))))";
|
|
assert!(env.run_script(valid).is_ok(), "Valid pipe should compile");
|
|
|
|
let typo = "(do (def ohlc (create-random-ohlc 42 5)) (pipe ohlc (fn [bar] (.nonexistent bar))))";
|
|
assert!(
|
|
env.run_script(typo).is_err(),
|
|
"Field typo in pipe lambda must be a compile error"
|
|
);
|
|
}
|
|
|
|
/// A chained pipe (output feeds another pipe) must preserve concrete Stream types.
|
|
#[test]
|
|
fn test_pipe_chain_preserves_concrete_types() {
|
|
let env = Environment::new();
|
|
let source = "(do
|
|
(def ohlc (create-random-ohlc 42 5))
|
|
(def closes (pipe ohlc (fn [bar] (.close bar))))
|
|
(pipe closes (fn [c] (* c 2.0)))
|
|
)";
|
|
let dump = env.dump_ast(source).expect("dump_ast failed");
|
|
assert!(
|
|
dump.contains("Stream(Float)"),
|
|
"Both pipe stages should yield Stream(Float).\nDump:\n{dump}"
|
|
);
|
|
assert!(env.run_script(source).is_ok(), "Chained pipe failed at runtime");
|
|
}
|
|
|
|
/// Pipe with 2 input streams but lambda expecting 1 parameter must be a compile error.
|
|
/// Regression: previously crashed at runtime with "No call frame" instead of a
|
|
/// clean compile-time diagnostic.
|
|
#[test]
|
|
fn test_pipe_param_count_mismatch_is_compile_error() {
|
|
let env = Environment::new();
|
|
let source = "(do
|
|
(def src (create-random-ohlc 42 5))
|
|
(pipe [src src] (fn [bar] (.close bar)))
|
|
)";
|
|
let res = env.run_script(source);
|
|
assert!(res.is_err(), "Pipe with 2 inputs but 1-param lambda must be a compile error");
|
|
|
|
let err = res.unwrap_err();
|
|
assert!(
|
|
err.contains("pipe") && err.contains("parameter"),
|
|
"Error should mention parameter mismatch, got: {err}"
|
|
);
|
|
}
|
|
|
|
/// Verifies that pipe with Optional return (filter pattern) still produces Stream(Float).
|
|
#[test]
|
|
fn test_pipeline_optional_type() {
|
|
let env = Environment::new();
|
|
// The lambda uses an `if` without an `else` returning a float constant.
|
|
// The TypeChecker should deduce `Optional(Float)` for the lambda body,
|
|
// and correctly unwrap it to `Series(Float)` for the pipeline output.
|
|
let source = "(do
|
|
(def src (create-random-ohlc 42 10))
|
|
(def filtered
|
|
(pipe [src]
|
|
(fn [tick]
|
|
(if true
|
|
42.0
|
|
)
|
|
)
|
|
)
|
|
)
|
|
filtered
|
|
)";
|
|
let res = env.run_script(source);
|
|
if let Err(e) = &res {
|
|
panic!("Script failed to compile/run: {:?}", e);
|
|
}
|
|
|
|
let val = res.unwrap();
|
|
if let Value::Stream(s) = val {
|
|
assert_eq!(s.stream_type_name(), "stream");
|
|
} else {
|
|
panic!("Expected a Stream(StreamNode)");
|
|
}
|
|
}
|