Refactor: Simplify series creation and infer element type
The `series` constructor now only accepts the `lookback` limit. The element type is inferred by the type checker and implicitly added as a schema argument during compilation. This simplifies the API and centralizes type inference. This change also includes: - Updates to example scripts (`HMA.myc`, `sma.myc`, `soa_series.myc`) to reflect the new `series` signature. - Modifications to the `TypeChecker` to handle the inferred schema injection. - An addition of a regression test for type inference through nested closures with `series`. - Removal of `#[allow(dead_code)]` from several `TypeChecker` methods as they are now used. - Update to `rtl/prelude.myc` macro `cache`. - Update to `rtl/series/mod.rs` to reflect new signature. - Update to `ast/types.rs` to allow `series` to be indexed with an integer to retrieve its element type.
This commit is contained in:
@@ -3,7 +3,7 @@ use crate::ast::nodes::{
|
||||
Node, NodeKind, TypedNode, TypedPhase, VirtualId,
|
||||
};
|
||||
use crate::ast::diagnostics::Diagnostics;
|
||||
use crate::ast::types::{Keyword, RecordLayout, Signature, StaticType};
|
||||
use crate::ast::types::{Keyword, NodeIdentity, RecordLayout, Signature, StaticType, Value};
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
@@ -149,7 +149,6 @@ impl TypeChecker {
|
||||
}
|
||||
|
||||
/// Creates a fresh, unique type variable.
|
||||
#[allow(dead_code)]
|
||||
fn fresh_var(&self) -> StaticType {
|
||||
let id = self.var_counter.get();
|
||||
self.var_counter.set(id + 1);
|
||||
@@ -158,7 +157,6 @@ impl TypeChecker {
|
||||
|
||||
/// Recursively applies the substitution map to a type, replacing all
|
||||
/// resolved `TypeVar`s with their concrete types.
|
||||
#[allow(dead_code)]
|
||||
fn apply_subst(ty: StaticType, subst: &HashMap<u32, StaticType>) -> StaticType {
|
||||
match ty {
|
||||
StaticType::TypeVar(n) => {
|
||||
@@ -191,7 +189,6 @@ impl TypeChecker {
|
||||
|
||||
/// Returns true if `TypeVar(var_id)` appears anywhere in `ty` under the
|
||||
/// current substitution. Used to prevent infinite types (occurs check).
|
||||
#[allow(dead_code)]
|
||||
fn occurs(var_id: u32, ty: &StaticType, subst: &HashMap<u32, StaticType>) -> bool {
|
||||
match ty {
|
||||
StaticType::TypeVar(n) => {
|
||||
@@ -220,7 +217,6 @@ impl TypeChecker {
|
||||
/// Unifies two types under the current substitution.
|
||||
/// On success, the substitution is extended so that `ty1` and `ty2` become equal.
|
||||
/// On failure (type mismatch or occurs check), a diagnostic error is emitted.
|
||||
#[allow(dead_code)]
|
||||
fn unify(&self, ty1: StaticType, ty2: StaticType, diag: &mut Diagnostics) {
|
||||
let mut subst = self.subst.borrow_mut();
|
||||
let ty1 = Self::apply_subst(ty1, &subst);
|
||||
@@ -256,13 +252,195 @@ 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)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let new_ty = Self::apply_subst(node.ty, subst);
|
||||
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(
|
||||
kind: NodeKind<TypedPhase>,
|
||||
subst: &HashMap<u32, StaticType>,
|
||||
node_ty: &StaticType,
|
||||
identity: &Rc<NodeIdentity>,
|
||||
) -> NodeKind<TypedPhase> {
|
||||
match kind {
|
||||
// Leaf nodes — nothing to recurse into
|
||||
NodeKind::Nop => NodeKind::Nop,
|
||||
NodeKind::Constant(v) => NodeKind::Constant(v),
|
||||
NodeKind::FieldAccessor(k) => NodeKind::FieldAccessor(k),
|
||||
NodeKind::Error => NodeKind::Error,
|
||||
NodeKind::Identifier { symbol, binding } => NodeKind::Identifier { symbol, binding },
|
||||
NodeKind::Extension(ext) => NodeKind::Extension(ext),
|
||||
|
||||
// Call: detect `(series n)` and inject the resolved schema arg
|
||||
NodeKind::Call { callee, args } => {
|
||||
let callee_fin = Rc::new(Self::finalize_node((*callee).clone(), subst));
|
||||
if Self::is_identifier_named("series", &callee_fin) {
|
||||
if let StaticType::Series(inner) = node_ty {
|
||||
if let NodeKind::Tuple { elements } = &args.kind {
|
||||
if elements.len() == 1 {
|
||||
if 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));
|
||||
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))),
|
||||
},
|
||||
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)),
|
||||
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)),
|
||||
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)),
|
||||
info,
|
||||
},
|
||||
NodeKind::Again { args } => NodeKind::Again {
|
||||
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)))
|
||||
.collect(),
|
||||
},
|
||||
NodeKind::Tuple { elements } => NodeKind::Tuple {
|
||||
elements: elements
|
||||
.into_iter()
|
||||
.map(|e| Rc::new(Self::finalize_node((*e).clone(), subst)))
|
||||
.collect(),
|
||||
},
|
||||
NodeKind::Record { fields, layout } => NodeKind::Record {
|
||||
fields: fields
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
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)),
|
||||
field,
|
||||
},
|
||||
NodeKind::Expansion { original_call, expanded } => NodeKind::Expansion {
|
||||
original_call,
|
||||
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)),
|
||||
},
|
||||
NodeKind::Template(inner) => {
|
||||
NodeKind::Template(Rc::new(Self::finalize_node((*inner).clone(), subst)))
|
||||
}
|
||||
NodeKind::Placeholder(inner) => {
|
||||
NodeKind::Placeholder(Rc::new(Self::finalize_node((*inner).clone(), subst)))
|
||||
}
|
||||
NodeKind::Splice(inner) => {
|
||||
NodeKind::Splice(Rc::new(Self::finalize_node((*inner).clone(), subst)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the final HM substitution to all type annotations in the tree and
|
||||
/// elaborates `(series n)` calls with their inferred schema arguments.
|
||||
/// 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)
|
||||
}
|
||||
|
||||
pub fn check(
|
||||
&self,
|
||||
node: &Node<BoundPhase>,
|
||||
arg_types: &[StaticType],
|
||||
diag: &mut Diagnostics,
|
||||
) -> TypedNode {
|
||||
self.check_node_as_bound(node, arg_types, diag)
|
||||
let typed = self.check_node_as_bound(node, arg_types, diag);
|
||||
self.finalize(typed)
|
||||
}
|
||||
|
||||
/// Allows re-checking a node from any phase as if it were a bound node.
|
||||
@@ -912,7 +1090,7 @@ impl TypeChecker {
|
||||
self.check_node(args, ctx, diag)
|
||||
};
|
||||
|
||||
let ret_ty = match callee_typed.ty.resolve_call(&args_typed.ty) {
|
||||
let mut ret_ty = match callee_typed.ty.resolve_call(&args_typed.ty) {
|
||||
Some(ty) => ty,
|
||||
None => {
|
||||
diag.push_error(
|
||||
@@ -926,6 +1104,43 @@ impl TypeChecker {
|
||||
}
|
||||
};
|
||||
|
||||
// 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) {
|
||||
if let StaticType::Series(inner) = &ret_ty {
|
||||
if **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 and propagate the resolved type back to the binding in the context.
|
||||
if Self::is_identifier_named("push", &callee_typed) {
|
||||
if let NodeKind::Tuple { elements } = &args_typed.kind {
|
||||
if 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();
|
||||
self.unify(inner_ty, val_ty, diag);
|
||||
if let NodeKind::Identifier {
|
||||
binding: IdentifierBinding::Reference(addr),
|
||||
..
|
||||
} = &series_arg.kind
|
||||
{
|
||||
let resolved = Self::apply_subst(
|
||||
series_arg.ty.clone(),
|
||||
&self.subst.borrow(),
|
||||
);
|
||||
ctx.set_type(*addr, resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
NodeKind::Call {
|
||||
callee: Rc::new(callee_typed),
|
||||
|
||||
Reference in New Issue
Block a user