From 807903efbb69f1a349132264b8c0cb05444c36fc Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Sun, 1 Mar 2026 23:19:00 +0100 Subject: [PATCH] Add Optional type for filter pipes Introduces `StaticType::Optional` to represent values that can be either of a specific type `T` or `Void`. This is crucial for handling situations in filter pipes where an expression might not always produce a value. The type checker now correctly deduces and propagates this `Optional` type, and the pipeline operator (`pipe`) is updated to unwrap `Optional(T)` results, yielding `T`. This ensures that the type system accurately reflects the potential absence of values in intermediate pipeline steps. Also includes a minor rename in the tuple-struct example from `pipe` to `p` for clarity, and registers the `streams` module. --- examples/tuple-struct.myc | 4 ++-- src/ast/compiler/type_checker.rs | 11 ++++++++--- src/ast/rtl/mod.rs | 1 + src/ast/types.rs | 8 +++++++- src/integration_test.rs | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/examples/tuple-struct.myc b/examples/tuple-struct.myc index fefe36b..e204290 100644 --- a/examples/tuple-struct.myc +++ b/examples/tuple-struct.myc @@ -1,7 +1,7 @@ ;; Benchmark: 890ns ;; Benchmark-Repeat: 2258 (do - (def pipe (fn [conf] + (def p (fn [conf] (do (def [str s] conf) (def [f ss] s) @@ -10,5 +10,5 @@ ) ) - (pipe ["btc" [:close "cls"]]) + (p ["btc" [:close "cls"]]) ) \ No newline at end of file diff --git a/src/ast/compiler/type_checker.rs b/src/ast/compiler/type_checker.rs index 86e1d9e..a70ae38 100644 --- a/src/ast/compiler/type_checker.rs +++ b/src/ast/compiler/type_checker.rs @@ -373,8 +373,8 @@ impl TypeChecker { } else_typed = Some(Box::new(et)); } else { - // If without else returns Void or Optional(Then) - final_ty = StaticType::Any; // Delphi: MakeOptional(Then) + // If without else returns Optional(Then) (T | Void) + final_ty = StaticType::Optional(Box::new(then_typed.ty.clone())); } ( @@ -394,7 +394,12 @@ impl TypeChecker { } let typed_lambda = self.check_node(*lambda, ctx)?; let ret_ty = if let StaticType::Function(sig) = &typed_lambda.ty { - sig.ret.clone() + // If the lambda returns an Optional(T), the pipeline filters Void and stores T! + if let StaticType::Optional(inner) = &sig.ret { + *inner.clone() + } else { + sig.ret.clone() + } } else { StaticType::Any }; diff --git a/src/ast/rtl/mod.rs b/src/ast/rtl/mod.rs index c5a300b..0f7535f 100644 --- a/src/ast/rtl/mod.rs +++ b/src/ast/rtl/mod.rs @@ -13,4 +13,5 @@ pub fn register(env: &Environment) { datetime::register(env); math::register(env); series::register(env); + streams::register(env); } \ No newline at end of file diff --git a/src/ast/types.rs b/src/ast/types.rs index 4eb49ce..ade336c 100644 --- a/src/ast/types.rs +++ b/src/ast/types.rs @@ -295,6 +295,7 @@ pub enum StaticType { DateTime, Text, Keyword, + Optional(Box), // Represents T | Void (e.g. for filter pipes) List(Box), // Legacy / Dynamic list Series(Box), // Time series of a specific type Tuple(Vec), // Heterogeneous fixed-size @@ -318,7 +319,8 @@ impl fmt::Display for StaticType { StaticType::DateTime => write!(f, "datetime"), StaticType::Text => write!(f, "text"), StaticType::Keyword => write!(f, "keyword"), - StaticType::List(inner) => write!(f, "[{}]", inner), + StaticType::Optional(inner) => write!(f, "optional({})", inner), + StaticType::List(inner) => write!(f, "list({})", inner), StaticType::Series(inner) => write!(f, "series<{}>", inner), StaticType::Tuple(elements) => { write!(f, "[")?; @@ -371,6 +373,10 @@ impl StaticType { } match (self, other) { + // Optional(T) is assignable from T or Void + (StaticType::Optional(inner), other) => { + matches!(other, StaticType::Void) || inner.is_assignable_from(other) + } // A Vector is a Tuple (StaticType::Tuple(elements), StaticType::Vector(inner, len)) => { if elements.len() != *len { diff --git a/src/integration_test.rs b/src/integration_test.rs index 918b5ef..222a6cf 100644 --- a/src/integration_test.rs +++ b/src/integration_test.rs @@ -360,6 +360,38 @@ mod tests { } } + #[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 crate::ast::types::Value::Object(obj) = val { + assert_eq!(obj.type_name(), "PipelineNode"); + } else { + panic!("Expected an Object(PipelineNode)"); + } + } + #[test] fn test_multi_level_destructuring() { let env = Environment::new();