Add call hooks for series and push
Introduces `CallHooks` to enable extensions to the type inference and AST finalization process. This allows for more dynamic and configurable behavior without hardcoding symbol names in the compiler core. Specifically, this commit adds: - **`series_post_call`**: A hook for the `(series n)` function. It ensures that the return type is a fresh `TypeVar` when the element type is `Any`, allowing for proper type inference from subsequent `push` calls. - **`series_finalize`**: A hook for `(series n)` that rewrites the AST node. It replaces the generic `series` call with a pre-configured factory closure that allocates the correct series storage type (e.g., `ScalarSeries<f64>` for floats, `RecordSeries` for records). This avoids runtime dispatch and relies on type information captured during compilation. - **`push_post_call`**: A hook for the `(push series val)` function. It unifies the series element `TypeVar` with the pushed value's type. It also handles numeric and record field promotion and updates the `TypeVar` binding if a promotion occurs.
This commit is contained in:
@@ -2,7 +2,7 @@ use crate::ast::nodes::{
|
||||
Address, AnalyzedNode, GlobalIdx, IdentifierBinding, Node,
|
||||
NodeKind, NodeMetrics, TypedNode, TypedPhase,
|
||||
};
|
||||
use crate::ast::types::{Identity, Purity};
|
||||
use crate::ast::types::{Identity, Purity, Value};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -70,7 +70,12 @@ impl<'a> Analyzer<'a> {
|
||||
let mut is_recursive = false;
|
||||
|
||||
let (new_kind, purity) = match &node.kind {
|
||||
NodeKind::Constant(v) => (NodeKind::Constant(v.clone()), Purity::Pure),
|
||||
// Propagate the declared purity of function constants so the constant
|
||||
// folder does not evaluate impure factory closures at compile time.
|
||||
NodeKind::Constant(v) => {
|
||||
let purity = if let Value::Function(f) = v { f.purity } else { Purity::Pure };
|
||||
(NodeKind::Constant(v.clone()), purity)
|
||||
}
|
||||
NodeKind::Nop => (NodeKind::Nop, Purity::Pure),
|
||||
|
||||
NodeKind::Identifier { symbol, binding } => {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::ast::diagnostics::Diagnostics;
|
||||
use crate::ast::nodes::{Address, NodeKind, TypedNode, TypedPhase, VirtualId};
|
||||
use crate::ast::types::StaticType;
|
||||
|
||||
/// Gives a call-hook read/write access to the current scope's slot types without
|
||||
/// exposing `TypeContext` internals.
|
||||
pub trait TypeSlotAccess {
|
||||
fn get_slot_type(&self, addr: Address<VirtualId>) -> StaticType;
|
||||
fn set_slot_type(&mut self, addr: Address<VirtualId>, ty: StaticType);
|
||||
}
|
||||
|
||||
/// Gives a call-hook access to the type-checker's core inference operations without
|
||||
/// depending on the concrete `TypeChecker` type (avoids a circular module dependency).
|
||||
pub trait TypeInferenceAccess {
|
||||
/// Allocate a fresh unique `TypeVar`.
|
||||
fn fresh_var(&self) -> StaticType;
|
||||
|
||||
/// Unify two types, recording the substitution. Emits a diagnostic on conflict.
|
||||
fn unify(&self, a: StaticType, b: StaticType, diag: &mut Diagnostics);
|
||||
|
||||
/// Directly bind a `TypeVar` ID to a resolved type in the substitution map.
|
||||
fn bind_typevar(&self, id: u32, ty: StaticType);
|
||||
|
||||
/// Apply the current substitution to a type, resolving all known `TypeVar`s.
|
||||
fn apply_subst_ty(&self, ty: StaticType) -> StaticType;
|
||||
|
||||
/// Int/Float numeric-widening rule. Returns the promoted type, or `None` if
|
||||
/// the combination is not a widening pair.
|
||||
fn try_numeric_widen(&self, a: &StaticType, b: &StaticType) -> Option<StaticType>;
|
||||
|
||||
/// Record-field promotion rule. Returns the promoted record type when one record
|
||||
/// is a strict widening of the other, or `None` otherwise.
|
||||
fn try_record_promote(&self, a: &StaticType, b: &StaticType) -> Option<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,
|
||||
inference: &dyn TypeInferenceAccess,
|
||||
ctx: &mut dyn TypeSlotAccess,
|
||||
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.
|
||||
#[derive(Default)]
|
||||
pub struct CallHooks {
|
||||
pub post_call: Option<PostCallHook>,
|
||||
pub finalize: Option<FinalizeHook>,
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod analyzer;
|
||||
pub mod binder;
|
||||
pub mod call_hooks;
|
||||
pub mod module_loader;
|
||||
pub mod captures;
|
||||
pub mod dumper;
|
||||
|
||||
+102
-146
@@ -1,9 +1,10 @@
|
||||
use crate::ast::compiler::call_hooks::{CallHooks, TypeInferenceAccess, TypeSlotAccess};
|
||||
use crate::ast::nodes::{
|
||||
Address, AssignBinding, BoundLike, BoundPhase, DefBinding, IdentifierBinding, LambdaBinding,
|
||||
Node, NodeKind, TypedNode, TypedPhase, VirtualId,
|
||||
};
|
||||
use crate::ast::diagnostics::Diagnostics;
|
||||
use crate::ast::types::{Keyword, NodeIdentity, RecordLayout, Signature, StaticType, Value};
|
||||
use crate::ast::types::{Keyword, NodeIdentity, RecordLayout, Signature, StaticType};
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
@@ -74,6 +75,42 @@ impl<'a> TypeContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeSlotAccess for TypeContext<'_> {
|
||||
fn get_slot_type(&self, addr: Address<VirtualId>) -> StaticType {
|
||||
self.get_type(addr)
|
||||
}
|
||||
|
||||
fn set_slot_type(&mut self, addr: Address<VirtualId>, ty: StaticType) {
|
||||
self.set_type(addr, ty);
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeInferenceAccess for TypeChecker {
|
||||
fn fresh_var(&self) -> StaticType {
|
||||
self.fresh_var()
|
||||
}
|
||||
|
||||
fn unify(&self, a: StaticType, b: StaticType, diag: &mut Diagnostics) {
|
||||
self.unify(a, b, diag);
|
||||
}
|
||||
|
||||
fn bind_typevar(&self, id: u32, ty: StaticType) {
|
||||
self.subst.borrow_mut().insert(id, ty);
|
||||
}
|
||||
|
||||
fn apply_subst_ty(&self, ty: StaticType) -> StaticType {
|
||||
Self::apply_subst(ty, &self.subst.borrow())
|
||||
}
|
||||
|
||||
fn try_numeric_widen(&self, a: &StaticType, b: &StaticType) -> Option<StaticType> {
|
||||
Self::numeric_widen(a, b)
|
||||
}
|
||||
|
||||
fn try_record_promote(&self, a: &StaticType, b: &StaticType) -> Option<StaticType> {
|
||||
Self::record_promote(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts expected lambda parameter types from the callee type for a specific argument position.
|
||||
/// Used for bidirectional type inference: when a Call's callee expects a function at `arg_index`,
|
||||
/// this returns the expected parameter types for that function, derived from the callee's signature
|
||||
@@ -137,14 +174,21 @@ pub struct TypeChecker {
|
||||
/// Global substitution map: TypeVar ID → resolved StaticType.
|
||||
/// Shared across all scopes within a single type-checking pass.
|
||||
subst: std::cell::RefCell<HashMap<u32, StaticType>>,
|
||||
/// Type-inference and finalization hooks keyed by global slot index.
|
||||
/// Populated from the frozen RTL snapshot; empty for non-RTL call sites.
|
||||
call_hooks: Rc<HashMap<u32, CallHooks>>,
|
||||
}
|
||||
|
||||
impl TypeChecker {
|
||||
pub fn new(root_types: Rc<std::cell::RefCell<Vec<StaticType>>>) -> Self {
|
||||
pub fn new(
|
||||
root_types: Rc<std::cell::RefCell<Vec<StaticType>>>,
|
||||
call_hooks: Rc<HashMap<u32, CallHooks>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
root_types,
|
||||
var_counter: Cell::new(0),
|
||||
subst: std::cell::RefCell::new(HashMap::new()),
|
||||
call_hooks,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,11 +305,6 @@ 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)
|
||||
}
|
||||
|
||||
/// After a `FunctionOverloads` call resolves successfully, unify the matched
|
||||
/// overload's concrete parameter types with the actual argument types.
|
||||
///
|
||||
@@ -371,49 +410,20 @@ impl TypeChecker {
|
||||
Some(StaticType::Record(RecordLayout::get_or_create(promoted_fields)))
|
||||
}
|
||||
|
||||
/// 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<Value> {
|
||||
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<Vec<Value>> = 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<u32, StaticType>) -> TypedNode {
|
||||
/// and dispatches finalization hooks (e.g. schema injection for `series`).
|
||||
fn finalize_node(&self, node: TypedNode, subst: &HashMap<u32, StaticType>) -> TypedNode {
|
||||
let new_ty = Self::apply_subst(node.ty, subst);
|
||||
let new_kind = Self::finalize_kind(node.kind, subst, &new_ty, &node.identity);
|
||||
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(
|
||||
&self,
|
||||
kind: NodeKind<TypedPhase>,
|
||||
subst: &HashMap<u32, StaticType>,
|
||||
node_ty: &StaticType,
|
||||
identity: &Rc<NodeIdentity>,
|
||||
_identity: &Rc<NodeIdentity>,
|
||||
) -> NodeKind<TypedPhase> {
|
||||
match kind {
|
||||
// Leaf nodes — nothing to recurse into
|
||||
@@ -424,71 +434,61 @@ impl TypeChecker {
|
||||
NodeKind::Identifier { symbol, binding } => NodeKind::Identifier { symbol, binding },
|
||||
NodeKind::Extension(ext) => NodeKind::Extension(ext),
|
||||
|
||||
// Call: detect `(series n)` and inject the resolved schema arg
|
||||
// Call: dispatch finalize hook registered by RTL (keyed by global slot index).
|
||||
// Hooks may rewrite the call node — e.g. series injects a schema argument.
|
||||
NodeKind::Call { callee, args } => {
|
||||
let callee_fin = Rc::new(Self::finalize_node((*callee).clone(), subst));
|
||||
if Self::is_identifier_named("series", &callee_fin)
|
||||
&& let StaticType::Series(inner) = node_ty
|
||||
&& let NodeKind::Tuple { elements } = &args.kind
|
||||
&& elements.len() == 1
|
||||
&& 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<StaticType> =
|
||||
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));
|
||||
let callee_fin = Rc::new(self.finalize_node((*callee).clone(), subst));
|
||||
let args_fin = Rc::new(self.finalize_node((*args).clone(), subst));
|
||||
if let NodeKind::Identifier {
|
||||
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;
|
||||
}
|
||||
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))),
|
||||
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)),
|
||||
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)),
|
||||
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)),
|
||||
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)),
|
||||
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)))
|
||||
.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)))
|
||||
.map(|e| Rc::new(self.finalize_node((*e).clone(), subst)))
|
||||
.collect(),
|
||||
},
|
||||
NodeKind::Record { fields, layout } => NodeKind::Record {
|
||||
@@ -496,34 +496,34 @@ impl TypeChecker {
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
Rc::new(Self::finalize_node((*k).clone(), subst)),
|
||||
Rc::new(Self::finalize_node((*v).clone(), subst)),
|
||||
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)),
|
||||
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)),
|
||||
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)),
|
||||
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::Template(Rc::new(self.finalize_node((*inner).clone(), subst)))
|
||||
}
|
||||
NodeKind::Placeholder(inner) => {
|
||||
NodeKind::Placeholder(Rc::new(Self::finalize_node((*inner).clone(), subst)))
|
||||
NodeKind::Placeholder(Rc::new(self.finalize_node((*inner).clone(), subst)))
|
||||
}
|
||||
NodeKind::Splice(inner) => {
|
||||
NodeKind::Splice(Rc::new(Self::finalize_node((*inner).clone(), subst)))
|
||||
NodeKind::Splice(Rc::new(self.finalize_node((*inner).clone(), subst)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -533,7 +533,7 @@ impl TypeChecker {
|
||||
/// 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)
|
||||
self.finalize_node(node, &subst)
|
||||
}
|
||||
|
||||
pub fn check(
|
||||
@@ -1225,62 +1225,18 @@ impl TypeChecker {
|
||||
// e.g. `(+ Float TypeVar(1))` resolves TypeVar(1) = Float.
|
||||
self.unify_matched_overload(&callee_typed.ty, &args_typed.ty, diag);
|
||||
|
||||
// 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)
|
||||
&& let StaticType::Series(inner) = &ret_ty
|
||||
&& **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, applying Int→Float numeric promotion when needed.
|
||||
//
|
||||
// We intentionally do NOT bake the resolved type into ctx after normal
|
||||
// unification. Keeping `Series(TypeVar(n))` in ctx allows a later push
|
||||
// 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.
|
||||
if Self::is_identifier_named("push", &callee_typed)
|
||||
&& let NodeKind::Tuple { elements } = &args_typed.kind
|
||||
&& 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();
|
||||
|
||||
if let NodeKind::Identifier {
|
||||
binding: IdentifierBinding::Reference(addr),
|
||||
..
|
||||
} = &series_arg.kind
|
||||
{
|
||||
// Recover the raw inner type from ctx (before apply_subst) so
|
||||
// we can find the TypeVar ID even after the first push resolved it.
|
||||
let raw_inner = match ctx.get_type(*addr) {
|
||||
StaticType::Series(ri) => *ri,
|
||||
_ => inner_ty.clone(),
|
||||
};
|
||||
|
||||
if let Some(promoted) = Self::numeric_widen(&inner_ty, &val_ty)
|
||||
.or_else(|| Self::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 {
|
||||
self.subst.borrow_mut().insert(n, promoted.clone());
|
||||
}
|
||||
ctx.set_type(*addr, StaticType::Series(Box::new(promoted)));
|
||||
} else {
|
||||
self.unify(inner_ty, val_ty, diag);
|
||||
}
|
||||
} else {
|
||||
self.unify(inner_ty, val_ty, diag);
|
||||
// Dispatch post-call 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
|
||||
if let NodeKind::Identifier {
|
||||
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 {
|
||||
ret_ty = post_call(&args_typed, ret_ty, self, ctx, diag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
NodeKind::Call {
|
||||
|
||||
Reference in New Issue
Block a user