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<dyn RtlCompilerHook>`, enabling
different RTL functions to register their specific compiler hooks. The
`SeriesHook` and `PushHook` structs are introduced to demonstrate this
new pattern.
This commit is contained in:
2026-03-29 14:52:58 +02:00
parent 916460ab03
commit a665e2c1a5
5 changed files with 216 additions and 204 deletions
+35 -26
View File
@@ -5,8 +5,8 @@ use crate::ast::diagnostics::Diagnostics;
use crate::ast::nodes::{Address, NodeKind, TypedNode, TypedPhase, VirtualId}; use crate::ast::nodes::{Address, NodeKind, TypedNode, TypedPhase, VirtualId};
use crate::ast::types::StaticType; use crate::ast::types::StaticType;
/// Gives a call-hook access to the type-checker's core inference operations and /// Gives a compiler hook access to the type-checker's core inference operations
/// scope slot types without depending on the concrete `TypeChecker` type /// and scope slot types without depending on the concrete `TypeChecker` type
/// (avoids a circular module dependency). /// (avoids a circular module dependency).
pub trait InferenceAccess { pub trait InferenceAccess {
/// Allocate a fresh unique `TypeVar`. /// Allocate a fresh unique `TypeVar`.
@@ -35,29 +35,38 @@ pub trait InferenceAccess {
fn get_slot_type(&self, addr: Address<VirtualId>) -> StaticType; fn get_slot_type(&self, addr: Address<VirtualId>) -> StaticType;
} }
/// Called after the call's return type is resolved. /// Extension point that lets RTL functions participate in the compiler's
/// May replace the return type and/or perform unification side-effects (e.g. push). /// type-inference and AST-finalization passes without hardcoding symbol
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<TypedNode>,
args: Rc<TypedNode>,
node_ty: &StaticType,
subst: &HashMap<u32, StaticType>,
) -> Option<NodeKind<TypedPhase>>;
/// Hooks attached to a specific RTL function slot.
/// Enables type-inference extensions and AST rewrites without hardcoding symbol
/// names anywhere in the compiler core. /// names anywhere in the compiler core.
#[derive(Default)] ///
pub struct CallHooks { /// Each RTL function that needs custom type-level behaviour (e.g. `series`,
pub post_call: Option<PostCallHook>, /// `push`) implements this trait. Hooks are registered by global slot index
pub finalize: Option<FinalizeHook>, /// 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<TypedNode>,
_args: Rc<TypedNode>,
_node_ty: &StaticType,
_subst: &HashMap<u32, StaticType>,
) -> Option<NodeKind<TypedPhase>> {
None
}
} }
+10 -12
View File
@@ -1,4 +1,4 @@
use crate::ast::compiler::call_hooks::{CallHooks, InferenceAccess}; use crate::ast::compiler::call_hooks::{InferenceAccess, RtlCompilerHook};
use crate::ast::nodes::{ use crate::ast::nodes::{
Address, AssignBinding, BoundLike, BoundPhase, DefBinding, IdentifierBinding, LambdaBinding, Address, AssignBinding, BoundLike, BoundPhase, DefBinding, IdentifierBinding, LambdaBinding,
Node, NodeKind, TypedNode, TypedPhase, VirtualId, 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, /// lazily in `bind_var`: when the callee TypeVar is bound to a concrete type,
/// the element type is extracted directly from the `Series` variant. /// the element type is extracted directly from the `Series` variant.
index_constraints: std::cell::RefCell<HashMap<u32, u32>>, index_constraints: std::cell::RefCell<HashMap<u32, u32>>,
/// 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. /// Populated from the frozen RTL snapshot; empty for non-RTL call sites.
call_hooks: Rc<HashMap<u32, CallHooks>>, compiler_hooks: Rc<HashMap<u32, Rc<dyn RtlCompilerHook>>>,
} }
impl TypeChecker { impl TypeChecker {
pub fn new( pub fn new(
root_types: Rc<std::cell::RefCell<Vec<StaticType>>>, root_types: Rc<std::cell::RefCell<Vec<StaticType>>>,
call_hooks: Rc<HashMap<u32, CallHooks>>, compiler_hooks: Rc<HashMap<u32, Rc<dyn RtlCompilerHook>>>,
) -> Self { ) -> Self {
Self { Self {
root_types, root_types,
var_counter: Cell::new(0), var_counter: Cell::new(0),
subst: std::cell::RefCell::new(HashMap::new()), subst: std::cell::RefCell::new(HashMap::new()),
index_constraints: std::cell::RefCell::new(HashMap::new()), index_constraints: std::cell::RefCell::new(HashMap::new()),
call_hooks, compiler_hooks,
} }
} }
@@ -621,9 +621,8 @@ impl TypeChecker {
binding: IdentifierBinding::Reference(Address::Global(idx)), binding: IdentifierBinding::Reference(Address::Global(idx)),
.. ..
} = &callee_fin.kind } = &callee_fin.kind
&& let Some(hook) = self.call_hooks.get(&idx.0) && let Some(hook) = self.compiler_hooks.get(&idx.0)
&& let Some(finalize) = hook.finalize && let Some(new_kind) = hook.finalize(
&& let Some(new_kind) = finalize(
Rc::clone(&callee_fin), Rc::clone(&callee_fin),
Rc::clone(&args_fin), Rc::clone(&args_fin),
node_ty, node_ty,
@@ -1459,7 +1458,7 @@ impl TypeChecker {
ret_ty = Self::apply_subst(ret_ty, &self.subst.borrow()); 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: // Hooks handle type-inference extensions such as:
// - series: inject a fresh TypeVar for the element type // - series: inject a fresh TypeVar for the element type
// - push: unify the series element TypeVar with the pushed value type // - push: unify the series element TypeVar with the pushed value type
@@ -1467,10 +1466,9 @@ impl TypeChecker {
binding: IdentifierBinding::Reference(Address::Global(idx)), binding: IdentifierBinding::Reference(Address::Global(idx)),
.. ..
} = &callee_typed.kind } = &callee_typed.kind
&& let Some(hook) = self.call_hooks.get(&idx.0) && let Some(hook) = self.compiler_hooks.get(&idx.0) {
&& let Some(post_call) = hook.post_call {
let hook_ctx = CheckerInferenceAccess { checker: self, ctx }; let hook_ctx = CheckerInferenceAccess { checker: self, ctx };
ret_ty = post_call(&args_typed, ret_ty, &hook_ctx, diag); ret_ty = hook.post_call(&args_typed, ret_ty, &hook_ctx, diag);
} }
( (
+20 -20
View File
@@ -1,6 +1,6 @@
use crate::ast::compiler::analyzer::Analyzer; use crate::ast::compiler::analyzer::Analyzer;
use crate::ast::compiler::binder::{Binder, CompilerScope, LocalInfo}; 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::compiler::{CapturePass, TypeChecker, TypedNode};
use crate::ast::nodes::{AnalyzedPhase, Symbol, SyntaxKind, SyntaxNode}; use crate::ast::nodes::{AnalyzedPhase, Symbol, SyntaxKind, SyntaxNode};
use crate::ast::parser::Parser; use crate::ast::parser::Parser;
@@ -54,9 +54,9 @@ pub struct Rtl {
pub purity: Vec<Purity>, pub purity: Vec<Purity>,
pub values: Rc<[Value]>, pub values: Rc<[Value]>,
pub rtl_docs: Vec<RtlDocEntry>, pub rtl_docs: Vec<RtlDocEntry>,
/// Type-inference and finalization hooks, keyed by global slot index. /// Compiler hooks, keyed by global slot index.
/// Populated during RTL registration via `RtlRegistration::with_call_hooks`. /// Populated during RTL registration via `RtlRegistration::with_compiler_hook`.
pub call_hooks: Rc<HashMap<u32, CallHooks>>, pub compiler_hooks: Rc<HashMap<u32, Rc<dyn RtlCompilerHook>>>,
} }
pub struct Environment { pub struct Environment {
@@ -83,9 +83,9 @@ pub struct Environment {
/// Documentation for symbols defined in Myc source (via `;;` comments before `def`/`macro`). /// Documentation for symbols defined in Myc source (via `;;` comments before `def`/`macro`).
/// Key: symbol name. Value: concatenated doc lines joined by newlines. /// Key: symbol name. Value: concatenated doc lines joined by newlines.
pub myc_docs: Rc<RefCell<HashMap<String, String>>>, pub myc_docs: Rc<RefCell<HashMap<String, String>>>,
/// Staging area for call hooks registered during RTL bootstrap. /// Staging area for compiler hooks registered during RTL bootstrap.
/// Frozen into `Rtl::call_hooks` after `rtl::register()` completes. /// Frozen into `Rtl::compiler_hooks` after `rtl::register()` completes.
pub call_hooks: Rc<RefCell<HashMap<u32, CallHooks>>>, pub compiler_hooks: Rc<RefCell<HashMap<u32, Rc<dyn RtlCompilerHook>>>>,
/// Frozen RTL snapshot from which this environment was created. /// Frozen RTL snapshot from which this environment was created.
pub rtl: Rc<Rtl>, pub rtl: Rc<Rtl>,
} }
@@ -111,7 +111,7 @@ struct RuntimeMacroEvaluator {
globals: GlobalStore, globals: GlobalStore,
root_purity: Rc<RefCell<Vec<Purity>>>, root_purity: Rc<RefCell<Vec<Purity>>>,
fixed_scope_idx: i32, fixed_scope_idx: i32,
call_hooks: Rc<HashMap<u32, CallHooks>>, compiler_hooks: Rc<HashMap<u32, Rc<dyn RtlCompilerHook>>>,
} }
impl MacroEvaluator for RuntimeMacroEvaluator { 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, 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 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); let typed_ast = checker.check(&bound_ast, &[], &mut diag);
if diag.has_errors() { if diag.has_errors() {
@@ -175,7 +175,7 @@ impl Environment {
loaded_modules: Rc::new(RefCell::new(HashSet::new())), loaded_modules: Rc::new(RefCell::new(HashSet::new())),
rtl_docs: Rc::new(RefCell::new(Vec::new())), rtl_docs: Rc::new(RefCell::new(Vec::new())),
myc_docs: Rc::new(RefCell::new(HashMap::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. // Placeholder — overwritten below after bootstrap.
rtl: Rc::new(Rtl { rtl: Rc::new(Rtl {
scope: CompilerScope::new(), scope: CompilerScope::new(),
@@ -184,7 +184,7 @@ impl Environment {
purity: vec![], purity: vec![],
values: Rc::from([]), values: Rc::from([]),
rtl_docs: vec![], 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(); let rtl_slot_count = *env.root_slot_count.borrow();
// user_values was used as the bootstrap scratch; freeze it into an immutable slice. // 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 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 { env.rtl = Rc::new(Rtl {
scope: env.root_scopes.borrow()[0].clone(), scope: env.root_scopes.borrow()[0].clone(),
slot_count: rtl_slot_count, slot_count: rtl_slot_count,
@@ -203,7 +203,7 @@ impl Environment {
purity: env.root_purity.borrow().clone(), purity: env.root_purity.borrow().clone(),
values: Rc::clone(&rtl_vals), values: Rc::clone(&rtl_vals),
rtl_docs: std::mem::take(&mut env.rtl_docs.borrow_mut()), 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 // Set the frozen RTL slice. Stream closures registered during bootstrap
@@ -257,7 +257,7 @@ impl Environment {
loaded_modules: Rc::new(RefCell::new(HashSet::new())), loaded_modules: Rc::new(RefCell::new(HashSet::new())),
rtl_docs: Rc::new(RefCell::new(Vec::new())), rtl_docs: Rc::new(RefCell::new(Vec::new())),
myc_docs: Rc::new(RefCell::new(HashMap::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, rtl,
}; };
@@ -319,7 +319,7 @@ impl Environment {
globals: self.global_store(), globals: self.global_store(),
root_purity: self.root_purity.clone(), root_purity: self.root_purity.clone(),
fixed_scope_idx: self.fixed_scope_idx, 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) MacroExpander::new(self.macro_registry.borrow().clone(), evaluator)
} }
@@ -480,7 +480,7 @@ impl Environment {
LambdaCollector::collect(&bound_ast, &mut self.function_registry.borrow_mut()); 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 { let wrapped_ast = if let NodeKind::Lambda { .. } = bound_ast.kind {
bound_ast bound_ast
} else { } else {
@@ -572,7 +572,7 @@ impl Environment {
name.to_string(), name.to_string(),
slot_idx, slot_idx,
Rc::clone(&self.rtl_docs), Rc::clone(&self.rtl_docs),
Rc::clone(&self.call_hooks), Rc::clone(&self.compiler_hooks),
) )
} }
@@ -582,7 +582,7 @@ impl Environment {
name.to_string(), name.to_string(),
slot_idx, slot_idx,
Rc::clone(&self.rtl_docs), 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_types = self.root_types.clone();
let root_purity = self.root_purity.clone(); let root_purity = self.root_purity.clone();
let optimization = self.optimization; 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( let compiler = Rc::new(
move |func_template: Rc<Node<AnalyzedPhase>>, move |func_template: Rc<Node<AnalyzedPhase>>,
arg_types: &[StaticType]| arg_types: &[StaticType]|
-> Result<(Value, StaticType), String> { -> Result<(Value, StaticType), String> {
let mut diag = Diagnostics::new(); 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 // Monomorphization: re-type-check the function template with the concrete
// call-site argument types, producing a specialized TypedNode. // call-site argument types, producing a specialized TypedNode.
+10 -16
View File
@@ -2,7 +2,7 @@ use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; 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. /// A generator function for data pipelines. Returns `true` while data is available.
pub type PipelineGenerator = Box<dyn FnMut() -> bool>; pub type PipelineGenerator = Box<dyn FnMut() -> bool>;
@@ -22,13 +22,13 @@ pub struct RtlDocEntry {
/// Builder returned by `register_native_fn` and `register_constant`. /// Builder returned by `register_native_fn` and `register_constant`.
/// Call `.doc(...)` to attach documentation; optional `.description(...)` and `.examples(...)` /// Call `.doc(...)` to attach documentation; optional `.description(...)` and `.examples(...)`
/// can be chained. The entry is written to the registry when the builder is dropped. /// 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 /// Optionally, call `.with_compiler_hook(...)` to register an [`RtlCompilerHook`]
/// hooks for this slot — keyed by its global slot index, not by name. /// for this slot — keyed by its global slot index, not by name.
pub struct RtlRegistration { pub struct RtlRegistration {
name: String, name: String,
slot_idx: u32, slot_idx: u32,
rtl_docs: Rc<RefCell<Vec<RtlDocEntry>>>, rtl_docs: Rc<RefCell<Vec<RtlDocEntry>>>,
call_hooks_registry: Rc<RefCell<HashMap<u32, CallHooks>>>, hook_registry: Rc<RefCell<HashMap<u32, Rc<dyn RtlCompilerHook>>>>,
one_liner: Option<&'static str>, one_liner: Option<&'static str>,
description: Option<&'static str>, description: Option<&'static str>,
examples: Option<&'static [&'static str]>, examples: Option<&'static [&'static str]>,
@@ -39,13 +39,13 @@ impl RtlRegistration {
name: String, name: String,
slot_idx: u32, slot_idx: u32,
rtl_docs: Rc<RefCell<Vec<RtlDocEntry>>>, rtl_docs: Rc<RefCell<Vec<RtlDocEntry>>>,
call_hooks_registry: Rc<RefCell<HashMap<u32, CallHooks>>>, hook_registry: Rc<RefCell<HashMap<u32, Rc<dyn RtlCompilerHook>>>>,
) -> Self { ) -> Self {
Self { Self {
name, name,
slot_idx, slot_idx,
rtl_docs, rtl_docs,
call_hooks_registry, hook_registry,
one_liner: None, one_liner: None,
description: None, description: None,
examples: None, examples: None,
@@ -70,18 +70,12 @@ impl RtlRegistration {
self 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. /// Hooks are keyed by the slot's global index — no name matching in the compiler.
pub fn with_call_hooks( pub fn with_compiler_hook(self, hook: Rc<dyn RtlCompilerHook>) -> Self {
self, self.hook_registry
post_call: Option<crate::ast::compiler::call_hooks::PostCallHook>,
finalize: Option<crate::ast::compiler::call_hooks::FinalizeHook>,
) -> Self {
if post_call.is_some() || finalize.is_some() {
self.call_hooks_registry
.borrow_mut() .borrow_mut()
.insert(self.slot_idx, CallHooks { post_call, finalize }); .insert(self.slot_idx, hook);
}
self self
} }
} }
+32 -21
View File
@@ -5,44 +5,48 @@ use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; 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::diagnostics::Diagnostics;
use crate::ast::environment::Environment; use crate::ast::environment::Environment;
use crate::ast::nodes::{IdentifierBinding, Node, NodeKind, TypedNode, TypedPhase}; use crate::ast::nodes::{IdentifierBinding, Node, NodeKind, TypedNode, TypedPhase};
use crate::ast::types::{NativeFunction, NodeIdentity, Purity, Signature, SourceLocation, StaticType, Value}; 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)`. /// Compiler 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). /// - **post_call:** Replaces the `Series(Any)` return type with a fresh TypeVar
fn series_post_call( /// 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::<f64>` for
/// Float, `RecordSeries` with a captured `Arc<RecordLayout>` for record types).
pub struct SeriesHook;
impl RtlCompilerHook for SeriesHook {
fn post_call(
&self,
_args: &TypedNode, _args: &TypedNode,
ret_ty: StaticType, ret_ty: StaticType,
ctx: &dyn InferenceAccess, ctx: &dyn InferenceAccess,
_diag: &mut Diagnostics, _diag: &mut Diagnostics,
) -> StaticType { ) -> StaticType {
if let StaticType::Series(inner) = &ret_ty if let StaticType::Series(inner) = &ret_ty
&& **inner == StaticType::Any && **inner == StaticType::Any
{ {
return StaticType::Series(Box::new(ctx.fresh_var())); return StaticType::Series(Box::new(ctx.fresh_var()));
} }
ret_ty ret_ty
} }
/// Finalization hook for `(series n)`. fn finalize(
/// Replaces the callee with a pre-configured factory closure that directly &self,
/// allocates the correct storage type (e.g. `ScalarSeries::<f64>` for Float,
/// `RecordSeries` with a captured `Arc<RecordLayout>` for record types).
/// No schema argument is needed — the type is captured in the closure.
fn series_finalize(
_callee: Rc<TypedNode>, _callee: Rc<TypedNode>,
args: Rc<TypedNode>, args: Rc<TypedNode>,
node_ty: &StaticType, node_ty: &StaticType,
_subst: &HashMap<u32, StaticType>, _subst: &HashMap<u32, StaticType>,
) -> Option<NodeKind<TypedPhase>> { ) -> Option<NodeKind<TypedPhase>> {
let StaticType::Series(inner) = node_ty else { return None }; let StaticType::Series(inner) = node_ty else { return None };
let NodeKind::Tuple { elements } = &args.kind else { return None }; let NodeKind::Tuple { elements } = &args.kind else { return None };
if elements.len() != 1 { if elements.len() != 1 {
@@ -100,9 +104,11 @@ fn series_finalize(
comments: Rc::from([]), comments: Rc::from([]),
}); });
Some(NodeKind::Call { callee: factory_node, args: Rc::clone(&args) }) 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 /// Unifies the series element TypeVar with the pushed value type, applying
/// Int→Float numeric promotion and record field promotion when needed. /// Int→Float numeric promotion and record field promotion when needed.
/// ///
@@ -111,12 +117,16 @@ fn series_finalize(
/// to recover the TypeVar ID and rebind it (e.g. Int → Float promotion). /// to recover the TypeVar ID and rebind it (e.g. Int → Float promotion).
/// All identifier lookups call `apply_subst`, so the resolved type is /// All identifier lookups call `apply_subst`, so the resolved type is
/// always correct regardless of what ctx stores. /// always correct regardless of what ctx stores.
fn push_post_call( pub struct PushHook;
impl RtlCompilerHook for PushHook {
fn post_call(
&self,
args: &TypedNode, args: &TypedNode,
ret_ty: StaticType, ret_ty: StaticType,
ctx: &dyn InferenceAccess, ctx: &dyn InferenceAccess,
diag: &mut Diagnostics, diag: &mut Diagnostics,
) -> StaticType { ) -> StaticType {
let NodeKind::Tuple { elements } = &args.kind else { return ret_ty }; let NodeKind::Tuple { elements } = &args.kind else { return ret_ty };
if elements.len() < 2 { if elements.len() < 2 {
return ret_ty; return ret_ty;
@@ -155,6 +165,7 @@ fn push_post_call(
ctx.unify(inner_ty, val_ty, diag); ctx.unify(inner_ty, val_ty, diag);
} }
ret_ty ret_ty
}
} }
// ============================================================================ // ============================================================================
@@ -174,14 +185,14 @@ pub fn register(env: &Environment) {
})), })),
Purity::Impure, Purity::Impure,
|args: &[Value]| { |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 // for resolved element types. This fallback only runs when the element
// type could not be determined at compile time (e.g. push inside a // type could not be determined at compile time (e.g. push inside a
// nested closure the type checker could not reach). // nested closure the type checker could not reach).
let lookback_limit = args[0].as_int().unwrap_or(0) as usize; let lookback_limit = args[0].as_int().unwrap_or(0) as usize;
Value::Series(Rc::new(ValueSeries::new(lookback_limit))) 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.") .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.") .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(&[ .examples(&[
@@ -209,7 +220,7 @@ pub fn register(env: &Environment) {
} }
panic!("push expects a series as the first argument") 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.") .doc("Pushes a new value into a series. After push, index 0 returns this value.")
.examples(&[ .examples(&[
"(push my-ticks {:price 10.5 :volume 100})", "(push my-ticks {:price 10.5 :volume 100})",