From 90de82e2953dac0cbe2fbe49d3d403748bd1b4eb Mon Sep 17 00:00:00 2001 From: ayazhafiz Date: Mon, 21 Feb 2022 18:25:19 -0500 Subject: [PATCH] Validation of opaques during canonicalization --- Cargo.lock | 1 + compiler/can/src/annotation.rs | 8 +- compiler/can/src/def.rs | 108 ++++++++++-------------- compiler/can/src/effect_module.rs | 6 +- compiler/can/src/expr.rs | 17 ++-- compiler/can/src/module.rs | 19 ++++- compiler/can/src/pattern.rs | 80 ++++++++++++++---- compiler/can/src/scope.rs | 81 ++++++++++++++++-- compiler/constrain/src/builtins.rs | 4 +- compiler/constrain/src/pattern.rs | 8 +- compiler/load/Cargo.toml | 1 + compiler/load/src/docs.rs | 24 +++++- compiler/load/tests/test_load.rs | 125 ++++++++++++++++++++++++++-- compiler/mono/src/ir.rs | 15 ++++ compiler/problem/src/can.rs | 14 +++- compiler/solve/src/solve.rs | 4 +- compiler/types/src/solved_types.rs | 4 +- compiler/types/src/types.rs | 19 ++++- reporting/src/error/canonicalize.rs | 77 ++++++++++++++++- reporting/tests/test_reporting.rs | 63 ++++++++++++++ 20 files changed, 546 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76c06199f6..7132457d1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3585,6 +3585,7 @@ dependencies = [ "roc_target", "roc_types", "roc_unify", + "strip-ansi-escapes", "tempfile", "ven_pretty", ] diff --git a/compiler/can/src/annotation.rs b/compiler/can/src/annotation.rs index c8e9ec4472..8d3febe818 100644 --- a/compiler/can/src/annotation.rs +++ b/compiler/can/src/annotation.rs @@ -6,7 +6,7 @@ use roc_module::symbol::{IdentIds, ModuleId, Symbol}; use roc_parse::ast::{AssignedField, Pattern, Tag, TypeAnnotation, TypeHeader}; use roc_region::all::{Loc, Region}; use roc_types::subs::{VarStore, Variable}; -use roc_types::types::{Alias, LambdaSet, Problem, RecordField, Type}; +use roc_types::types::{Alias, AliasKind, LambdaSet, Problem, RecordField, Type}; #[derive(Clone, Debug, PartialEq)] pub struct Annotation { @@ -360,7 +360,7 @@ fn can_annotation_help( type_arguments: vars, lambda_set_variables, actual: Box::new(actual), - is_opaque: alias.is_opaque, + kind: alias.kind, } } None => Type::Apply(symbol, args, region), @@ -499,7 +499,7 @@ fn can_annotation_help( region, lowercase_vars, alias_actual, - false, // aliases in "as" are never opaque + AliasKind::Structural, // aliases in "as" are never opaque ); let alias = scope.lookup_alias(symbol).unwrap(); @@ -523,7 +523,7 @@ fn can_annotation_help( type_arguments: vars, lambda_set_variables: alias.lambda_set_variables.clone(), actual: Box::new(alias.typ.clone()), - is_opaque: alias.is_opaque, + kind: alias.kind, } } } diff --git a/compiler/can/src/def.rs b/compiler/can/src/def.rs index b32bf8ea56..07a48d5580 100644 --- a/compiler/can/src/def.rs +++ b/compiler/can/src/def.rs @@ -20,6 +20,7 @@ use roc_parse::pattern::PatternType; use roc_problem::can::{CycleEntry, Problem, RuntimeError}; use roc_region::all::{Loc, Region}; use roc_types::subs::{VarStore, Variable}; +use roc_types::types::AliasKind; use roc_types::types::{Alias, Type}; use std::collections::HashMap; use std::fmt::Debug; @@ -73,28 +74,18 @@ enum PendingDef<'a> { &'a Loc>, ), - /// A type alias, e.g. `Ints : List Int` + /// A structural or opaque type alias, e.g. `Ints : List Int` or `Age := U32` respectively. Alias { name: Loc, vars: Vec>, ann: &'a Loc>, - }, - - /// An opaque type, e.g. `Age := U32` - Opaque { - name: Loc, - vars: Vec>, - ann: &'a Loc>, + kind: AliasKind, }, /// An invalid alias, that is ignored in the rest of the pipeline /// e.g. a shadowed alias, or a definition like `MyAlias 1 : Int` /// with an incorrect pattern - InvalidAlias, - - /// An invalid opaque, that is ignored in the rest of the pipeline. - /// E.g. a shadowed opaque, or a definition like `MyOpaq 1 := Int`. - InvalidOpaque, + InvalidAlias { kind: AliasKind }, } // See github.com/rtfeldman/roc/issues/800 for discussion of the large_enum_variant check. @@ -240,10 +231,7 @@ pub fn canonicalize_defs<'a>( // Type definitions aren't value definitions, so we don't need to do // anything for them here. - PendingDef::Alias { .. } - | PendingDef::InvalidAlias - | PendingDef::Opaque { .. } - | PendingDef::InvalidOpaque => {} + PendingDef::Alias { .. } | PendingDef::InvalidAlias { .. } => {} } } // Record the ast::Expr for later. We'll do another pass through these @@ -264,13 +252,16 @@ pub fn canonicalize_defs<'a>( let mut value_defs = Vec::new(); let mut alias_defs = MutMap::default(); - let mut opaque_defs = MutMap::default(); let mut referenced_type_symbols = MutMap::default(); for pending_def in pending.into_iter() { - let is_alias = matches!(pending_def, PendingDef::Alias { .. }); match pending_def { - PendingDef::Alias { name, vars, ann } | PendingDef::Opaque { name, vars, ann } => { + PendingDef::Alias { + name, + vars, + ann, + kind, + } => { let referenced_symbols = crate::annotation::find_type_def_symbols( env.home, &mut env.ident_ids, @@ -279,11 +270,7 @@ pub fn canonicalize_defs<'a>( referenced_type_symbols.insert(name.value, referenced_symbols); - if is_alias { - alias_defs.insert(name.value, (name, vars, ann)); - } else { - opaque_defs.insert(name.value, (name, vars, ann)); - } + alias_defs.insert(name.value, (name, vars, ann, kind)); } other => value_defs.push(other), } @@ -292,13 +279,7 @@ pub fn canonicalize_defs<'a>( let sorted = sort_type_defs_before_introduction(referenced_type_symbols); for type_name in sorted { - let (is_alias, name, vars, ann) = match alias_defs.remove(&type_name) { - Some((name, vars, ann)) => (true, name, vars, ann), - None => { - let (name, vars, ann) = opaque_defs.remove(&type_name).unwrap(); - (false, name, vars, ann) - } - }; + let (name, vars, ann, kind) = alias_defs.remove(&type_name).unwrap(); let symbol = name.value; let can_ann = canonicalize_annotation(env, &mut scope, &ann.value, ann.region, var_store); @@ -343,7 +324,7 @@ pub fn canonicalize_defs<'a>( name.region, can_vars.clone(), can_ann.typ.clone(), - !is_alias, + kind, ); aliases.insert(symbol, alias.clone()); } @@ -357,7 +338,7 @@ pub fn canonicalize_defs<'a>( alias.region, alias.type_variables.clone(), alias.typ.clone(), - alias.is_opaque, + alias.kind, ); } @@ -854,6 +835,15 @@ fn pattern_to_vars_by_symbol( } } + UnwrappedOpaque { + arguments, opaque, .. + } => { + for (var, nested) in arguments { + pattern_to_vars_by_symbol(vars_by_symbol, &nested.value, *var); + } + vars_by_symbol.insert(*opaque, expr_var); + } + RecordDestructure { destructs, .. } => { for destruct in destructs { vars_by_symbol.insert(destruct.value.symbol, destruct.value.var); @@ -866,7 +856,8 @@ fn pattern_to_vars_by_symbol( | StrLiteral(_) | Underscore | MalformedPattern(_, _) - | UnsupportedPattern(_) => {} + | UnsupportedPattern(_) + | OpaqueNotInScope(..) => {} } } @@ -1000,9 +991,8 @@ fn canonicalize_pending_def<'a>( } Alias { .. } => unreachable!("Aliases are handled in a separate pass"), - Opaque { .. } => unreachable!("Opaques are handled in a separate pass"), - InvalidAlias | InvalidOpaque => { + InvalidAlias { .. } => { // invalid aliases and opaques (shadowed, incorrect patterns) get ignored } TypedBody(loc_pattern, loc_can_pattern, loc_ann, loc_expr) => { @@ -1493,11 +1483,10 @@ fn to_pending_def<'a>( header: TypeHeader { name, vars }, typ: ann, } => { - let is_alias = matches!(def, Alias { .. }); - let invalid_pending_def = if is_alias { - PendingDef::InvalidAlias + let kind = if matches!(def, Alias { .. }) { + AliasKind::Structural } else { - PendingDef::InvalidOpaque + AliasKind::Opaque }; let region = Region::span_across(&name.region, &ann.region); @@ -1524,20 +1513,16 @@ fn to_pending_def<'a>( } _ => { // any other pattern in this position is a syntax error. - let problem = if is_alias { - Problem::InvalidAliasRigid { - alias_name: symbol, - region: loc_var.region, - } - } else { - Problem::InvalidOpaqueRigid { - opaque_name: symbol, - region: loc_var.region, - } + let problem = Problem::InvalidAliasRigid { + alias_name: symbol, + region: loc_var.region, }; env.problems.push(problem); - return Some((Output::default(), invalid_pending_def)); + return Some(( + Output::default(), + PendingDef::InvalidAlias { kind }, + )); } } } @@ -1547,18 +1532,11 @@ fn to_pending_def<'a>( value: symbol, }; - let pending_def = if is_alias { - PendingDef::Alias { - name, - vars: can_rigids, - ann, - } - } else { - PendingDef::Opaque { - name, - vars: can_rigids, - ann, - } + let pending_def = PendingDef::Alias { + name, + vars: can_rigids, + ann, + kind, }; Some((Output::default(), pending_def)) @@ -1570,7 +1548,7 @@ fn to_pending_def<'a>( shadow: loc_shadowed_symbol, }); - Some((Output::default(), invalid_pending_def)) + Some((Output::default(), PendingDef::InvalidAlias { kind })) } } } diff --git a/compiler/can/src/effect_module.rs b/compiler/can/src/effect_module.rs index 07c1a954b7..6c04645b14 100644 --- a/compiler/can/src/effect_module.rs +++ b/compiler/can/src/effect_module.rs @@ -10,7 +10,7 @@ use roc_module::ident::TagName; use roc_module::symbol::Symbol; use roc_region::all::{Loc, Region}; use roc_types::subs::{VarStore, Variable}; -use roc_types::types::Type; +use roc_types::types::{AliasKind, Type}; #[derive(Default, Clone, Copy)] pub(crate) struct HostedGeneratedFunctions { @@ -1140,7 +1140,7 @@ fn build_effect_loop( closure_var, ))], actual: Box::new(actual), - is_opaque: false, + kind: AliasKind::Structural, } }; @@ -1580,7 +1580,7 @@ fn build_effect_alias( type_arguments: vec![(a_name.into(), Type::Variable(a_var))], lambda_set_variables: vec![roc_types::types::LambdaSet(Type::Variable(closure_var))], actual: Box::new(actual), - is_opaque: false, + kind: AliasKind::Structural, } } diff --git a/compiler/can/src/expr.rs b/compiler/can/src/expr.rs index 6b9462fbf8..e61a153016 100644 --- a/compiler/can/src/expr.rs +++ b/compiler/can/src/expr.rs @@ -705,18 +705,19 @@ pub fn canonicalize_expr<'a>( Output::default(), ) } - ast::Expr::OpaqueRef(name) => { - let name_ident = env.ident_ids.get_or_insert(&(*name).into()); - let symbol = Symbol::new(env.home, name_ident); - - ( + ast::Expr::OpaqueRef(opaque_ref) => match scope.lookup_opaque_ref(opaque_ref, region) { + Ok(name) => ( OpaqueRef { - name: symbol, + name, arguments: vec![], }, Output::default(), - ) - } + ), + Err(runtime_error) => { + env.problem(Problem::RuntimeError(runtime_error.clone())); + (RuntimeError(runtime_error), Output::default()) + } + }, ast::Expr::Expect(condition, continuation) => { let mut output = Output::default(); diff --git a/compiler/can/src/module.rs b/compiler/can/src/module.rs index 8b1633ce29..39936e8d6e 100644 --- a/compiler/can/src/module.rs +++ b/compiler/can/src/module.rs @@ -16,7 +16,7 @@ use roc_parse::pattern::PatternType; use roc_problem::can::{Problem, RuntimeError}; use roc_region::all::{Loc, Region}; use roc_types::subs::{VarStore, Variable}; -use roc_types::types::{Alias, Type}; +use roc_types::types::{Alias, AliasKind, Type}; #[derive(Debug)] pub struct Module { @@ -86,7 +86,13 @@ pub fn canonicalize_module_defs<'a>( let num_deps = dep_idents.len(); for (name, alias) in aliases.into_iter() { - scope.add_alias(name, alias.region, alias.type_variables, alias.typ, false); + scope.add_alias( + name, + alias.region, + alias.type_variables, + alias.typ, + alias.kind, + ); } struct Hosted { @@ -131,7 +137,7 @@ pub fn canonicalize_module_defs<'a>( Region::zero(), vec![Loc::at_zero(("a".into(), a_var))], actual, - /* is_opaque */ false, + AliasKind::Structural, ); } @@ -537,6 +543,10 @@ fn fix_values_captured_in_closure_pattern( AppliedTag { arguments: loc_args, .. + } + | UnwrappedOpaque { + arguments: loc_args, + .. } => { for (_, loc_arg) in loc_args.iter_mut() { fix_values_captured_in_closure_pattern(&mut loc_arg.value, no_capture_symbols); @@ -565,7 +575,8 @@ fn fix_values_captured_in_closure_pattern( | Underscore | Shadowed(..) | MalformedPattern(_, _) - | UnsupportedPattern(_) => (), + | UnsupportedPattern(_) + | OpaqueNotInScope(..) => (), } } diff --git a/compiler/can/src/pattern.rs b/compiler/can/src/pattern.rs index 39765b1a21..e167a612d3 100644 --- a/compiler/can/src/pattern.rs +++ b/compiler/can/src/pattern.rs @@ -25,6 +25,11 @@ pub enum Pattern { tag_name: TagName, arguments: Vec<(Variable, Loc)>, }, + UnwrappedOpaque { + whole_var: Variable, + opaque: Symbol, + arguments: Vec<(Variable, Loc)>, + }, RecordDestructure { whole_var: Variable, ext_var: Variable, @@ -38,6 +43,7 @@ pub enum Pattern { // Runtime Exceptions Shadowed(Region, Loc, Symbol), + OpaqueNotInScope(Loc), // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! UnsupportedPattern(Region), // parse error patterns @@ -79,6 +85,14 @@ pub fn symbols_from_pattern_help(pattern: &Pattern, symbols: &mut Vec) { symbols_from_pattern_help(&nested.value, symbols); } } + UnwrappedOpaque { + opaque, arguments, .. + } => { + symbols.push(*opaque); + for (_, nested) in arguments { + symbols_from_pattern_help(&nested.value, symbols); + } + } RecordDestructure { destructs, .. } => { for destruct in destructs { // when a record field has a pattern guard, only symbols in the guard are introduced @@ -96,7 +110,8 @@ pub fn symbols_from_pattern_help(pattern: &Pattern, symbols: &mut Vec) { | StrLiteral(_) | Underscore | MalformedPattern(_, _) - | UnsupportedPattern(_) => {} + | UnsupportedPattern(_) + | OpaqueNotInScope(..) => {} } } @@ -156,16 +171,6 @@ pub fn canonicalize_pattern<'a>( } OpaqueRef(..) => todo_opaques!(), Apply(tag, patterns) => { - let tag_name = match tag.value { - GlobalTag(name) => TagName::Global(name.into()), - PrivateTag(name) => { - let ident_id = env.ident_ids.get_or_insert(&name.into()); - - TagName::Private(Symbol::new(env.home, ident_id)) - } - _ => unreachable!("Other patterns cannot be applied"), - }; - let mut can_patterns = Vec::with_capacity(patterns.len()); for loc_pattern in *patterns { let (new_output, can_pattern) = canonicalize_pattern( @@ -182,11 +187,41 @@ pub fn canonicalize_pattern<'a>( can_patterns.push((var_store.fresh(), can_pattern)); } - Pattern::AppliedTag { - whole_var: var_store.fresh(), - ext_var: var_store.fresh(), - tag_name, - arguments: can_patterns, + match tag.value { + GlobalTag(name) => { + let tag_name = TagName::Global(name.into()); + Pattern::AppliedTag { + whole_var: var_store.fresh(), + ext_var: var_store.fresh(), + tag_name, + arguments: can_patterns, + } + } + PrivateTag(name) => { + let ident_id = env.ident_ids.get_or_insert(&name.into()); + let tag_name = TagName::Private(Symbol::new(env.home, ident_id)); + + Pattern::AppliedTag { + whole_var: var_store.fresh(), + ext_var: var_store.fresh(), + tag_name, + arguments: can_patterns, + } + } + + OpaqueRef(name) => match scope.lookup_opaque_ref(name, tag.region) { + Ok(opaque) => Pattern::UnwrappedOpaque { + whole_var: var_store.fresh(), + opaque, + arguments: can_patterns, + }, + Err(runtime_error) => { + env.problem(Problem::RuntimeError(runtime_error)); + + Pattern::OpaqueNotInScope(Loc::at(tag.region, name.into())) + } + }, + _ => unreachable!("Other patterns cannot be applied"), } } @@ -502,6 +537,16 @@ fn add_bindings_from_patterns( add_bindings_from_patterns(&loc_arg.region, &loc_arg.value, answer); } } + UnwrappedOpaque { + arguments: loc_args, + opaque, + .. + } => { + for (_, loc_arg) in loc_args { + add_bindings_from_patterns(&loc_arg.region, &loc_arg.value, answer); + } + answer.push((*opaque, *region)); + } RecordDestructure { destructs, .. } => { for Loc { region, @@ -517,7 +562,8 @@ fn add_bindings_from_patterns( | StrLiteral(_) | Underscore | MalformedPattern(_, _) - | UnsupportedPattern(_) => (), + | UnsupportedPattern(_) + | OpaqueNotInScope(..) => (), } } diff --git a/compiler/can/src/scope.rs b/compiler/can/src/scope.rs index 029382d96e..b7b354d084 100644 --- a/compiler/can/src/scope.rs +++ b/compiler/can/src/scope.rs @@ -4,7 +4,7 @@ use roc_module::symbol::{IdentIds, ModuleId, Symbol}; use roc_problem::can::RuntimeError; use roc_region::all::{Loc, Region}; use roc_types::subs::{VarStore, Variable}; -use roc_types::types::{Alias, Type}; +use roc_types::types::{Alias, AliasKind, Type}; #[derive(Clone, Debug, PartialEq)] pub struct Scope { @@ -51,7 +51,7 @@ impl Scope { recursion_variables: MutSet::default(), type_variables: variables, // TODO(opaques): replace when opaques are included in the stdlib - is_opaque: false, + kind: AliasKind::Structural, }; aliases.insert(symbol, alias); @@ -102,6 +102,75 @@ impl Scope { self.aliases.get(&symbol) } + /// Check if there is an opaque type alias referenced by `opaque_ref` referenced in the + /// current scope. E.g. `$Age` must reference an opaque `Age` declared in this module, not any + /// other! + pub fn lookup_opaque_ref( + &self, + opaque_ref: &str, + lookup_region: Region, + ) -> Result { + debug_assert!(opaque_ref.starts_with('$')); + let opaque = opaque_ref[1..].into(); + + match self.idents.get(&opaque) { + // TODO: is it worth caching any of these results? + Some((symbol, decl_region)) => { + if symbol.module_id() != self.home { + // The reference is to an opaque type declared in another module - this is + // illegal, as opaque types can only be wrapped/unwrapped in the scope they're + // declared. + return Err(RuntimeError::OpaqueOutsideScope { + opaque, + referenced_region: lookup_region, + imported_region: *decl_region, + }); + } + + match self.aliases.get(symbol) { + None => Err(self.opaque_not_defined_error(opaque, lookup_region, None)), + + Some(alias) => match alias.kind { + // The reference is to a proper alias like `Age : U32`, not an opaque type! + AliasKind::Structural => Err(self.opaque_not_defined_error( + opaque, + lookup_region, + Some(alias.header_region()), + )), + // All is good + AliasKind::Opaque => Ok(*symbol), + }, + } + } + None => Err(self.opaque_not_defined_error(opaque, lookup_region, None)), + } + } + + fn opaque_not_defined_error( + &self, + opaque: Ident, + lookup_region: Region, + opt_defined_alias: Option, + ) -> RuntimeError { + let opaques_in_scope = self + .idents() + .filter(|(_, (sym, _))| { + self.aliases + .get(sym) + .map(|alias| alias.kind) + .unwrap_or(AliasKind::Structural) + == AliasKind::Opaque + }) + .map(|(v, _)| v.as_ref().into()) + .collect(); + + RuntimeError::OpaqueNotDefined { + usage: Loc::at(lookup_region, opaque), + opaques_in_scope, + opt_defined_alias, + } + } + /// Introduce a new ident to scope. /// /// Returns Err if this would shadow an existing ident, including the @@ -182,9 +251,9 @@ impl Scope { region: Region, vars: Vec>, typ: Type, - is_opaque: bool, + kind: AliasKind, ) { - let alias = create_alias(name, region, vars, typ, is_opaque); + let alias = create_alias(name, region, vars, typ, kind); self.aliases.insert(name, alias); } @@ -198,7 +267,7 @@ pub fn create_alias( region: Region, vars: Vec>, typ: Type, - is_opaque: bool, + kind: AliasKind, ) -> Alias { let roc_types::types::VariableDetail { type_variables, @@ -234,6 +303,6 @@ pub fn create_alias( lambda_set_variables, recursion_variables, typ, - is_opaque, + kind, } } diff --git a/compiler/constrain/src/builtins.rs b/compiler/constrain/src/builtins.rs index cc3d7bf76a..8812d2ec85 100644 --- a/compiler/constrain/src/builtins.rs +++ b/compiler/constrain/src/builtins.rs @@ -7,9 +7,9 @@ use roc_module::ident::{Lowercase, TagName}; use roc_module::symbol::Symbol; use roc_region::all::Region; use roc_types::subs::Variable; -use roc_types::types::Category; use roc_types::types::Reason; use roc_types::types::Type::{self, *}; +use roc_types::types::{AliasKind, Category}; #[must_use] pub fn add_numeric_bound_constr( @@ -163,7 +163,7 @@ fn builtin_alias( actual, lambda_set_variables: vec![], // TODO(opaques): revisit later - is_opaque: false, + kind: AliasKind::Structural, } } diff --git a/compiler/constrain/src/pattern.rs b/compiler/constrain/src/pattern.rs index bf77a86f36..4d97f7bcdc 100644 --- a/compiler/constrain/src/pattern.rs +++ b/compiler/constrain/src/pattern.rs @@ -5,6 +5,7 @@ use roc_can::expected::{Expected, PExpected}; use roc_can::pattern::Pattern::{self, *}; use roc_can::pattern::{DestructType, RecordDestruct}; use roc_collections::all::{Index, SendMap}; +use roc_error_macros::todo_opaques; use roc_module::ident::Lowercase; use roc_module::symbol::Symbol; use roc_region::all::{Loc, Region}; @@ -55,6 +56,7 @@ fn headers_from_annotation_help( Underscore | MalformedPattern(_, _) | UnsupportedPattern(_) + | OpaqueNotInScope(..) | NumLiteral(..) | IntLiteral(..) | FloatLiteral(..) @@ -114,6 +116,8 @@ fn headers_from_annotation_help( } _ => false, }, + + UnwrappedOpaque { .. } => todo_opaques!(), } } @@ -158,7 +162,7 @@ pub fn constrain_pattern( PresenceConstraint::IsOpen, )); } - Underscore | UnsupportedPattern(_) | MalformedPattern(_, _) => { + Underscore | UnsupportedPattern(_) | MalformedPattern(_, _) | OpaqueNotInScope(..) => { // Neither the _ pattern nor erroneous ones add any constraints. } @@ -444,5 +448,7 @@ pub fn constrain_pattern( state.constraints.push(whole_con); state.constraints.push(tag_con); } + + UnwrappedOpaque { .. } => todo_opaques!(), } } diff --git a/compiler/load/Cargo.toml b/compiler/load/Cargo.toml index c4092784f5..081a055f6d 100644 --- a/compiler/load/Cargo.toml +++ b/compiler/load/Cargo.toml @@ -33,3 +33,4 @@ tempfile = "3.2.0" pretty_assertions = "1.0.0" maplit = "1.0.2" indoc = "1.0.3" +strip-ansi-escapes = "0.1.1" diff --git a/compiler/load/src/docs.rs b/compiler/load/src/docs.rs index 89cd049616..bd8e526344 100644 --- a/compiler/load/src/docs.rs +++ b/compiler/load/src/docs.rs @@ -4,7 +4,6 @@ use crate::docs::TypeAnnotation::{ }; use crate::file::LoadedModule; use roc_can::scope::Scope; -use roc_error_macros::todo_opaques; use roc_module::ident::ModuleName; use roc_module::symbol::IdentIds; use roc_parse::ast::CommentOrNewline; @@ -229,7 +228,28 @@ fn generate_entry_doc<'a>( (acc, None) } - Def::Opaque { .. } => todo_opaques!("figure out documentation for opaques"), + Def::Opaque { + header: TypeHeader { name, vars }, + .. + } => { + let mut type_vars = Vec::new(); + + for var in vars.iter() { + if let Pattern::Identifier(ident_name) = var.value { + type_vars.push(ident_name.to_string()); + } + } + + let doc_def = DocDef { + name: name.value.to_string(), + type_annotation: TypeAnnotation::NoTypeAnn, + type_vars, + docs: before_comments_or_new_lines.and_then(comments_or_new_lines_to_docs), + }; + acc.push(DocEntry::DocDef(doc_def)); + + (acc, None) + } Def::Body(_, _) => (acc, None), diff --git a/compiler/load/tests/test_load.rs b/compiler/load/tests/test_load.rs index dc1afddcc0..894954d4dc 100644 --- a/compiler/load/tests/test_load.rs +++ b/compiler/load/tests/test_load.rs @@ -23,14 +23,45 @@ mod test_load { use roc_load::file::LoadedModule; use roc_module::ident::ModuleName; use roc_module::symbol::{Interns, ModuleId}; + use roc_problem::can::Problem; + use roc_region::all::LineInfo; + use roc_reporting::report::can_problem; + use roc_reporting::report::RocDocAllocator; use roc_types::pretty_print::{content_to_string, name_all_type_vars}; use roc_types::subs::Subs; use std::collections::HashMap; + use std::path::PathBuf; const TARGET_INFO: roc_target::TargetInfo = roc_target::TargetInfo::default_x86_64(); // HELPERS + fn format_can_problems( + problems: Vec, + home: ModuleId, + interns: &Interns, + filename: PathBuf, + src: &str, + ) -> String { + use ven_pretty::DocAllocator; + + let src_lines: Vec<&str> = src.split('\n').collect(); + let lines = LineInfo::new(src); + let alloc = RocDocAllocator::new(&src_lines, home, &interns); + let reports = problems + .into_iter() + .map(|problem| can_problem(&alloc, &lines, filename.clone(), problem).pretty(&alloc)); + + let mut buf = String::new(); + alloc + .stack(reports) + .append(alloc.line()) + .1 + .render_raw(70, &mut roc_reporting::report::CiWrite::new(&mut buf)) + .unwrap(); + buf + } + fn multiple_modules(files: Vec<(&str, &str)>) -> Result { use roc_load::file::LoadingProblem; @@ -43,11 +74,19 @@ mod test_load { Ok(Err(loading_problem)) => Err(format!("{:?}", loading_problem)), Ok(Ok(mut loaded_module)) => { let home = loaded_module.module_id; + let (filepath, src) = loaded_module.sources.get(&home).unwrap(); + + let can_problems = loaded_module.can_problems.remove(&home).unwrap_or_default(); + if !can_problems.is_empty() { + return Err(format_can_problems( + can_problems, + home, + &loaded_module.interns, + filepath.clone(), + src, + )); + } - assert_eq!( - loaded_module.can_problems.remove(&home).unwrap_or_default(), - Vec::new() - ); assert_eq!( loaded_module .type_problems @@ -67,7 +106,6 @@ mod test_load { ) -> Result>, std::io::Error> { use std::fs::{self, File}; use std::io::Write; - use std::path::PathBuf; use tempfile::tempdir; let stdlib = roc_builtins::std::standard_stdlib(); @@ -682,4 +720,81 @@ mod test_load { assert!(multiple_modules(modules).is_ok()); } + + #[test] + fn opaque_wrapped_unwrapped_outside_defining_module() { + let modules = vec![ + ( + "Age", + indoc!( + r#" + interface Age exposes [ Age ] imports [] + + Age := U32 + "# + ), + ), + ( + "Main", + indoc!( + r#" + interface Main exposes [ twenty, readAge ] imports [ Age.{ Age } ] + + twenty = $Age 20 + + readAge = \$Age n -> n + "# + ), + ), + ]; + + let err = multiple_modules(modules).unwrap_err(); + let err = strip_ansi_escapes::strip(err).unwrap(); + let err = String::from_utf8(err).unwrap(); + assert_eq!( + err, + indoc!( + r#" + ── OPAQUE DECLARED OUTSIDE SCOPE ─────────────────────────────────────────────── + + The unwrapped opaque type Age referenced here: + + 3│ twenty = $Age 20 + ^^^^ + + is imported from another module: + + 1│ interface Main exposes [ twenty, readAge ] imports [ Age.{ Age } ] + ^^^^^^^^^^^ + + Note: Opaque types can only be wrapped and unwrapped in the module they are defined in! + + ── OPAQUE DECLARED OUTSIDE SCOPE ─────────────────────────────────────────────── + + The unwrapped opaque type Age referenced here: + + 5│ readAge = \$Age n -> n + ^^^^ + + is imported from another module: + + 1│ interface Main exposes [ twenty, readAge ] imports [ Age.{ Age } ] + ^^^^^^^^^^^ + + Note: Opaque types can only be wrapped and unwrapped in the module they are defined in! + + ── UNUSED IMPORT ─────────────────────────────────────────────────────────────── + + Nothing from Age is used in this module. + + 1│ interface Main exposes [ twenty, readAge ] imports [ Age.{ Age } ] + ^^^^^^^^^^^ + + Since Age isn't used, you don't need to import it. + "# + ), + "\n{}", + err + ); + } } diff --git a/compiler/mono/src/ir.rs b/compiler/mono/src/ir.rs index 5dc93cb3eb..07e87e687c 100644 --- a/compiler/mono/src/ir.rs +++ b/compiler/mono/src/ir.rs @@ -2013,6 +2013,13 @@ fn pattern_to_when<'a>( (env.unique_symbol(), Loc::at_zero(RuntimeError(error))) } + OpaqueNotInScope(loc_ident) => { + // create the runtime error here, instead of delegating to When. + // TODO(opaques) should be `RuntimeError::OpaqueNotDefined` + let error = roc_problem::can::RuntimeError::UnsupportedPattern(loc_ident.region); + (env.unique_symbol(), Loc::at_zero(RuntimeError(error))) + } + AppliedTag { .. } | RecordDestructure { .. } => { let symbol = env.unique_symbol(); @@ -2031,6 +2038,8 @@ fn pattern_to_when<'a>( (symbol, Loc::at_zero(wrapped_body)) } + UnwrappedOpaque { .. } => todo_opaques!(), + IntLiteral(..) | NumLiteral(..) | FloatLiteral(..) | StrLiteral(_) => { // These patters are refutable, and thus should never occur outside a `when` expression // They should have been replaced with `UnsupportedPattern` during canonicalization @@ -7743,6 +7752,10 @@ fn from_can_pattern_help<'a>( // TODO preserve malformed problem information here? Err(RuntimeError::UnsupportedPattern(*region)) } + OpaqueNotInScope(loc_ident) => { + // TODO(opaques) should be `RuntimeError::OpaqueNotDefined` + Err(RuntimeError::UnsupportedPattern(loc_ident.region)) + } NumLiteral(var, num_str, num, _bound) => { match num_argument_to_int_or_float(env.subs, env.target_info, *var, false) { IntOrFloat::Int(precision) => Ok(match num { @@ -8191,6 +8204,8 @@ fn from_can_pattern_help<'a>( Ok(result) } + UnwrappedOpaque { .. } => todo_opaques!(), + RecordDestructure { whole_var, destructs, diff --git a/compiler/problem/src/can.rs b/compiler/problem/src/can.rs index 1956e2d87d..55a26469b0 100644 --- a/compiler/problem/src/can.rs +++ b/compiler/problem/src/can.rs @@ -76,10 +76,6 @@ pub enum Problem { alias_name: Symbol, region: Region, }, - InvalidOpaqueRigid { - opaque_name: Symbol, - region: Region, - }, InvalidInterpolation(Region), InvalidHexadecimal(Region), InvalidUnicodeCodePt(Region), @@ -159,6 +155,16 @@ pub enum RuntimeError { ErroneousType, LookupNotInScope(Loc, MutSet>), + OpaqueNotDefined { + usage: Loc, + opaques_in_scope: MutSet>, + opt_defined_alias: Option, + }, + OpaqueOutsideScope { + opaque: Ident, + referenced_region: Region, + imported_region: Region, + }, ValueNotExposed { module_name: ModuleName, ident: Ident, diff --git a/compiler/solve/src/solve.rs b/compiler/solve/src/solve.rs index 9216045c05..37678aef34 100644 --- a/compiler/solve/src/solve.rs +++ b/compiler/solve/src/solve.rs @@ -914,8 +914,8 @@ fn type_to_variable<'a>( type_arguments, actual, lambda_set_variables, - // TODO(opaques): revisit opacity - is_opaque: _, + // TODO(opaques): revisit kind + kind: _, } => { if let Some(reserved) = Variable::get_reserved(*symbol) { if rank.is_none() { diff --git a/compiler/types/src/solved_types.rs b/compiler/types/src/solved_types.rs index 3e9873edd6..f6286a50a3 100644 --- a/compiler/types/src/solved_types.rs +++ b/compiler/types/src/solved_types.rs @@ -1,5 +1,5 @@ use crate::subs::{FlatType, GetSubsSlice, Subs, VarId, VarStore, Variable}; -use crate::types::{Problem, RecordField, Type}; +use crate::types::{AliasKind, Problem, RecordField, Type}; use roc_collections::all::{ImMap, MutSet, SendMap}; use roc_module::ident::{Lowercase, TagName}; use roc_module::symbol::Symbol; @@ -560,7 +560,7 @@ pub fn to_type( lambda_set_variables, actual: Box::new(actual), // TODO(opaques): revisit when opaques are in the solver - is_opaque: false, + kind: AliasKind::Structural, } } HostExposedAlias { diff --git a/compiler/types/src/types.rs b/compiler/types/src/types.rs index da0c7c764e..e7e26bdf9d 100644 --- a/compiler/types/src/types.rs +++ b/compiler/types/src/types.rs @@ -185,7 +185,7 @@ pub enum Type { type_arguments: Vec<(Lowercase, Type)>, lambda_set_variables: Vec, actual: Box, - is_opaque: bool, + kind: AliasKind, }, HostExposedAlias { name: Symbol, @@ -854,7 +854,7 @@ impl Type { type_arguments: named_args, lambda_set_variables, actual: Box::new(actual), - is_opaque: alias.is_opaque, + kind: alias.kind, }; } else { // one of the special-cased Apply types. @@ -1327,6 +1327,19 @@ pub enum PatternCategory { Float, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum AliasKind { + /// A structural alias is something like + /// List a : [ Nil, Cons a (List a) ] + /// It is typed structurally, so that a `List U8` is always equal to a `[ Nil ]_`, for example. + Structural, + /// An opaque alias corresponds to an opaque type from the langauge syntax, like + /// Age := U32 + /// It is type nominally, so that `Age` is never equal to `U8` - the only way to unwrap the + /// structural type inside `Age` is to unwrap the opaque, so `Age` = `@Age U8`. + Opaque, +} + #[derive(Clone, Debug, PartialEq)] pub struct Alias { pub region: Region, @@ -1340,7 +1353,7 @@ pub struct Alias { pub typ: Type, - pub is_opaque: bool, + pub kind: AliasKind, } impl Alias { diff --git a/reporting/src/error/canonicalize.rs b/reporting/src/error/canonicalize.rs index 719f770553..b909b16668 100644 --- a/reporting/src/error/canonicalize.rs +++ b/reporting/src/error/canonicalize.rs @@ -29,6 +29,8 @@ const NESTED_DATATYPE: &str = "NESTED DATATYPE"; const CONFLICTING_NUMBER_SUFFIX: &str = "CONFLICTING NUMBER SUFFIX"; const NUMBER_OVERFLOWS_SUFFIX: &str = "NUMBER OVERFLOWS SUFFIX"; const NUMBER_UNDERFLOWS_SUFFIX: &str = "NUMBER UNDERFLOWS SUFFIX"; +const OPAQUE_NOT_DEFINED: &str = "OPAQUE NOT DEFINED"; +const OPAQUE_DECLARED_OUTSIDE_SCOPE: &str = "OPAQUE DECLARED OUTSIDE SCOPE"; pub fn can_problem<'b>( alloc: &'b RocDocAllocator<'b>, @@ -389,10 +391,6 @@ pub fn can_problem<'b>( Problem::InvalidAliasRigid { alias_name: type_name, region, - } - | Problem::InvalidOpaqueRigid { - opaque_name: type_name, - region, } => { doc = alloc.stack(vec![ alloc.concat(vec![ @@ -1335,6 +1333,77 @@ fn pretty_runtime_error<'b>( title = MISSING_DEFINITION; } + RuntimeError::OpaqueNotDefined { + usage: + Loc { + region: used_region, + value: opaque, + }, + opaques_in_scope, + opt_defined_alias, + } => { + let mut suggestions = suggest::sort( + opaque.as_inline_str().as_str(), + opaques_in_scope.iter().map(|v| v.as_ref()).collect(), + ); + suggestions.truncate(4); + + let details = if suggestions.is_empty() { + alloc.note("It looks like there are no opaque types declared in this module yet!") + } else { + let qualified_suggestions = + suggestions.into_iter().map(|v| alloc.string(v.to_string())); + alloc.stack(vec![ + alloc + .tip() + .append(alloc.reflow("Did you mean one of these opaque types?")), + alloc.vcat(qualified_suggestions).indent(4), + ]) + }; + + let mut stack = vec![ + alloc.concat(vec![ + alloc.reflow("The opaque type "), + alloc.type_str(opaque.as_inline_str().as_str()), + alloc.reflow(" referenced here is not defined:"), + ]), + alloc.region(lines.convert_region(used_region)), + ]; + + if let Some(defined_alias_region) = opt_defined_alias { + stack.push(alloc.stack(vec![ + alloc.note("There is an alias of the same name:"), + alloc.region(lines.convert_region(defined_alias_region)), + ])); + } + + stack.push(details); + + doc = alloc.stack(stack); + + title = OPAQUE_NOT_DEFINED; + } + RuntimeError::OpaqueOutsideScope { + opaque, + referenced_region, + imported_region, + } => { + doc = alloc.stack(vec![ + alloc.concat(vec![ + alloc.reflow("The unwrapped opaque type "), + alloc.type_str(opaque.as_inline_str().as_str()), + alloc.reflow(" referenced here:"), + ]), + alloc.region(lines.convert_region(referenced_region)), + alloc.reflow("is imported from another module:"), + alloc.region(lines.convert_region(imported_region)), + alloc.note( + "Opaque types can only be wrapped and unwrapped in the module they are defined in!", + ), + ]); + + title = OPAQUE_DECLARED_OUTSIDE_SCOPE; + } } (doc, title) diff --git a/reporting/tests/test_reporting.rs b/reporting/tests/test_reporting.rs index a3c354784b..8e41d5c62e 100644 --- a/reporting/tests/test_reporting.rs +++ b/reporting/tests/test_reporting.rs @@ -8136,4 +8136,67 @@ I need all branches in an `if` to have the same type! "", ) } + + #[test] + fn opaque_type_not_in_scope() { + report_problem_as( + indoc!( + r#" + $Age 21 + "# + ), + indoc!( + r#" + ── OPAQUE NOT DEFINED ────────────────────────────────────────────────────────── + + The opaque type Age referenced here is not defined: + + 1│ $Age 21 + ^^^^ + + Note: It looks like there are no opaque types declared in this module yet! + "# + ), + ) + } + + #[test] + fn opaque_reference_not_opaque_type() { + report_problem_as( + indoc!( + r#" + Age : U8 + + $Age 21 + "# + ), + indoc!( + r#" + ── OPAQUE NOT DEFINED ────────────────────────────────────────────────────────── + + The opaque type Age referenced here is not defined: + + 3│ $Age 21 + ^^^^ + + Note: There is an alias of the same name: + + 1│ Age : U8 + ^^^ + + Note: It looks like there are no opaque types declared in this module yet! + + ── UNUSED DEFINITION ─────────────────────────────────────────────────────────── + + `Age` is not used anywhere in your code. + + 1│ Age : U8 + ^^^^^^^^ + + If you didn't intend on using `Age` then remove it so future readers of + your code don't wonder why it is there. + "# + ), + ) + } }