Fix unused assignment detection bug.

This commit is contained in:
Richard Feldman 2019-07-28 16:49:44 -04:00
parent 5161a03638
commit 1b5df3f1c3
3 changed files with 336 additions and 154 deletions

View file

@ -2,10 +2,11 @@ use region::{Located, Region};
use operator::Operator; use operator::Operator;
use operator::Operator::Pizza; use operator::Operator::Pizza;
use operator::Associativity::*; use operator::Associativity::*;
use collections::{ImSet, ImMap}; use collections::{ImSet, ImMap, MutMap};
use std::cmp::Ordering; use std::cmp::Ordering;
use expr::{Ident, VariantName, Path}; use expr::{Ident, VariantName, Path};
use expr; use expr;
use self::PatternType::*;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -41,7 +42,7 @@ pub enum Expr {
If(Box<Located<Expr>>, Box<Located<Expr>>, Box<Located<Expr>>), If(Box<Located<Expr>>, Box<Located<Expr>>, Box<Located<Expr>>),
Operator(Box<Located<Expr>>, Located<Operator>, Box<Located<Expr>>), Operator(Box<Located<Expr>>, Located<Operator>, Box<Located<Expr>>),
// Runtime Error // Runtime Errors
InvalidPrecedence(PrecedenceProblem, Box<Located<Expr>>) InvalidPrecedence(PrecedenceProblem, Box<Located<Expr>>)
} }
@ -49,15 +50,33 @@ pub enum Expr {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Problem { pub enum Problem {
Shadowing(Located<expr::Ident>), Shadowing(Located<expr::Ident>),
UnrecognizedFunctionName(Located<expr::Ident>), UnrecognizedFunctionName(Located<Ident>),
UnrecognizedConstant(Located<expr::Ident>), UnrecognizedConstant(Located<Ident>),
UnrecognizedVariant(Located<expr::VariantName>), UnrecognizedVariant(Located<VariantName>),
UnusedAssignment(Located<expr::Ident>), UnusedAssignment(Located<Ident>),
PrecedenceProblem(PrecedenceProblem), PrecedenceProblem(PrecedenceProblem),
// Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments!
UnsupportedPattern(PatternType, Located<expr::Pattern>)
}
/// A pattern, including possible problems (e.g. shadowing) so that
/// codegen can generate a runtime error if this pattern is reached.
#[derive(Clone, Debug, PartialEq)]
pub enum Pattern {
Identifier(LocalSymbol),
Variant(Resolved<Symbol>, Option<Vec<Located<Pattern>>>),
Integer(i64),
Fraction(i64, i64),
ExactString(String),
EmptyRecordLiteral,
Underscore,
Shadowed(Located<Ident>),
// Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments!
UnsupportedPattern(Located<expr::Pattern>)
} }
/// An ident or variant name, possibly unrecognized, possibly referring to either a toplevel or local symbol. /// An ident or variant name, possibly unrecognized, possibly referring to either a toplevel or local symbol.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Symbol { pub enum Symbol {
/// An ident or variant name referencing a toplevel declaration. /// An ident or variant name referencing a toplevel declaration.
Global(GlobalSymbol), Global(GlobalSymbol),
@ -67,13 +86,21 @@ pub enum Symbol {
} }
/// An ident or variant name in the globlal scope; that is, something defined in the toplevel of some module. /// An ident or variant name in the globlal scope; that is, something defined in the toplevel of some module.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct GlobalSymbol(String); pub struct GlobalSymbol(String);
/// An ident referencing a local assignment - *not* something defined in the toplevel. /// An ident referencing a local assignment - *not* something defined in the toplevel.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LocalSymbol(String); pub struct LocalSymbol(String);
impl Into<Ident> for LocalSymbol {
fn into(self) -> Ident {
let LocalSymbol(name) = self;
Ident::Unqualified(name)
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Resolved<T> { pub enum Resolved<T> {
/// This is the unique symbol we'll use in codegen. /// This is the unique symbol we'll use in codegen.
@ -86,12 +113,12 @@ pub enum Resolved<T> {
} }
impl GlobalSymbol { impl GlobalSymbol {
pub fn new((path, name): (Path, String)) -> GlobalSymbol { pub fn new(path: Path, name: String) -> GlobalSymbol {
GlobalSymbol(format!("{}.{}", path.into_string(), name)) GlobalSymbol(format!("{}.{}", path.into_string(), name))
} }
pub fn recognized(info: (Path, String)) -> Resolved<GlobalSymbol> { pub fn recognized(path: Path, name: String) -> Resolved<GlobalSymbol> {
Resolved::Recognized(GlobalSymbol::new(info)) Resolved::Recognized(GlobalSymbol::new(path, name))
} }
} }
@ -106,16 +133,16 @@ impl LocalSymbol {
} }
impl Symbol { impl Symbol {
pub fn resolved_global(info: (Path, String)) -> Resolved<Symbol> { pub fn resolved_global(path: Path, name: String) -> Resolved<Symbol> {
Resolved::Recognized(Symbol::Global(GlobalSymbol::new(info))) Resolved::Recognized(Symbol::Global(GlobalSymbol::new(path, name)))
} }
pub fn resolved_local(name: String) -> Resolved<Symbol> { pub fn resolved_local(name: String) -> Resolved<Symbol> {
Resolved::Recognized(Symbol::Local(LocalSymbol::new(name))) Resolved::Recognized(Symbol::Local(LocalSymbol::new(name)))
} }
pub fn global(info: (Path, String)) -> Symbol { pub fn global(path: Path, name: String) -> Symbol {
Symbol::Global(GlobalSymbol::new(info)) Symbol::Global(GlobalSymbol::new(path, name))
} }
pub fn local(name: String) -> Symbol { pub fn local(name: String) -> Symbol {
@ -123,20 +150,6 @@ impl Symbol {
} }
} }
/// A pattern, including possible problems (e.g. shadowing) so that
/// codegen can generate a runtime error if this pattern is reached.
#[derive(Clone, Debug, PartialEq)]
pub enum Pattern {
Identifier(Symbol),
Variant(Resolved<Symbol>, Option<Vec<Located<Pattern>>>),
Integer(i64),
Fraction(i64, i64),
ExactString(String),
EmptyRecordLiteral,
Underscore,
Shadowed(Located<expr::Ident>)
}
/// The canonicalization environment for a particular module. /// The canonicalization environment for a particular module.
struct Env { struct Env {
/// The module's path. Unqualified references to identifiers and variant names are assumed /// The module's path. Unqualified references to identifiers and variant names are assumed
@ -160,19 +173,10 @@ impl Env {
} }
} }
pub fn canonicalize_declarations(
home: Path,
decls: Vec<(Located<Pattern>, Located<expr::Expr>)>,
declared_idents: &ImMap<(Option<Path>, String), Located<expr::Ident>>,
declared_variants: &ImMap<(Path, String), Located<expr::VariantName>>,
) {
panic!("TODO: handle decls like assignments; check for shadowing, tail recursion, named closures functions, etc");
}
pub fn canonicalize_declaration( pub fn canonicalize_declaration(
home: Path, home: Path,
loc_expr: Located<expr::Expr>, loc_expr: Located<expr::Expr>,
declared_idents: &ImMap<(Option<Path>, String), Located<expr::Ident>>, declared_idents: &ImMap<Ident, Located<expr::Ident>>,
declared_variants: &ImMap<(Path, String), Located<expr::VariantName>>, declared_variants: &ImMap<(Path, String), Located<expr::VariantName>>,
) -> (Located<Expr>, Output, Vec<Problem>) { ) -> (Located<Expr>, Output, Vec<Problem>) {
let mut env = Env::new(home, declared_variants.clone()); let mut env = Env::new(home, declared_variants.clone());
@ -190,7 +194,7 @@ pub fn canonicalize_declaration(
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Output { pub struct Output {
pub referenced_idents: ImSet<(Option<Path>, String)>, pub referenced_idents: ImSet<Ident>,
pub applied_variants: ImSet<(Path, String)>, pub applied_variants: ImSet<(Path, String)>,
pub tail_call: Option<Symbol>, pub tail_call: Option<Symbol>,
} }
@ -215,7 +219,7 @@ impl Output {
fn canonicalize( fn canonicalize(
env: &mut Env, env: &mut Env,
loc_expr: Located<expr::Expr>, loc_expr: Located<expr::Expr>,
idents_in_scope: &ImMap<(Option<Path>, String), Located<expr::Ident>>, idents_in_scope: &ImMap<Ident, Located<expr::Ident>>,
) -> (Located<Expr>, Output) { ) -> (Located<Expr>, Output) {
use self::Expr::*; use self::Expr::*;
@ -433,7 +437,6 @@ fn canonicalize(
expr::Expr::Assign(assignments, box_loc_returned) => { expr::Expr::Assign(assignments, box_loc_returned) => {
use self::Pattern::*; use self::Pattern::*;
let mut referenced_idents = ImSet::default();
let mut applied_variants = ImSet::default(); let mut applied_variants = ImSet::default();
let mut new_idents_in_scope = ImMap::default(); let mut new_idents_in_scope = ImMap::default();
@ -446,13 +449,13 @@ fn canonicalize(
// it means there was shadowing, so keep the original mapping from ident_in_scope. // it means there was shadowing, so keep the original mapping from ident_in_scope.
// Shadowing means the mapping from new_idents_in_scope will be removed later. // Shadowing means the mapping from new_idents_in_scope will be removed later.
let mut combined_idents_in_scope = idents_in_scope.clone().union(new_idents_in_scope.clone()); let mut combined_idents_in_scope = idents_in_scope.clone().union(new_idents_in_scope.clone());
let mut referenced_idents_by_assigned_name: MutMap<Ident, (Region, ImSet<Ident>)> = MutMap::default();
let can_assignments = assignments.into_iter().map(|(loc_pattern, expr)| { let can_assignments = assignments.into_iter().map(|(loc_pattern, expr)| {
// Each assignment gets to have all the idents in scope that are assigned in this // Each assignment gets to have all the idents in scope that are assigned in this
// block. Order of assignments doesn't matter, thanks to referential transparency! // block. Order of assignments doesn't matter, thanks to referential transparency!
let (loc_can_expr, can_output) = canonicalize(env, expr, &combined_idents_in_scope); let (loc_can_expr, can_output) = canonicalize(env, expr, &combined_idents_in_scope);
referenced_idents = referenced_idents.clone().union(can_output.referenced_idents);
applied_variants = applied_variants.clone().union(can_output.applied_variants); applied_variants = applied_variants.clone().union(can_output.applied_variants);
// Exclude the current ident from shadowable_idents; you can't shadow yourself! // Exclude the current ident from shadowable_idents; you can't shadow yourself!
@ -460,38 +463,43 @@ fn canonicalize(
let mut shadowable_idents = combined_idents_in_scope.clone(); let mut shadowable_idents = combined_idents_in_scope.clone();
remove_idents(loc_pattern.value.clone(), &mut shadowable_idents); remove_idents(loc_pattern.value.clone(), &mut shadowable_idents);
let can_pattern = canonicalize_pattern(env, loc_pattern, &mut combined_idents_in_scope, &mut shadowable_idents); let can_pattern = canonicalize_pattern(env, &Assignment, loc_pattern.clone(), &mut combined_idents_in_scope, &mut shadowable_idents);
// Store the referenced idents in a map, so we can later figure out which
// assigned names reference each other.
for name in local_idents_in_pattern(&can_pattern.value) {
referenced_idents_by_assigned_name.insert(name, (loc_pattern.region, can_output.referenced_idents.clone()));
}
// Give closures names (and tail-recursive status) where appropriate // Give closures names (and tail-recursive status) where appropriate
let region = loc_can_expr.region; let region = loc_can_expr.region;
let can_expr = let can_expr = match &can_pattern.value {
match &can_pattern.value { // First, make sure we are actually assigning an identifier instead of (for example) a variant.
// First, make sure we are actually assigning an identifier instead of (for example) a variant. // If we're assigning (UserId userId) = ... then this is by definition not a self tail call!
// If we're assigning (UserId userId) = ... then this is by definition not a self tail call! // (We could theoretically support certain scenarios like that, but it doesn't seem worthwhile;
// (We could theoretically support certain scenarios like that, but it doesn't seem worthwhile; // all we'd be saving anyone is the step of refactoring the closure out to have its own name.)
// all we'd be saving anyone is the step of refactoring the closure out to have its own name.) // By our definition, only assignments of the form (foo = ...) can be self tail calls.
// By our definition, only assignments of the form (foo = ...) can be self tail calls. &Identifier(ref local_symbol_ref) => {
&Identifier(ref closure_symbol_ref) => { match loc_can_expr.value {
match loc_can_expr.value { AnonymousClosure(closed_over, args, body) => {
AnonymousClosure(closed_over, args, body) => { let closure_symbol = Symbol::Local(local_symbol_ref.clone());
let closure_symbol = closure_symbol_ref.clone(); let is_self_tail_recursive = match can_output.tail_call {
let is_self_tail_recursive = match can_output.tail_call { None => false,
None => false, Some(symbol) => symbol == closure_symbol
Some(symbol) => symbol == closure_symbol };
};
if is_self_tail_recursive { if is_self_tail_recursive {
TailRecursiveClosure(closure_symbol, closed_over, args, body) TailRecursiveClosure(closure_symbol, closed_over, args, body)
} else { } else {
NamedClosure(closure_symbol, closed_over, args, body) NamedClosure(closure_symbol, closed_over, args, body)
} }
}, },
non_closure => non_closure non_closure => non_closure
} }
}, },
&Shadowed(_) | &Variant(_, _) | &Integer(_) | &Fraction(_, _) &Variant(_, _) | &EmptyRecordLiteral | &Shadowed(_) | &UnsupportedPattern(_)
| &ExactString(_) | &EmptyRecordLiteral | &Underscore => loc_can_expr.value | &Integer(_) | &Fraction(_, _) | &ExactString(_) | &Underscore => loc_can_expr.value,
}; };
(can_pattern, Located {region, value: can_expr}) (can_pattern, Located {region, value: can_expr})
}).collect(); }).collect();
@ -500,17 +508,60 @@ fn canonicalize(
// We use its output as a starting point because its tail_call already has the right answer! // We use its output as a starting point because its tail_call already has the right answer!
let (ret_expr, mut output) = canonicalize(env, *box_loc_returned, &combined_idents_in_scope); let (ret_expr, mut output) = canonicalize(env, *box_loc_returned, &combined_idents_in_scope);
output.referenced_idents = output.referenced_idents.clone().union(referenced_idents);
output.applied_variants = output.applied_variants.clone().union(applied_variants); output.applied_variants = output.applied_variants.clone().union(applied_variants);
// Determine the full set of referenced_idents by traversing the graph
let mut referenced_idents = output.referenced_idents.clone();
let mut visited = ImSet::default();
// Start with the return expression's referenced_idents. They are the only ones that count!
// For example, if I have two assignments which reference each other, but neither of them
// is referenced in the return expression, I don't want either of them to end up in the
// final referenced_idents.
//
// The reason we need a graph here is so we don't overlook transitive dependencies.
// For example, if I have `a = b + 1` and the assignment returns `a + 1`, then the
// assignment as a whole references both `a` *and* `b`, even though it doesn't
// directly mention `b` - because `a` depends on `b`. If we didn't traverse a graph here,
// we'd erroneously give a warning that `b` was unused since it wasn't directly referenced.
for ident in output.referenced_idents.clone() {
match ident {
// Only consider local referenced idents; the global ones don't need the graph.
unqualified @ Ident::Unqualified(_) => {
// Traverse the graph and look up *all* the references for this name
let refs = get_all_referenced(unqualified, &mut visited, &referenced_idents_by_assigned_name);
referenced_idents = referenced_idents.union(refs);
}
Ident::Qualified(_, _) => ()
};
}
// Now that we've collected all the references, check to see if any of the new idents // Now that we've collected all the references, check to see if any of the new idents
// we defined were unused. If any were, report it. // we defined were unused. If any were, report it.
for (ident, loc_expr) in new_idents_in_scope { for ident in new_idents_in_scope.keys() {
if !output.referenced_idents.contains(&ident) { if !ident.is_qualified() && !referenced_idents.contains(&ident) {
env.problem(Problem::UnusedAssignment(loc_expr)); match ident {
unqualified @ Ident::Unqualified(_) => {
match referenced_idents_by_assigned_name.get(&unqualified) {
Some((region, _)) => {
let loc_ident = Located {region: region.clone(), value: unqualified.clone()};
env.problem(Problem::UnusedAssignment(loc_ident));
},
None => unreachable!()
};
},
Ident::Qualified(_, _) => ()
}
} }
} }
// TODO somewhere in here, build 2 graphs of all the idents
// 1. eval_deps - topological sort this and look for cycles (add a cycle test and an eval order test!)
// 2. references - do a dfs search on each referenced local value in ret, to see which
output.referenced_idents = referenced_idents;
(Assign(can_assignments, Box::new(ret_expr)), output) (Assign(can_assignments, Box::new(ret_expr)), output)
}, },
@ -533,7 +584,7 @@ fn canonicalize(
let mut shadowable_idents = combined_idents_in_scope.clone(); let mut shadowable_idents = combined_idents_in_scope.clone();
remove_idents(loc_pattern.value.clone(), &mut shadowable_idents); remove_idents(loc_pattern.value.clone(), &mut shadowable_idents);
canonicalize_pattern(env, loc_pattern, &mut combined_idents_in_scope, &mut shadowable_idents) canonicalize_pattern(env, &FunctionArg, loc_pattern, &mut combined_idents_in_scope, &mut shadowable_idents)
}).collect(); }).collect();
let (body_expr, output) = canonicalize(env, *box_loc_body_expr, &combined_idents_in_scope); let (body_expr, output) = canonicalize(env, *box_loc_body_expr, &combined_idents_in_scope);
@ -550,14 +601,14 @@ fn canonicalize(
let closed_over_locals = if output.referenced_idents.is_empty() { let closed_over_locals = if output.referenced_idents.is_empty() {
None None
} else { } else {
let locals = output.referenced_idents.iter().filter_map(|(opt_path, name)| { let locals = output.referenced_idents.iter().filter_map(|ident| {
// Only close over locally assigned idents; globals are always available. // Only close over locally assigned idents; globals are always available.
if opt_path.is_none() if !ident.is_qualified()
// If it's not in scope, it'll be a NAMING ERROR at runtime, and // If it's not in scope, it'll be a NAMING ERROR at runtime, and
// attempting to close over it will fail. Leave it out! // attempting to close over it will fail. Leave it out!
&& combined_idents_in_scope.contains_key(&(None, name.clone())) && combined_idents_in_scope.contains_key(&ident)
{ {
Some(LocalSymbol::new(name.clone())) Some(LocalSymbol::new(ident.clone().name()))
} else { } else {
None None
} }
@ -591,7 +642,7 @@ fn canonicalize(
let mut shadowable_idents = idents_in_scope.clone(); let mut shadowable_idents = idents_in_scope.clone();
remove_idents(loc_pattern.value.clone(), &mut shadowable_idents); remove_idents(loc_pattern.value.clone(), &mut shadowable_idents);
let can_pattern = canonicalize_pattern(env, loc_pattern.clone(), &mut idents_in_scope.clone(), &mut idents_in_scope.clone()); let can_pattern = canonicalize_pattern(env, &CaseBranch, loc_pattern.clone(), &mut idents_in_scope.clone(), &mut idents_in_scope.clone());
// Patterns introduce new idents to the scope! // Patterns introduce new idents to the scope!
let mut new_idents_in_scope = ImMap::default(); let mut new_idents_in_scope = ImMap::default();
@ -644,9 +695,69 @@ fn canonicalize(
(Located {region, value: expr}, output) (Located {region, value: expr}, output)
} }
fn get_all_referenced(
name: Ident,
visited: &mut ImSet<Ident>,
referenced_idents_by_assigned_name: &MutMap<Ident, (Region, ImSet<Ident>)>
) -> ImSet<Ident> {
match referenced_idents_by_assigned_name.get(&name) {
Some((_, idents)) => {
let mut output = ImSet::default();
visited.insert(name);
for ident in idents {
match ident {
unqualified @ Ident::Unqualified(_) => {
output.insert(unqualified.clone());
if !visited.contains(&unqualified) {
let other_refs = get_all_referenced(unqualified.clone(), visited, referenced_idents_by_assigned_name);
output = output.union(other_refs);
}
}
Ident::Qualified(_, _) => ()
};
}
output
},
None => ImSet::default()
}
}
fn local_idents_in_pattern(pattern: &Pattern) -> ImSet<Ident> {
use self::Pattern::*;
let mut output = ImSet::default();
// Insert any identifiers we find into the referenced_idents_by_name map.
match pattern {
&Identifier(ref local_symbol_ref) => {
output.insert(local_symbol_ref.clone().into());
},
&Variant(_, ref opt_args) => {
match opt_args {
&Some(ref loc_args) => {
for loc_arg in loc_args {
output = output.union(local_idents_in_pattern(&loc_arg.value));
}
},
&None => ()
}
},
&Shadowed(_) | &Integer(_) | &Fraction(_, _) | &UnsupportedPattern(_)
| &ExactString(_) | &EmptyRecordLiteral | &Underscore => ()
};
output
}
fn add_idents_to_scope( fn add_idents_to_scope(
pattern: Located<expr::Pattern>, pattern: Located<expr::Pattern>,
idents_in_scope: &mut ImMap<(Option<Path>, String), Located<expr::Ident>> idents_in_scope: &mut ImMap<Ident, Located<expr::Ident>>
) { ) {
use expr::Pattern::*; use expr::Pattern::*;
@ -657,7 +768,7 @@ fn add_idents_to_scope(
value: Ident::Unqualified(name.clone()) value: Ident::Unqualified(name.clone())
}; };
idents_in_scope.insert((None, name), loc_ident); idents_in_scope.insert(Ident::Unqualified(name), loc_ident);
}, },
Variant(_, Some(loc_args)) => { Variant(_, Some(loc_args)) => {
for loc_arg in loc_args { for loc_arg in loc_args {
@ -671,12 +782,12 @@ fn add_idents_to_scope(
fn remove_idents( fn remove_idents(
pattern: expr::Pattern, pattern: expr::Pattern,
idents: &mut ImMap<(Option<Path>, String), Located<expr::Ident>> idents: &mut ImMap<Ident, Located<expr::Ident>>
) { ) {
use expr::Pattern::*; use expr::Pattern::*;
match pattern { match pattern {
Identifier(name) => { idents.remove(&(None, name)); }, Identifier(name) => { idents.remove(&(Ident::Unqualified(name))); },
Variant(_, Some(loc_args)) => { Variant(_, Some(loc_args)) => {
for loc_arg in loc_args { for loc_arg in loc_args {
remove_idents(loc_arg.value, idents); remove_idents(loc_arg.value, idents);
@ -692,40 +803,36 @@ fn remove_idents(
fn resolve_ident( fn resolve_ident(
env: &Env, env: &Env,
ident: Ident, ident: Ident,
referenced_idents: &mut ImSet<(Option<Path>, String)>, referenced_idents: &mut ImSet<Ident>,
idents_in_scope: &ImMap<(Option<Path>, String), Located<expr::Ident>>, idents_in_scope: &ImMap<Ident, Located<expr::Ident>>,
) -> Result<Resolved<Symbol>, Ident> { ) -> Result<Resolved<Symbol>, Ident> {
match ident { if idents_in_scope.contains_key(&ident) {
Ident::Unqualified(name) => { referenced_idents.insert(ident.clone());
let unqualified = ( None, name );
if idents_in_scope.contains_key(&unqualified) { match ident {
referenced_idents.insert(unqualified.clone()); Ident::Unqualified(name) => Ok(Symbol::resolved_local(name)),
Ident::Qualified(path, name) => Ok(Symbol::resolved_global(path, name))
Ok(Symbol::resolved_local(unqualified.1)) }
} else { } else {
let qualified = ( Some(env.home.clone()), unqualified.1 ); match ident {
Ident::Unqualified(name) => {
// Try again, this time using the current module as the path.
let path = env.home.clone();
let qualified = Ident::Qualified(path.clone(), name.clone());
if idents_in_scope.contains_key(&qualified) { if idents_in_scope.contains_key(&qualified) {
referenced_idents.insert(qualified.clone()); referenced_idents.insert(qualified.clone());
Ok(Symbol::resolved_global((qualified.0.unwrap(), qualified.1))) Ok(Symbol::resolved_global(path, name))
} else { } else {
// We couldn't find the unqualified ident in scope. NAMING PROBLEM! // We couldn't find the unqualified ident in scope. NAMING PROBLEM!
Err(Ident::Unqualified(qualified.1)) Err(Ident::Unqualified(name))
} }
}
},
Ident::Qualified((path, name)) => {
let qualified = (Some(path), name);
if idents_in_scope.contains_key(&qualified) { },
referenced_idents.insert(qualified.clone()); qualified @ Ident::Qualified(_, _) => {
Ok(Symbol::resolved_global((qualified.0.unwrap(), qualified.1)))
} else {
// We couldn't find the qualified ident in scope. NAMING PROBLEM! // We couldn't find the qualified ident in scope. NAMING PROBLEM!
Err(Ident::Qualified((qualified.0.unwrap(), qualified.1))) Err(qualified)
} }
} }
} }
@ -740,38 +847,44 @@ fn resolve_variant_name(
applied_variants: &mut ImSet<(Path, String)>, applied_variants: &mut ImSet<(Path, String)>,
) -> Result<Resolved<GlobalSymbol>, VariantName> { ) -> Result<Resolved<GlobalSymbol>, VariantName> {
let qualified = match variant_name { let qualified = match variant_name {
VariantName::Unqualified(name) => ( env.home.clone(), name ), VariantName::Unqualified(name) => (env.home.clone(), name),
VariantName::Qualified(qualified) => qualified VariantName::Qualified(path, name) => (path, name)
}; };
if env.declared_variants.contains_key(&qualified) { if env.declared_variants.contains_key(&qualified) {
applied_variants.insert(qualified.clone()); applied_variants.insert(qualified.clone());
Ok(GlobalSymbol::recognized(qualified)) Ok(GlobalSymbol::recognized(qualified.0, qualified.1))
} else { } else {
// We couldn't find the qualified variant name in scope. NAMING PROBLEM! // We couldn't find the qualified variant name in scope. NAMING PROBLEM!
Err(VariantName::Qualified(qualified)) Err(VariantName::Qualified(qualified.0, qualified.1))
} }
} }
/// Different patterns are supported in different circumstances.
/// For example, case branches can pattern match on number literals, but
/// assignments and function args can't. Underscore is supported in function
/// arg patterns and in case branch patterns, but not in assignments.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PatternType {
Assignment,
FunctionArg,
CaseBranch
}
fn canonicalize_pattern( fn canonicalize_pattern(
env: &mut Env, env: &mut Env,
pattern_type: &PatternType,
loc_pattern: Located<expr::Pattern>, loc_pattern: Located<expr::Pattern>,
idents_in_scope: &mut ImMap<(Option<Path>, String), Located<expr::Ident>>, idents_in_scope: &mut ImMap<Ident, Located<expr::Ident>>,
shadowable_idents: &mut ImMap<(Option<Path>, String), Located<expr::Ident>>, shadowable_idents: &mut ImMap<Ident, Located<expr::Ident>>,
) -> Located<Pattern> { ) -> Located<Pattern> {
use self::Pattern::*; use expr::Pattern::*;
let region = loc_pattern.region; let region = loc_pattern.region;
let new_pattern = match loc_pattern.value { let new_pattern = match &loc_pattern.value {
expr::Pattern::Integer(num) => Integer(num), &Identifier(ref name) => {
expr::Pattern::Fraction(numerator, denominator) => Fraction(numerator, denominator), let unqualified_ident = Ident::Unqualified(name.clone());
expr::Pattern::EmptyRecordLiteral => EmptyRecordLiteral,
expr::Pattern::ExactString(string) => ExactString(string),
expr::Pattern::Underscore => Underscore,
expr::Pattern::Identifier(name) => {
let unqualified_ident = ( None, name );
// We use shadowable_idents for this, and not idents_in_scope, because for assignments // We use shadowable_idents for this, and not idents_in_scope, because for assignments
// they are different. When canonicalizing a particular assignment, that new // they are different. When canonicalizing a particular assignment, that new
@ -788,10 +901,10 @@ fn canonicalize_pattern(
// Change this Pattern to a Shadowed variant, so that // Change this Pattern to a Shadowed variant, so that
// codegen knows to generate a runtime exception here. // codegen knows to generate a runtime exception here.
Shadowed(shadowed_ident.clone()) Pattern::Shadowed(shadowed_ident.clone())
}, },
None => { None => {
let qualified_ident = ( Some(env.home.clone()), unqualified_ident.1 ); let qualified_ident = Ident::Qualified(env.home.clone(), unqualified_ident.name());
match idents_in_scope.get(&qualified_ident) { match idents_in_scope.get(&qualified_ident) {
Some(shadowed_ident) => { Some(shadowed_ident) => {
@ -801,10 +914,11 @@ fn canonicalize_pattern(
// Change this Pattern to a Shadowed variant, so that // Change this Pattern to a Shadowed variant, so that
// codegen knows to generate a runtime exception here. // codegen knows to generate a runtime exception here.
Shadowed(shadowed_ident.clone()) Pattern::Shadowed(shadowed_ident.clone())
}, },
None => { None => {
let new_name = qualified_ident.1.clone(); let new_ident = qualified_ident.clone();
let new_name = qualified_ident.name();
// This is a fresh identifier that wasn't already in scope. // This is a fresh identifier that wasn't already in scope.
// Add it to scope! // Add it to scope!
@ -814,19 +928,19 @@ fn canonicalize_pattern(
// The latter is relevant when recursively canonicalizing Variant patterns, // The latter is relevant when recursively canonicalizing Variant patterns,
// which can bring multiple new idents into scope. For example, it's important // which can bring multiple new idents into scope. For example, it's important
// that we catch (Blah foo foo) as being an example of shadowing. // that we catch (Blah foo foo) as being an example of shadowing.
idents_in_scope.insert(qualified_ident.clone(), located.clone()); idents_in_scope.insert(new_ident.clone(), located.clone());
shadowable_idents.insert(qualified_ident, located); shadowable_idents.insert(new_ident, located);
Identifier(Symbol::local(new_name)) Pattern::Identifier(LocalSymbol::new(new_name))
} }
} }
} }
} }
}, },
expr::Pattern::Variant(loc_name, opt_args) => { &Variant(ref loc_name, ref opt_args) => {
// Canonicalize the variant's name. // Canonicalize the variant's name.
let can_name = canonicalize_variant_name(env, loc_name); let can_name = canonicalize_variant_name(env, loc_name.clone());
// Canonicalize the variant's arguments, if it has any. // Canonicalize the variant's arguments, if it has any.
let opt_can_args: Option<Vec<Located<Pattern>>> = match opt_args { let opt_can_args: Option<Vec<Located<Pattern>>> = match opt_args {
@ -835,7 +949,7 @@ fn canonicalize_pattern(
let mut can_args:Vec<Located<Pattern>> = Vec::new(); let mut can_args:Vec<Located<Pattern>> = Vec::new();
for loc_arg in loc_args { for loc_arg in loc_args {
let can_arg = canonicalize_pattern(env, loc_arg, idents_in_scope, shadowable_idents); let can_arg = canonicalize_pattern(env, pattern_type, loc_arg.clone(), idents_in_scope, shadowable_idents);
can_args.push(can_arg); can_args.push(can_arg);
} }
@ -844,25 +958,65 @@ fn canonicalize_pattern(
} }
}; };
Variant(can_name, opt_can_args) Pattern::Variant(can_name, opt_can_args)
} },
&Integer(ref num) => {
match pattern_type {
CaseBranch => Pattern::Integer(*num),
ptype @ Assignment | ptype @ FunctionArg => unsupported_pattern(env, *ptype, &region, &loc_pattern.value)
}
},
&Fraction(ref numerator, ref denominator) => {
match pattern_type {
CaseBranch => Pattern::Fraction(*numerator, *denominator),
ptype @ Assignment | ptype @ FunctionArg => unsupported_pattern(env, *ptype, &region, &loc_pattern.value)
}
},
&ExactString(ref string) => {
match pattern_type {
CaseBranch => Pattern::ExactString(string.clone()),
ptype @ Assignment | ptype @ FunctionArg => unsupported_pattern(env, *ptype, &region, &loc_pattern.value)
}
},
&Underscore => {
match pattern_type {
CaseBranch | FunctionArg => Pattern::Underscore,
Assignment => unsupported_pattern(env, Assignment, &region, &loc_pattern.value)
}
},
&EmptyRecordLiteral => Pattern::EmptyRecordLiteral,
}; };
Located {region, value: new_pattern} Located {region, value: new_pattern}
} }
/// When we detect an unsupported pattern type (e.g. 5 = 1 + 2 is unsupported because you can't
/// assign to Int patterns), report it to Env and return an UnsupportedPattern runtime error pattern.
fn unsupported_pattern(env: &mut Env, pattern_type: PatternType, region: &Region, pattern: &expr::Pattern) -> Pattern {
let loc_problem_pattern = Located {region: region.clone(), value: pattern.clone()};
env.problem(Problem::UnsupportedPattern(pattern_type, loc_problem_pattern.clone()));
Pattern::UnsupportedPattern(loc_problem_pattern)
}
fn canonicalize_variant_name( fn canonicalize_variant_name(
env: &mut Env, env: &mut Env,
loc_name: Located<VariantName> loc_name: Located<VariantName>
) -> Resolved<Symbol> { ) -> Resolved<Symbol> {
let qualified_name = match &loc_name.value { let qualified_name = match &loc_name.value {
&VariantName::Unqualified(ref name) => ( env.home.clone(), name.clone() ), &VariantName::Unqualified(ref name) => ( env.home.clone(), name.clone() ),
&VariantName::Qualified(ref qualified) => qualified.clone() &VariantName::Qualified(ref path, ref name) => ( path.clone(), name.clone() )
}; };
if env.declared_variants.contains_key(&qualified_name) { if env.declared_variants.contains_key(&qualified_name) {
// No problems; the qualified variant name was in scope! // No problems; the qualified variant name was in scope!
Symbol::resolved_global(qualified_name) Symbol::resolved_global(qualified_name.0, qualified_name.1)
} else { } else {
// We couldn't find the variant name in scope. NAMING PROBLEM! // We couldn't find the variant name in scope. NAMING PROBLEM!
env.problem(Problem::UnrecognizedVariant(loc_name.clone())); env.problem(Problem::UnrecognizedVariant(loc_name.clone()));

View file

@ -37,19 +37,35 @@ pub enum Expr {
/// A variant name, possibly fully-qualified with a module name /// A variant name, possibly fully-qualified with a module name
/// e.g. (Result.Ok) /// e.g. (Result.Ok)
/// Parameterized on a phantom marker for whether it has been canonicalized /// Parameterized on a phantom marker for whether it has been canonicalized
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum VariantName { pub enum VariantName {
Unqualified(String), Unqualified(String),
Qualified((Path, String)), Qualified(Path, String),
} }
/// An identifier, possibly fully-qualified with a module name /// An identifier, possibly fully-qualified with a module name
/// e.g. (Http.Request from http) /// e.g. (Http.Request from http)
/// Parameterized on a phantom marker for whether it has been canonicalized /// Parameterized on a phantom marker for whether it has been canonicalized
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Ident { pub enum Ident {
Unqualified(String), Unqualified(String),
Qualified((Path, String)), Qualified(Path, String),
}
impl Ident {
pub fn is_qualified(&self) -> bool {
match &self {
&Ident::Unqualified(_) => false,
&Ident::Qualified(_, _) => true,
}
}
pub fn name(self) -> String {
match self {
Ident::Unqualified(name) => name,
Ident::Qualified(_, name) => name
}
}
} }
/// A path to a module, which may include the package it came from. /// A path to a module, which may include the package it came from.
@ -86,7 +102,7 @@ impl fmt::Display for Ident {
Ident::Unqualified(name) => { Ident::Unqualified(name) => {
write!(f, "{}", name) write!(f, "{}", name)
}, },
Ident::Qualified((path, name)) => { Ident::Qualified(path, name) => {
write!(f, "{}.{}", path.clone().into_string(), name) write!(f, "{}.{}", path.clone().into_string(), name)
} }
} }
@ -99,7 +115,7 @@ impl fmt::Display for VariantName {
VariantName::Unqualified(name) => { VariantName::Unqualified(name) => {
write!(f, "{}", name) write!(f, "{}", name)
}, },
VariantName::Qualified((path, name)) => { VariantName::Qualified(path, name) => {
write!(f, "{}.{}", path.clone().into_string(), name) write!(f, "{}.{}", path.clone().into_string(), name)
} }
} }

View file

@ -29,7 +29,7 @@ mod test_canonicalize {
fn can_expr_with( fn can_expr_with(
expr_str: &str, expr_str: &str,
declared_idents: &ImMap<(Option<Path>, String), Located<expr::Ident>>, declared_idents: &ImMap<Ident, Located<expr::Ident>>,
declared_variants: &ImMap<(Path, String), Located<expr::VariantName>>, declared_variants: &ImMap<(Path, String), Located<expr::VariantName>>,
) -> (Expr, Output, Vec<Problem>) { ) -> (Expr, Output, Vec<Problem>) {
let parse_state: State<&str, IndentablePosition> = State::with_positioner(expr_str, IndentablePosition::default()); let parse_state: State<&str, IndentablePosition> = State::with_positioner(expr_str, IndentablePosition::default());
@ -66,6 +66,14 @@ mod test_canonicalize {
LocalSymbol::new(string.to_string()) LocalSymbol::new(string.to_string())
} }
fn unqualified(string :&str) -> Ident {
Ident::Unqualified(string.to_string())
}
fn unused(string: &str) -> Problem {
Problem::UnusedAssignment(loc(unqualified(string)))
}
fn check_output( fn check_output(
output: Output, output: Output,
applied_variants: Vec<(Path, &str)>, applied_variants: Vec<(Path, &str)>,
@ -78,13 +86,16 @@ mod test_canonicalize {
referenced_idents: referenced_idents:
ImSet::from( ImSet::from(
referenced_idents.into_iter().map(|(opt_path, str_ref)| referenced_idents.into_iter().map(|(opt_path, str_ref)|
(opt_path, str_ref.to_string()) match opt_path {
Some(path) => Ident::Qualified(path, str_ref.to_string()),
None => Ident::Unqualified(str_ref.to_string())
}
).collect::<Vec<_>>() ).collect::<Vec<_>>()
), ),
applied_variants: applied_variants:
ImSet::from( ImSet::from(
applied_variants.into_iter().map(|(path, str_ref)| applied_variants.into_iter().map(|(path, str_ref)|
(path, str_ref.to_string()) (path, str_ref.to_string()),
).collect::<Vec<_>>()), ).collect::<Vec<_>>()),
tail_call tail_call
} }
@ -124,8 +135,8 @@ mod test_canonicalize {
assert_eq!(expr, assert_eq!(expr,
Assign( Assign(
vec![ vec![
(loc(Identifier(local_sym("a"))), loc(Int(5))), (loc(Identifier(local("a"))), loc(Int(5))),
(loc(Identifier(local_sym("b"))), loc(Int(6))), (loc(Identifier(local("b"))), loc(Int(6))),
], ],
loc_box(Operator( loc_box(Operator(
loc_box(Var(recognized_local_sym("a"))), loc_box(Var(recognized_local_sym("a"))),
@ -153,10 +164,7 @@ mod test_canonicalize {
c c
"#)); "#));
assert_eq!(problems, vec![ assert_eq!(problems, vec![unused("b"), unused("a")]);
// TODO Problem::UnusedAssignment("a")
// TODO Problem::UnusedAssignment("b")
]);
check_output(output, vec![], vec![(None, "c")], None); check_output(output, vec![], vec![(None, "c")], None);
} }
@ -183,6 +191,10 @@ mod test_canonicalize {
); );
} }
// UNSUPPORTED PATTERNS
// TODO verify that in closures and assignments, you can't assign to int/string/underscore/etc
// OPERATOR PRECEDENCE // OPERATOR PRECEDENCE