From a665e2c1a5a3090fb3023cdcd925b6880aa6b10c Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 29 Mar 2026 14:52:58 +0200 Subject: [PATCH] Refactor call hooks to use trait objects This commit refactors the call hook mechanism in the compiler. Instead of using function pointer types (`PostCallHook`, `FinalizeHook`) and a struct to hold them (`CallHooks`), it introduces a trait `RtlCompilerHook` with default implementations for `post_call` and `finalize`. This change aligns with Rust's idiomatic way of handling extension points and allows for more flexible and extensible compiler behavior. The `TypeChecker` now holds a map of `Rc`, enabling different RTL functions to register their specific compiler hooks. The `SeriesHook` and `PushHook` structs are introduced to demonstrate this new pattern. --- src/ast/compiler/call_hooks.rs | 61 ++++---- src/ast/compiler/type_checker.rs | 40 +++-- src/ast/environment.rs | 40 ++--- src/ast/rtl/docs.rs | 28 ++-- src/ast/rtl/series/mod.rs | 251 ++++++++++++++++--------------- 5 files changed, 216 insertions(+), 204 deletions(-) diff --git a/src/ast/compiler/call_hooks.rs b/src/ast/compiler/call_hooks.rs index 8ef4daf..35b6180 100644 --- a/src/ast/compiler/call_hooks.rs +++ b/src/ast/compiler/call_hooks.rs @@ -5,8 +5,8 @@ use crate::ast::diagnostics::Diagnostics; use crate::ast::nodes::{Address, NodeKind, TypedNode, TypedPhase, VirtualId}; use crate::ast::types::StaticType; -/// Gives a call-hook access to the type-checker's core inference operations and -/// scope slot types without depending on the concrete `TypeChecker` type +/// Gives a compiler hook access to the type-checker's core inference operations +/// and scope slot types without depending on the concrete `TypeChecker` type /// (avoids a circular module dependency). pub trait InferenceAccess { /// Allocate a fresh unique `TypeVar`. @@ -35,29 +35,38 @@ pub trait InferenceAccess { fn get_slot_type(&self, addr: Address) -> StaticType; } -/// Called after the call's return type is resolved. -/// May replace the return type and/or perform unification side-effects (e.g. push). -pub type PostCallHook = fn( - args: &TypedNode, - ret_ty: StaticType, - ctx: &dyn InferenceAccess, - diag: &mut Diagnostics, -) -> StaticType; - -/// Called during AST finalization. -/// Returns `Some(new_kind)` to rewrite the node, or `None` to keep the original. -pub type FinalizeHook = fn( - callee: Rc, - args: Rc, - node_ty: &StaticType, - subst: &HashMap, -) -> Option>; - -/// Hooks attached to a specific RTL function slot. -/// Enables type-inference extensions and AST rewrites without hardcoding symbol +/// Extension point that lets RTL functions participate in the compiler's +/// type-inference and AST-finalization passes without hardcoding symbol /// names anywhere in the compiler core. -#[derive(Default)] -pub struct CallHooks { - pub post_call: Option, - pub finalize: Option, +/// +/// Each RTL function that needs custom type-level behaviour (e.g. `series`, +/// `push`) implements this trait. Hooks are registered by global slot index +/// during RTL bootstrap and dispatched automatically by the type checker. +/// +/// Both methods have no-op defaults so that implementors only override what +/// they need. +pub trait RtlCompilerHook { + /// Called after the call's return type is resolved during type inference. + /// May replace the return type and/or perform unification side-effects. + fn post_call( + &self, + _args: &TypedNode, + ret_ty: StaticType, + _ctx: &dyn InferenceAccess, + _diag: &mut Diagnostics, + ) -> StaticType { + ret_ty + } + + /// Called during AST finalization. + /// Returns `Some(new_kind)` to rewrite the node, or `None` to keep the original. + fn finalize( + &self, + _callee: Rc, + _args: Rc, + _node_ty: &StaticType, + _subst: &HashMap, + ) -> Option> { + None + } } diff --git a/src/ast/compiler/type_checker.rs b/src/ast/compiler/type_checker.rs index f8c001d..dcab4d7 100644 --- a/src/ast/compiler/type_checker.rs +++ b/src/ast/compiler/type_checker.rs @@ -1,4 +1,4 @@ -use crate::ast::compiler::call_hooks::{CallHooks, InferenceAccess}; +use crate::ast::compiler::call_hooks::{InferenceAccess, RtlCompilerHook}; use crate::ast::nodes::{ Address, AssignBinding, BoundLike, BoundPhase, DefBinding, IdentifierBinding, LambdaBinding, Node, NodeKind, TypedNode, TypedPhase, VirtualId, @@ -184,22 +184,22 @@ pub struct TypeChecker { /// lazily in `bind_var`: when the callee TypeVar is bound to a concrete type, /// the element type is extracted directly from the `Series` variant. index_constraints: std::cell::RefCell>, - /// Type-inference and finalization hooks keyed by global slot index. + /// Compiler hooks keyed by global slot index. /// Populated from the frozen RTL snapshot; empty for non-RTL call sites. - call_hooks: Rc>, + compiler_hooks: Rc>>, } impl TypeChecker { pub fn new( root_types: Rc>>, - call_hooks: Rc>, + compiler_hooks: Rc>>, ) -> Self { Self { root_types, var_counter: Cell::new(0), subst: std::cell::RefCell::new(HashMap::new()), index_constraints: std::cell::RefCell::new(HashMap::new()), - call_hooks, + compiler_hooks, } } @@ -621,16 +621,15 @@ impl TypeChecker { binding: IdentifierBinding::Reference(Address::Global(idx)), .. } = &callee_fin.kind - && let Some(hook) = self.call_hooks.get(&idx.0) - && let Some(finalize) = hook.finalize - && let Some(new_kind) = finalize( - Rc::clone(&callee_fin), - Rc::clone(&args_fin), - node_ty, - subst, - ) { - return new_kind; - } + && let Some(hook) = self.compiler_hooks.get(&idx.0) + && let Some(new_kind) = hook.finalize( + Rc::clone(&callee_fin), + Rc::clone(&args_fin), + node_ty, + subst, + ) { + return new_kind; + } NodeKind::Call { callee: callee_fin, args: args_fin } } @@ -1459,7 +1458,7 @@ impl TypeChecker { ret_ty = Self::apply_subst(ret_ty, &self.subst.borrow()); } - // Dispatch post-call hooks registered by the RTL (keyed by global slot index). + // Dispatch compiler hooks registered by the RTL (keyed by global slot index). // Hooks handle type-inference extensions such as: // - series: inject a fresh TypeVar for the element type // - push: unify the series element TypeVar with the pushed value type @@ -1467,11 +1466,10 @@ impl TypeChecker { binding: IdentifierBinding::Reference(Address::Global(idx)), .. } = &callee_typed.kind - && let Some(hook) = self.call_hooks.get(&idx.0) - && let Some(post_call) = hook.post_call { - let hook_ctx = CheckerInferenceAccess { checker: self, ctx }; - ret_ty = post_call(&args_typed, ret_ty, &hook_ctx, diag); - } + && let Some(hook) = self.compiler_hooks.get(&idx.0) { + let hook_ctx = CheckerInferenceAccess { checker: self, ctx }; + ret_ty = hook.post_call(&args_typed, ret_ty, &hook_ctx, diag); + } ( NodeKind::Call { diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 29de487..7efd9dd 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -1,6 +1,6 @@ use crate::ast::compiler::analyzer::Analyzer; use crate::ast::compiler::binder::{Binder, CompilerScope, LocalInfo}; -use crate::ast::compiler::call_hooks::CallHooks; +use crate::ast::compiler::call_hooks::RtlCompilerHook; use crate::ast::compiler::{CapturePass, TypeChecker, TypedNode}; use crate::ast::nodes::{AnalyzedPhase, Symbol, SyntaxKind, SyntaxNode}; use crate::ast::parser::Parser; @@ -54,9 +54,9 @@ pub struct Rtl { pub purity: Vec, pub values: Rc<[Value]>, pub rtl_docs: Vec, - /// Type-inference and finalization hooks, keyed by global slot index. - /// Populated during RTL registration via `RtlRegistration::with_call_hooks`. - pub call_hooks: Rc>, + /// Compiler hooks, keyed by global slot index. + /// Populated during RTL registration via `RtlRegistration::with_compiler_hook`. + pub compiler_hooks: Rc>>, } pub struct Environment { @@ -83,9 +83,9 @@ pub struct Environment { /// Documentation for symbols defined in Myc source (via `;;` comments before `def`/`macro`). /// Key: symbol name. Value: concatenated doc lines joined by newlines. pub myc_docs: Rc>>, - /// Staging area for call hooks registered during RTL bootstrap. - /// Frozen into `Rtl::call_hooks` after `rtl::register()` completes. - pub call_hooks: Rc>>, + /// Staging area for compiler hooks registered during RTL bootstrap. + /// Frozen into `Rtl::compiler_hooks` after `rtl::register()` completes. + pub compiler_hooks: Rc>>>, /// Frozen RTL snapshot from which this environment was created. pub rtl: Rc, } @@ -111,7 +111,7 @@ struct RuntimeMacroEvaluator { globals: GlobalStore, root_purity: Rc>>, fixed_scope_idx: i32, - call_hooks: Rc>, + compiler_hooks: Rc>>, } impl MacroEvaluator for RuntimeMacroEvaluator { @@ -132,7 +132,7 @@ impl MacroEvaluator for RuntimeMacroEvaluator { let (bound_ast, captures, _, _) = Binder::bind_root(initial_scopes, initial_slot_count, self.fixed_scope_idx, node, &mut diag)?; let bound_ast = CapturePass::apply(bound_ast, &captures); - let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.call_hooks)); + let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.compiler_hooks)); let typed_ast = checker.check(&bound_ast, &[], &mut diag); if diag.has_errors() { @@ -175,7 +175,7 @@ impl Environment { loaded_modules: Rc::new(RefCell::new(HashSet::new())), rtl_docs: Rc::new(RefCell::new(Vec::new())), myc_docs: Rc::new(RefCell::new(HashMap::new())), - call_hooks: Rc::new(RefCell::new(HashMap::new())), + compiler_hooks: Rc::new(RefCell::new(HashMap::new())), // Placeholder — overwritten below after bootstrap. rtl: Rc::new(Rtl { scope: CompilerScope::new(), @@ -184,7 +184,7 @@ impl Environment { purity: vec![], values: Rc::from([]), rtl_docs: vec![], - call_hooks: Rc::new(HashMap::new()), + compiler_hooks: Rc::new(HashMap::new()), }), }; @@ -195,7 +195,7 @@ impl Environment { let rtl_slot_count = *env.root_slot_count.borrow(); // user_values was used as the bootstrap scratch; freeze it into an immutable slice. let rtl_vals: Rc<[Value]> = env.user_values.borrow().clone().into(); - let frozen_hooks = Rc::new(std::mem::take(&mut *env.call_hooks.borrow_mut())); + let frozen_hooks = Rc::new(std::mem::take(&mut *env.compiler_hooks.borrow_mut())); env.rtl = Rc::new(Rtl { scope: env.root_scopes.borrow()[0].clone(), slot_count: rtl_slot_count, @@ -203,7 +203,7 @@ impl Environment { purity: env.root_purity.borrow().clone(), values: Rc::clone(&rtl_vals), rtl_docs: std::mem::take(&mut env.rtl_docs.borrow_mut()), - call_hooks: frozen_hooks, + compiler_hooks: frozen_hooks, }); // Set the frozen RTL slice. Stream closures registered during bootstrap @@ -257,7 +257,7 @@ impl Environment { loaded_modules: Rc::new(RefCell::new(HashSet::new())), rtl_docs: Rc::new(RefCell::new(Vec::new())), myc_docs: Rc::new(RefCell::new(HashMap::new())), - call_hooks: Rc::new(RefCell::new(HashMap::new())), + compiler_hooks: Rc::new(RefCell::new(HashMap::new())), rtl, }; @@ -319,7 +319,7 @@ impl Environment { globals: self.global_store(), root_purity: self.root_purity.clone(), fixed_scope_idx: self.fixed_scope_idx, - call_hooks: Rc::clone(&self.rtl.call_hooks), + compiler_hooks: Rc::clone(&self.rtl.compiler_hooks), }; MacroExpander::new(self.macro_registry.borrow().clone(), evaluator) } @@ -480,7 +480,7 @@ impl Environment { LambdaCollector::collect(&bound_ast, &mut self.function_registry.borrow_mut()); - let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.rtl.call_hooks)); + let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.rtl.compiler_hooks)); let wrapped_ast = if let NodeKind::Lambda { .. } = bound_ast.kind { bound_ast } else { @@ -572,7 +572,7 @@ impl Environment { name.to_string(), slot_idx, Rc::clone(&self.rtl_docs), - Rc::clone(&self.call_hooks), + Rc::clone(&self.compiler_hooks), ) } @@ -582,7 +582,7 @@ impl Environment { name.to_string(), slot_idx, Rc::clone(&self.rtl_docs), - Rc::clone(&self.call_hooks), + Rc::clone(&self.compiler_hooks), ) } @@ -827,14 +827,14 @@ impl Environment { let root_types = self.root_types.clone(); let root_purity = self.root_purity.clone(); let optimization = self.optimization; - let call_hooks = Rc::clone(&self.rtl.call_hooks); + let compiler_hooks = Rc::clone(&self.rtl.compiler_hooks); let compiler = Rc::new( move |func_template: Rc>, arg_types: &[StaticType]| -> Result<(Value, StaticType), String> { let mut diag = Diagnostics::new(); - let checker = TypeChecker::new(root_types.clone(), Rc::clone(&call_hooks)); + let checker = TypeChecker::new(root_types.clone(), Rc::clone(&compiler_hooks)); // Monomorphization: re-type-check the function template with the concrete // call-site argument types, producing a specialized TypedNode. diff --git a/src/ast/rtl/docs.rs b/src/ast/rtl/docs.rs index 4495409..055b1b3 100644 --- a/src/ast/rtl/docs.rs +++ b/src/ast/rtl/docs.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use crate::ast::compiler::call_hooks::CallHooks; +use crate::ast::compiler::call_hooks::RtlCompilerHook; /// A generator function for data pipelines. Returns `true` while data is available. pub type PipelineGenerator = Box bool>; @@ -22,13 +22,13 @@ pub struct RtlDocEntry { /// Builder returned by `register_native_fn` and `register_constant`. /// Call `.doc(...)` to attach documentation; optional `.description(...)` and `.examples(...)` /// can be chained. The entry is written to the registry when the builder is dropped. -/// Optionally, call `.with_call_hooks(...)` to register type-inference or finalization -/// hooks for this slot — keyed by its global slot index, not by name. +/// Optionally, call `.with_compiler_hook(...)` to register an [`RtlCompilerHook`] +/// for this slot — keyed by its global slot index, not by name. pub struct RtlRegistration { name: String, slot_idx: u32, rtl_docs: Rc>>, - call_hooks_registry: Rc>>, + hook_registry: Rc>>>, one_liner: Option<&'static str>, description: Option<&'static str>, examples: Option<&'static [&'static str]>, @@ -39,13 +39,13 @@ impl RtlRegistration { name: String, slot_idx: u32, rtl_docs: Rc>>, - call_hooks_registry: Rc>>, + hook_registry: Rc>>>, ) -> Self { Self { name, slot_idx, rtl_docs, - call_hooks_registry, + hook_registry, one_liner: None, description: None, examples: None, @@ -70,18 +70,12 @@ impl RtlRegistration { self } - /// Register type-inference and/or finalization hooks for this slot. + /// Register a compiler hook for this slot. /// Hooks are keyed by the slot's global index — no name matching in the compiler. - pub fn with_call_hooks( - self, - post_call: Option, - finalize: Option, - ) -> Self { - if post_call.is_some() || finalize.is_some() { - self.call_hooks_registry - .borrow_mut() - .insert(self.slot_idx, CallHooks { post_call, finalize }); - } + pub fn with_compiler_hook(self, hook: Rc) -> Self { + self.hook_registry + .borrow_mut() + .insert(self.slot_idx, hook); self } } diff --git a/src/ast/rtl/series/mod.rs b/src/ast/rtl/series/mod.rs index c0d0240..80b1d5f 100644 --- a/src/ast/rtl/series/mod.rs +++ b/src/ast/rtl/series/mod.rs @@ -5,104 +5,110 @@ use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; -use crate::ast::compiler::call_hooks::{FinalizeHook, InferenceAccess, PostCallHook}; +use crate::ast::compiler::call_hooks::{InferenceAccess, RtlCompilerHook}; use crate::ast::diagnostics::Diagnostics; use crate::ast::environment::Environment; use crate::ast::nodes::{IdentifierBinding, Node, NodeKind, TypedNode, TypedPhase}; use crate::ast::types::{NativeFunction, NodeIdentity, Purity, Signature, SourceLocation, StaticType, Value}; // ============================================================================ -// 4. Type-Inference Hooks (registered via RTL bootstrap, no name coupling) +// 4. Compiler Hooks (registered via RTL bootstrap, no name coupling) // ============================================================================ -/// Post-call hook for `(series n)`. -/// Replaces the `Series(Any)` return type with a fresh TypeVar so that -/// subsequent `push` calls can unify the element type (HM inference). -fn series_post_call( - _args: &TypedNode, - ret_ty: StaticType, - ctx: &dyn InferenceAccess, - _diag: &mut Diagnostics, -) -> StaticType { - if let StaticType::Series(inner) = &ret_ty - && **inner == StaticType::Any - { - return StaticType::Series(Box::new(ctx.fresh_var())); - } - ret_ty -} +/// Compiler hook for `(series n)`. +/// +/// - **post_call:** Replaces the `Series(Any)` return type with a fresh TypeVar +/// so that subsequent `push` calls can unify the element type (HM inference). +/// - **finalize:** Replaces the callee with a pre-configured factory closure that +/// directly allocates the correct storage type (e.g. `ScalarSeries::` for +/// Float, `RecordSeries` with a captured `Arc` for record types). +pub struct SeriesHook; -/// Finalization hook for `(series n)`. -/// Replaces the callee with a pre-configured factory closure that directly -/// allocates the correct storage type (e.g. `ScalarSeries::` for Float, -/// `RecordSeries` with a captured `Arc` for record types). -/// No schema argument is needed — the type is captured in the closure. -fn series_finalize( - _callee: Rc, - args: Rc, - node_ty: &StaticType, - _subst: &HashMap, -) -> Option> { - let StaticType::Series(inner) = node_ty else { return None }; - let NodeKind::Tuple { elements } = &args.kind else { return None }; - if elements.len() != 1 { - return None; +impl RtlCompilerHook for SeriesHook { + fn post_call( + &self, + _args: &TypedNode, + ret_ty: StaticType, + ctx: &dyn InferenceAccess, + _diag: &mut Diagnostics, + ) -> StaticType { + if let StaticType::Series(inner) = &ret_ty + && **inner == StaticType::Any + { + return StaticType::Series(Box::new(ctx.fresh_var())); + } + ret_ty } - // Build a factory Value::Function whose closure already knows the exact - // storage type — no keyword encoding, no runtime dispatch. - let factory_value: Value = match inner.as_ref() { - StaticType::Float => Value::Function(Rc::new(NativeFunction { - func: Rc::new(|args: &[Value]| { - let n = args[0].as_int().unwrap_or(0) as usize; - Value::Series(Rc::new(ScalarSeries::::new("FloatSeries", n))) - }), - purity: Purity::Impure, - })), - StaticType::Int | StaticType::DateTime => Value::Function(Rc::new(NativeFunction { - func: Rc::new(|args: &[Value]| { - let n = args[0].as_int().unwrap_or(0) as usize; - Value::Series(Rc::new(ScalarSeries::::new("IntSeries", n))) - }), - purity: Purity::Impure, - })), - StaticType::Bool => Value::Function(Rc::new(NativeFunction { - func: Rc::new(|args: &[Value]| { - let n = args[0].as_int().unwrap_or(0) as usize; - Value::Series(Rc::new(ScalarSeries::::new("BoolSeries", n))) - }), - purity: Purity::Impure, - })), - StaticType::Text => Value::Function(Rc::new(NativeFunction { - func: Rc::new(|args: &[Value]| { - let n = args[0].as_int().unwrap_or(0) as usize; - Value::Series(Rc::new(ValueSeries::new(n))) - }), - purity: Purity::Impure, - })), - StaticType::Record(layout) => { - let layout = Arc::clone(layout); - Value::Function(Rc::new(NativeFunction { - func: Rc::new(move |args: &[Value]| { + fn finalize( + &self, + _callee: Rc, + args: Rc, + node_ty: &StaticType, + _subst: &HashMap, + ) -> Option> { + let StaticType::Series(inner) = node_ty else { return None }; + let NodeKind::Tuple { elements } = &args.kind else { return None }; + if elements.len() != 1 { + return None; + } + + // Build a factory Value::Function whose closure already knows the exact + // storage type — no keyword encoding, no runtime dispatch. + let factory_value: Value = match inner.as_ref() { + StaticType::Float => Value::Function(Rc::new(NativeFunction { + func: Rc::new(|args: &[Value]| { let n = args[0].as_int().unwrap_or(0) as usize; - Value::Series(Rc::new(RecordSeries::new(Arc::clone(&layout), n))) + Value::Series(Rc::new(ScalarSeries::::new("FloatSeries", n))) }), purity: Purity::Impure, - })) - } - _ => return None, - }; + })), + StaticType::Int | StaticType::DateTime => Value::Function(Rc::new(NativeFunction { + func: Rc::new(|args: &[Value]| { + let n = args[0].as_int().unwrap_or(0) as usize; + Value::Series(Rc::new(ScalarSeries::::new("IntSeries", n))) + }), + purity: Purity::Impure, + })), + StaticType::Bool => Value::Function(Rc::new(NativeFunction { + func: Rc::new(|args: &[Value]| { + let n = args[0].as_int().unwrap_or(0) as usize; + Value::Series(Rc::new(ScalarSeries::::new("BoolSeries", n))) + }), + purity: Purity::Impure, + })), + StaticType::Text => Value::Function(Rc::new(NativeFunction { + func: Rc::new(|args: &[Value]| { + let n = args[0].as_int().unwrap_or(0) as usize; + Value::Series(Rc::new(ValueSeries::new(n))) + }), + purity: Purity::Impure, + })), + StaticType::Record(layout) => { + let layout = Arc::clone(layout); + Value::Function(Rc::new(NativeFunction { + func: Rc::new(move |args: &[Value]| { + let n = args[0].as_int().unwrap_or(0) as usize; + Value::Series(Rc::new(RecordSeries::new(Arc::clone(&layout), n))) + }), + purity: Purity::Impure, + })) + } + _ => return None, + }; - let factory_node = Rc::new(Node { - kind: NodeKind::Constant(factory_value), - ty: StaticType::Any, - identity: NodeIdentity::new(SourceLocation { line: 0, col: 0 }), - comments: Rc::from([]), - }); - Some(NodeKind::Call { callee: factory_node, args: Rc::clone(&args) }) + let factory_node = Rc::new(Node { + kind: NodeKind::Constant(factory_value), + ty: StaticType::Any, + identity: NodeIdentity::new(SourceLocation { line: 0, col: 0 }), + comments: Rc::from([]), + }); + Some(NodeKind::Call { callee: factory_node, args: Rc::clone(&args) }) + } } -/// Post-call hook for `(push series val)`. +/// Compiler hook for `(push series val)`. +/// /// Unifies the series element TypeVar with the pushed value type, applying /// Int→Float numeric promotion and record field promotion when needed. /// @@ -111,50 +117,55 @@ fn series_finalize( /// to recover the TypeVar ID and rebind it (e.g. Int → Float promotion). /// All identifier lookups call `apply_subst`, so the resolved type is /// always correct regardless of what ctx stores. -fn push_post_call( - args: &TypedNode, - ret_ty: StaticType, - ctx: &dyn InferenceAccess, - diag: &mut Diagnostics, -) -> StaticType { - let NodeKind::Tuple { elements } = &args.kind else { return ret_ty }; - if elements.len() < 2 { - return ret_ty; - } - let series_arg = &elements[0]; - let value_arg = &elements[1]; - let StaticType::Series(inner) = &series_arg.ty else { return ret_ty }; - let inner_ty = (**inner).clone(); - let val_ty = value_arg.ty.clone(); +pub struct PushHook; - if let NodeKind::Identifier { - binding: IdentifierBinding::Reference(addr), - .. - } = &series_arg.kind - { - // Recover the raw inner type from the scope slot (before apply_subst) - // so we can find the TypeVar ID even after the first push resolved it. - let raw_inner = match ctx.get_slot_type(*addr) { - StaticType::Series(ri) => *ri, - _ => inner_ty.clone(), - }; +impl RtlCompilerHook for PushHook { + fn post_call( + &self, + args: &TypedNode, + ret_ty: StaticType, + ctx: &dyn InferenceAccess, + diag: &mut Diagnostics, + ) -> StaticType { + let NodeKind::Tuple { elements } = &args.kind else { return ret_ty }; + if elements.len() < 2 { + return ret_ty; + } + let series_arg = &elements[0]; + let value_arg = &elements[1]; + let StaticType::Series(inner) = &series_arg.ty else { return ret_ty }; + let inner_ty = (**inner).clone(); + let val_ty = value_arg.ty.clone(); - if let Some(promoted) = ctx - .try_numeric_widen(&inner_ty, &val_ty) - .or_else(|| ctx.try_record_promote(&inner_ty, &val_ty)) + if let NodeKind::Identifier { + binding: IdentifierBinding::Reference(addr), + .. + } = &series_arg.kind { - // Rebind the TypeVar to the promoted type so that finalize - // injects the correct schema (e.g. :float instead of :int). - if let StaticType::TypeVar(n) = raw_inner { - ctx.bind_typevar(n, promoted.clone()); + // Recover the raw inner type from the scope slot (before apply_subst) + // so we can find the TypeVar ID even after the first push resolved it. + let raw_inner = match ctx.get_slot_type(*addr) { + StaticType::Series(ri) => *ri, + _ => inner_ty.clone(), + }; + + if let Some(promoted) = ctx + .try_numeric_widen(&inner_ty, &val_ty) + .or_else(|| ctx.try_record_promote(&inner_ty, &val_ty)) + { + // Rebind the TypeVar to the promoted type so that finalize + // injects the correct schema (e.g. :float instead of :int). + if let StaticType::TypeVar(n) = raw_inner { + ctx.bind_typevar(n, promoted.clone()); + } + } else { + ctx.unify(inner_ty, val_ty, diag); } } else { ctx.unify(inner_ty, val_ty, diag); } - } else { - ctx.unify(inner_ty, val_ty, diag); + ret_ty } - ret_ty } // ============================================================================ @@ -174,14 +185,14 @@ pub fn register(env: &Environment) { })), Purity::Impure, |args: &[Value]| { - // The FinalizeHook replaces this call with a pre-configured factory + // The finalize hook replaces this call with a pre-configured factory // for resolved element types. This fallback only runs when the element // type could not be determined at compile time (e.g. push inside a // nested closure the type checker could not reach). let lookback_limit = args[0].as_int().unwrap_or(0) as usize; Value::Series(Rc::new(ValueSeries::new(lookback_limit))) }, - ).with_call_hooks(Some(series_post_call as PostCallHook), Some(series_finalize as FinalizeHook)) + ).with_compiler_hook(Rc::new(SeriesHook)) .doc("Creates a new series (ring buffer) with the given lookback limit.") .description("Takes a single lookback limit (int). The element type is inferred at compile time from subsequent push calls via HM type inference. The compiler injects a second schema argument automatically; the runtime also accepts an explicit schema for cases where inference is not possible.") .examples(&[ @@ -209,7 +220,7 @@ pub fn register(env: &Environment) { } panic!("push expects a series as the first argument") }, - ).with_call_hooks(Some(push_post_call as PostCallHook), None) + ).with_compiler_hook(Rc::new(PushHook)) .doc("Pushes a new value into a series. After push, index 0 returns this value.") .examples(&[ "(push my-ticks {:price 10.5 :volume 100})",