diff --git a/compiler/can/src/effect_module.rs b/compiler/can/src/effect_module.rs index 0d4a276f01..3735c85239 100644 --- a/compiler/can/src/effect_module.rs +++ b/compiler/can/src/effect_module.rs @@ -12,69 +12,81 @@ use roc_region::all::{Loc, Region}; use roc_types::subs::{VarStore, Variable}; use roc_types::types::Type; -/// Functions that are always implemented for Effect -type Builder = for<'r, 's, 't0, 't1> fn( - &'r mut Env<'s>, - &'t0 mut Scope, - Symbol, - TagName, - &'t1 mut VarStore, -) -> (Symbol, Def); +#[derive(Default, Clone, Copy)] +pub(crate) struct HostedGeneratedFunctions { + pub(crate) after: bool, + pub(crate) map: bool, + pub(crate) always: bool, + pub(crate) loop_: bool, + pub(crate) forever: bool, +} -pub const BUILTIN_EFFECT_FUNCTIONS: &[(&str, Builder)] = &[ - // Effect.after : Effect a, (a -> Effect b) -> Effect b - ("after", build_effect_after), - // Effect.map : Effect a, (a -> b) -> Effect b - ("map", build_effect_map), - // Effect.always : a -> Effect a - ("always", build_effect_always), - // Effect.forever : Effect a -> Effect b - ("forever", build_effect_forever), - // Effect.loop : a, (a -> Effect [ Step a, Done b ]) -> Effect b - ("loop", build_effect_loop), -]; - -const RECURSIVE_BUILTIN_EFFECT_FUNCTIONS: &[&str] = &["forever", "loop"]; - -// the Effects alias & associated functions -// -// A platform can define an Effect type in its header. It can have an arbitrary name -// (e.g. Task, IO), but we'll call it an Effect in general. -// -// From that name, we generate an effect module, an effect alias, and some functions. -// -// The effect alias is implemented as -// -// Effect a : [ @Effect ({} -> a) ] -// -// For this alias we implement the functions defined in BUILTIN_EFFECT_FUNCTIONS with the -// standard implementation. - -pub fn build_effect_builtins( +/// the Effects alias & associated functions +/// +/// A platform can define an Effect type in its header. It can have an arbitrary name +/// (e.g. Task, IO), but we'll call it an Effect in general. +/// +/// From that name, we generate an effect module, an effect alias, and some functions. +/// +/// The effect alias is implemented as +/// +/// Effect a : [ @Effect ({} -> a) ] +/// +/// For this alias we implement the functions specified in HostedGeneratedFunctions with the +/// standard implementation. +pub(crate) fn build_effect_builtins( env: &mut Env, scope: &mut Scope, effect_symbol: Symbol, var_store: &mut VarStore, exposed_symbols: &mut MutSet, declarations: &mut Vec, + generated_functions: HostedGeneratedFunctions, ) { - for (name, f) in BUILTIN_EFFECT_FUNCTIONS.iter() { - let (symbol, def) = f( - env, - scope, - effect_symbol, - TagName::Private(effect_symbol), - var_store, - ); + macro_rules! helper { + ($f:expr) => {{ + let (symbol, def) = $f( + env, + scope, + effect_symbol, + TagName::Private(effect_symbol), + var_store, + ); - exposed_symbols.insert(symbol); + // make the outside world know this symbol exists + exposed_symbols.insert(symbol); - let is_recursive = RECURSIVE_BUILTIN_EFFECT_FUNCTIONS.iter().any(|n| n == name); - if is_recursive { - declarations.push(Declaration::DeclareRec(vec![def])); - } else { - declarations.push(Declaration::Declare(def)); - } + def + }}; + } + + if generated_functions.after { + let def = helper!(build_effect_after); + declarations.push(Declaration::Declare(def)); + } + + // Effect.map : Effect a, (a -> b) -> Effect b + if generated_functions.map { + let def = helper!(build_effect_map); + declarations.push(Declaration::Declare(def)); + } + + // Effect.always : a -> Effect a + if generated_functions.always { + let def = helper!(build_effect_always); + declarations.push(Declaration::Declare(def)); + } + + // Effect.forever : Effect a -> Effect b + if generated_functions.forever { + let def = helper!(build_effect_forever); + declarations.push(Declaration::DeclareRec(vec![def])); + } + + // Effect.loop : a, (a -> Effect [ Step a, Done b ]) -> Effect b + if generated_functions.loop_ { + let def = helper!(build_effect_loop); + declarations.push(Declaration::DeclareRec(vec![def])); } // Useful when working on functions in this module. By default symbols that we named do now diff --git a/compiler/can/src/module.rs b/compiler/can/src/module.rs index ba4d5ce0b9..8fe6ff5cef 100644 --- a/compiler/can/src/module.rs +++ b/compiler/can/src/module.rs @@ -1,4 +1,5 @@ use crate::def::{canonicalize_defs, sort_can_defs, Declaration, Def}; +use crate::effect_module::HostedGeneratedFunctions; use crate::env::Env; use crate::expr::{ClosureData, Expr, Output}; use crate::operator::desugar_def; @@ -40,6 +41,30 @@ pub struct ModuleOutput { pub scope: Scope, } +fn validate_generate_with<'a>( + generate_with: &'a [Loc>], +) -> (HostedGeneratedFunctions, Vec>) { + let mut functions = HostedGeneratedFunctions::default(); + let mut unknown = Vec::new(); + + for generated in generate_with { + match generated.value.as_str() { + "after" => functions.after = true, + "map" => functions.map = true, + "always" => functions.always = true, + "loop" => functions.loop_ = true, + "forever" => functions.forever = true, + other => { + // we don't know how to generate this function + let ident = Ident::from(other); + unknown.push(Loc::at(generated.region, ident)); + } + } + } + + (functions, unknown) +} + // TODO trim these down #[allow(clippy::too_many_arguments)] pub fn canonicalize_module_defs<'a, F>( @@ -68,9 +93,23 @@ where scope.add_alias(name, alias.region, alias.type_variables, alias.typ); } - let effect_symbol = if let HeaderFor::Hosted { generates, .. } = header_for { - // TODO extract effect name from the header + struct Hosted { + effect_symbol: Symbol, + generated_functions: HostedGeneratedFunctions, + } + + let hosted_info = if let HeaderFor::Hosted { + generates, + generates_with, + } = header_for + { let name: &str = generates.into(); + let (generated_functions, unknown_generated) = validate_generate_with(generates_with); + + for unknown in unknown_generated { + env.problem(Problem::UnknownGeneratesWith(unknown)); + } + let effect_symbol = scope .introduce( name.into(), @@ -99,7 +138,10 @@ where ); } - Some(effect_symbol) + Some(Hosted { + effect_symbol, + generated_functions, + }) } else { None }; @@ -246,7 +288,11 @@ where (Ok(mut declarations), output) => { use crate::def::Declaration::*; - if let Some(effect_symbol) = effect_symbol { + if let Some(Hosted { + effect_symbol, + generated_functions, + }) = hosted_info + { let mut exposed_symbols = MutSet::default(); // NOTE this currently builds all functions, not just the ones that the user requested @@ -257,6 +303,7 @@ where var_store, &mut exposed_symbols, &mut declarations, + generated_functions, ); } @@ -277,7 +324,7 @@ where // Temporary hack: we don't know exactly what symbols are hosted symbols, // and which are meant to be normal definitions without a body. So for now // we just assume they are hosted functions (meant to be provided by the platform) - if let Some(effect_symbol) = effect_symbol { + if let Some(Hosted { effect_symbol, .. }) = hosted_info { macro_rules! make_hosted_def { () => { let symbol = def.pattern_vars.iter().next().unwrap().0; @@ -358,7 +405,7 @@ where let mut aliases = MutMap::default(); - if let Some(effect_symbol) = effect_symbol { + if let Some(Hosted { effect_symbol, .. }) = hosted_info { // Remove this from exposed_symbols, // so that at the end of the process, // we can see if there were any diff --git a/compiler/problem/src/can.rs b/compiler/problem/src/can.rs index f8d964e8fc..a4eeddebe7 100644 --- a/compiler/problem/src/can.rs +++ b/compiler/problem/src/can.rs @@ -25,6 +25,7 @@ pub enum Problem { UnusedDef(Symbol, Region), UnusedImport(ModuleId, Region), ExposedButNotDefined(Symbol), + UnknownGeneratesWith(Loc), /// First symbol is the name of the closure with that argument /// Second symbol is the name of the argument that is unused UnusedArgument(Symbol, Symbol, Region), diff --git a/reporting/src/error/canonicalize.rs b/reporting/src/error/canonicalize.rs index 3ad20cebf6..1dfd07ad61 100644 --- a/reporting/src/error/canonicalize.rs +++ b/reporting/src/error/canonicalize.rs @@ -16,7 +16,7 @@ const UNUSED_DEF: &str = "UNUSED DEFINITION"; const UNUSED_IMPORT: &str = "UNUSED IMPORT"; const UNUSED_ALIAS_PARAM: &str = "UNUSED TYPE ALIAS PARAMETER"; const UNUSED_ARG: &str = "UNUSED ARGUMENT"; -const MISSING_DEFINITION: &str = "MISSING_DEFINITION"; +const MISSING_DEFINITION: &str = "MISSING DEFINITION"; const DUPLICATE_FIELD_NAME: &str = "DUPLICATE FIELD NAME"; const DUPLICATE_TAG_NAME: &str = "DUPLICATE TAG NAME"; const INVALID_UNICODE: &str = "INVALID UNICODE"; @@ -92,6 +92,21 @@ pub fn can_problem<'b>( title = MISSING_DEFINITION.to_string(); severity = Severity::RuntimeError; } + Problem::UnknownGeneratesWith(loc_ident) => { + doc = alloc.stack(vec![ + alloc + .reflow("I don't know how to generate the ") + .append(alloc.ident(loc_ident.value)) + .append(alloc.reflow("function.")), + alloc.region(lines.convert_region(loc_ident.region)), + alloc + .reflow("Only specific functions like `after` and `map` can be generated.") + .append(alloc.reflow("Learn more about hosted modules at TODO.")), + ]); + + title = MISSING_DEFINITION.to_string(); + severity = Severity::RuntimeError; + } Problem::UnusedArgument(closure_symbol, argument_symbol, region) => { let line = "\". Adding an underscore at the start of a variable name is a way of saying that the variable is not used."; diff --git a/reporting/src/error/parse.rs b/reporting/src/error/parse.rs index 1e767a38fd..79cccf96a2 100644 --- a/reporting/src/error/parse.rs +++ b/reporting/src/error/parse.rs @@ -517,6 +517,26 @@ fn to_expr_report<'a>( } } + EExpr::Record(_erecord, pos) => { + let surroundings = Region::new(start, *pos); + let region = LineColumnRegion::from_pos(lines.convert_pos(*pos)); + + let doc = alloc.stack(vec![ + alloc.reflow(r"I am partway through parsing an record, but I got stuck here:"), + alloc.region_with_subregion(lines.convert_region(surroundings), region), + alloc.concat(vec![ + alloc.reflow("TODO provide more context.") + ]), + ]); + + Report { + filename, + doc, + title: "RECORD PARSE PROBLEM".to_string(), + severity: Severity::RuntimeError, + } + } + EExpr::Space(error, pos) => to_space_report(alloc, lines, filename, error, *pos), &EExpr::Number(ENumber::End, pos) => {