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:
+131
-120
@@ -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::<f64>` for
|
||||
/// Float, `RecordSeries` with a captured `Arc<RecordLayout>` 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::<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>,
|
||||
args: Rc<TypedNode>,
|
||||
node_ty: &StaticType,
|
||||
_subst: &HashMap<u32, StaticType>,
|
||||
) -> Option<NodeKind<TypedPhase>> {
|
||||
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::<f64>::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::<i64>::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::<bool>::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<TypedNode>,
|
||||
args: Rc<TypedNode>,
|
||||
node_ty: &StaticType,
|
||||
_subst: &HashMap<u32, StaticType>,
|
||||
) -> Option<NodeKind<TypedPhase>> {
|
||||
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::<f64>::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::<i64>::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::<bool>::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})",
|
||||
|
||||
Reference in New Issue
Block a user