10a7fcb576
The `Object` trait is too generic and has been causing confusion. This commit introduces `StreamStorage` as a dedicated trait for reactive stream types. This change involves: - Renaming `Object` to `StreamStorage` in relevant places. - Updating the `Value::Object` enum variant to `Value::Stream`. - Modifying how streams are handled in the compiler, VM, and tests to use the new `StreamStorage` trait. - Adjusting example scripts and tests to reflect the change in type representation. - The `StreamNode` struct now explicitly holds its `element_type`.
128 lines
4.6 KiB
Rust
128 lines
4.6 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");
|
|
}
|
|
|
|
/// 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)");
|
|
}
|
|
}
|