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,
|
Address, AnalyzedNode, GlobalIdx, IdentifierBinding, Node,
|
||||||
NodeKind, NodeMetrics, TypedNode, TypedPhase,
|
NodeKind, NodeMetrics, TypedNode, TypedPhase,
|
||||||
};
|
};
|
||||||
use crate::ast::types::{Identity, Purity};
|
use crate::ast::types::{Identity, Purity, Value};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
@@ -70,7 +70,12 @@ impl<'a> Analyzer<'a> {
|
|||||||
let mut is_recursive = false;
|
let mut is_recursive = false;
|
||||||
|
|
||||||
let (new_kind, purity) = match &node.kind {
|
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::Nop => (NodeKind::Nop, Purity::Pure),
|
||||||
|
|
||||||
NodeKind::Identifier { symbol, binding } => {
|
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 analyzer;
|
||||||
pub mod binder;
|
pub mod binder;
|
||||||
|
pub mod call_hooks;
|
||||||
pub mod module_loader;
|
pub mod module_loader;
|
||||||
pub mod captures;
|
pub mod captures;
|
||||||
pub mod dumper;
|
pub mod dumper;
|
||||||
|
|||||||
+102
-146
@@ -1,9 +1,10 @@
|
|||||||
|
use crate::ast::compiler::call_hooks::{CallHooks, TypeInferenceAccess, TypeSlotAccess};
|
||||||
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,
|
||||||
};
|
};
|
||||||
use crate::ast::diagnostics::Diagnostics;
|
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::cell::Cell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
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.
|
/// 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`,
|
/// 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
|
/// 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.
|
/// Global substitution map: TypeVar ID → resolved StaticType.
|
||||||
/// Shared across all scopes within a single type-checking pass.
|
/// Shared across all scopes within a single type-checking pass.
|
||||||
subst: std::cell::RefCell<HashMap<u32, StaticType>>,
|
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 {
|
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 {
|
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()),
|
||||||
|
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
|
/// After a `FunctionOverloads` call resolves successfully, unify the matched
|
||||||
/// overload's concrete parameter types with the actual argument types.
|
/// 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)))
|
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,
|
/// Walks the typed AST, applies the HM substitution to every type annotation,
|
||||||
/// and elaborates `(series n)` calls by injecting the resolved schema as a
|
/// and dispatches finalization hooks (e.g. schema injection for `series`).
|
||||||
/// second argument so the runtime can allocate the correct storage.
|
fn finalize_node(&self, node: TypedNode, subst: &HashMap<u32, StaticType>) -> TypedNode {
|
||||||
fn finalize_node(node: TypedNode, subst: &HashMap<u32, StaticType>) -> TypedNode {
|
|
||||||
let new_ty = Self::apply_subst(node.ty, subst);
|
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 }
|
Node { kind: new_kind, ty: new_ty, identity: node.identity, comments: node.comments }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finalize_kind(
|
fn finalize_kind(
|
||||||
|
&self,
|
||||||
kind: NodeKind<TypedPhase>,
|
kind: NodeKind<TypedPhase>,
|
||||||
subst: &HashMap<u32, StaticType>,
|
subst: &HashMap<u32, StaticType>,
|
||||||
node_ty: &StaticType,
|
node_ty: &StaticType,
|
||||||
identity: &Rc<NodeIdentity>,
|
_identity: &Rc<NodeIdentity>,
|
||||||
) -> NodeKind<TypedPhase> {
|
) -> NodeKind<TypedPhase> {
|
||||||
match kind {
|
match kind {
|
||||||
// Leaf nodes — nothing to recurse into
|
// Leaf nodes — nothing to recurse into
|
||||||
@@ -424,71 +434,61 @@ impl TypeChecker {
|
|||||||
NodeKind::Identifier { symbol, binding } => NodeKind::Identifier { symbol, binding },
|
NodeKind::Identifier { symbol, binding } => NodeKind::Identifier { symbol, binding },
|
||||||
NodeKind::Extension(ext) => NodeKind::Extension(ext),
|
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 } => {
|
NodeKind::Call { callee, args } => {
|
||||||
let callee_fin = Rc::new(Self::finalize_node((*callee).clone(), subst));
|
let callee_fin = Rc::new(self.finalize_node((*callee).clone(), subst));
|
||||||
if Self::is_identifier_named("series", &callee_fin)
|
let args_fin = Rc::new(self.finalize_node((*args).clone(), subst));
|
||||||
&& let StaticType::Series(inner) = node_ty
|
if let NodeKind::Identifier {
|
||||||
&& let NodeKind::Tuple { elements } = &args.kind
|
binding: IdentifierBinding::Reference(Address::Global(idx)),
|
||||||
&& elements.len() == 1
|
..
|
||||||
&& let Some(schema_val) = Self::series_element_to_schema_value(inner)
|
} = &callee_fin.kind
|
||||||
{
|
&& let Some(hook) = self.call_hooks.get(&idx.0)
|
||||||
let orig_arg = Rc::new(Self::finalize_node((*elements[0]).clone(), subst));
|
&& let Some(finalize) = hook.finalize
|
||||||
let schema_ty = schema_val.static_type();
|
&& let Some(new_kind) = finalize(
|
||||||
let schema_node = Rc::new(Node {
|
Rc::clone(&callee_fin),
|
||||||
kind: NodeKind::Constant(schema_val),
|
Rc::clone(&args_fin),
|
||||||
ty: schema_ty,
|
node_ty,
|
||||||
identity: identity.clone(),
|
subst,
|
||||||
comments: Rc::from([]),
|
) {
|
||||||
});
|
return new_kind;
|
||||||
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));
|
|
||||||
NodeKind::Call { callee: callee_fin, args: args_fin }
|
NodeKind::Call { callee: callee_fin, args: args_fin }
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeKind::If { cond, then_br, else_br } => NodeKind::If {
|
NodeKind::If { cond, then_br, else_br } => NodeKind::If {
|
||||||
cond: Rc::new(Self::finalize_node((*cond).clone(), subst)),
|
cond: Rc::new(self.finalize_node((*cond).clone(), subst)),
|
||||||
then_br: Rc::new(Self::finalize_node((*then_br).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))),
|
else_br: else_br.map(|e| Rc::new(self.finalize_node((*e).clone(), subst))),
|
||||||
},
|
},
|
||||||
NodeKind::Def { pattern, value, info } => NodeKind::Def {
|
NodeKind::Def { pattern, value, info } => NodeKind::Def {
|
||||||
pattern: Rc::new(Self::finalize_node((*pattern).clone(), subst)),
|
pattern: Rc::new(self.finalize_node((*pattern).clone(), subst)),
|
||||||
value: Rc::new(Self::finalize_node((*value).clone(), subst)),
|
value: Rc::new(self.finalize_node((*value).clone(), subst)),
|
||||||
info,
|
info,
|
||||||
},
|
},
|
||||||
NodeKind::Assign { target, value, info } => NodeKind::Assign {
|
NodeKind::Assign { target, value, info } => NodeKind::Assign {
|
||||||
target: Rc::new(Self::finalize_node((*target).clone(), subst)),
|
target: Rc::new(self.finalize_node((*target).clone(), subst)),
|
||||||
value: Rc::new(Self::finalize_node((*value).clone(), subst)),
|
value: Rc::new(self.finalize_node((*value).clone(), subst)),
|
||||||
info,
|
info,
|
||||||
},
|
},
|
||||||
NodeKind::Lambda { params, body, info } => NodeKind::Lambda {
|
NodeKind::Lambda { params, body, info } => NodeKind::Lambda {
|
||||||
params: Rc::new(Self::finalize_node((*params).clone(), subst)),
|
params: Rc::new(self.finalize_node((*params).clone(), subst)),
|
||||||
body: Rc::new(Self::finalize_node((*body).clone(), subst)),
|
body: Rc::new(self.finalize_node((*body).clone(), subst)),
|
||||||
info,
|
info,
|
||||||
},
|
},
|
||||||
NodeKind::Again { args } => NodeKind::Again {
|
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 {
|
NodeKind::Block { exprs } => NodeKind::Block {
|
||||||
exprs: exprs
|
exprs: exprs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|e| Rc::new(Self::finalize_node((*e).clone(), subst)))
|
.map(|e| Rc::new(self.finalize_node((*e).clone(), subst)))
|
||||||
.collect(),
|
.collect(),
|
||||||
},
|
},
|
||||||
NodeKind::Tuple { elements } => NodeKind::Tuple {
|
NodeKind::Tuple { elements } => NodeKind::Tuple {
|
||||||
elements: elements
|
elements: elements
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|e| Rc::new(Self::finalize_node((*e).clone(), subst)))
|
.map(|e| Rc::new(self.finalize_node((*e).clone(), subst)))
|
||||||
.collect(),
|
.collect(),
|
||||||
},
|
},
|
||||||
NodeKind::Record { fields, layout } => NodeKind::Record {
|
NodeKind::Record { fields, layout } => NodeKind::Record {
|
||||||
@@ -496,34 +496,34 @@ impl TypeChecker {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, v)| {
|
.map(|(k, v)| {
|
||||||
(
|
(
|
||||||
Rc::new(Self::finalize_node((*k).clone(), subst)),
|
Rc::new(self.finalize_node((*k).clone(), subst)),
|
||||||
Rc::new(Self::finalize_node((*v).clone(), subst)),
|
Rc::new(self.finalize_node((*v).clone(), subst)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
layout,
|
layout,
|
||||||
},
|
},
|
||||||
NodeKind::GetField { rec, field } => NodeKind::GetField {
|
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,
|
field,
|
||||||
},
|
},
|
||||||
NodeKind::Expansion { original_call, expanded } => NodeKind::Expansion {
|
NodeKind::Expansion { original_call, expanded } => NodeKind::Expansion {
|
||||||
original_call,
|
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 {
|
NodeKind::MacroDecl { name, params, body } => NodeKind::MacroDecl {
|
||||||
name,
|
name,
|
||||||
params: Rc::new(Self::finalize_node((*params).clone(), subst)),
|
params: Rc::new(self.finalize_node((*params).clone(), subst)),
|
||||||
body: Rc::new(Self::finalize_node((*body).clone(), subst)),
|
body: Rc::new(self.finalize_node((*body).clone(), subst)),
|
||||||
},
|
},
|
||||||
NodeKind::Template(inner) => {
|
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(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(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.
|
/// Must be called after `check()` or `check_node_as_bound()` completes.
|
||||||
pub fn finalize(&self, node: TypedNode) -> TypedNode {
|
pub fn finalize(&self, node: TypedNode) -> TypedNode {
|
||||||
let subst = self.subst.borrow().clone();
|
let subst = self.subst.borrow().clone();
|
||||||
Self::finalize_node(node, &subst)
|
self.finalize_node(node, &subst)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check(
|
pub fn check(
|
||||||
@@ -1225,62 +1225,18 @@ impl TypeChecker {
|
|||||||
// e.g. `(+ Float TypeVar(1))` resolves TypeVar(1) = Float.
|
// e.g. `(+ Float TypeVar(1))` resolves TypeVar(1) = Float.
|
||||||
self.unify_matched_overload(&callee_typed.ty, &args_typed.ty, diag);
|
self.unify_matched_overload(&callee_typed.ty, &args_typed.ty, diag);
|
||||||
|
|
||||||
// HM: (series n) returns Series(Any) — replace with a fresh TypeVar so
|
// Dispatch post-call hooks registered by the RTL (keyed by global slot index).
|
||||||
// subsequent push calls can unify the element type.
|
// Hooks handle type-inference extensions such as:
|
||||||
if Self::is_identifier_named("series", &callee_typed)
|
// - series: inject a fresh TypeVar for the element type
|
||||||
&& let StaticType::Series(inner) = &ret_ty
|
// - push: unify the series element TypeVar with the pushed value type
|
||||||
&& **inner == StaticType::Any
|
if let NodeKind::Identifier {
|
||||||
{
|
binding: IdentifierBinding::Reference(Address::Global(idx)),
|
||||||
ret_ty = StaticType::Series(Box::new(self.fresh_var()));
|
..
|
||||||
}
|
} = &callee_typed.kind
|
||||||
|
&& let Some(hook) = self.call_hooks.get(&idx.0)
|
||||||
// HM: (push series val) — unify the series element TypeVar with the value
|
&& let Some(post_call) = hook.post_call {
|
||||||
// type, applying Int→Float numeric promotion when needed.
|
ret_ty = post_call(&args_typed, ret_ty, self, ctx, diag);
|
||||||
//
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(
|
(
|
||||||
NodeKind::Call {
|
NodeKind::Call {
|
||||||
|
|||||||
+39
-11
@@ -1,5 +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::{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;
|
||||||
@@ -53,6 +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.
|
||||||
|
/// Populated during RTL registration via `RtlRegistration::with_call_hooks`.
|
||||||
|
pub call_hooks: Rc<HashMap<u32, CallHooks>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Environment {
|
pub struct Environment {
|
||||||
@@ -79,6 +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.
|
||||||
|
/// Frozen into `Rtl::call_hooks` after `rtl::register()` completes.
|
||||||
|
pub call_hooks: Rc<RefCell<HashMap<u32, CallHooks>>>,
|
||||||
/// 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>,
|
||||||
}
|
}
|
||||||
@@ -104,6 +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>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MacroEvaluator for RuntimeMacroEvaluator {
|
impl MacroEvaluator for RuntimeMacroEvaluator {
|
||||||
@@ -124,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());
|
let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.call_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() {
|
||||||
@@ -167,6 +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())),
|
||||||
// Placeholder — overwritten below after bootstrap.
|
// Placeholder — overwritten below after bootstrap.
|
||||||
rtl: Rc::new(Rtl {
|
rtl: Rc::new(Rtl {
|
||||||
scope: CompilerScope::new(),
|
scope: CompilerScope::new(),
|
||||||
@@ -175,6 +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()),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,6 +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()));
|
||||||
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,
|
||||||
@@ -192,6 +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,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set the frozen RTL slice. Stream closures registered during bootstrap
|
// Set the frozen RTL slice. Stream closures registered during bootstrap
|
||||||
@@ -245,6 +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())),
|
||||||
rtl,
|
rtl,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -306,6 +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),
|
||||||
};
|
};
|
||||||
MacroExpander::new(self.macro_registry.borrow().clone(), evaluator)
|
MacroExpander::new(self.macro_registry.borrow().clone(), evaluator)
|
||||||
}
|
}
|
||||||
@@ -466,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());
|
let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.rtl.call_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 {
|
||||||
@@ -508,10 +522,12 @@ impl Environment {
|
|||||||
|
|
||||||
/// Allocates a new global slot and registers a value in the fixed RTL scope (scope index 0).
|
/// Allocates a new global slot and registers a value in the fixed RTL scope (scope index 0).
|
||||||
/// This is the single source of truth for all global slot allocation.
|
/// This is the single source of truth for all global slot allocation.
|
||||||
fn allocate_slot(&self, name: &str, ty: StaticType, purity: Purity, val: Value) {
|
/// Returns the allocated slot index so callers can key call-hooks against it.
|
||||||
|
fn allocate_slot(&self, name: &str, ty: StaticType, purity: Purity, val: Value) -> u32 {
|
||||||
let mut root_scopes = self.root_scopes.borrow_mut();
|
let mut root_scopes = self.root_scopes.borrow_mut();
|
||||||
let mut slot_count = self.root_slot_count.borrow_mut();
|
let mut slot_count = self.root_slot_count.borrow_mut();
|
||||||
let global_idx = GlobalIdx(*slot_count);
|
let idx = *slot_count;
|
||||||
|
let global_idx = GlobalIdx(idx);
|
||||||
root_scopes[0].locals.insert(
|
root_scopes[0].locals.insert(
|
||||||
Symbol::from(name),
|
Symbol::from(name),
|
||||||
LocalInfo {
|
LocalInfo {
|
||||||
@@ -525,6 +541,7 @@ impl Environment {
|
|||||||
self.root_types.borrow_mut().push(ty);
|
self.root_types.borrow_mut().push(ty);
|
||||||
self.root_purity.borrow_mut().push(purity);
|
self.root_purity.borrow_mut().push(purity);
|
||||||
self.user_values.borrow_mut().push(val);
|
self.user_values.borrow_mut().push(val);
|
||||||
|
idx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_native(
|
pub fn register_native(
|
||||||
@@ -532,8 +549,8 @@ impl Environment {
|
|||||||
name: &str,
|
name: &str,
|
||||||
ty: StaticType,
|
ty: StaticType,
|
||||||
func: Rc<NativeFunction>,
|
func: Rc<NativeFunction>,
|
||||||
) {
|
) -> u32 {
|
||||||
self.allocate_slot(name, ty, func.purity, Value::Function(func));
|
self.allocate_slot(name, ty, func.purity, Value::Function(func))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_native_fn(
|
pub fn register_native_fn(
|
||||||
@@ -543,7 +560,7 @@ impl Environment {
|
|||||||
purity_level: Purity,
|
purity_level: Purity,
|
||||||
func: impl Fn(&[Value]) -> Value + 'static,
|
func: impl Fn(&[Value]) -> Value + 'static,
|
||||||
) -> RtlRegistration {
|
) -> RtlRegistration {
|
||||||
self.register_native(
|
let slot_idx = self.register_native(
|
||||||
name,
|
name,
|
||||||
ty,
|
ty,
|
||||||
Rc::new(NativeFunction {
|
Rc::new(NativeFunction {
|
||||||
@@ -551,12 +568,22 @@ impl Environment {
|
|||||||
purity: purity_level,
|
purity: purity_level,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
RtlRegistration::new(name.to_string(), Rc::clone(&self.rtl_docs))
|
RtlRegistration::new(
|
||||||
|
name.to_string(),
|
||||||
|
slot_idx,
|
||||||
|
Rc::clone(&self.rtl_docs),
|
||||||
|
Rc::clone(&self.call_hooks),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_constant(&self, name: &str, ty: StaticType, val: Value) -> RtlRegistration {
|
pub fn register_constant(&self, name: &str, ty: StaticType, val: Value) -> RtlRegistration {
|
||||||
self.allocate_slot(name, ty, Purity::Pure, val);
|
let slot_idx = self.allocate_slot(name, ty, Purity::Pure, val);
|
||||||
RtlRegistration::new(name.to_string(), Rc::clone(&self.rtl_docs))
|
RtlRegistration::new(
|
||||||
|
name.to_string(),
|
||||||
|
slot_idx,
|
||||||
|
Rc::clone(&self.rtl_docs),
|
||||||
|
Rc::clone(&self.call_hooks),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -800,13 +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 = 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());
|
let checker = TypeChecker::new(root_types.clone(), Rc::clone(&call_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.
|
||||||
|
|||||||
+30
-1
@@ -1,6 +1,9 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::ast::compiler::call_hooks::CallHooks;
|
||||||
|
|
||||||
/// 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>;
|
||||||
|
|
||||||
@@ -19,19 +22,30 @@ 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
|
||||||
|
/// hooks 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,
|
||||||
rtl_docs: Rc<RefCell<Vec<RtlDocEntry>>>,
|
rtl_docs: Rc<RefCell<Vec<RtlDocEntry>>>,
|
||||||
|
call_hooks_registry: Rc<RefCell<HashMap<u32, CallHooks>>>,
|
||||||
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]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RtlRegistration {
|
impl RtlRegistration {
|
||||||
pub(crate) fn new(name: String, rtl_docs: Rc<RefCell<Vec<RtlDocEntry>>>) -> Self {
|
pub(crate) fn new(
|
||||||
|
name: String,
|
||||||
|
slot_idx: u32,
|
||||||
|
rtl_docs: Rc<RefCell<Vec<RtlDocEntry>>>,
|
||||||
|
call_hooks_registry: Rc<RefCell<HashMap<u32, CallHooks>>>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name,
|
name,
|
||||||
|
slot_idx,
|
||||||
rtl_docs,
|
rtl_docs,
|
||||||
|
call_hooks_registry,
|
||||||
one_liner: None,
|
one_liner: None,
|
||||||
description: None,
|
description: None,
|
||||||
examples: None,
|
examples: None,
|
||||||
@@ -55,6 +69,21 @@ impl RtlRegistration {
|
|||||||
self.examples = Some(ex);
|
self.examples = Some(ex);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register type-inference and/or finalization hooks 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<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()
|
||||||
|
.insert(self.slot_idx, CallHooks { post_call, finalize });
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for RtlRegistration {
|
impl Drop for RtlRegistration {
|
||||||
|
|||||||
+164
-45
@@ -1,10 +1,164 @@
|
|||||||
pub mod data;
|
pub mod data;
|
||||||
pub use data::*;
|
pub use data::*;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::ast::compiler::call_hooks::{FinalizeHook, PostCallHook, TypeInferenceAccess, TypeSlotAccess};
|
||||||
|
use crate::ast::diagnostics::Diagnostics;
|
||||||
use crate::ast::environment::Environment;
|
use crate::ast::environment::Environment;
|
||||||
use crate::ast::types::{Purity, RecordLayout, Signature, StaticType, Value};
|
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)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
inference: &dyn TypeInferenceAccess,
|
||||||
|
_ctx: &mut dyn TypeSlotAccess,
|
||||||
|
_diag: &mut Diagnostics,
|
||||||
|
) -> StaticType {
|
||||||
|
if let StaticType::Series(inner) = &ret_ty
|
||||||
|
&& **inner == StaticType::Any
|
||||||
|
{
|
||||||
|
return StaticType::Series(Box::new(inference.fresh_var()));
|
||||||
|
}
|
||||||
|
ret_ty
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]| {
|
||||||
|
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) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Post-call 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.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
fn push_post_call(
|
||||||
|
args: &TypedNode,
|
||||||
|
ret_ty: StaticType,
|
||||||
|
inference: &dyn TypeInferenceAccess,
|
||||||
|
ctx: &mut dyn TypeSlotAccess,
|
||||||
|
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 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_slot_type(*addr) {
|
||||||
|
StaticType::Series(ri) => *ri,
|
||||||
|
_ => inner_ty.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(promoted) = inference
|
||||||
|
.try_numeric_widen(&inner_ty, &val_ty)
|
||||||
|
.or_else(|| inference.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 {
|
||||||
|
inference.bind_typevar(n, promoted.clone());
|
||||||
|
}
|
||||||
|
ctx.set_slot_type(*addr, StaticType::Series(Box::new(promoted)));
|
||||||
|
} else {
|
||||||
|
inference.unify(inner_ty, val_ty, diag);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inference.unify(inner_ty, val_ty, diag);
|
||||||
|
}
|
||||||
|
ret_ty
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 5. Script Integration (RTL Registration)
|
// 5. Script Integration (RTL Registration)
|
||||||
@@ -23,51 +177,15 @@ pub fn register(env: &Environment) {
|
|||||||
})),
|
})),
|
||||||
Purity::Impure,
|
Purity::Impure,
|
||||||
|args: &[Value]| {
|
|args: &[Value]| {
|
||||||
|
// The FinalizeHook 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;
|
let lookback_limit = args[0].as_int().unwrap_or(0) as usize;
|
||||||
// The type checker injects the schema as a second argument via HM inference.
|
Value::Series(Rc::new(ValueSeries::new(lookback_limit)))
|
||||||
// If schema injection was not possible (e.g. the first push is inside a nested
|
|
||||||
// closure that the type checker could not reach), fall back to a ValueSeries
|
|
||||||
// that stores unboxed Values. The HM type checker still guarantees type safety
|
|
||||||
// at compile time; this path is only a runtime fallback for storage allocation.
|
|
||||||
if args.len() < 2 {
|
|
||||||
return Value::Series(Rc::new(ValueSeries::new(lookback_limit)));
|
|
||||||
}
|
|
||||||
let arg = &args[1];
|
|
||||||
match arg {
|
|
||||||
Value::Keyword(k) => {
|
|
||||||
match k.name().to_lowercase().as_str() {
|
|
||||||
"float" => Value::Series(Rc::new(ScalarSeries::<f64>::new("FloatSeries", lookback_limit))),
|
|
||||||
"int" | "datetime" => Value::Series(Rc::new(ScalarSeries::<i64>::new("IntSeries", lookback_limit))),
|
|
||||||
"bool" => Value::Series(Rc::new(ScalarSeries::<bool>::new("BoolSeries", lookback_limit))),
|
|
||||||
"text" => Value::Series(Rc::new(ValueSeries::new(lookback_limit))),
|
|
||||||
_ => panic!("Unknown or unsupported series type keyword: :{}", k.name()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Value::Record(schema_layout, schema_values) => {
|
|
||||||
// Interpret the record as a schema: { :field :type_keyword }
|
|
||||||
let mut fields = Vec::with_capacity(schema_layout.fields.len());
|
|
||||||
for (i, (name, _)) in schema_layout.fields.iter().enumerate() {
|
|
||||||
let type_keyword = match &schema_values[i] {
|
|
||||||
Value::Keyword(tk) => tk.name().to_lowercase(),
|
|
||||||
_ => panic!("Field :{} in series schema must be a type keyword (e.g., :float, :int)", name.name()),
|
|
||||||
};
|
|
||||||
let ty = match type_keyword.as_str() {
|
|
||||||
"float" => StaticType::Float,
|
|
||||||
"int" => StaticType::Int,
|
|
||||||
"bool" => StaticType::Bool,
|
|
||||||
"datetime" => StaticType::DateTime,
|
|
||||||
"text" => StaticType::Text,
|
|
||||||
_ => panic!("Unsupported type keyword :{} for series field :{}", type_keyword, name.name()),
|
|
||||||
};
|
|
||||||
fields.push((*name, ty));
|
|
||||||
}
|
|
||||||
let final_layout = RecordLayout::get_or_create(fields);
|
|
||||||
Value::Series(Rc::new(RecordSeries::new(final_layout, lookback_limit)))
|
|
||||||
}
|
|
||||||
_ => panic!("series: second arg must be a type keyword (:float, :int…) or a schema record ({{:field :type}})"),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
).doc("Creates a new series (ring buffer) with the given lookback limit.")
|
).with_call_hooks(Some(series_post_call as PostCallHook), Some(series_finalize as FinalizeHook))
|
||||||
|
.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(&[
|
||||||
"(series 100)",
|
"(series 100)",
|
||||||
@@ -94,7 +212,8 @@ pub fn register(env: &Environment) {
|
|||||||
}
|
}
|
||||||
panic!("push expects a series as the first argument")
|
panic!("push expects a series as the first argument")
|
||||||
},
|
},
|
||||||
).doc("Pushes a new value into a series. After push, index 0 returns this value.")
|
).with_call_hooks(Some(push_post_call as PostCallHook), None)
|
||||||
|
.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})",
|
||||||
"(push prices 42.0)",
|
"(push prices 42.0)",
|
||||||
|
|||||||
Reference in New Issue
Block a user