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/multi_mapper.rs b/fir/src/iter/multi_mapper.rs index 74e2a1b3..ec4f34b2 100644 --- a/fir/src/iter/multi_mapper.rs +++ b/fir/src/iter/multi_mapper.rs @@ -1,11 +1,6 @@ use crate::{Fir, Incomplete, Kind, Node, OriginIdx, RefIdx}; -pub trait MultiMapper, E> { - /// Each implementer of [`MultiMapper`] should keep its own [`OriginIdx`] counter in order to supply the [`Fir`] - /// with new nodes. This can be done by keeping an [`OriginIdx`] as part of the context, and repeatedly - /// calling [`OriginIdx::next`] on it. - fn next_origin(&mut self) -> OriginIdx; - +pub trait MultiMapper, E> { fn map_constant( &mut self, _data: T, @@ -285,20 +280,23 @@ pub trait MultiMapper, E> { /// all valid mapped nodes. This allows an interpreter to keep trying /// passes and emit as many errors as possible fn multi_map(&mut self, fir: Fir) -> Result, Incomplete> { - let (fir, errs) = fir.nodes.into_values().fold( - (Fir::default(), Vec::new()), - |(new_fir, mut errs), node| match self.map_node(node) { - Ok(nodes) => nodes - .into_iter() - .fold((new_fir, errs), |(new_fir, errs), node| { - (new_fir.append(node), errs) - }), - Err(e) => { - errs.push(e); - (new_fir, errs) - } - }, - ); + let (fir, errs) = + fir.nodes + .into_values() + .fold( + (Fir::new(), Vec::new()), + |(new_fir, mut errs), node| match self.map_node(node) { + Ok(nodes) => nodes + .into_iter() + .fold((new_fir, errs), |(new_fir, errs), node| { + (new_fir.append(node), errs) + }), + Err(e) => { + errs.push(e); + (new_fir, errs) + } + }, + ); if errs.is_empty() { Ok(fir) diff --git a/fir/src/iter/tree_like.rs b/fir/src/iter/tree_like.rs new file mode 100644 index 00000000..fbd5c58d --- /dev/null +++ b/fir/src/iter/tree_like.rs @@ -0,0 +1,203 @@ +use crate::{Fir, Kind, Node, OriginIdx, RefIdx}; + +// FIXME: Should we still be able to emit errors? +pub trait TreeLike { + 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: &Option, + ty: &Option, + ) { + self.visit_optional(fir, to); + self.visit_optional(fir, ty); + } + + 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_node_reference(&mut self, fir: &Fir, _node: &Node, to: &RefIdx) { + self.visit_reference(fir, to) + } + + 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::NodeRef(to) => self.visit_node_reference(fir, node, to), + 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, ty } => self.visit_binding(fir, node, to, ty), + 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), + } + } + + fn visit_all(&mut self, fir: &Fir) { + fir.nodes + .last_key_value() + .map(|last| self.visit(fir, last.0)); + } +} diff --git a/fir/src/lib.rs b/fir/src/lib.rs index 9977ae67..3571a354 100644 --- a/fir/src/lib.rs +++ b/fir/src/lib.rs @@ -303,6 +303,14 @@ impl Fir { } } +impl Clone for Fir { + fn clone(&self) -> Fir { + Fir { + nodes: self.nodes.clone(), + } + } +} + // FIXME: The `pass` function should return an Incomplete Fir. How to make it so that we can chain Fir operations // nicely? pub trait Pass { diff --git a/fire/src/lib.rs b/fire/src/lib.rs index 9e4eaccc..557439c5 100644 --- a/fire/src/lib.rs +++ b/fire/src/lib.rs @@ -112,7 +112,7 @@ 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] } #[must_use] diff --git a/name_resolve/src/declarator.rs b/name_resolve/src/declarator.rs index 416d4f08..e903437f 100644 --- a/name_resolve/src/declarator.rs +++ b/name_resolve/src/declarator.rs @@ -97,4 +97,13 @@ impl<'ast> Traversal, NameResolutionError> for Declarator<'_, ) -> 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/actual.rs b/typecheck/src/actual.rs index 5a00a9c5..7415b4bf 100644 --- a/typecheck/src/actual.rs +++ b/typecheck/src/actual.rs @@ -47,6 +47,7 @@ impl TypeLinkResolver<'_> { /// Recursively try and resolve a type link within the type context. This will update the given node's type /// within the type context. fn resolve_link(&mut self, to_resolve: OriginIdx, fir: &Fir) -> OriginIdx { + dbg!(to_resolve); let ChainEnd { intermediate_nodes, final_type, @@ -78,6 +79,10 @@ impl TypeLinkResolver<'_> { self.find_end_inner(fir, ty_ref.expect_resolved(), intermediate_nodes) } + TypeVariable::Generic(g) => ControlFlow::Break(ChainEnd { + intermediate_nodes, + final_type: Type::generic(*g), + }), TypeVariable::Record(r) => { // we don't insert here so that we can get the typeref directly later on - does that make sense? // self.new.types.new_type(final_type.clone()); diff --git a/typecheck/src/generics.rs b/typecheck/src/generics.rs new file mode 100644 index 00000000..cf7cd95a --- /dev/null +++ b/typecheck/src/generics.rs @@ -0,0 +1,32 @@ +// 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? + +// let subs = Substitutions::build(); +// let monod = Monomorphize(subs).map(fir)?; +// let replaced = Replace(monod).map(fir)?; + +mod constraint_builder; +mod mono; +mod replace; +mod substitutions; + +pub use constraint_builder::ConstraintBuilder; +pub use mono::Monomorphize; +pub use replace::Replace; +pub use substitutions::Substitutions; diff --git a/typecheck/src/generics/constraint_builder.rs b/typecheck/src/generics/constraint_builder.rs new file mode 100644 index 00000000..2d7c8714 --- /dev/null +++ b/typecheck/src/generics/constraint_builder.rs @@ -0,0 +1,266 @@ +//! 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. +// FIXME: Actually, type instantiations can have constraints, because type definitions +// can contain default fields which can be instantiated from a generic type's field, w/ +// structural typing +// e.g. +// +// ```rust +// type HasDefault[T](foo: T.make_foo()) +// ``` + +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> 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> for CallConstraintBuilder<'a> { + fn visit_call( + &mut self, + fir: &Fir>, + node: &Node>, + to: &RefIdx, + generics: &[RefIdx], + args: &[RefIdx], + ) { + dbg!(args); + for arg in args { + match fir[arg].kind { + Kind::NodeRef(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_ty = match fir[*arg].kind { + Kind::Binding { ty, .. } => ty, + _ => unreachable!(), + }; + + // we can unwrap here since arguments always have a type + let arg_ty = match fir[&binding_ty.unwrap()].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 + dbg!(&stmt); + + 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(); + let constraints = HashMap::new(); + + // 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/generics/mono.rs b/typecheck/src/generics/mono.rs new file mode 100644 index 00000000..099de3b8 --- /dev/null +++ b/typecheck/src/generics/mono.rs @@ -0,0 +1,124 @@ +// How do we want this to work? Something like collect all the things that need to be monomorphized, mono them and then recreate the links in the FIR? +// E.g if we have +// +// ```rust +// func foo[T](a: T) -> T { a.bar() } +// +// foo(15); +// ``` +// +// `foo(15)` will be a generic call, so we'll mono the function we're calling into - but then we need to mono each of its statements +// (this is similar for types etc) +// so we want our Mono call to return the new OriginIdx we'll be resolved to +// +// e.g. `foo(15)` currently resolves to `foo[T]`, so we'll mono and create `foo[int]` which will return a new OriginIdx +// then, we want `foo(15)` to instead resolve to `foo[int]` +// this is the same thing for all the statements - currently `foo[T]` statements are [ `a.bar()` ], which is the un-mono'd version where `a` is still `T`. +// we need to mono that and get a list of mono'd statements back, which we'll assign to `foo[int]` - this is tree-like. +// should we have a tree-like mapper? how would that even work? +// how do we do that properly within the current FIR system? +// +// so this should probably be split into two visitors and one generator +// +// 1. Collect what needs to be mono'd. Create a Map with the expected new OriginIdx, thanks to a counter updated in the visitor +// GenericCollector(Map). This is done through a TreeLike visitor because the subs +// are available at the definition level (type/function) but need to be used for all the children (fields/statements) +// so this is how we collect them. Each child gets its entry in the map and a copy of the substitutions to perform +// +// 2. Mono all required nodes, basically doing copy paste. does this need to be a Mapper/multimapper? How do we mono based on the kind? +// Something like (if to_mono.contains(this) -> mono, return vec![this, mono(this)] +// this would work and be quite simple! +// +// 3. Finally, replace all references to their mono'd equivalent - so this is just a mapper, looking at the current ctx and doing a replace when necessary +// if (to_mono.contains(this) { this.resolved = to_mono.get(this) }); +// +// (1) should be used alongside the constraint builder, which builds constraints for generic functions, but that can be done later? +// (2) is a simple multimapper we can do here, and (3) is a super simple mapper which is going to be just a couple functions for function calls and type instantiations. name it MonoReplace? +// TODO(Arthur): How to name (1)? SubstitutionBuilder? +// +// wait, is this actually going to work? when we create the substitution builder, we need to recreate tree-like structures in the map, so that nodes are mono'd properly +// e.g. we can't just copy over the fields of a type declaration, because then (3) would replace them each time since the FIR is one big linked list +// e.g. let's take a look at this: +// ```rust +// type Foo[T](a: T, b: T) +// ``` +// this is represented as the following FIR: +// +// ``` +// { +// 1: Field(a, T), +// 2: Field(b, T), +// 3: TypeDec(Foo, [T], [1, 2]), +// } +// ``` +// if we mono on `int` and `string` and just copy over the fields, we'll have this: +// +// ``` +// { +// 1: Field(a, T), +// 2: Field(b, T), +// 3: TypeDec(Foo, [T], [1, 2]), +// 4: TypeDec(Foo[int], [], [1, 2]), +// 5: TypeDec(Foo[string], [], [1, 2]), // note the same nodes being used +// } +// ``` +// which will be an issue since our SubstitutionMap will contain something like { 1: Monod(1, int), 2: Monod(2, int) } +// or { 1: Monod(1, string), 2: Monod(2, string) } +// meaning that when we do perform the generation and replacement we'll have the following: +// ``` +// { +// 1: Field(a, T), +// 2: Field(b, T), +// 3: TypeDec(Foo, [T], [1, 2]), +// 4: TypeDec(Foo[int], [], [1, 2]), +// 5: TypeDec(Foo[string], [], [1, 2]), +// 6: Field(a, string) +// 7: Field(b, string) +// } +// ``` +// and +// +// ``` +// { +// 1: Field(a, T), +// 2: Field(b, T), +// 3: TypeDec(Foo, [T], [1, 2]), +// 4: TypeDec(Foo[int], [], [6, 7]), // HERE: Wrong mono! +// 5: TypeDec(Foo[string], [], [6, 7]), // good mono +// 6: Field(a, string) +// 7: Field(b, string) +// } +// ``` +// so we actually have to "allocate" (and by that I mean create new OriginIdx) before doing the generation and replacement +// so when building the subs map, we need to keep that context alive - and visit a definition point everytime we visit a call point! +// NOTE(Arthur): Important! basically our SubstitutionMap will be more something like `Map` +// how does that affect our generator? something like multimapper where we take in Foo[T], a Map { Foo[T]: Foo[int], Foo[string] } and return vec![foo[int], foo[string]]? +// do we return the generic version? no, right? + +use fir::{Fir, Incomplete, Kind, MultiMapper, Node, OriginIdx, RefIdx}; +use flatten::FlattenData; + +use super::substitutions; + +pub struct Monomorphize { + input: substitutions::Output, +} + +#[derive(Debug)] +pub struct Error; + +// FIXME: This can probably benefit from using a different `U` here - something like T -> U becomes FlattenData -> (MonodIdx, FlattenData) +// Also the `From: T` in Mapper should probably be a `From: (&Node, T)` - so that we get all the info possible +impl<'ast> MultiMapper, FlattenData<'ast>, Error> for Monomorphize { + // fn map_record_type( + // &mut self, + // _data: FlattenData<'ast>, + // origin: OriginIdx, + // generics: Vec, + // fields: Vec, + // ) -> Result>>, Error> { + // self.input.to_mono.get(&origin).map(|request| { + // request + // }) + // } +} diff --git a/typecheck/src/generics/replace.rs b/typecheck/src/generics/replace.rs new file mode 100644 index 00000000..4511845a --- /dev/null +++ b/typecheck/src/generics/replace.rs @@ -0,0 +1,11 @@ +use fir::iter::Mapper; +use flatten::FlattenData; + +pub struct Replace; + +// FIXME: Can there be errors? +pub struct Error; + +// FIXME: Can we have a better type here for `U`? Or should that be part of a later rewrite where we get rid of FlattenData +// this far in the pipeline anyway? +impl<'ast> Mapper, FlattenData<'ast>, Error> for Replace {} diff --git a/typecheck/src/generics/substitutions.rs b/typecheck/src/generics/substitutions.rs new file mode 100644 index 00000000..f42e1cae --- /dev/null +++ b/typecheck/src/generics/substitutions.rs @@ -0,0 +1,202 @@ +// them with. + +// so we would like to traverse call points, and to build a map of declaration points with what to substitute them +// Find all the generic callpoints - find their associated declpoints - add that to the Map> +// TODO: How to handle children? We need to visit those as well and create a mono request too +// NOTE: We want to mono ALL children - even if they do not make use of the substitutions. Otherwise we'll probably run into issues, right? +// maybe we can revisit this later as a space optimization? + +// e.g. let's look at the following code: +// +// ```text +// type Foo[T](a: T, b: T); +// +// where f = Foo[int](a: 15, b: 14); +// ``` +// the FIR for this looks like the following: +// +// ```text +// { +// 1: Field(a, T) +// 2: Field(b, T) +// 3: Type(Foo, [T], [1, 2]) +// 4: Assign(1, Constant(15)) +// 5: Assign(2, Constant(14)) +// 6: Instantiation(3 /* Foo */, [int], [4, 5]) +// } +// ``` +// NOTE: Do we need two maps? One for declpoints and one for callpoints? or is that not necessary and I'm going about it the wrong way +// the output we want is something like this: +// ```text +// { 1: Subs(T -> int), NewIdx(7) } +// { 2: Subs(T -> int), NewIdx(8) } +// { 3: Subs(T -> int), NewIdx(9) } +// ``` +// then we'll have to figure out 1. how to do those substitutions 2. how to replace `3` by `9` (and others) in the FIR + +use fir::iter::TreeLike; +use fir::{Fir, Kind, Node, OriginIdx, RefIdx}; +use flatten::FlattenData; + +use core::mem; +use std::collections::HashMap; +use std::marker::PhantomData; + +#[derive(Debug, Copy, Clone)] +pub struct NewOrigin(OriginIdx); + +#[derive(Debug)] +pub struct OriginVector(Vec); + +impl From<&[RefIdx]> for OriginVector { + fn from(references: &[RefIdx]) -> OriginVector { + let origins = references.iter().map(|r| r.expect_resolved()).collect(); + + OriginVector(origins) + } +} + +#[derive(Debug)] +pub struct Generics { + generics: OriginVector, + _marker: PhantomData, +} + +impl Generics { + pub fn new(generics: impl Into) -> Generics { + Generics { + generics: generics.into(), + _marker: PhantomData, + } + } +} + +#[derive(Debug)] +pub struct Decls(Generics); +#[derive(Debug)] +pub struct Args(Generics); + +#[derive(Debug)] +pub struct SubsTarget { + from: Decls, + to: Args, +} + +impl SubsTarget { + pub fn new(from: Decls, to: Args) -> SubsTarget { + SubsTarget { from, to } + } +} + +#[derive(Default, Debug)] +pub struct Output { + // TODO: Should we store OriginIdx instead of NewOrigins in both maps? + pub(crate) to_mono: HashMap>, + pub(crate) substitutions: HashMap, +} + +// TODO: We also need to keep a stack of Context (SubsitutionCtx?) in order to put in monomorphize requests for the statements of a function, and add these to the substitution list +pub struct Substitutions { + output: Output, + next_idx: OriginIdx, +} + +impl Substitutions { + // FIXME: Should this return Self? + pub fn find<'ast>(fir: &Fir>) -> Output { + let next_idx = fir + .nodes + .last_key_value() + .map(|kv| kv.0.next()) + .unwrap_or(OriginIdx(1)); + + let mut ctx = Substitutions { + output: Output::default(), + next_idx, + }; + + ctx.visit_all(fir); + + ctx.output + } + + pub fn next_origin(&mut self) -> NewOrigin { + let new_next = self.next_idx.next(); + let next = mem::replace(&mut self.next_idx, new_next); + + NewOrigin(next) + } + + #[must_use] + // TODO: Rename + fn add_mono_request( + &mut self, + fir: &Fir>, + to: &RefIdx, + generics: &[RefIdx], + ) -> NewOrigin { + let from = &fir[to]; + let from = match &from.kind { + Kind::Function { generics, .. } => generics.as_slice(), + _ => unreachable!(), + }; + let next_origin = self.next_origin(); + let mono_request = ( + SubsTarget::new(Decls(Generics::new(from)), Args(Generics::new(generics))), + next_origin, + ); + + self.output + .to_mono + .entry(to.expect_resolved()) + .or_insert_with(|| vec![]) + .push(mono_request); + + next_origin + } + + fn substitute(&mut self, from: OriginIdx, to: NewOrigin) { + assert!(self.output.substitutions.insert(from, to).is_none()); + } +} + +impl<'ast> TreeLike> for Substitutions { + fn visit_call( + &mut self, + fir: &Fir>, + node: &Node>, + to: &RefIdx, + generics: &[RefIdx], + _args: &[RefIdx], + ) { + if !generics.is_empty() { + let new_fn = self.add_mono_request(fir, to, generics); + + // how does that work actually? arguments are just regular expressions - we don't need to monomorphize them, actually, right? + // we only need to change what they resolve to? how do we do that?? we don't need to create a new node for the call either, actually - we just change what it resolves to + // that's gonna be one extra TreeLike? + // self.visit_many(fir, args) + + // or we can actually store an extra map in the Substitutions - after all, this is also a substitution! - from the current OriginIdx to the one we just created + + self.substitute(node.origin, new_fn); + } + } + + fn visit_instantiation( + &mut self, + fir: &Fir>, + node: &Node>, + to: &RefIdx, + generics: &[RefIdx], + _fields: &[RefIdx], + ) { + if !generics.is_empty() { + let new_type = self.add_mono_request(fir, to, generics); + + // self.visit_many(fir, fields) + + self.substitute(node.origin, new_type); + } + } +} diff --git a/typecheck/src/lib.rs b/typecheck/src/lib.rs index 0aa6f62c..964524dc 100644 --- a/typecheck/src/lib.rs +++ b/typecheck/src/lib.rs @@ -1,6 +1,7 @@ mod actual; mod checker; mod collectors; +mod generics; mod typemap; mod typer; mod widen; @@ -83,6 +84,11 @@ impl Type { Type(origin, TypeSet(variants.collect())) } + pub fn generic(origin: OriginIdx) -> Type { + // FIXME: This needs explanations, and is it the proper thing??? + Type::record(origin) + } + pub fn set(&self) -> &TypeSet { &self.1 } @@ -100,7 +106,8 @@ impl Type { pub(crate) enum TypeVariable { Union(OriginIdx), Record(OriginIdx), - Reference(RefIdx), // specifically we're interested in `type_ctx.type_of(< that refidx >)` + Generic(OriginIdx), // generics are also declaration points + Reference(RefIdx), // specifically we're interested in `type_ctx.type_of(< that refidx >)` } type TypeLinkMap = HashMap; @@ -164,7 +171,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 @@ -180,10 +189,29 @@ impl<'ast> Pass, FlattenData<'ast>, Error> for TypeCtx Mapper, FlattenData<'ast>, Error> for Typer<'_> { // map_record_type and map_union_type are the two only functions which *create* actual types - all of the other // mappers create type references. they are declaration points. + // TODO: Should we create Generic types as well? Probably, yeah? + fn map_record_type( &mut self, data: FlattenData<'ast>,