diff --git a/fir/src/iter.rs b/fir/src/iter.rs index 8e700f7c..5507c2c8 100644 --- a/fir/src/iter.rs +++ b/fir/src/iter.rs @@ -22,10 +22,12 @@ mod mapper; mod multi_mapper; mod traverse; +mod tree_like; pub use mapper::Mapper; pub use multi_mapper::MultiMapper; pub use traverse::Traversal; +pub use tree_like::TreeLike; use crate::Fir; diff --git a/fir/src/iter/tree_like.rs b/fir/src/iter/tree_like.rs new file mode 100644 index 00000000..1a8efe67 --- /dev/null +++ b/fir/src/iter/tree_like.rs @@ -0,0 +1,186 @@ +use crate::{Fir, Kind, Node, OriginIdx, RefIdx}; + +// FIXME: Should we still be able to emit errors? +pub trait TreeLike<'ast, 'fir, T> { + fn visit_many(&mut self, fir: &Fir, many: &[RefIdx]) { + many.iter().for_each(|r| self.visit_reference(fir, r)) + } + + fn visit_optional(&mut self, fir: &Fir, reference: &Option) { + if let Some(r) = reference { + self.visit_reference(fir, r) + } + } + + fn visit_reference(&mut self, fir: &Fir, reference: &RefIdx) { + self.visit(fir, &reference.expect_resolved()) + } + + fn visit_constant(&mut self, fir: &Fir, _node: &Node, c: &RefIdx) { + self.visit_reference(fir, c) + } + + fn visit_type_reference(&mut self, fir: &Fir, _node: &Node, to: &RefIdx) { + self.visit_reference(fir, to) + } + + fn visit_typed_value(&mut self, fir: &Fir, _node: &Node, value: &RefIdx, ty: &RefIdx) { + self.visit_reference(fir, value); + self.visit_reference(fir, ty); + } + + fn visit_generic(&mut self, fir: &Fir, _node: &Node, default: &Option) { + self.visit_optional(fir, default) + } + + fn visit_record_type( + &mut self, + fir: &Fir, + _node: &Node, + generics: &[RefIdx], + fields: &[RefIdx], + ) { + self.visit_many(fir, generics); + self.visit_many(fir, fields); + } + + fn visit_union_type( + &mut self, + fir: &Fir, + _node: &Node, + generics: &[RefIdx], + variants: &[RefIdx], + ) { + self.visit_many(fir, generics); + self.visit_many(fir, variants); + } + + fn visit_function( + &mut self, + fir: &Fir, + _node: &Node, + generics: &[RefIdx], + args: &[RefIdx], + return_type: &Option, + block: &Option, + ) { + self.visit_many(fir, generics); + self.visit_many(fir, args); + self.visit_optional(fir, return_type); + self.visit_optional(fir, block); + } + + fn visit_binding(&mut self, fir: &Fir, _node: &Node, to: &RefIdx) { + self.visit_reference(fir, to) + } + + fn visit_assignment(&mut self, fir: &Fir, _node: &Node, to: &RefIdx, from: &RefIdx) { + self.visit_reference(fir, to); + self.visit_reference(fir, from); + } + + fn visit_instantiation( + &mut self, + fir: &Fir, + _node: &Node, + to: &RefIdx, + generics: &[RefIdx], + fields: &[RefIdx], + ) { + self.visit_reference(fir, to); + self.visit_many(fir, generics); + self.visit_many(fir, fields); + } + + fn visit_type_offset( + &mut self, + fir: &Fir, + _node: &Node, + instance: &RefIdx, + field: &RefIdx, + ) { + self.visit_reference(fir, instance); + self.visit_reference(fir, field); + } + + fn visit_call( + &mut self, + fir: &Fir, + _node: &Node, + to: &RefIdx, + generics: &[RefIdx], + args: &[RefIdx], + ) { + self.visit_reference(fir, to); + self.visit_many(fir, generics); + self.visit_many(fir, args); + } + + fn visit_conditional( + &mut self, + fir: &Fir, + _node: &Node, + condition: &RefIdx, + true_block: &RefIdx, + false_block: &Option, + ) { + self.visit_reference(fir, condition); + self.visit_reference(fir, true_block); + self.visit_optional(fir, false_block); + } + + fn visit_loop(&mut self, fir: &Fir, _node: &Node, condition: &RefIdx, block: &RefIdx) { + self.visit_reference(fir, condition); + self.visit_reference(fir, block); + } + + fn visit_statements(&mut self, fir: &Fir, _node: &Node, stmts: &[RefIdx]) { + self.visit_many(fir, stmts) + } + + fn visit_return(&mut self, fir: &Fir, _node: &Node, value: &Option) { + self.visit_optional(fir, value) + } + + fn visit(&mut self, fir: &Fir, start: &OriginIdx) { + let node = &fir[start]; + + match &node.kind { + Kind::Constant(c) => self.visit_constant(fir, node, c), + Kind::TypeReference(to) => self.visit_type_reference(fir, node, to), + Kind::TypedValue { value, ty } => self.visit_typed_value(fir, node, value, ty), + Kind::Generic { default } => self.visit_generic(fir, node, default), + Kind::RecordType { generics, fields } => { + self.visit_record_type(fir, node, generics, fields) + } + Kind::UnionType { generics, variants } => { + self.visit_union_type(fir, node, generics, variants) + } + Kind::Function { + generics, + args, + return_type, + block, + } => self.visit_function(fir, node, generics, args, return_type, block), + Kind::Binding { to } => self.visit_binding(fir, node, to), + Kind::Assignment { to, from } => self.visit_assignment(fir, node, to, from), + Kind::Instantiation { + to, + generics, + fields, + } => self.visit_instantiation(fir, node, to, generics, fields), + Kind::TypeOffset { instance, field } => { + self.visit_type_offset(fir, node, instance, field) + } + Kind::Call { to, generics, args } => self.visit_call(fir, node, to, generics, args), + Kind::Conditional { + condition, + true_block, + false_block, + } => self.visit_conditional(fir, node, condition, true_block, false_block), + Kind::Loop { condition, block } => self.visit_loop(fir, node, condition, block), + Kind::Statements(stmts) => self.visit_statements(fir, node, stmts), + Kind::Return(value) => self.visit_return(fir, node, value), + } + } +} diff --git a/fire/src/lib.rs b/fire/src/lib.rs index 40018d6a..5878f86a 100644 --- a/fire/src/lib.rs +++ b/fire/src/lib.rs @@ -112,12 +112,12 @@ impl<'ast, 'fir> Fire<'ast, 'fir> { // TODO: Rename? `node`? `node_from_ref`? `rnode`? `ref`? `view`? // should this return an `AccessedNode` or w/ever which we can then `.fire()`? fn access(&self, r: &RefIdx) -> &'fir Node> { - &self.fir.nodes[&r.expect_resolved()] + &self.fir[r] } // TODO: Rename: `access_origin`? fn access_resolved(&self, resolved: &OriginIdx) -> &'fir Node> { - &self.fir.nodes[&resolved] + &self.fir[resolved] } #[must_use] diff --git a/name_resolve/src/declarator.rs b/name_resolve/src/declarator.rs index 75f78142..5a75731b 100644 --- a/name_resolve/src/declarator.rs +++ b/name_resolve/src/declarator.rs @@ -90,4 +90,13 @@ impl<'ast, 'ctx, 'enclosing> Traversal, NameResolutionError> ) -> Fallible { self.define(DefinitionKind::Binding, node) } + + fn traverse_generic( + &mut self, + _: &Fir>, + node: &Node>, + _: &Option, + ) -> Fallible { + self.define(DefinitionKind::Type, node) + } } diff --git a/typecheck/src/generics.rs b/typecheck/src/generics.rs new file mode 100644 index 00000000..e4d5ae6b --- /dev/null +++ b/typecheck/src/generics.rs @@ -0,0 +1,22 @@ +// TODO: Typer could also be responsible for building the list of monomorphization requests (and constraints?) +// so something like a constraints: Map? +// -> this is good but it needs to be per invocation - so per function call/type instantiation +// -> so is it more like a Map? +// -> Then Checker will be responsible for checking that T: Constraints? +// -> this seems annoying to deal with +// a good API would be something like - ConstraintBuilder -> Checker -> Monormorphizer +// with Checker checking the constraints? does that make sense? +// do we want to go with an easier first step where we have ConstraintBuilder -> Monomorphizer -> Checker? And we +// don't need to add anything to Checker? Since all functions and types will already be mono'd. But then the errors +// will be shit so we might as well build the constraints from the get-go. +// Now do we want to instead have a ConstraintChecker? who just takes care of checking constraints? then Mono and we +// can probably pass all of that to checker anyway. This is probably a better split. In that case, we should move +// this over to a new module. + +// ConstraintBuilder -> ConstraintMap +// ConstraintChecker(ConstraintMap) -> Result +// Monomorphizer(MonoRequests) -> Result // infaillible? + +mod constraint_builder; + +pub use constraint_builder::ConstraintBuilder; diff --git a/typecheck/src/generics/constraint_builder.rs b/typecheck/src/generics/constraint_builder.rs new file mode 100644 index 00000000..6a68175e --- /dev/null +++ b/typecheck/src/generics/constraint_builder.rs @@ -0,0 +1,259 @@ +//! The goal of the [`ConstraintBuilder`] is to build a list of required +//! constraints when calling or instantiating generic functions and types. It is +//! perfectly valid for a generic call to have zero constraints - in fact, generic +//! type instantiations will not have any constraints, as they do not perform any +//! function calls in their declarations. As a consequence, only function calls +//! will be considered for all examples in this module. + +use std::collections::HashMap; + +use fir::{iter::TreeLike, Fallible, Fir, Kind, Node, OriginIdx, RefIdx, Traversal}; +use flatten::FlattenData; + +// this should probably be a HashMap instead - multiple generics per generic call, multiple constraints per generic +type Constraints = HashMap>; +type GenericCall = OriginIdx; + +// TODO: Alright, so how do we want to organize that constraint map. A list of constraints per invocation/call? +type ConstraintMap = HashMap; + +// TODO: Do we need the type context here? +#[derive(Default)] +pub struct ConstraintBuilder { + constraints: ConstraintMap, +} + +// No errors? +#[derive(Debug)] +pub struct Error; + +struct FnCtx<'fir, 'ast> { + generics: &'fir [RefIdx], + args: &'fir [RefIdx], + return_type: Option, + stmts: &'fir [RefIdx], + fir: &'fir Fir>, +} + +struct Woobler<'a> { + pub(crate) to_see: &'a [RefIdx], + pub(crate) seen: bool, +} + +impl<'a> Woobler<'a> { + pub fn new(to_see: &'a [RefIdx]) -> Woobler { + Woobler { + to_see, + seen: false, + } + } +} + +impl<'a, 'ast, 'fir> TreeLike<'ast, 'fir, FlattenData<'ast>> for Woobler<'a> { + fn visit_reference(&mut self, fir: &Fir>, reference: &RefIdx) { + if self.to_see.contains(&reference) { + self.seen = true; + } + + // Otherwise, a bunch of unresolved types error out + // FIXME: Is that correct? + if let RefIdx::Resolved(origin) = reference { + self.visit(fir, origin) + } + } + + // FIXME: + // Adding a hack around TypedValues because arguments are resolved weirdly feels wrong :/ + // this is done because in the name resolver, an arg's typed value resolves to its own binding for some reason + fn visit_typed_value( + &mut self, + fir: &Fir>, + _node: &Node>, + value: &RefIdx, + ty: &RefIdx, + ) { + // FIXME: Refactor? + if self.to_see.contains(&value) { + self.seen = true; + } + + self.visit_reference(fir, ty); + } +} + +pub struct CallConstraintBuilder<'a> { + constrained_args: &'a [RefIdx], +} + +impl<'a, 'ast, 'fir> TreeLike<'ast, 'fir, FlattenData<'ast>> for CallConstraintBuilder<'a> { + fn visit_call( + &mut self, + fir: &Fir>, + node: &Node>, + to: &RefIdx, + generics: &[RefIdx], + args: &[RefIdx], + ) { + for arg in args { + match fir[arg].kind { + Kind::TypedValue { value, .. } if value == *arg => { + dbg!(node); + } + _ => unreachable!(), + } + } + } +} + +impl<'fir, 'ast> FnCtx<'fir, 'ast> { + pub fn from_invocation( + fir: &'fir Fir>, + resolved_call: &RefIdx, + ) -> FnCtx<'fir, 'ast> { + let definition = &fir[resolved_call]; + + match &definition.kind { + Kind::Function { + generics, + args, + return_type, + block: Some(block), + } => { + let block = &fir[block]; + + let stmts = match &block.kind { + Kind::Statements(stmts) => stmts, + _ => unreachable!(), + }; + + FnCtx { + generics: generics.as_slice(), + args: args.as_slice(), + return_type: *return_type, + stmts: stmts.as_slice(), + fir, + } + } + _ => unreachable!(), + } + } + + // FIXME: Should we consume self here instead? + // generics, args -> Vec, Vec + // collect_constrained_args -> Map + // collect_constrained_stmts -> Map + // collect_constraints_per_stmt -> Map + // collect_constraints_per_stmt -> Map + fn collect_constraints(&self) -> Constraints { + let FnCtx { + generics, + args, + return_type, + stmts, + fir, + } = self; + + // we first have to build a list of possibly constrained args - if an arg's type is in the list of generics? + let constrained_args: Vec = args + .iter() + .filter(|arg| { + // first, we get the actual typed value we are dealing with - args are flattened as bindings, but + // we're looking for the underlying value who's being bound. + let binding = match fir[*arg].kind { + Kind::Binding { to } => to, + _ => unreachable!(), + }; + + let arg_ty = match fir[&binding].kind { + Kind::TypedValue { ty, .. } => ty, + _ => unreachable!(), + }; + + let arg_ty = match fir[&arg_ty].kind { + Kind::TypeReference(to) => to, + _ => unreachable!(), + }; + + generics.contains(&arg_ty) + }) + .copied() + .collect(); + + // then, we collect the statements which use one or more of these + // constrained args - meaning that these statements are the ones applying + // "constraints" to our generic types. + // we can then collect the constraints for each statement based on the args they use, + // with another micro visitor + let constrained_stmts = stmts.iter().filter_map(|stmt| { + let mut woobler = Woobler::new(&constrained_args); + woobler.visit_reference(fir, stmt); + + woobler.seen.then_some(stmt) + }); + + // for each of these constrained statements, we build a map of constraints: + // Map> + let constraints = constrained_stmts.for_each(|stmt| { + // we want another micro visitor, basically - that starts at stmt and goes through all its children + // then, when it sees a function call, it builds a constraint for that call if that call contains the argument + // we are looking for + + let mut visitor = CallConstraintBuilder { + constrained_args: &constrained_args, + }; + + visitor.visit_reference(fir, stmt); + + // TODO: How do we do that? + // what to do when we see a call? + }); + + // .fold(Constraints::new(), |mut constraints, _constraint| { + // // here we mostly want to insert or update + // constraints.insert(generics[0], vec![]); + + // constraints + // }); + + // constrained_stmts + + todo!() + } +} + +impl Traversal, Error> for ConstraintBuilder { + fn traverse_call( + &mut self, + fir: &Fir>, + node: &Node>, + to: &RefIdx, + generics: &[RefIdx], + _args: &[RefIdx], + ) -> Fallible { + let fn_ctx = FnCtx::from_invocation(fir, to); + let constraints = fn_ctx.collect_constraints(); + + // get the definition + // run through it + // build constraints + + self.constraints.insert(node.origin, constraints); + + Ok(()) + } + + fn traverse_instantiation( + &mut self, + _fir: &Fir>, + node: &Node>, + to: &RefIdx, + generics: &[RefIdx], + _fields: &[RefIdx], + ) -> Fallible { + // not much to do here? but we should still build a constraint so that this gets turned into a mono' request, which will be easier for the Monormorphizer + + self.constraints.insert(node.origin, HashMap::new()); + + Ok(()) + } +} diff --git a/typecheck/src/lib.rs b/typecheck/src/lib.rs index e1e90d56..b965f165 100644 --- a/typecheck/src/lib.rs +++ b/typecheck/src/lib.rs @@ -1,5 +1,6 @@ mod actual; mod checker; +mod generics; mod primitives; mod typemap; mod typer; @@ -103,7 +104,9 @@ impl<'ast> TypeCheck>> for Fir> { impl<'ast> Pass, FlattenData<'ast>, Error> for TypeCtx { fn pre_condition(_fir: &Fir) {} - fn post_condition(_fir: &Fir) {} + fn post_condition(_fir: &Fir) { + // TODO: Assert that there are no unresolved generics + } fn transform(&mut self, fir: Fir>) -> Result>, Error> { // Typing pass @@ -119,8 +122,14 @@ impl<'ast> Pass, FlattenData<'ast>, Error> for TypeCtx