Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix function binding name resolution #620

Merged
merged 7 commits into from
Oct 15, 2023
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
nr: Cleanup scoper, add documentation
CohenArthur committed Oct 15, 2023
commit d3d17c94465b5196fe29ef00570284270c378368
39 changes: 38 additions & 1 deletion name_resolve/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,40 @@
//! The name-resolve module takes care of resolving each "name" within a `jinko` program to
//! its definition site. You can think of it as a function, which will take an unresolved
//! [`Fir`] as input, and output a new [`Fir`] where each node points to the definition it
//! uses. To do this, multiple passes are applied on the input [`Fir`].
//!
//! 1. We start by "scoping" *all* of the definitions and usages in the program.
//! This is done via the [`Scoper`] type, which will assign an enclosing scope to
//! each node of our [`Fir`]. This is important, since name resolution in `jinko`
//! can go "backwards" or "upwards". You can access a definition in an *outer*
//! scope from an *inner* scope. This means that the outermost scope is able to
//! use definitions from the outermost scope. But if this "enclosing" or "parent"
//! relationship does not exist, then the usage is invalid:
//!
//! ```ignore
//! {
//! func foo() {}
//! }
//! {
//! { foo() }
//! // this scope does not have `foo`'s scope as a parent, so it will need to error out later on
//! }
//! ```
//!
//! 2. We collect all of the definitions within the program using the [`Declarator`] struct.
//! A definition can be a function definition, a new type, as well as a new binding. This
//! "definition collection" pass will only error out if a definition is present twice, e.g.:
//!
//! ```ignore
//! func foo() {}
//! func foo(different: int, arguments: int) -> string { "oh no" }
//! ```
//!
//! 2. Finally, we resolve all *usages* to their *definitions* using the [`Resolver`] type.
//! If a usage cannot be resolved to a definition, then it is an error. Similarly, if a usage
//! can be resolved to more than one definitions, we error out. The resolver does not take care
//! of resolving complex usages, such as methods, generic function calls or specialization.

use std::{collections::HashMap, ops::Index};

use error::{ErrKind, Error};
@@ -273,7 +310,7 @@ impl<'enclosing> NameResolveCtx<'enclosing> {
let root = fir.nodes.last_key_value().unwrap();

let mut scoper = Scoper {
current_scope: *root.0,
current_scope: scoper::Scope(*root.0),
enclosing_scope: HashMap::new(),
};

72 changes: 35 additions & 37 deletions name_resolve/src/scoper.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
// FIXME: Documentation

use std::collections::HashMap;
use std::{collections::HashMap, mem};

use fir::{Fallible, Fir, Kind, Node, OriginIdx, RefIdx, Traversal};
use flatten::FlattenData;

pub(crate) struct Scope(pub(crate) OriginIdx);

impl Scope {
pub fn replace(&mut self, new: OriginIdx) -> OriginIdx {
mem::replace(&mut self.0, new)
}

pub fn origin(&self) -> OriginIdx {
self.0
}
}

pub(crate) struct Scoper {
pub(crate) current_scope: OriginIdx, // TODO: Wrap in a new type with a .replace() method
pub(crate) current_scope: Scope,
pub(crate) enclosing_scope: HashMap<OriginIdx, OriginIdx>,
}

impl Scoper {
/// Set the enclosing scope of `to_scope` to the current scope
fn scope(&mut self, to_scope: &Node<FlattenData>) {
self.enclosing_scope
.insert(to_scope.origin, self.current_scope);
.insert(to_scope.origin, self.current_scope.origin());
}

/// Enter a new scope, replacing the context's current scope. This returns the old scope,
/// which you will need to reuse when you exit the scoped node you are visiting
fn enter_scope(&mut self, new_scope: OriginIdx) -> OriginIdx {
self.current_scope.replace(new_scope)
}

fn maybe_visit_child(&mut self, fir: &Fir<FlattenData<'_>>, ref_idx: &RefIdx) -> Fallible<()> {
@@ -24,26 +40,11 @@ impl Scoper {
RefIdx::Unresolved => Ok(()),
}
}

fn visit_field_instantiation(
&mut self,
fir: &Fir<FlattenData<'_>>,
ref_idx: &RefIdx,
) -> Fallible<()> {
self.maybe_visit_child(fir, ref_idx)

// let field_instantiation = dbg!(&fir[ref_idx]);

// if let Kind::TypedValue { value: _, ty } = field_instantiation.kind {
// // do not visit the `value` - it's always unresolved at this point
// self.maybe_visit_child(fir, &ty)
// } else {
// unreachable!()
// }
}
}

impl<'ast> Traversal<FlattenData<'ast>, () /* FIXME: Ok? */> for Scoper {
impl<'ast> Traversal<FlattenData<'ast>, () /* FIXME: Ok to have void as an error type? */>
for Scoper
{
fn traverse_assignment(
&mut self,
fir: &Fir<FlattenData<'ast>>,
@@ -64,9 +65,7 @@ impl<'ast> Traversal<FlattenData<'ast>, () /* FIXME: Ok? */> for Scoper {
return_ty: &Option<RefIdx>,
block: &Option<RefIdx>,
) -> Fallible<()> {
// TODO: Factor in a function
let old_scope = self.current_scope;
self.current_scope = node.origin;
let old = self.enter_scope(node.origin);

generics
.iter()
@@ -78,7 +77,7 @@ impl<'ast> Traversal<FlattenData<'ast>, () /* FIXME: Ok? */> for Scoper {
block.map(|definition| self.maybe_visit_child(fir, &definition));
return_ty.map(|ty| self.maybe_visit_child(fir, &ty));

self.current_scope = old_scope;
self.enter_scope(old);

Ok(())
}
@@ -89,14 +88,14 @@ impl<'ast> Traversal<FlattenData<'ast>, () /* FIXME: Ok? */> for Scoper {
node: &Node<FlattenData<'ast>>,
stmts: &[RefIdx],
) -> Fallible<()> {
let old_scope = self.current_scope;
self.current_scope = node.origin;
// TODO: Ugly but can we do anything better? Can we have types which force you to exit a scope if you enter one?
let old = self.enter_scope(node.origin);

stmts
.iter()
.for_each(|stmt| self.maybe_visit_child(fir, stmt).unwrap());

self.current_scope = old_scope;
self.enter_scope(old);

Ok(())
}
@@ -119,16 +118,15 @@ impl<'ast> Traversal<FlattenData<'ast>, () /* FIXME: Ok? */> for Scoper {
self.maybe_visit_child(fir, ty)
}
Kind::Type { fields, .. } => {
fields.iter().for_each(|field| {
// Factor in a function?
let old_scope = self.current_scope;
self.current_scope = node.origin;
let old = self.enter_scope(node.origin);

fields.iter().for_each(|field| {
// FIXME: Is unwrap okay here?
self.maybe_visit_child(fir, field).unwrap();

self.current_scope = old_scope;
});

self.enter_scope(old);

Ok(())
}
Kind::Generic { default } => default
@@ -147,7 +145,7 @@ impl<'ast> Traversal<FlattenData<'ast>, () /* FIXME: Ok? */> for Scoper {

fields
.iter()
.for_each(|field| self.visit_field_instantiation(fir, field).unwrap());
.for_each(|field| { self.maybe_visit_child(fir, field) }.unwrap());

Ok(())
}