diff --git a/examples/HMA.myc b/examples/HMA.myc index 8339483..993a5e5 100644 --- a/examples/HMA.myc +++ b/examples/HMA.myc @@ -5,7 +5,7 @@ (def make-sma (fn [lookback] (do - (def s (series lookback :float)) + (def s (series lookback)) (def running-sum 0.0) (def count 0) (fn [x] @@ -22,7 +22,7 @@ (do ;; Sicherstellen, dass lookback mindestens 1 ist (def n (if (< lookback 1) 1 lookback)) - (def s (series n :float)) + (def s (series n)) (def price-sum 0.0) (def weighted-sum 0.0) (def count 0) diff --git a/examples/sma.myc b/examples/sma.myc index c13f5c7..36b2fec 100644 --- a/examples/sma.myc +++ b/examples/sma.myc @@ -11,11 +11,11 @@ (pipe [(pipe [(.close src)] (SMA 5))] (printx "sma5=")) (pipe [(pipe [(.close src)] (SMA 100))] (printx "sma100=")) - (macro cache [lookback type src] `(do (def data (series ~lookback ~type)) (pipe [~src] (fn [s] (do (push data s)))) data)) + (macro cache [lookback src] `(do (def data (series ~lookback)) (pipe [~src] (fn [s] (do (push data s)))) data)) -; (def bars (series 20 :float)) +; (def bars (series 20)) ; (pipe [src] (fn [s] (do (push bars (.close s))))) - (cache 20 :float src) + (cache 20 src) ) diff --git a/examples/soa_series.myc b/examples/soa_series.myc index ad53679..80c1a6d 100644 --- a/examples/soa_series.myc +++ b/examples/soa_series.myc @@ -1,7 +1,7 @@ ;; Benchmark: 1.1us ;; Benchmark-Repeat: 1877 (do - (def my_ticks (series 100 {:price :float :volume :int :msg :text})) + (def my_ticks (series 100)) (push my_ticks {:price 10.5 :volume 100 :msg "A"}) (push my_ticks {:price 11.2 :volume 200 :msg "B"}) diff --git a/rtl/sma.myc b/rtl/sma.myc index e17f549..f4e918e 100644 --- a/rtl/sma.myc +++ b/rtl/sma.myc @@ -2,7 +2,7 @@ (def SMA (fn [length] (do - (def history (series length :float)) + (def history (series length)) (def sum 0.0) (fn [val] diff --git a/src/ast/compiler/type_checker.rs b/src/ast/compiler/type_checker.rs index 9b629d6..0f31f10 100644 --- a/src/ast/compiler/type_checker.rs +++ b/src/ast/compiler/type_checker.rs @@ -3,7 +3,7 @@ use crate::ast::nodes::{ Node, NodeKind, TypedNode, TypedPhase, VirtualId, }; use crate::ast::diagnostics::Diagnostics; -use crate::ast::types::{Keyword, RecordLayout, Signature, StaticType}; +use crate::ast::types::{Keyword, NodeIdentity, RecordLayout, Signature, StaticType, Value}; use std::cell::Cell; use std::collections::HashMap; use std::rc::Rc; @@ -149,7 +149,6 @@ impl TypeChecker { } /// Creates a fresh, unique type variable. - #[allow(dead_code)] fn fresh_var(&self) -> StaticType { let id = self.var_counter.get(); self.var_counter.set(id + 1); @@ -158,7 +157,6 @@ impl TypeChecker { /// Recursively applies the substitution map to a type, replacing all /// resolved `TypeVar`s with their concrete types. - #[allow(dead_code)] fn apply_subst(ty: StaticType, subst: &HashMap) -> StaticType { match ty { StaticType::TypeVar(n) => { @@ -191,7 +189,6 @@ impl TypeChecker { /// Returns true if `TypeVar(var_id)` appears anywhere in `ty` under the /// current substitution. Used to prevent infinite types (occurs check). - #[allow(dead_code)] fn occurs(var_id: u32, ty: &StaticType, subst: &HashMap) -> bool { match ty { StaticType::TypeVar(n) => { @@ -220,7 +217,6 @@ impl TypeChecker { /// Unifies two types under the current substitution. /// On success, the substitution is extended so that `ty1` and `ty2` become equal. /// On failure (type mismatch or occurs check), a diagnostic error is emitted. - #[allow(dead_code)] fn unify(&self, ty1: StaticType, ty2: StaticType, diag: &mut Diagnostics) { let mut subst = self.subst.borrow_mut(); let ty1 = Self::apply_subst(ty1, &subst); @@ -256,13 +252,195 @@ impl TypeChecker { } } + /// Returns true if `node` is an identifier with the given symbol name. + fn is_identifier_named(name: &str, node: &TypedNode) -> bool { + matches!(&node.kind, NodeKind::Identifier { symbol, .. } if symbol.name.as_ref() == name) + } + + /// Converts a resolved series element type to the schema `Value` passed to the + /// `series` runtime: a type keyword (`:float`, `:int`, …) for scalar types, or a + /// schema record (`{:price :float …}`) for record types. + /// Returns `None` if the type is still unresolved (`TypeVar`, `Any`, …). + fn series_element_to_schema_value(ty: &StaticType) -> Option { + match ty { + StaticType::Float => Some(Value::Keyword(Keyword::intern("float"))), + StaticType::Int | StaticType::DateTime => Some(Value::Keyword(Keyword::intern("int"))), + StaticType::Bool => Some(Value::Keyword(Keyword::intern("bool"))), + StaticType::Text => Some(Value::Keyword(Keyword::intern("text"))), + StaticType::Record(layout) => { + // Build schema record: { :field :type_keyword ... } + let schema_fields: Vec<(Keyword, StaticType)> = layout + .fields + .iter() + .map(|(k, _)| (*k, StaticType::Keyword)) + .collect(); + let schema_layout = RecordLayout::get_or_create(schema_fields); + let schema_values: Option> = layout + .fields + .iter() + .map(|(_, field_ty)| Self::series_element_to_schema_value(field_ty)) + .collect(); + schema_values.map(|vals| Value::Record(schema_layout, std::rc::Rc::new(vals))) + } + _ => None, + } + } + + /// Walks the typed AST, applies the HM substitution to every type annotation, + /// and elaborates `(series n)` calls by injecting the resolved schema as a + /// second argument so the runtime can allocate the correct storage. + fn finalize_node(node: TypedNode, subst: &HashMap) -> TypedNode { + let new_ty = Self::apply_subst(node.ty, subst); + let new_kind = Self::finalize_kind(node.kind, subst, &new_ty, &node.identity); + Node { kind: new_kind, ty: new_ty, identity: node.identity, comments: node.comments } + } + + fn finalize_kind( + kind: NodeKind, + subst: &HashMap, + node_ty: &StaticType, + identity: &Rc, + ) -> NodeKind { + match kind { + // Leaf nodes — nothing to recurse into + NodeKind::Nop => NodeKind::Nop, + NodeKind::Constant(v) => NodeKind::Constant(v), + NodeKind::FieldAccessor(k) => NodeKind::FieldAccessor(k), + NodeKind::Error => NodeKind::Error, + NodeKind::Identifier { symbol, binding } => NodeKind::Identifier { symbol, binding }, + NodeKind::Extension(ext) => NodeKind::Extension(ext), + + // Call: detect `(series n)` and inject the resolved schema arg + NodeKind::Call { callee, args } => { + let callee_fin = Rc::new(Self::finalize_node((*callee).clone(), subst)); + if Self::is_identifier_named("series", &callee_fin) { + if let StaticType::Series(inner) = node_ty { + if let NodeKind::Tuple { elements } = &args.kind { + if elements.len() == 1 { + if let Some(schema_val) = + Self::series_element_to_schema_value(inner) + { + let orig_arg = Rc::new(Self::finalize_node( + (*elements[0]).clone(), + subst, + )); + let schema_ty = schema_val.static_type(); + let schema_node = Rc::new(Node { + kind: NodeKind::Constant(schema_val), + ty: schema_ty, + identity: identity.clone(), + comments: Rc::from([]), + }); + let new_elems = vec![orig_arg, schema_node]; + let elem_types: Vec = + new_elems.iter().map(|e| e.ty.clone()).collect(); + let new_args = Node { + kind: NodeKind::Tuple { elements: new_elems }, + ty: StaticType::Tuple(elem_types), + identity: args.identity.clone(), + comments: args.comments.clone(), + }; + return NodeKind::Call { + callee: callee_fin, + args: Rc::new(new_args), + }; + } + } + } + } + } + let args_fin = Rc::new(Self::finalize_node((*args).clone(), subst)); + NodeKind::Call { callee: callee_fin, args: args_fin } + } + + NodeKind::If { cond, then_br, else_br } => NodeKind::If { + cond: Rc::new(Self::finalize_node((*cond).clone(), subst)), + then_br: Rc::new(Self::finalize_node((*then_br).clone(), subst)), + else_br: else_br.map(|e| Rc::new(Self::finalize_node((*e).clone(), subst))), + }, + NodeKind::Def { pattern, value, info } => NodeKind::Def { + pattern: Rc::new(Self::finalize_node((*pattern).clone(), subst)), + value: Rc::new(Self::finalize_node((*value).clone(), subst)), + info, + }, + NodeKind::Assign { target, value, info } => NodeKind::Assign { + target: Rc::new(Self::finalize_node((*target).clone(), subst)), + value: Rc::new(Self::finalize_node((*value).clone(), subst)), + info, + }, + NodeKind::Lambda { params, body, info } => NodeKind::Lambda { + params: Rc::new(Self::finalize_node((*params).clone(), subst)), + body: Rc::new(Self::finalize_node((*body).clone(), subst)), + info, + }, + NodeKind::Again { args } => NodeKind::Again { + args: Rc::new(Self::finalize_node((*args).clone(), subst)), + }, + NodeKind::Block { exprs } => NodeKind::Block { + exprs: exprs + .into_iter() + .map(|e| Rc::new(Self::finalize_node((*e).clone(), subst))) + .collect(), + }, + NodeKind::Tuple { elements } => NodeKind::Tuple { + elements: elements + .into_iter() + .map(|e| Rc::new(Self::finalize_node((*e).clone(), subst))) + .collect(), + }, + NodeKind::Record { fields, layout } => NodeKind::Record { + fields: fields + .into_iter() + .map(|(k, v)| { + ( + Rc::new(Self::finalize_node((*k).clone(), subst)), + Rc::new(Self::finalize_node((*v).clone(), subst)), + ) + }) + .collect(), + layout, + }, + NodeKind::GetField { rec, field } => NodeKind::GetField { + rec: Rc::new(Self::finalize_node((*rec).clone(), subst)), + field, + }, + NodeKind::Expansion { original_call, expanded } => NodeKind::Expansion { + original_call, + expanded: Rc::new(Self::finalize_node((*expanded).clone(), subst)), + }, + NodeKind::MacroDecl { name, params, body } => NodeKind::MacroDecl { + name, + params: Rc::new(Self::finalize_node((*params).clone(), subst)), + body: Rc::new(Self::finalize_node((*body).clone(), subst)), + }, + NodeKind::Template(inner) => { + NodeKind::Template(Rc::new(Self::finalize_node((*inner).clone(), subst))) + } + NodeKind::Placeholder(inner) => { + NodeKind::Placeholder(Rc::new(Self::finalize_node((*inner).clone(), subst))) + } + NodeKind::Splice(inner) => { + NodeKind::Splice(Rc::new(Self::finalize_node((*inner).clone(), subst))) + } + } + } + + /// Applies the final HM substitution to all type annotations in the tree and + /// elaborates `(series n)` calls with their inferred schema arguments. + /// Must be called after `check()` or `check_node_as_bound()` completes. + pub fn finalize(&self, node: TypedNode) -> TypedNode { + let subst = self.subst.borrow().clone(); + Self::finalize_node(node, &subst) + } + pub fn check( &self, node: &Node, arg_types: &[StaticType], diag: &mut Diagnostics, ) -> TypedNode { - self.check_node_as_bound(node, arg_types, diag) + let typed = self.check_node_as_bound(node, arg_types, diag); + self.finalize(typed) } /// Allows re-checking a node from any phase as if it were a bound node. @@ -912,7 +1090,7 @@ impl TypeChecker { self.check_node(args, ctx, diag) }; - let ret_ty = match callee_typed.ty.resolve_call(&args_typed.ty) { + let mut ret_ty = match callee_typed.ty.resolve_call(&args_typed.ty) { Some(ty) => ty, None => { diag.push_error( @@ -926,6 +1104,43 @@ impl TypeChecker { } }; + // HM: (series n) returns Series(Any) — replace with a fresh TypeVar so + // subsequent push calls can unify the element type. + if Self::is_identifier_named("series", &callee_typed) { + if let StaticType::Series(inner) = &ret_ty { + if **inner == StaticType::Any { + ret_ty = StaticType::Series(Box::new(self.fresh_var())); + } + } + } + + // HM: (push series val) — unify the series element TypeVar with the value + // type and propagate the resolved type back to the binding in the context. + if Self::is_identifier_named("push", &callee_typed) { + if let NodeKind::Tuple { elements } = &args_typed.kind { + if elements.len() >= 2 { + let series_arg = &elements[0]; + let value_arg = &elements[1]; + if let StaticType::Series(inner) = &series_arg.ty { + let inner_ty = (**inner).clone(); + let val_ty = value_arg.ty.clone(); + self.unify(inner_ty, val_ty, diag); + if let NodeKind::Identifier { + binding: IdentifierBinding::Reference(addr), + .. + } = &series_arg.kind + { + let resolved = Self::apply_subst( + series_arg.ty.clone(), + &self.subst.borrow(), + ); + ctx.set_type(*addr, resolved); + } + } + } + } + } + ( NodeKind::Call { callee: Rc::new(callee_typed), diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 582bb68..dced83c 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -819,6 +819,7 @@ impl Environment { // us re-run type inference on the TypedNode with concrete `arg_types`: existing // `ty` metadata is discarded and all types are inferred fresh from the call site. let retyped_ast = checker.check_node_as_bound(func_template.ty.original.as_ref(), arg_types, &mut diag); + let retyped_ast = checker.finalize(retyped_ast); if diag.has_errors() { return Err(diag.format_errors()); diff --git a/src/ast/rtl/prelude.myc b/src/ast/rtl/prelude.myc index 1057b8a..718f63d 100644 --- a/src/ast/rtl/prelude.myc +++ b/src/ast/rtl/prelude.myc @@ -20,9 +20,10 @@ ) ;; Creates a stateful cache (Series) from a stateless stream. - (macro cache [lookback type src] + ;; The element type is inferred from the stream's item type. + (macro cache [lookback src] `(do - (def data (series ~lookback ~type)) + (def data (series ~lookback)) (pipe [~src] (fn [s] (do (push data s)))) data ) diff --git a/src/ast/rtl/series/mod.rs b/src/ast/rtl/series/mod.rs index 9091790..54e3f74 100644 --- a/src/ast/rtl/series/mod.rs +++ b/src/ast/rtl/series/mod.rs @@ -11,12 +11,15 @@ use crate::ast::types::{Purity, RecordLayout, Signature, StaticType, Value}; // ============================================================================ pub fn register(env: &Environment) { - // (series lookback_limit template_or_type_keyword) -> Series + // (series lookback_limit) -> Series + // The element type is inferred by the HM type checker from subsequent push calls. + // After type checking, the compiler elaborates (series n) into (series n schema) + // so the runtime always receives the explicit schema argument. env.register_native_fn( "series", StaticType::Function(Box::new(Signature { - params: StaticType::Tuple(vec![StaticType::Int, StaticType::Any]), - ret: StaticType::Any, + params: StaticType::Tuple(vec![StaticType::Int]), + ret: StaticType::Series(Box::new(StaticType::Any)), })), Purity::Impure, |args: &[Value]| { diff --git a/src/ast/types.rs b/src/ast/types.rs index 4b63187..22f3d0e 100644 --- a/src/ast/types.rs +++ b/src/ast/types.rs @@ -596,6 +596,21 @@ impl StaticType { .map(|sig| sig.ret.clone()), StaticType::PolymorphicFn { resolve_return, .. } => resolve_return(args_ty), StaticType::TypeVar(_) => Some(StaticType::Any), + // Lookback indexing: (my-series 0) → element type + StaticType::Series(inner) => { + let is_int = matches!( + args_ty, + StaticType::Int | StaticType::Any | StaticType::TypeVar(_) + ) || matches!(args_ty, StaticType::Tuple(elems) + if elems.len() == 1 + && matches!(&elems[0], StaticType::Int | StaticType::Any | StaticType::TypeVar(_)) + ); + if is_int { + Some(*inner.clone()) + } else { + None + } + } _ => None, } } diff --git a/tests/rtl_series.rs b/tests/rtl_series.rs index b965ddd..09d36a7 100644 --- a/tests/rtl_series.rs +++ b/tests/rtl_series.rs @@ -10,6 +10,36 @@ fn test_series_api_with_limit() { assert!(result.is_ok(), "Series creation with limit should work: {:?}", result.err()); } +/// Regression test for HM type inference through nested closures. +/// +/// `series` is created in an outer closure, `push` happens in an inner closure. +/// The pushed value type must propagate back through the TypeVar chain so that +/// the compiler can inject the `:float` schema — no explicit schema given. +#[test] +fn test_series_infer_type_from_nested_closure() { + let env = Environment::new(); + let source = r#" + (do + (def make-acc + (fn [n] + (do + (def s (series n)) + (def total 0.0) + (fn [x] + (do + (assign total (+ total x)) + (push s x) + (s 0)))))) + (def acc (make-acc 5)) + (acc 1.5)) + "#; + let result = env.run_script(source); + match result { + Ok(Value::Float(v)) if (v - 1.5).abs() < 1e-9 => {} + other => panic!("Expected Float(1.5), got {:?}", other), + } +} + #[test] fn test_record_structural_equality_order() { let env = Environment::new();