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.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
;; Benchmark: 890ns
|
;; Benchmark: 890ns
|
||||||
;; Benchmark-Repeat: 2258
|
;; Benchmark-Repeat: 2258
|
||||||
(do
|
(do
|
||||||
(def pipe (fn [conf]
|
(def p (fn [conf]
|
||||||
(do
|
(do
|
||||||
(def [str s] conf)
|
(def [str s] conf)
|
||||||
(def [f ss] s)
|
(def [f ss] s)
|
||||||
@@ -10,5 +10,5 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
(pipe ["btc" [:close "cls"]])
|
(p ["btc" [:close "cls"]])
|
||||||
)
|
)
|
||||||
@@ -373,8 +373,8 @@ impl TypeChecker {
|
|||||||
}
|
}
|
||||||
else_typed = Some(Box::new(et));
|
else_typed = Some(Box::new(et));
|
||||||
} else {
|
} else {
|
||||||
// If without else returns Void or Optional(Then)
|
// If without else returns Optional(Then) (T | Void)
|
||||||
final_ty = StaticType::Any; // Delphi: MakeOptional(Then)
|
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 typed_lambda = self.check_node(*lambda, ctx)?;
|
||||||
let ret_ty = if let StaticType::Function(sig) = &typed_lambda.ty {
|
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 {
|
} else {
|
||||||
StaticType::Any
|
StaticType::Any
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ pub fn register(env: &Environment) {
|
|||||||
datetime::register(env);
|
datetime::register(env);
|
||||||
math::register(env);
|
math::register(env);
|
||||||
series::register(env);
|
series::register(env);
|
||||||
|
streams::register(env);
|
||||||
}
|
}
|
||||||
+7
-1
@@ -295,6 +295,7 @@ pub enum StaticType {
|
|||||||
DateTime,
|
DateTime,
|
||||||
Text,
|
Text,
|
||||||
Keyword,
|
Keyword,
|
||||||
|
Optional(Box<StaticType>), // Represents T | Void (e.g. for filter pipes)
|
||||||
List(Box<StaticType>), // Legacy / Dynamic list
|
List(Box<StaticType>), // Legacy / Dynamic list
|
||||||
Series(Box<StaticType>), // Time series of a specific type
|
Series(Box<StaticType>), // Time series of a specific type
|
||||||
Tuple(Vec<StaticType>), // Heterogeneous fixed-size
|
Tuple(Vec<StaticType>), // Heterogeneous fixed-size
|
||||||
@@ -318,7 +319,8 @@ impl fmt::Display for StaticType {
|
|||||||
StaticType::DateTime => write!(f, "datetime"),
|
StaticType::DateTime => write!(f, "datetime"),
|
||||||
StaticType::Text => write!(f, "text"),
|
StaticType::Text => write!(f, "text"),
|
||||||
StaticType::Keyword => write!(f, "keyword"),
|
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::Series(inner) => write!(f, "series<{}>", inner),
|
||||||
StaticType::Tuple(elements) => {
|
StaticType::Tuple(elements) => {
|
||||||
write!(f, "[")?;
|
write!(f, "[")?;
|
||||||
@@ -371,6 +373,10 @@ impl StaticType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match (self, other) {
|
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
|
// A Vector is a Tuple
|
||||||
(StaticType::Tuple(elements), StaticType::Vector(inner, len)) => {
|
(StaticType::Tuple(elements), StaticType::Vector(inner, len)) => {
|
||||||
if elements.len() != *len {
|
if elements.len() != *len {
|
||||||
|
|||||||
@@ -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]
|
#[test]
|
||||||
fn test_multi_level_destructuring() {
|
fn test_multi_level_destructuring() {
|
||||||
let env = Environment::new();
|
let env = Environment::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user