Add Stream support for field accessors

The type checker now correctly handles unwrapping `Stream(T)` types for
lambda arguments.
A new `build_map_stream` function has been added to `rtl/streams.rs` to
facilitate field access on streams.
The `create-random-ohlc` function now returns a `Stream` instead of a
`Series`.
Examples have been updated to reflect these changes and demonstrate
stream usage.
This commit is contained in:
Michael Schimmel
2026-03-07 20:47:05 +01:00
parent 2023df2f62
commit f88992da61
6 changed files with 105 additions and 28 deletions
+6 -8
View File
@@ -1,12 +1,10 @@
#use rtl #use rtl
(do (do
(def sma20 (SMA 20)) (def src (create-random-ohlc 42 200))
(def n 100) (macro printx [s] `(fn [x] (print ~s x)))
(while (> n 0)
(do (pipe [(.close src)] (printx "close="))
(print "val=" n " sma20=" (sma20 n)) (pipe [(pipe [(.close src)] (SMA 20))] (printx "sma="))
(assign n (- n 1))
))
) )
+10
View File
@@ -0,0 +1,10 @@
(do
(def stream (create-random-ohlc 42 200))
(macro prt [m] `(pipe (~m stream) (fn [s] (print ~m "=" s))))
(prt .open)
(prt .high)
(prt .low)
(prt .close)
)
+3 -1
View File
@@ -422,9 +422,11 @@ impl TypeChecker {
let mut arg_types = Vec::with_capacity(inputs.len()); let mut arg_types = Vec::with_capacity(inputs.len());
for input in inputs { for input in inputs {
let typed_input = self.check_node(input, ctx, diag); let typed_input = self.check_node(input, ctx, diag);
// Unwrap Series(T) to T for the lambda argument // Unwrap Series(T) or Stream(T) to T for the lambda argument
let arg_ty = if let StaticType::Series(inner) = &typed_input.ty { let arg_ty = if let StaticType::Series(inner) = &typed_input.ty {
*inner.clone() *inner.clone()
} else if let StaticType::Stream(inner) = &typed_input.ty {
*inner.clone()
} else { } else {
StaticType::Any StaticType::Any
}; };
+29 -1
View File
@@ -416,6 +416,34 @@ pub fn build_pipeline_node(
}) })
} }
pub fn build_map_stream(input: Rc<dyn ObservableStream>, field: Keyword) -> StreamNode {
let executor: Box<PipeFn> = Box::new(move |args: &[Value]| -> Value {
let val = &args[0];
if let Value::Record(layout, values) = val
&& let Some(idx) = layout.index_of(field)
{
return values[idx].clone();
}
Value::Void // In streams, Void acts as a filter
});
let pipe = Rc::new(RefCell::new(PipeStream::new(
format!("map:{}", field.name()),
1,
Some(executor),
)));
let adapter = Rc::new(RefCell::new(SourceAdapter {
target: pipe.clone(),
target_index: 0,
}));
input.add_observer(adapter);
StreamNode {
inner: pipe,
}
}
// ============================================================================ // ============================================================================
// Script Integration (RTL Registration) // Script Integration (RTL Registration)
// ============================================================================ // ============================================================================
@@ -439,7 +467,7 @@ pub fn register(env: &Environment) {
"create-random-ohlc", "create-random-ohlc",
StaticType::Function(Box::new(Signature { StaticType::Function(Box::new(Signature {
params: StaticType::Tuple(vec![StaticType::Int, StaticType::Int]), params: StaticType::Tuple(vec![StaticType::Int, StaticType::Int]),
ret: StaticType::Series(Box::new(StaticType::Record(ohlc_layout.clone()))), ret: StaticType::Stream(Box::new(StaticType::Record(ohlc_layout.clone()))),
})), })),
Purity::Impure, // Modifies global generator registry Purity::Impure, // Modifies global generator registry
move |args: &[Value]| { move |args: &[Value]| {
+20
View File
@@ -311,6 +311,7 @@ pub enum StaticType {
Optional(Box<StaticType>), // Represents T | Void (e.g. for filter pipes) 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
Stream(Box<StaticType>), // Reactive stream of a specific type
Tuple(Vec<StaticType>), // Heterogeneous fixed-size Tuple(Vec<StaticType>), // Heterogeneous fixed-size
Vector(Box<StaticType>, usize), // Homogeneous fixed-size Vector(Box<StaticType>, usize), // Homogeneous fixed-size
Matrix(Box<StaticType>, Vec<usize>), // Multi-dimensional homogeneous Matrix(Box<StaticType>, Vec<usize>), // Multi-dimensional homogeneous
@@ -337,6 +338,7 @@ impl fmt::Display for StaticType {
StaticType::Optional(inner) => write!(f, "optional({})", inner), StaticType::Optional(inner) => write!(f, "optional({})", inner),
StaticType::List(inner) => write!(f, "list({})", inner), StaticType::List(inner) => write!(f, "list({})", inner),
StaticType::Series(inner) => write!(f, "series<{}>", inner), StaticType::Series(inner) => write!(f, "series<{}>", inner),
StaticType::Stream(inner) => write!(f, "stream<{}>", inner),
StaticType::Tuple(elements) => { StaticType::Tuple(elements) => {
write!(f, "[")?; write!(f, "[")?;
for (i, el) in elements.iter().enumerate() { for (i, el) in elements.iter().enumerate() {
@@ -440,6 +442,10 @@ impl StaticType {
(StaticType::Series(inner_a), StaticType::Series(inner_b)) => { (StaticType::Series(inner_a), StaticType::Series(inner_b)) => {
inner_a.is_assignable_from(inner_b) inner_a.is_assignable_from(inner_b)
} }
// Streams are assignable if their inner types are assignable
(StaticType::Stream(inner_a), StaticType::Stream(inner_b)) => {
inner_a.is_assignable_from(inner_b)
}
_ => false, _ => false,
} }
} }
@@ -463,6 +469,20 @@ impl StaticType {
return Some(layout.fields[idx].1.clone()); return Some(layout.fields[idx].1.clone());
} }
} }
StaticType::Series(inner) => {
if let StaticType::Record(layout) = inner.as_ref()
&& let Some(idx) = layout.index_of(*k)
{
return Some(StaticType::Series(Box::new(layout.fields[idx].1.clone())));
}
}
StaticType::Stream(inner) => {
if let StaticType::Record(layout) = inner.as_ref()
&& let Some(idx) = layout.index_of(*k)
{
return Some(StaticType::Stream(Box::new(layout.fields[idx].1.clone())));
}
}
StaticType::Any => return Some(StaticType::Any), StaticType::Any => return Some(StaticType::Any),
_ => {} _ => {}
} }
+37 -18
View File
@@ -384,6 +384,12 @@ impl VM {
field.name() field.name()
)); ));
} }
} else if let Some(sn) = obj.as_any().downcast_ref::<crate::ast::rtl::streams::StreamNode>() {
let mapped = crate::ast::rtl::streams::build_map_stream(sn.inner.clone(), *field);
return Ok(Value::Object(Rc::new(mapped)));
} else if let Some(pn) = obj.as_any().downcast_ref::<crate::ast::rtl::streams::PipelineNode>() {
let mapped = crate::ast::rtl::streams::build_map_stream(pn.stream.clone(), *field);
return Ok(Value::Object(Rc::new(mapped)));
} }
// Fallback if it's another type of object that is not a RecordSeries. // Fallback if it's another type of object that is not a RecordSeries.
@@ -550,26 +556,33 @@ impl VM {
k, k,
); );
return Ok(Value::Object(std::rc::Rc::new(view))); return Ok(Value::Object(std::rc::Rc::new(view)));
} else { } else {
return Err(format!( return Err(format!(
"RecordSeries does not have field :{}", "RecordSeries does not have field :{}",
k.name() k.name()
)); ));
} }
} } else if let Some(sn) = obj.as_any().downcast_ref::<crate::ast::rtl::streams::StreamNode>() {
return Err(format!( let mapped = crate::ast::rtl::streams::build_map_stream(sn.inner.clone(), k);
"Field accessor .{} expects a record or RecordSeries, got {}", return Ok(Value::Object(Rc::new(mapped)));
k.name(), } else if let Some(pn) = obj.as_any().downcast_ref::<crate::ast::rtl::streams::PipelineNode>() {
obj.type_name() let mapped = crate::ast::rtl::streams::build_map_stream(pn.stream.clone(), k);
)); return Ok(Value::Object(Rc::new(mapped)));
} else { }
return Err(format!( return Err(format!(
"Field accessor .{} expects a record or RecordSeries, got {}", "Field accessor .{} expects a record, RecordSeries or Stream, got {}",
k.name(), k.name(),
rec obj.type_name()
)); ));
} } else {
} return Err(format!(
"Field accessor .{} expects a record, RecordSeries or Stream, got {}",
k.name(),
rec
));
}
}
_ => { _ => {
return Err(format!( return Err(format!(
"Tail call target is not a function: {}", "Tail call target is not a function: {}",
@@ -621,16 +634,22 @@ impl VM {
k.name() k.name()
)) ))
} }
} else if let Some(sn) = obj.as_any().downcast_ref::<crate::ast::rtl::streams::StreamNode>() {
let mapped = crate::ast::rtl::streams::build_map_stream(sn.inner.clone(), *k);
Ok(Value::Object(Rc::new(mapped)))
} else if let Some(pn) = obj.as_any().downcast_ref::<crate::ast::rtl::streams::PipelineNode>() {
let mapped = crate::ast::rtl::streams::build_map_stream(pn.stream.clone(), *k);
Ok(Value::Object(Rc::new(mapped)))
} else { } else {
Err(format!( Err(format!(
"Field accessor .{} expects a record or RecordSeries, got {}", "Field accessor .{} expects a record, RecordSeries or Stream, got {}",
k.name(), k.name(),
obj.type_name() obj.type_name()
)) ))
} }
} else { } else {
Err(format!( Err(format!(
"Field accessor .{} expects a record or RecordSeries, got {}", "Field accessor .{} expects a record, RecordSeries or Stream, got {}",
k.name(), k.name(),
rec rec
)) ))