diff --git a/.cargo/config b/.cargo/config index d9e675b5bd..b6733e42bb 100644 --- a/.cargo/config +++ b/.cargo/config @@ -19,6 +19,7 @@ ROC_WORKSPACE_DIR = { value = "", relative = true } # Set = "1" to turn a debug flag on. ROC_PRETTY_PRINT_ALIAS_CONTENTS = "0" ROC_PRINT_UNIFICATIONS = "0" +ROC_PRINT_UNIFICATIONS_DERIVED = "0" ROC_PRINT_MISMATCHES = "0" ROC_VERIFY_RIGID_LET_GENERALIZED = "0" ROC_PRINT_IR_AFTER_SPECIALIZATION = "0" @@ -26,4 +27,4 @@ ROC_PRINT_IR_AFTER_RESET_REUSE = "0" ROC_PRINT_IR_AFTER_REFCOUNT = "0" ROC_DEBUG_ALIAS_ANALYSIS = "0" ROC_PRINT_LLVM_FN_VERIFICATION = "0" -ROC_PRINT_LOAD_LOG = "0" \ No newline at end of file +ROC_PRINT_LOAD_LOG = "0" diff --git a/Cargo.lock b/Cargo.lock index 407db63a00..cb42e21bf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3670,6 +3670,18 @@ dependencies = [ name = "roc_debug_flags" version = "0.1.0" +[[package]] +name = "roc_derive_key" +version = "0.1.0" +dependencies = [ + "roc_can", + "roc_collections", + "roc_error_macros", + "roc_module", + "roc_region", + "roc_types", +] + [[package]] name = "roc_docs" version = "0.1.0" @@ -3951,6 +3963,7 @@ dependencies = [ "roc_can", "roc_collections", "roc_debug_flags", + "roc_derive_key", "roc_error_macros", "roc_exhaustive", "roc_late_solve", @@ -4797,6 +4810,31 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test_derive" +version = "0.1.0" +dependencies = [ + "bumpalo", + "indoc", + "lazy_static", + "pretty_assertions", + "roc_builtins", + "roc_can", + "roc_collections", + "roc_constrain", + "roc_debug_flags", + "roc_derive_key", + "roc_load_internal", + "roc_module", + "roc_mono", + "roc_region", + "roc_reporting", + "roc_solve", + "roc_target", + "roc_types", + "ven_pretty", +] + [[package]] name = "test_gen" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c104f5ddb4..0c6dfa106d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,11 @@ members = [ "compiler/solve", "compiler/late_solve", "compiler/fmt", + "compiler/derive_key", "compiler/mono", "compiler/alias_analysis", "compiler/test_mono", + "compiler/test_derive", "compiler/load", "compiler/load_internal", "compiler/gen_llvm", diff --git a/README.md b/README.md index 7be87d5bba..681b6d2661 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Many programs can however be compiled correctly. Check out [examples](examples) Run examples as follows: ``` -cargo run examples/hello-world/helloWorld.roc +cargo run examples/hello-world/main.roc ``` Some examples like `examples/benchmarks/NQueens.roc` require input after running. For NQueens, input 10 in the terminal and press enter. diff --git a/bindgen/src/load.rs b/bindgen/src/load.rs index 5deb77ff61..48dde27ccb 100644 --- a/bindgen/src/load.rs +++ b/bindgen/src/load.rs @@ -69,6 +69,7 @@ pub fn load_types( unreachable!("Builtin decl in userspace module?") } Declaration::InvalidCycle(..) => Vec::new(), + Declaration::Expects(..) => Vec::new(), }); let vars_iter = defs_iter.filter_map( diff --git a/cli/src/build.rs b/cli/src/build.rs index a485a7fde1..abad31d730 100644 --- a/cli/src/build.rs +++ b/cli/src/build.rs @@ -176,11 +176,15 @@ pub fn build_file<'a>( "Find Specializations", module_timing.find_specializations, ); - report_timing( - buf, - "Make Specializations", - module_timing.make_specializations, - ); + let multiple_make_specializations_passes = module_timing.make_specializations.len() > 1; + for (i, pass_time) in module_timing.make_specializations.iter().enumerate() { + let suffix = if multiple_make_specializations_passes { + format!(" (Pass {})", i) + } else { + String::new() + }; + report_timing(buf, &format!("Make Specializations{}", suffix), *pass_time); + } report_timing(buf, "Other", module_timing.other()); buf.push('\n'); report_timing(buf, "Total", module_timing.total()); @@ -460,16 +464,6 @@ pub fn check_file( report_timing(buf, "Canonicalize", module_timing.canonicalize); report_timing(buf, "Constrain", module_timing.constrain); report_timing(buf, "Solve", module_timing.solve); - report_timing( - buf, - "Find Specializations", - module_timing.find_specializations, - ); - report_timing( - buf, - "Make Specializations", - module_timing.make_specializations, - ); report_timing(buf, "Other", module_timing.other()); buf.push('\n'); report_timing(buf, "Total", module_timing.total()); diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 53c6216066..5c6e123f4a 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -3,7 +3,7 @@ extern crate const_format; use build::BuiltFile; use bumpalo::Bump; -use clap::{Arg, ArgMatches, Command}; +use clap::{Arg, ArgMatches, Command, ValueSource}; use roc_build::link::{LinkType, LinkingStrategy}; use roc_error_macros::{internal_error, user_error}; use roc_load::{LoadingProblem, Threading}; @@ -24,6 +24,8 @@ pub mod build; mod format; pub use format::format; +const DEFAULT_ROC_FILENAME: &str = "main.roc"; + pub const CMD_BUILD: &str = "build"; pub const CMD_RUN: &str = "run"; pub const CMD_REPL: &str = "repl"; @@ -57,13 +59,11 @@ pub fn build_app<'a>() -> Command<'a> { let flag_optimize = Arg::new(FLAG_OPTIMIZE) .long(FLAG_OPTIMIZE) .help("Optimize the compiled program to run faster. (Optimization takes time to complete.)") - .requires(ROC_FILE) .required(false); let flag_max_threads = Arg::new(FLAG_MAX_THREADS) .long(FLAG_MAX_THREADS) .help("Limit the number of threads (and hence cores) used during compilation.") - .requires(ROC_FILE) .takes_value(true) .validator(|s| s.parse::()) .required(false); @@ -81,7 +81,6 @@ pub fn build_app<'a>() -> Command<'a> { let flag_debug = Arg::new(FLAG_DEBUG) .long(FLAG_DEBUG) .help("Store LLVM debug information in the generated program.") - .requires(ROC_FILE) .required(false); let flag_valgrind = Arg::new(FLAG_VALGRIND) @@ -108,13 +107,17 @@ pub fn build_app<'a>() -> Command<'a> { let roc_file_to_run = Arg::new(ROC_FILE) .help("The .roc file of an app to run") - .allow_invalid_utf8(true); + .allow_invalid_utf8(true) + .required(false) + .default_value(DEFAULT_ROC_FILENAME); let args_for_app = Arg::new(ARGS_FOR_APP) - .help("Arguments to pass into the app being run") - .requires(ROC_FILE) + .help("Arguments to pass into the app being run, e.g. `roc run -- arg1 arg2`") .allow_invalid_utf8(true) - .multiple_values(true); + .multiple_values(true) + .takes_value(true) + .allow_hyphen_values(true) + .last(true); let app = Command::new("roc") .version(concatcp!(VERSION, "\n")) @@ -154,7 +157,8 @@ pub fn build_app<'a>() -> Command<'a> { Arg::new(ROC_FILE) .help("The .roc file to build") .allow_invalid_utf8(true) - .required(true), + .required(false) + .default_value(DEFAULT_ROC_FILENAME), ) ) .subcommand(Command::new(CMD_REPL) @@ -171,7 +175,7 @@ pub fn build_app<'a>() -> Command<'a> { .arg(flag_linker.clone()) .arg(flag_precompiled.clone()) .arg(flag_valgrind.clone()) - .arg(roc_file_to_run.clone().required(true)) + .arg(roc_file_to_run.clone()) .arg(args_for_app.clone()) ) .subcommand(Command::new(CMD_FORMAT) @@ -199,7 +203,8 @@ pub fn build_app<'a>() -> Command<'a> { Arg::new(ROC_FILE) .help("The .roc file of an app to check") .allow_invalid_utf8(true) - .required(true), + .required(false) + .default_value(DEFAULT_ROC_FILENAME), ) ) .subcommand( @@ -317,9 +322,17 @@ pub fn build( match err.kind() { NotFound => { - match path.to_str() { - Some(path_str) => println!("File not found: {}", path_str), - None => println!("Malformed file path : {:?}", path), + let path_string = path.to_string_lossy(); + + // TODO these should use roc_reporting to display nicer error messages. + match matches.value_source(ROC_FILE) { + Some(ValueSource::DefaultValue) => { + eprintln!( + "\nNo `.roc` file was specified, and the current directory does not contain a {} file to use as a default.\n\nYou can run `roc help` for more information on how to provide a .roc file.\n", + DEFAULT_ROC_FILENAME + ) + } + _ => eprintln!("\nThis file was not found: {}\n\nYou can run `roc help` for more information on how to provide a .roc file.\n", path_string), } process::exit(1); diff --git a/cli/tests/cli_run.rs b/cli/tests/cli_run.rs index d0b9136ff7..95e451024d 100644 --- a/cli/tests/cli_run.rs +++ b/cli/tests/cli_run.rs @@ -94,21 +94,23 @@ mod cli_run { file: &'a Path, args: I, stdin: &[&str], - input_file: Option, + opt_input_file: Option, ) -> Out { - let compile_out = match input_file { - Some(input_file) => run_roc( + let compile_out = if let Some(input_file) = opt_input_file { + run_roc( // converting these all to String avoids lifetime issues args.into_iter().map(|arg| arg.to_string()).chain([ file.to_str().unwrap().to_string(), + "--".to_string(), input_file.to_str().unwrap().to_string(), ]), stdin, - ), - None => run_roc( + ) + } else { + run_roc( args.into_iter().chain(iter::once(file.to_str().unwrap())), stdin, - ), + ) }; // If there is any stderr, it should be reporting the runtime and that's it! @@ -131,7 +133,7 @@ mod cli_run { stdin: &[&str], executable_filename: &str, flags: &[&str], - input_file: Option, + opt_input_file: Option, expected_ending: &str, use_valgrind: bool, ) { @@ -151,7 +153,7 @@ mod cli_run { run_roc_on(file, iter::once(CMD_BUILD).chain(flags.clone()), &[], None); if use_valgrind && ALLOW_VALGRIND { - let (valgrind_out, raw_xml) = if let Some(ref input_file) = input_file { + let (valgrind_out, raw_xml) = if let Some(ref input_file) = opt_input_file { run_with_valgrind( stdin.iter().copied(), &[ @@ -201,7 +203,7 @@ mod cli_run { } valgrind_out - } else if let Some(ref input_file) = input_file { + } else if let Some(ref input_file) = opt_input_file { run_cmd( file.with_file_name(executable_filename).to_str().unwrap(), stdin.iter().copied(), @@ -215,12 +217,12 @@ mod cli_run { ) } } - CliMode::Roc => run_roc_on(file, flags.clone(), stdin, input_file.clone()), + CliMode::Roc => run_roc_on(file, flags.clone(), stdin, opt_input_file.clone()), CliMode::RocRun => run_roc_on( file, iter::once(CMD_RUN).chain(flags.clone()), stdin, - input_file.clone(), + opt_input_file.clone(), ), }; @@ -382,8 +384,8 @@ mod cli_run { // ] examples! { helloWorld:"hello-world" => Example { - filename: "helloWorld.roc", - executable_filename: "helloWorld", + filename: "main.roc", + executable_filename: "hello", stdin: &[], input_file: None, expected_ending:"Hello, World!\n", diff --git a/compiler/builtins/roc/Encode.roc b/compiler/builtins/roc/Encode.roc index 0f6ea9e80a..c180229368 100644 --- a/compiler/builtins/roc/Encode.roc +++ b/compiler/builtins/roc/Encode.roc @@ -21,6 +21,8 @@ interface Encode bool, string, list, + record, + tag, custom, appendWith, append, @@ -51,6 +53,8 @@ EncoderFormatting has bool : Bool -> Encoder fmt | fmt has EncoderFormatting string : Str -> Encoder fmt | fmt has EncoderFormatting list : List elem, (elem -> Encoder fmt) -> Encoder fmt | fmt has EncoderFormatting + record : List { key : Str, value : Encoder fmt } -> Encoder fmt | fmt has EncoderFormatting + tag : Str, List (Encoder fmt) -> Encoder fmt | fmt has EncoderFormatting custom : (List U8, fmt -> List U8) -> Encoder fmt | fmt has EncoderFormatting custom = \encoder -> @Encoder encoder diff --git a/compiler/builtins/roc/Json.roc b/compiler/builtins/roc/Json.roc index a8f7059de1..7d7675c98f 100644 --- a/compiler/builtins/roc/Json.roc +++ b/compiler/builtins/roc/Json.roc @@ -7,6 +7,7 @@ interface Json imports [ Encode.{ + Encoder, custom, appendWith, u8, @@ -25,6 +26,8 @@ interface Json bool, string, list, + record, + tag, }, ] @@ -81,3 +84,52 @@ list = \lst, encodeElem -> withList = List.walk lst head (\bytes1, elem -> appendWith bytes1 (encodeElem elem) (@Json {})) List.append withList (Num.toU8 ']') + +record = \fields -> + custom \bytes, @Json {} -> + writeRecord = \{ buffer, fieldsLeft }, { key, value } -> + bufferWithKeyValue = + List.append buffer (Num.toU8 '"') + |> List.concat (Str.toUtf8 key) + |> List.append (Num.toU8 '"') + |> List.append (Num.toU8 ':') + |> appendWith value (@Json {}) + + bufferWithSuffix = + if fieldsLeft > 0 then + List.append bufferWithKeyValue (Num.toU8 ',') + else + bufferWithKeyValue + + { buffer: bufferWithSuffix, fieldsLeft: fieldsLeft - 1 } + + bytesHead = List.append bytes (Num.toU8 '{') + { buffer: bytesWithRecord } = List.walk fields { buffer: bytesHead, fieldsLeft: List.len fields } writeRecord + + List.append bytesWithRecord (Num.toU8 '}') + +tag = \name, payload -> + custom \bytes, @Json {} -> + # Idea: encode `A v1 v2` as `{"A": [v1, v2]}` + writePayload = \{ buffer, itemsLeft }, encoder -> + bufferWithValue = appendWith buffer encoder (@Json {}) + bufferWithSuffix = + if itemsLeft > 0 then + List.append bufferWithValue (Num.toU8 ',') + else + bufferWithValue + + { buffer: bufferWithSuffix, itemsLeft: itemsLeft - 1 } + + bytesHead = + List.append bytes (Num.toU8 '{') + |> List.append (Num.toU8 '"') + |> List.concat (Str.toUtf8 name) + |> List.append (Num.toU8 '"') + |> List.append (Num.toU8 ':') + |> List.append (Num.toU8 '[') + + { buffer: bytesWithPayload } = List.walk payload { buffer: bytesHead, itemsLeft: List.len payload } writePayload + + List.append bytesWithPayload (Num.toU8 ']') + |> List.append (Num.toU8 '}') diff --git a/compiler/can/src/abilities.rs b/compiler/can/src/abilities.rs index 28cccfedfa..464527f9e4 100644 --- a/compiler/can/src/abilities.rs +++ b/compiler/can/src/abilities.rs @@ -220,6 +220,73 @@ impl IAbilitiesStore { self.next_specialization_id += 1; id } + + /// Creates a store from [`self`] that closes over the abilities/members given by the + /// imported `symbols`, and their specializations (if any). + pub fn closure_from_imported(&self, symbols: &VecSet) -> PendingAbilitiesStore { + let Self { + members_of_ability, + ability_members, + declared_specializations, + + // Covered by `declared_specializations` + specialization_to_root: _, + + // Taking closure for a new module, so specialization IDs can be fresh + next_specialization_id: _, + resolved_specializations: _, + } = self; + + let mut new = PendingAbilitiesStore::default(); + + // 1. Figure out the abilities we need to introduce. + let mut abilities_to_introduce = VecSet::with_capacity(2); + symbols.iter().for_each(|symbol| { + if let Some(member_data) = ability_members.get(symbol) { + // If the symbol is member of an ability, we need to capture the entire ability. + abilities_to_introduce.insert(member_data.parent_ability); + } + if members_of_ability.contains_key(symbol) { + abilities_to_introduce.insert(*symbol); + } + }); + + // 2. Add each ability, and any specializations of its members we know about. + for ability in abilities_to_introduce.into_iter() { + let members = members_of_ability.get(&ability).unwrap(); + let mut imported_member_data = Vec::with_capacity(members.len()); + for member in members { + let AbilityMemberData { + parent_ability, + region, + typ: _, + } = ability_members.get(member).unwrap().clone(); + // All external members need to be marked as imported. We'll figure out their real + // type variables when it comes time to solve the module we're currently importing + // into. + let imported_data = AbilityMemberData { + parent_ability, + region, + typ: PendingMemberType::Imported, + }; + + imported_member_data.push((*member, imported_data)); + } + + new.register_ability(ability, imported_member_data); + + // Add any specializations of the ability's members we know about. + declared_specializations + .iter() + .filter(|((member, _), _)| members.contains(member)) + .for_each(|(&(member, typ), specialization)| { + new.register_specializing_symbol(specialization.symbol, member); + new.import_specialization(member, typ, specialization); + }); + } + + new + } } impl IAbilitiesStore { @@ -326,73 +393,6 @@ impl IAbilitiesStore { debug_assert!(old_spec.is_none(), "Replacing existing specialization"); } - /// Creates a store from [`self`] that closes over the abilities/members given by the - /// imported `symbols`, and their specializations (if any). - pub fn closure_from_imported(&self, symbols: &VecSet) -> Self { - let Self { - members_of_ability, - ability_members, - declared_specializations, - - // Covered by `declared_specializations` - specialization_to_root: _, - - // Taking closure for a new module, so specialization IDs can be fresh - next_specialization_id: _, - resolved_specializations: _, - } = self; - - let mut new = PendingAbilitiesStore::default(); - - // 1. Figure out the abilities we need to introduce. - let mut abilities_to_introduce = VecSet::with_capacity(2); - symbols.iter().for_each(|symbol| { - if let Some(member_data) = ability_members.get(symbol) { - // If the symbol is member of an ability, we need to capture the entire ability. - abilities_to_introduce.insert(member_data.parent_ability); - } - if members_of_ability.contains_key(symbol) { - abilities_to_introduce.insert(*symbol); - } - }); - - // 2. Add each ability, and any specializations of its members we know about. - for ability in abilities_to_introduce.into_iter() { - let members = members_of_ability.get(&ability).unwrap(); - let mut imported_member_data = Vec::with_capacity(members.len()); - for member in members { - let AbilityMemberData { - parent_ability, - region, - typ: _, - } = ability_members.get(member).unwrap().clone(); - // All external members need to be marked as imported. We'll figure out their real - // type variables when it comes time to solve the module we're currently importing - // into. - let imported_data = AbilityMemberData { - parent_ability, - region, - typ: PendingMemberType::Imported, - }; - - imported_member_data.push((*member, imported_data)); - } - - new.register_ability(ability, imported_member_data); - - // Add any specializations of the ability's members we know about. - declared_specializations - .iter() - .filter(|((member, _), _)| members.contains(member)) - .for_each(|(&(member, typ), specialization)| { - new.register_specializing_symbol(specialization.symbol, member); - new.import_specialization(member, typ, specialization); - }); - } - - new - } - pub fn union(&mut self, other: Self) { let Self { members_of_ability: other_members_of_ability, diff --git a/compiler/can/src/def.rs b/compiler/can/src/def.rs index a22ccc5624..58a744cbe1 100644 --- a/compiler/can/src/def.rs +++ b/compiler/can/src/def.rs @@ -79,10 +79,28 @@ pub struct Annotation { #[derive(Debug)] pub(crate) struct CanDefs { defs: Vec>, + expects: Expects, def_ordering: DefOrdering, aliases: VecMap, } +#[derive(Clone, Debug)] +pub struct Expects { + pub conditions: Vec, + pub regions: Vec, + pub preceding_comment: Vec, +} + +impl Expects { + fn with_capacity(capacity: usize) -> Self { + Self { + conditions: Vec::with_capacity(capacity), + regions: Vec::with_capacity(capacity), + preceding_comment: Vec::with_capacity(capacity), + } + } +} + /// A Def that has had patterns and type annnotations canonicalized, /// but no Expr canonicalization has happened yet. Also, it has had spaces /// and nesting resolved, and knows whether annotations are standalone or not. @@ -201,6 +219,7 @@ pub enum Declaration { Declare(Def), DeclareRec(Vec, IllegalCycleMark), Builtin(Def), + Expects(Expects), /// If we know a cycle is illegal during canonicalization. /// Otherwise we will try to detect this during solving; see [`IllegalCycleMark`]. InvalidCycle(Vec), @@ -214,6 +233,7 @@ impl Declaration { DeclareRec(defs, _) => defs.len(), InvalidCycle { .. } => 0, Builtin(_) => 0, + Expects(_) => 0, } } @@ -229,6 +249,10 @@ impl Declaration { &cycles.first().unwrap().expr_region, &cycles.last().unwrap().expr_region, ), + Declaration::Expects(expects) => Region::span_across( + expects.regions.first().unwrap(), + expects.regions.last().unwrap(), + ), } } } @@ -541,18 +565,21 @@ fn canonicalize_value_defs<'a>( // the ast::Expr values in pending_exprs for further canonicalization // once we've finished assembling the entire scope. let mut pending_value_defs = Vec::with_capacity(value_defs.len()); + let mut pending_expects = Vec::with_capacity(value_defs.len()); + for loc_def in value_defs { let mut new_output = Output::default(); - match to_pending_value_def( + let pending = to_pending_value_def( env, var_store, loc_def.value, scope, &mut new_output, pattern_type, - ) { - None => { /* skip */ } - Some(pending_def) => { + ); + + match pending { + PendingValue::Def(pending_def) => { // Record the ast::Expr for later. We'll do another pass through these // once we have the entire scope assembled. If we were to canonicalize // the exprs right now, they wouldn't have symbols in scope from defs @@ -560,6 +587,10 @@ fn canonicalize_value_defs<'a>( pending_value_defs.push(pending_def); output.union(new_output); } + PendingValue::SignatureDefMismatch => { /* skip */ } + PendingValue::Expect(pending_expect) => { + pending_expects.push(pending_expect); + } } } @@ -607,8 +638,27 @@ fn canonicalize_value_defs<'a>( def_ordering.insert_symbol_references(def_id as u32, &temp_output.references) } + let mut expects = Expects::with_capacity(pending_expects.len()); + + for pending in pending_expects { + let (loc_can_condition, can_output) = canonicalize_expr( + env, + var_store, + scope, + pending.condition.region, + &pending.condition.value, + ); + + expects.conditions.push(loc_can_condition.value); + expects.regions.push(loc_can_condition.region); + expects.preceding_comment.push(pending.preceding_comment); + + output.union(can_output); + } + let can_defs = CanDefs { defs, + expects, def_ordering, aliases, }; @@ -1009,6 +1059,7 @@ pub(crate) fn sort_can_defs( ) -> (Vec, Output) { let CanDefs { mut defs, + expects, def_ordering, aliases, } = defs; @@ -1119,6 +1170,10 @@ pub(crate) fn sort_can_defs( } } + if !expects.conditions.is_empty() { + declarations.push(Declaration::Expects(expects)); + } + (declarations, output) } @@ -1574,6 +1629,10 @@ fn decl_to_let(decl: Declaration, loc_ret: Loc) -> Loc { // Builtins should only be added to top-level decls, not to let-exprs! unreachable!() } + Declaration::Expects(expects) => { + // Expects should only be added to top-level decls, not to let-exprs! + unreachable!("{:?}", &expects) + } } } @@ -1739,6 +1798,17 @@ fn to_pending_type_def<'a>( } } +enum PendingValue<'a> { + Def(PendingValueDef<'a>), + Expect(PendingExpect<'a>), + SignatureDefMismatch, +} + +struct PendingExpect<'a> { + condition: &'a Loc>, + preceding_comment: Region, +} + fn to_pending_value_def<'a>( env: &mut Env<'a>, var_store: &mut VarStore, @@ -1746,7 +1816,7 @@ fn to_pending_value_def<'a>( scope: &mut Scope, output: &mut Output, pattern_type: PatternType, -) -> Option> { +) -> PendingValue<'a> { use ast::ValueDef::*; match def { @@ -1762,7 +1832,7 @@ fn to_pending_value_def<'a>( loc_pattern.region, ); - Some(PendingValueDef::AnnotationOnly( + PendingValue::Def(PendingValueDef::AnnotationOnly( loc_pattern, loc_can_pattern, loc_ann, @@ -1780,7 +1850,7 @@ fn to_pending_value_def<'a>( loc_pattern.region, ); - Some(PendingValueDef::Body( + PendingValue::Def(PendingValueDef::Body( loc_pattern, loc_can_pattern, loc_expr, @@ -1812,7 +1882,7 @@ fn to_pending_value_def<'a>( body_pattern.region, ); - Some(PendingValueDef::TypedBody( + PendingValue::Def(PendingValueDef::TypedBody( body_pattern, loc_can_pattern, ann_type, @@ -1828,11 +1898,14 @@ fn to_pending_value_def<'a>( // TODO: Should we instead build some PendingValueDef::InvalidAnnotatedBody ? This would // remove the `Option` on this function (and be probably more reliable for further // problem/error reporting) - None + PendingValue::SignatureDefMismatch } } - Expect(_condition) => todo!(), + Expect(condition) => PendingValue::Expect(PendingExpect { + condition, + preceding_comment: Region::zero(), + }), } } diff --git a/compiler/can/src/module.rs b/compiler/can/src/module.rs index b25e5d49ea..020c932c87 100644 --- a/compiler/can/src/module.rs +++ b/compiler/can/src/module.rs @@ -493,6 +493,7 @@ pub fn canonicalize_module_defs<'a>( .iter() .all(|(symbol, _)| !exposed_but_not_defined.contains(symbol))); } + Expects(_) => { /* ignore */ } } } @@ -574,7 +575,7 @@ pub fn canonicalize_module_defs<'a>( DeclareRec(defs, _) => { fix_values_captured_in_closure_defs(defs, &mut VecSet::default()) } - InvalidCycle(_) | Builtin(_) => {} + InvalidCycle(_) | Builtin(_) | Expects(_) => {} } } diff --git a/compiler/can/src/traverse.rs b/compiler/can/src/traverse.rs index bd5af6bc14..525b7c1af8 100644 --- a/compiler/can/src/traverse.rs +++ b/compiler/can/src/traverse.rs @@ -31,6 +31,12 @@ pub fn walk_decl(visitor: &mut V, decl: &Declaration) { Declaration::DeclareRec(defs, _cycle_mark) => { visit_list!(visitor, visit_def, defs) } + Declaration::Expects(expects) => { + let it = expects.regions.iter().zip(expects.conditions.iter()); + for (region, condition) in it { + visitor.visit_expr(condition, *region, Variable::BOOL); + } + } Declaration::Builtin(def) => visitor.visit_def(def), Declaration::InvalidCycle(_cycles) => { // ignore diff --git a/compiler/constrain/src/expr.rs b/compiler/constrain/src/expr.rs index eaf144b9eb..0b7b4e5b44 100644 --- a/compiler/constrain/src/expr.rs +++ b/compiler/constrain/src/expr.rs @@ -4,7 +4,7 @@ use crate::builtins::{ use crate::pattern::{constrain_pattern, PatternState}; use roc_can::annotation::IntroducedVariables; use roc_can::constraint::{Constraint, Constraints, OpportunisticResolve}; -use roc_can::def::{Declaration, Def}; +use roc_can::def::{Declaration, Def, Expects}; use roc_can::exhaustive::{sketch_pattern_to_rows, sketch_when_branches, ExhaustiveContext}; use roc_can::expected::Expected::{self, *}; use roc_can::expected::PExpected; @@ -801,12 +801,7 @@ pub fn constrain_expr( let branch_constraints = constraints.and_constraint(total_cons); constraints.exists( - [ - exhaustive.variable_for_introduction(), - branches_cond_var, - real_cond_var, - *expr_var, - ], + [branches_cond_var, real_cond_var, *expr_var], branch_constraints, ) } @@ -1309,6 +1304,9 @@ pub fn constrain_decls( constraint = constrain_recursive_defs(constraints, &mut env, defs, constraint, *cycle_mark); } + Declaration::Expects(expects) => { + constraint = constrain_expects(constraints, &mut env, expects, constraint); + } Declaration::InvalidCycle(_) => { // invalid cycles give a canonicalization error. we skip them here. continue; @@ -1705,6 +1703,35 @@ fn attach_resolution_constraints( constraints.and_constraint([constraint, resolution_constrs]) } +fn constrain_expects( + constraints: &mut Constraints, + env: &mut Env, + expects: &Expects, + body_con: Constraint, +) -> Constraint { + let expect_bool = |region| { + let bool_type = Type::Variable(Variable::BOOL); + Expected::ForReason(Reason::ExpectCondition, bool_type, region) + }; + + let mut expect_constraints = Vec::with_capacity(expects.conditions.len()); + + let it = expects.regions.iter().zip(expects.conditions.iter()); + for (region, condition) in it { + let expected = expect_bool(*region); + expect_constraints.push(constrain_expr( + constraints, + env, + *region, + condition, + expected, + )); + } + + let defs_constraint = constraints.and_constraint(expect_constraints); + constraints.let_constraint([], [], [], defs_constraint, body_con) +} + fn constrain_def( constraints: &mut Constraints, env: &mut Env, diff --git a/compiler/debug_flags/src/lib.rs b/compiler/debug_flags/src/lib.rs index 5af06aff4c..4980098cf6 100644 --- a/compiler/debug_flags/src/lib.rs +++ b/compiler/debug_flags/src/lib.rs @@ -62,8 +62,13 @@ flags! { // ===Solve=== /// Prints type unifications, before and after they happen. + /// Only use this in single-threaded mode! ROC_PRINT_UNIFICATIONS + /// Like ROC_PRINT_UNIFICATIONS, in the context of typechecking derived implementations. + /// Only use this in single-threaded mode! + ROC_PRINT_UNIFICATIONS_DERIVED + /// Prints all type mismatches hit during type unification. ROC_PRINT_MISMATCHES diff --git a/compiler/derive_key/Cargo.toml b/compiler/derive_key/Cargo.toml new file mode 100644 index 0000000000..e674089371 --- /dev/null +++ b/compiler/derive_key/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "roc_derive_key" +version = "0.1.0" +authors = ["The Roc Contributors"] +license = "UPL-1.0" +edition = "2021" + +[dependencies] +roc_collections = { path = "../collections" } +roc_error_macros = { path = "../../error_macros" } +roc_region = { path = "../region" } +roc_module = { path = "../module" } +roc_types = { path = "../types" } +roc_can = { path = "../can" } diff --git a/compiler/derive_key/src/encoding.rs b/compiler/derive_key/src/encoding.rs new file mode 100644 index 0000000000..6169830f4e --- /dev/null +++ b/compiler/derive_key/src/encoding.rs @@ -0,0 +1,120 @@ +use roc_error_macros::internal_error; +use roc_module::{ + ident::{Lowercase, TagName}, + symbol::Symbol, +}; +use roc_types::subs::{Content, FlatType, GetSubsSlice, Subs, SubsFmtContent, Variable}; + +#[derive(Hash)] +pub enum FlatEncodable<'a> { + Immediate(Symbol), + Key(FlatEncodableKey<'a>), +} + +#[derive(Hash, PartialEq, Eq, Debug)] +pub enum FlatEncodableKey<'a> { + List(/* takes one variable */), + Set(/* takes one variable */), + Dict(/* takes two variables */), + // Unfortunate that we must allocate here, c'est la vie + Record(Vec<&'a Lowercase>), + TagUnion(Vec<(&'a TagName, u16)>), +} + +macro_rules! unexpected { + ($subs:expr, $var:expr) => { + internal_error!( + "Invalid content for toEncoder: {:?}", + SubsFmtContent($subs.get_content_without_compacting($var), $subs) + ) + }; +} + +impl FlatEncodable<'_> { + pub(crate) fn from_var(subs: &Subs, var: Variable) -> FlatEncodable { + use FlatEncodable::*; + match *subs.get_content_without_compacting(var) { + Content::Structure(flat_type) => match flat_type { + FlatType::Apply(sym, _) => match sym { + Symbol::LIST_LIST => Key(FlatEncodableKey::List()), + Symbol::SET_SET => Key(FlatEncodableKey::Set()), + Symbol::DICT_DICT => Key(FlatEncodableKey::Dict()), + Symbol::STR_STR => Immediate(Symbol::ENCODE_STRING), + _ => unexpected!(subs, var), + }, + FlatType::Record(fields, ext) => { + debug_assert!(matches!( + subs.get_content_without_compacting(ext), + Content::Structure(FlatType::EmptyRecord) + )); + + let mut field_names: Vec<_> = + subs.get_subs_slice(fields.field_names()).iter().collect(); + field_names.sort(); + Key(FlatEncodableKey::Record(field_names)) + } + FlatType::TagUnion(tags, ext) | FlatType::RecursiveTagUnion(_, tags, ext) => { + // The recursion var doesn't matter, because the derived implementation will only + // look on the surface of the tag union type, and more over the payloads of the + // arguments will be left generic for the monomorphizer to fill in with the + // appropriate type. That is, + // [ A t1, B t1 t2 ] + // and + // [ A t1, B t1 t2 ] as R + // look the same on the surface, because `R` is only somewhere inside of the + // `t`-prefixed payload types. + debug_assert!(matches!( + subs.get_content_without_compacting(ext), + Content::Structure(FlatType::EmptyTagUnion) + )); + let mut tag_names_and_payload_sizes: Vec<_> = tags + .iter_all() + .map(|(name_index, payload_slice_index)| { + let payload_slice = subs[payload_slice_index]; + let payload_size = payload_slice.length; + let name = &subs[name_index]; + (name, payload_size) + }) + .collect(); + tag_names_and_payload_sizes.sort_by_key(|t| t.0); + Key(FlatEncodableKey::TagUnion(tag_names_and_payload_sizes)) + } + FlatType::FunctionOrTagUnion(name_index, _, _) => { + Key(FlatEncodableKey::TagUnion(vec![(&subs[name_index], 0)])) + } + FlatType::EmptyRecord => Key(FlatEncodableKey::Record(vec![])), + FlatType::EmptyTagUnion => Key(FlatEncodableKey::TagUnion(vec![])), + // + FlatType::Erroneous(_) => unexpected!(subs, var), + FlatType::Func(..) => unexpected!(subs, var), + }, + Content::Alias(sym, _, real_var, _) => match sym { + Symbol::NUM_U8 => Immediate(Symbol::ENCODE_U8), + Symbol::NUM_U16 => Immediate(Symbol::ENCODE_U16), + Symbol::NUM_U32 => Immediate(Symbol::ENCODE_U32), + Symbol::NUM_U64 => Immediate(Symbol::ENCODE_U64), + Symbol::NUM_U128 => Immediate(Symbol::ENCODE_U128), + Symbol::NUM_I8 => Immediate(Symbol::ENCODE_I8), + Symbol::NUM_I16 => Immediate(Symbol::ENCODE_I16), + Symbol::NUM_I32 => Immediate(Symbol::ENCODE_I32), + Symbol::NUM_I64 => Immediate(Symbol::ENCODE_I64), + Symbol::NUM_I128 => Immediate(Symbol::ENCODE_I128), + Symbol::NUM_DEC => Immediate(Symbol::ENCODE_DEC), + Symbol::NUM_F32 => Immediate(Symbol::ENCODE_F32), + Symbol::NUM_F64 => Immediate(Symbol::ENCODE_F64), + // TODO: I believe it is okay to unwrap opaques here because derivers are only used + // by the backend, and the backend treats opaques like structural aliases. + _ => Self::from_var(subs, real_var), + }, + Content::RangedNumber(real_var, _) => Self::from_var(subs, real_var), + // + Content::RecursionVar { .. } => unexpected!(subs, var), + Content::Error => unexpected!(subs, var), + Content::FlexVar(_) + | Content::RigidVar(_) + | Content::FlexAbleVar(_, _) + | Content::RigidAbleVar(_, _) => unexpected!(subs, var), + Content::LambdaSet(_) => unexpected!(subs, var), + } + } +} diff --git a/compiler/derive_key/src/lib.rs b/compiler/derive_key/src/lib.rs new file mode 100644 index 0000000000..7a7c006ac8 --- /dev/null +++ b/compiler/derive_key/src/lib.rs @@ -0,0 +1,63 @@ +//! To avoid duplicating derived implementations for the same type, derived implementations are +//! addressed by a key of their type content. However, different derived implementations can be +//! reused based on different properties of the type. For example: +//! +//! - `Eq` does not care about surface type representations; its derived implementations can be +//! uniquely addressed by the [`Layout`][crate::layout::Layout] of a type. +//! - `Encoding` must care about surface type representations; for example, `{ a: "" }` and +//! `{ b: "" }` have different derived implementations. However, it does not need to distinguish +//! between e.g. required and optional record fields. +//! - `Decoding` is like encoding, but has some differences. For one, it *does* need to distinguish +//! between required and optional record fields. +//! +//! For these reasons the content keying is based on a [`Strategy`] as well. + +pub mod encoding; + +use encoding::{FlatEncodable, FlatEncodableKey}; + +use roc_module::symbol::Symbol; +use roc_types::subs::{Subs, Variable}; + +#[derive(Hash, PartialEq, Eq, Debug)] +#[repr(u8)] +enum Strategy { + Encoding, + #[allow(unused)] + Decoding, +} + +#[derive(Hash, PartialEq, Eq, Debug)] +pub enum Derived +where + R: std::hash::Hash + PartialEq + Eq + std::fmt::Debug, +{ + /// If a derived implementation name is well-known ahead-of-time, we can inline the symbol + /// directly rather than associating a key for an implementation to be made later on. + Immediate(Symbol), + /// Key of the derived implementation to use. This allows association of derived implementation + /// names to a key, when the key is known ahead-of-time but the implementation (and it's name) + /// is yet-to-be-made. + Key(DeriveKey), +} + +#[derive(Hash, PartialEq, Eq, Debug)] +pub struct DeriveKey +where + R: std::hash::Hash + PartialEq + Eq + std::fmt::Debug, +{ + strategy: Strategy, + pub repr: R, +} + +impl<'a> Derived> { + pub fn encoding(subs: &'a Subs, var: Variable) -> Self { + match encoding::FlatEncodable::from_var(subs, var) { + FlatEncodable::Immediate(imm) => Derived::Immediate(imm), + FlatEncodable::Key(repr) => Derived::Key(DeriveKey { + strategy: Strategy::Encoding, + repr, + }), + } + } +} diff --git a/compiler/gen_dev/src/object_builder.rs b/compiler/gen_dev/src/object_builder.rs index 92c01583d1..8dc88576a6 100644 --- a/compiler/gen_dev/src/object_builder.rs +++ b/compiler/gen_dev/src/object_builder.rs @@ -330,7 +330,9 @@ fn build_proc_symbol<'a, B: Backend<'a>>( let base_name = backend.symbol_to_string(sym, layout_id); let fn_name = if backend.env().exposed_to_host.contains(&sym) { - format!("roc_{}_exposed", base_name) + layout_ids + .get_toplevel(sym, &layout) + .to_exposed_symbol_string(sym, backend.interns()) } else { base_name }; diff --git a/compiler/gen_llvm/src/llvm/build.rs b/compiler/gen_llvm/src/llvm/build.rs index 7b7a6404ae..0d8f7536e8 100644 --- a/compiler/gen_llvm/src/llvm/build.rs +++ b/compiler/gen_llvm/src/llvm/build.rs @@ -3296,12 +3296,20 @@ fn expose_function_to_host<'a, 'ctx, 'env>( env: &Env<'a, 'ctx, 'env>, symbol: Symbol, roc_function: FunctionValue<'ctx>, - arguments: &[Layout<'a>], + arguments: &'a [Layout<'a>], return_layout: Layout<'a>, + layout_ids: &mut LayoutIds<'a>, ) { - // Assumption: there is only one specialization of a host-exposed function let ident_string = symbol.as_str(&env.interns); - let c_function_name: String = format!("roc__{}_1_exposed", ident_string); + + let proc_layout = ProcLayout { + arguments, + result: return_layout, + }; + + let c_function_name: String = layout_ids + .get_toplevel(symbol, &proc_layout) + .to_exposed_symbol_string(symbol, &env.interns); expose_function_to_host_help_c_abi( env, @@ -4077,6 +4085,7 @@ pub fn build_proc_headers<'a, 'ctx, 'env>( mod_solutions: &'a ModSolutions, procedures: MutMap<(Symbol, ProcLayout<'a>), roc_mono::ir::Proc<'a>>, scope: &mut Scope<'a, 'ctx>, + layout_ids: &mut LayoutIds<'a>, // alias_analysis_solutions: AliasAnalysisSolutions, ) -> Vec< 'a, @@ -4096,7 +4105,7 @@ pub fn build_proc_headers<'a, 'ctx, 'env>( let it = func_solutions.specs(); let mut function_values = Vec::with_capacity_in(it.size_hint().0, env.arena); for specialization in it { - let fn_val = build_proc_header(env, *specialization, symbol, &proc); + let fn_val = build_proc_header(env, *specialization, symbol, &proc, layout_ids); if proc.args.is_empty() { // this is a 0-argument thunk, i.e. a top-level constant definition @@ -4167,7 +4176,7 @@ fn build_procedures_help<'a, 'ctx, 'env>( // Add all the Proc headers to the module. // We have to do this in a separate pass first, // because their bodies may reference each other. - let headers = build_proc_headers(env, mod_solutions, procedures, &mut scope); + let headers = build_proc_headers(env, mod_solutions, procedures, &mut scope, &mut layout_ids); let (_, function_pass) = construct_optimization_passes(env.module, opt_level); @@ -4256,6 +4265,7 @@ fn build_proc_header<'a, 'ctx, 'env>( func_spec: FuncSpec, symbol: Symbol, proc: &roc_mono::ir::Proc<'a>, + layout_ids: &mut LayoutIds<'a>, ) -> FunctionValue<'ctx> { let args = proc.args; let arena = env.arena; @@ -4293,6 +4303,7 @@ fn build_proc_header<'a, 'ctx, 'env>( fn_val, arguments.into_bump_slice(), proc.ret_layout, + layout_ids, ); } diff --git a/compiler/gen_wasm/src/backend.rs b/compiler/gen_wasm/src/backend.rs index 0ac4d9e1eb..7fd8b8fde2 100644 --- a/compiler/gen_wasm/src/backend.rs +++ b/compiler/gen_wasm/src/backend.rs @@ -252,6 +252,8 @@ impl<'a> WasmBackend<'a> { .to_symbol_string(symbol, self.interns); let name = String::from_str_in(&name, self.env.arena).into_bump_str(); + // dbg!(name); + self.proc_lookup.push(ProcLookupData { name: symbol, layout, diff --git a/compiler/gen_wasm/src/lib.rs b/compiler/gen_wasm/src/lib.rs index 34d69ad8e8..aeaa49c049 100644 --- a/compiler/gen_wasm/src/lib.rs +++ b/compiler/gen_wasm/src/lib.rs @@ -85,7 +85,7 @@ pub fn build_app_module<'a>( host_module: WasmModule<'a>, procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, ) -> (WasmModule<'a>, BitVec, u32) { - let layout_ids = LayoutIds::default(); + let mut layout_ids = LayoutIds::default(); let mut procs = Vec::with_capacity_in(procedures.len(), env.arena); let mut proc_lookup = Vec::with_capacity_in(procedures.len() * 2, env.arena); let mut host_to_app_map = Vec::with_capacity_in(env.exposed_to_host.len(), env.arena); @@ -110,10 +110,13 @@ pub fn build_app_module<'a>( if env.exposed_to_host.contains(&sym) { maybe_main_fn_index = Some(fn_index); - // Assumption: there is only one specialization of a host-exposed function - let ident_string = sym.as_str(interns); - let c_function_name = bumpalo::format!(in env.arena, "roc__{}_1_exposed", ident_string); - host_to_app_map.push((c_function_name.into_bump_str(), fn_index)); + let exposed_name = layout_ids + .get_toplevel(sym, &proc_layout) + .to_exposed_symbol_string(sym, interns); + + let exposed_name_bump: &'a str = env.arena.alloc_str(&exposed_name); + + host_to_app_map.push((exposed_name_bump, fn_index)); } proc_lookup.push(ProcLookupData { diff --git a/compiler/load_internal/src/file.rs b/compiler/load_internal/src/file.rs index 13bdbe0d22..0577a2954d 100644 --- a/compiler/load_internal/src/file.rs +++ b/compiler/load_internal/src/file.rs @@ -46,7 +46,7 @@ use roc_solve::module::SolvedModule; use roc_solve::solve; use roc_target::TargetInfo; use roc_types::solved_types::Solved; -use roc_types::subs::{Subs, VarStore, Variable}; +use roc_types::subs::{ExposedTypesStorageSubs, Subs, VarStore, Variable}; use roc_types::types::{Alias, AliasKind}; use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::HashMap; @@ -510,6 +510,7 @@ pub struct LoadedModule { pub dep_idents: IdentIdsByModule, pub exposed_aliases: MutMap, pub exposed_values: Vec, + pub exposed_types_storage: ExposedTypesStorageSubs, pub sources: MutMap)>, pub timings: MutMap, pub documentation: MutMap, @@ -687,6 +688,7 @@ enum Msg<'a> { solved_subs: Solved, exposed_vars_by_symbol: Vec<(Symbol, Variable)>, exposed_aliases_by_symbol: MutMap, + exposed_types_storage: ExposedTypesStorageSubs, dep_idents: IdentIdsByModule, documentation: MutMap, abilities_store: AbilitiesStore, @@ -877,7 +879,8 @@ pub struct ModuleTiming { pub constrain: Duration, pub solve: Duration, pub find_specializations: Duration, - pub make_specializations: Duration, + // indexed by make specializations pass + pub make_specializations: Vec, // TODO pub monomorphize: Duration, /// Total duration will always be more than the sum of the other fields, due /// to things like state lookups in between phases, waiting on other threads, etc. @@ -895,7 +898,7 @@ impl ModuleTiming { constrain: Duration::default(), solve: Duration::default(), find_specializations: Duration::default(), - make_specializations: Duration::default(), + make_specializations: Vec::with_capacity(2), start_time, end_time: start_time, // just for now; we'll overwrite this at the end } @@ -921,8 +924,9 @@ impl ModuleTiming { } = self; let calculate = |t: Result| -> Option { - t.ok()? - .checked_sub(*make_specializations)? + make_specializations + .iter() + .fold(t.ok(), |t, pass_time| t?.checked_sub(*pass_time))? .checked_sub(*find_specializations)? .checked_sub(*solve)? .checked_sub(*constrain)? @@ -1404,6 +1408,7 @@ fn state_thread_step<'a>( solved_subs, exposed_vars_by_symbol, exposed_aliases_by_symbol, + exposed_types_storage, dep_idents, documentation, abilities_store, @@ -1421,6 +1426,7 @@ fn state_thread_step<'a>( solved_subs, exposed_aliases_by_symbol, exposed_vars_by_symbol, + exposed_types_storage, dep_idents, documentation, abilities_store, @@ -2209,6 +2215,7 @@ fn update<'a>( solved_subs, exposed_vars_by_symbol: solved_module.exposed_vars_by_symbol, exposed_aliases_by_symbol: solved_module.aliases, + exposed_types_storage: solved_module.exposed_types, dep_idents, documentation, abilities_store, @@ -2658,11 +2665,13 @@ fn finish_specialization( }) } +#[allow(clippy::too_many_arguments)] fn finish( state: State, solved: Solved, exposed_aliases_by_symbol: MutMap, exposed_vars_by_symbol: Vec<(Symbol, Variable)>, + exposed_types_storage: ExposedTypesStorageSubs, dep_idents: IdentIdsByModule, documentation: MutMap, abilities_store: AbilitiesStore, @@ -2697,6 +2706,7 @@ fn finish( exposed_aliases: exposed_aliases_by_symbol, exposed_values, exposed_to_host: exposed_vars_by_symbol.into_iter().collect(), + exposed_types_storage, sources, timings: state.timings, documentation, @@ -3738,7 +3748,7 @@ impl<'a> BuildTask<'a> { } } -fn add_imports( +pub fn add_imports( my_module: ModuleId, subs: &mut Subs, mut pending_abilities: PendingAbilitiesStore, @@ -4407,9 +4417,11 @@ fn make_specializations<'a>( mono_env.home.register_debug_idents(mono_env.ident_ids); let make_specializations_end = SystemTime::now(); - module_timing.make_specializations = make_specializations_end - .duration_since(make_specializations_start) - .unwrap(); + module_timing.make_specializations.push( + make_specializations_end + .duration_since(make_specializations_start) + .unwrap(), + ); Msg::MadeSpecializations { module_id: home, @@ -4495,6 +4507,7 @@ fn build_pending_specializations<'a>( ) } } + Expects(_) => todo!("toplevel expect codgen"), InvalidCycle(_) | DeclareRec(..) => { // do nothing? // this may mean the loc_symbols are not defined during codegen; is that a problem? @@ -5013,7 +5026,7 @@ fn to_missing_platform_report(module_id: ModuleId, other: PlatformPath) -> Strin /// Generic number types (Num, Int, Float, etc.) are treated as `DelayedAlias`es resolved during /// type solving. /// All that remains are Signed8, Signed16, etc. -fn default_aliases() -> roc_solve::solve::Aliases { +pub fn default_aliases() -> roc_solve::solve::Aliases { use roc_types::types::Type; let mut solve_aliases = roc_solve::solve::Aliases::default(); diff --git a/compiler/load_internal/tests/test_load.rs b/compiler/load_internal/tests/test_load.rs index af351b7490..870867209a 100644 --- a/compiler/load_internal/tests/test_load.rs +++ b/compiler/load_internal/tests/test_load.rs @@ -290,6 +290,10 @@ mod test_load { cycle @ InvalidCycle(_) => { panic!("Unexpected cyclic def in module declarations: {:?}", cycle); } + expects @ Expects(_) => { + // at least at the moment this does not happen + panic!("Unexpected expects in module declarations: {:?}", expects); + } }; } diff --git a/compiler/module/src/symbol.rs b/compiler/module/src/symbol.rs index 6891cff969..2ecb743155 100644 --- a/compiler/module/src/symbol.rs +++ b/compiler/module/src/symbol.rs @@ -1321,10 +1321,12 @@ define_builtins! { 17 ENCODE_BOOL: "bool" 18 ENCODE_STRING: "string" 19 ENCODE_LIST: "list" - 20 ENCODE_CUSTOM: "custom" - 21 ENCODE_APPEND_WITH: "appendWith" - 22 ENCODE_APPEND: "append" - 23 ENCODE_TO_BYTES: "toBytes" + 20 ENCODE_RECORD: "record" + 21 ENCODE_TAG: "tag" + 22 ENCODE_CUSTOM: "custom" + 23 ENCODE_APPEND_WITH: "appendWith" + 24 ENCODE_APPEND: "append" + 25 ENCODE_TO_BYTES: "toBytes" } 10 JSON: "Json" => { 0 JSON_JSON: "Json" diff --git a/compiler/mono/Cargo.toml b/compiler/mono/Cargo.toml index 04aa87b880..fb5a44434c 100644 --- a/compiler/mono/Cargo.toml +++ b/compiler/mono/Cargo.toml @@ -12,6 +12,7 @@ roc_region = { path = "../region" } roc_module = { path = "../module" } roc_types = { path = "../types" } roc_can = { path = "../can" } +roc_derive_key = { path = "../derive_key" } roc_late_solve = { path = "../late_solve" } roc_std = { path = "../../roc_std", default-features = false } roc_problem = { path = "../problem" } diff --git a/compiler/mono/src/derive/encoding.rs b/compiler/mono/src/derive/encoding.rs new file mode 100644 index 0000000000..ecda52e61d --- /dev/null +++ b/compiler/mono/src/derive/encoding.rs @@ -0,0 +1,599 @@ +//! Derivers for the `Encoding` ability. + +use std::iter::once; + +use bumpalo::Bump; + +use roc_can::abilities::AbilitiesStore; +use roc_can::expr::{AnnotatedMark, ClosureData, Expr, Field, Recursive, WhenBranch}; +use roc_can::pattern::Pattern; +use roc_collections::SendMap; +use roc_derive_key::encoding::FlatEncodableKey; +use roc_error_macros::internal_error; +use roc_late_solve::{instantiate_rigids, AbilitiesView}; +use roc_module::called_via::CalledVia; +use roc_module::ident::{Lowercase, TagName}; +use roc_module::symbol::{IdentIds, ModuleId, Symbol}; +use roc_region::all::{Loc, Region}; +use roc_types::subs::{ + Content, ExhaustiveMark, ExposedTypesStorageSubs, FlatType, GetSubsSlice, LambdaSet, + OptVariable, RecordFields, RedundantMark, Subs, SubsFmtContent, SubsSlice, UnionLambdas, + UnionTags, Variable, VariableSubsSlice, +}; +use roc_types::types::{AliasKind, RecordField}; + +use crate::derive::synth_var; + +macro_rules! bad_input { + ($subs:expr, $var:expr) => { + bad_input!($subs, $var, "Invalid content") + }; + ($subs:expr, $var:expr, $msg:expr) => { + internal_error!( + "{:?} for toEncoder deriver: {:?}", + $msg, + SubsFmtContent($subs.get_content_without_compacting($var), $subs) + ) + }; +} + +pub struct Env<'a> { + pub home: ModuleId, + pub arena: &'a Bump, + pub subs: &'a mut Subs, + pub ident_ids: &'a mut IdentIds, + pub exposed_encode_types: &'a mut ExposedTypesStorageSubs, +} + +impl Env<'_> { + fn unique_symbol(&mut self) -> Symbol { + let ident_id = self.ident_ids.gen_unique(); + + Symbol::new(self.home, ident_id) + } + + fn import_encode_symbol(&mut self, symbol: Symbol) -> Variable { + debug_assert_eq!(symbol.module_id(), ModuleId::ENCODE); + + let storage_var = self + .exposed_encode_types + .stored_vars_by_symbol + .get(&symbol) + .unwrap(); + + let imported = self + .exposed_encode_types + .storage_subs + .export_variable_to(self.subs, *storage_var); + + instantiate_rigids(self.subs, imported.variable); + + imported.variable + } + + fn unify(&mut self, left: Variable, right: Variable) { + // NOTE: I don't believe the abilities store is necessary for unification at this point! + roc_late_solve::unify( + self.home, + self.arena, + self.subs, + &AbilitiesView::Module(&AbilitiesStore::default()), + left, + right, + ) + .expect("unification failed!") + } +} + +// TODO: decide whether it will be better to pass the whole signature, or just the argument type. +// For now we are only using the argument type for convinience of testing. +#[allow(dead_code)] +fn verify_signature(env: &mut Env<'_>, signature: Variable) { + // Verify the signature is what we expect: input -> Encoder fmt | fmt has EncoderFormatting + // and get the input type + match env.subs.get_content_without_compacting(signature) { + Content::Structure(FlatType::Func(input, _, output)) => { + // Check the output is Encoder fmt | fmt has EncoderFormatting + match env.subs.get_content_without_compacting(*output) { + Content::Alias(Symbol::ENCODE_ENCODER, args, _, AliasKind::Opaque) => { + match env.subs.get_subs_slice(args.all_variables()) { + [one] => match env.subs.get_content_without_compacting(*one) { + Content::FlexAbleVar(_, Symbol::ENCODE_ENCODERFORMATTING) => {} + _ => bad_input!(env.subs, signature), + }, + _ => bad_input!(env.subs, signature), + } + } + _ => bad_input!(env.subs, signature), + } + + // Get the only parameter into toEncoder + match env.subs.get_subs_slice(*input) { + [one] => *one, + _ => bad_input!(env.subs, signature), + } + } + _ => bad_input!(env.subs, signature), + }; +} + +enum OwnedFlatEncodable { + List, + Set, + Dict, + Record(Vec), + TagUnion(Vec<(TagName, u16)>), +} + +pub struct OwnedFlatEncodableKey(OwnedFlatEncodable); + +impl From> for OwnedFlatEncodableKey { + fn from(key: FlatEncodableKey) -> Self { + use OwnedFlatEncodable::*; + let key = match key { + FlatEncodableKey::List() => List, + FlatEncodableKey::Set() => Set, + FlatEncodableKey::Dict() => Dict, + FlatEncodableKey::Record(fields) => Record(fields.into_iter().cloned().collect()), + FlatEncodableKey::TagUnion(tags) => TagUnion( + tags.into_iter() + .map(|(tag, arity)| (tag.clone(), arity)) + .collect(), + ), + }; + Self(key) + } +} + +pub fn derive_to_encoder(env: &mut Env<'_>, key: OwnedFlatEncodableKey) -> Expr { + match key.0 { + OwnedFlatEncodable::List => todo!(), + OwnedFlatEncodable::Set => todo!(), + OwnedFlatEncodable::Dict => todo!(), + OwnedFlatEncodable::Record(fields) => { + // Generalized record var so we can reuse this impl between many records: + // if fields = { a, b }, this is { a: t1, b: t2 } for fresh t1, t2. + let flex_fields = fields + .into_iter() + .map(|name| { + ( + name, + RecordField::Required(env.subs.fresh_unnamed_flex_var()), + ) + }) + .collect::>(); + let fields = RecordFields::insert_into_subs(env.subs, flex_fields); + let record_var = synth_var( + env.subs, + Content::Structure(FlatType::Record(fields, Variable::EMPTY_RECORD)), + ); + + to_encoder_record(env, record_var, fields) + } + OwnedFlatEncodable::TagUnion(tags) => { + // Generalized tag union var so we can reuse this impl between many unions: + // if tags = [ A arity=2, B arity=1 ], this is [ A t1 t2, B t3 ] for fresh t1, t2, t3 + let flex_tag_labels = tags + .into_iter() + .map(|(label, arity)| { + let variables_slice = + VariableSubsSlice::reserve_into_subs(env.subs, arity.into()); + for var_index in variables_slice { + env.subs[var_index] = env.subs.fresh_unnamed_flex_var(); + } + (label, variables_slice) + }) + .collect::>(); + let union_tags = UnionTags::insert_slices_into_subs(env.subs, flex_tag_labels); + let tag_union_var = synth_var( + env.subs, + Content::Structure(FlatType::TagUnion(union_tags, Variable::EMPTY_TAG_UNION)), + ); + + to_encoder_tag_union(env, tag_union_var, union_tags) + } + } +} + +fn to_encoder_record(env: &mut Env<'_>, record_var: Variable, fields: RecordFields) -> Expr { + // Suppose rcd = { a: t1, b: t2 }. Build + // + // \rcd -> Encode.record [ + // { key: "a", value: Encode.toEncoder rcd.a }, + // { key: "b", value: Encode.toEncoder rcd.b }, + // ] + + let rcd_sym = env.unique_symbol(); + let whole_rcd_var = env.subs.fresh_unnamed_flex_var(); // type of the { key, value } records in the list + + use Expr::*; + + let fields_list = fields + .iter_all() + .map(|(field_name_index, field_var_index, _)| { + let field_name = env.subs[field_name_index].clone(); + let field_var = env.subs[field_var_index]; + let field_var_slice = VariableSubsSlice::new(field_var_index.index, 1); + + // key: "a" + let key_field = Field { + var: Variable::STR, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(Str(field_name.as_str().into()))), + }; + + // rcd.a + let field_access = Access { + record_var, + ext_var: env.subs.fresh_unnamed_flex_var(), + field_var, + loc_expr: Box::new(Loc::at_zero(Var(rcd_sym))), + field: field_name, + }; + + // build `toEncoder rcd.a` type + // val -[uls]-> Encoder fmt | fmt has EncoderFormatting + let to_encoder_fn_var = env.import_encode_symbol(Symbol::ENCODE_TO_ENCODER); + + // (typeof rcd.a) -[clos]-> t1 + let to_encoder_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let encoder_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_to_encoder_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + field_var_slice, + to_encoder_clos_var, + encoder_var, + )), + ); + + // val -[uls]-> Encoder fmt | fmt has EncoderFormatting + // ~ (typeof rcd.a) -[clos]-> t1 + env.unify(to_encoder_fn_var, this_to_encoder_fn_var); + + // toEncoder : (typeof rcd.a) -[clos]-> Encoder fmt | fmt has EncoderFormatting + let to_encoder_fn = Box::new(( + to_encoder_fn_var, + Loc::at_zero(Var(Symbol::ENCODE_TO_ENCODER)), + to_encoder_clos_var, + encoder_var, + )); + + // toEncoder rcd.a + let to_encoder_call = Call( + to_encoder_fn, + vec![(field_var, Loc::at_zero(field_access))], + CalledVia::Space, + ); + + // value: toEncoder rcd.a + let value_field = Field { + var: encoder_var, + region: Region::zero(), + loc_expr: Box::new(Loc::at_zero(to_encoder_call)), + }; + + // { key: "a", value: toEncoder rcd.a } + let mut kv = SendMap::default(); + kv.insert("key".into(), key_field); + kv.insert("value".into(), value_field); + + let this_record_fields = RecordFields::insert_into_subs( + env.subs, + (once(("key".into(), RecordField::Required(Variable::STR)))) + .chain(once(("value".into(), RecordField::Required(encoder_var)))), + ); + let this_record_var = synth_var( + env.subs, + Content::Structure(FlatType::Record(this_record_fields, Variable::EMPTY_RECORD)), + ); + // NOTE: must be done to unify the lambda sets under `encoder_var` + env.unify(this_record_var, whole_rcd_var); + + Loc::at_zero(Record { + record_var: whole_rcd_var, + fields: kv, + }) + }) + .collect::>(); + + // typeof [ { key: .., value: .. }, { key: .., value: .. } ] + let fields_rcd_var_slice = VariableSubsSlice::insert_into_subs(env.subs, once(whole_rcd_var)); + let fields_list_var = synth_var( + env.subs, + Content::Structure(FlatType::Apply(Symbol::LIST_LIST, fields_rcd_var_slice)), + ); + + // [ { key: .., value: ..}, .. ] + let fields_list = List { + elem_var: whole_rcd_var, + loc_elems: fields_list, + }; + + // build `Encode.record [ { key: .., value: ..}, .. ]` type + // List { key : Str, value : Encoder fmt } -[uls]-> Encoder fmt | fmt has EncoderFormatting + let encode_record_fn_var = env.import_encode_symbol(Symbol::ENCODE_RECORD); + + // fields_list_var -[clos]-> t1 + let fields_list_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, once(fields_list_var)); + let encode_record_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let encoder_var = env.subs.fresh_unnamed_flex_var(); // t1 + let this_encode_record_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + fields_list_var_slice, + encode_record_clos_var, + encoder_var, + )), + ); + + // List { key : Str, value : Encoder fmt } -[uls]-> Encoder fmt | fmt has EncoderFormatting + // ~ fields_list_var -[clos]-> t1 + env.unify(encode_record_fn_var, this_encode_record_fn_var); + + // Encode.record : fields_list_var -[clos]-> Encoder fmt | fmt has EncoderFormatting + let encode_record_fn = Box::new(( + encode_record_fn_var, + Loc::at_zero(Var(Symbol::ENCODE_RECORD)), + encode_record_clos_var, + encoder_var, + )); + + // Encode.record [ { key: .., value: .. }, .. ] + let encode_record_call = Call( + encode_record_fn, + vec![(fields_list_var, Loc::at_zero(fields_list))], + CalledVia::Space, + ); + + let fn_name = env.unique_symbol(); + let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![]))); + // -[fn_name]-> + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + }), + ); + // typeof rcd -[fn_name]-> (typeof Encode.record [ .. ] = Encoder fmt) + let record_var_slice = SubsSlice::insert_into_subs(env.subs, once(record_var)); + let fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func(record_var_slice, fn_clos_var, encoder_var)), + ); + + // \rcd -[fn_name]-> Encode.record [ { key: .., value: .. }, .. ] + Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: encoder_var, + name: fn_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + record_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(rcd_sym)), + )], + loc_body: Box::new(Loc::at_zero(encode_record_call)), + }) +} + +fn to_encoder_tag_union(env: &mut Env<'_>, tag_union_var: Variable, tags: UnionTags) -> Expr { + // Suppose tag = [ A t1 t2, B t3 ]. Build + // + // \tag -> when tag is + // A v1 v2 -> Encode.tag "A" [ Encode.toEncoder v1, Encode.toEncoder v2 ] + // B v3 -> Encode.tag "B" [ Encode.toEncoder v3 ] + + let tag_sym = env.unique_symbol(); + let whole_tag_encoders_var = env.subs.fresh_unnamed_flex_var(); // type of the Encode.tag ... calls in the branch bodies + + use Expr::*; + + let branches = tags + .iter_all() + .map(|(tag_name_index, tag_vars_slice_index)| { + // A + let tag_name = &env.subs[tag_name_index].clone(); + let vars_slice = env.subs[tag_vars_slice_index]; + // t1 t2 + let payload_vars = env.subs.get_subs_slice(vars_slice).to_vec(); + // v1 v2 + let payload_syms: Vec<_> = std::iter::repeat_with(|| env.unique_symbol()) + .take(payload_vars.len()) + .collect(); + + // `A v1 v2` pattern + let pattern = Pattern::AppliedTag { + whole_var: tag_union_var, + tag_name: tag_name.clone(), + ext_var: Variable::EMPTY_TAG_UNION, + // (t1, v1) (t2, v2) + arguments: (payload_vars.iter()) + .zip(payload_syms.iter()) + .map(|(var, sym)| (*var, Loc::at_zero(Pattern::Identifier(*sym)))) + .collect(), + }; + + // whole type of the elements in [ Encode.toEncoder v1, Encode.toEncoder v2 ] + let whole_payload_encoders_var = env.subs.fresh_unnamed_flex_var(); + // [ Encode.toEncoder v1, Encode.toEncoder v2 ] + let payload_to_encoders = (payload_syms.iter()) + .zip(payload_vars.iter()) + .map(|(&sym, &sym_var)| { + // build `toEncoder v1` type + // expected: val -[uls]-> Encoder fmt | fmt has EncoderFormatting + let to_encoder_fn_var = env.import_encode_symbol(Symbol::ENCODE_TO_ENCODER); + + // wanted: t1 -[clos]-> t' + let var_slice_of_sym_var = + VariableSubsSlice::insert_into_subs(env.subs, [sym_var]); // [ t1 ] + let to_encoder_clos_var = env.subs.fresh_unnamed_flex_var(); // clos + let encoder_var = env.subs.fresh_unnamed_flex_var(); // t' + let this_to_encoder_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + var_slice_of_sym_var, + to_encoder_clos_var, + encoder_var, + )), + ); + + // val -[uls]-> Encoder fmt | fmt has EncoderFormatting + // ~ t1 -[clos]-> t' + env.unify(to_encoder_fn_var, this_to_encoder_fn_var); + + // toEncoder : t1 -[clos]-> Encoder fmt | fmt has EncoderFormatting + let to_encoder_fn = Box::new(( + this_to_encoder_fn_var, + Loc::at_zero(Var(Symbol::ENCODE_TO_ENCODER)), + to_encoder_clos_var, + encoder_var, + )); + + // toEncoder rcd.a + let to_encoder_call = Call( + to_encoder_fn, + vec![(sym_var, Loc::at_zero(Var(sym)))], + CalledVia::Space, + ); + + // NOTE: must be done to unify the lambda sets under `encoder_var` + env.unify(encoder_var, whole_payload_encoders_var); + + Loc::at_zero(to_encoder_call) + }) + .collect(); + + // typeof [ Encode.toEncoder v1, Encode.toEncoder v2 ] + let whole_encoders_var_slice = + VariableSubsSlice::insert_into_subs(env.subs, [whole_payload_encoders_var]); + let payload_encoders_list_var = synth_var( + env.subs, + Content::Structure(FlatType::Apply(Symbol::LIST_LIST, whole_encoders_var_slice)), + ); + + // [ Encode.toEncoder v1, Encode.toEncoder v2 ] + let payload_encoders_list = List { + elem_var: whole_payload_encoders_var, + loc_elems: payload_to_encoders, + }; + + // build `Encode.tag "A" [ ... ]` type + // expected: Str, List (Encoder fmt) -[uls]-> Encoder fmt | fmt has EncoderFormatting + let encode_tag_fn_var = env.import_encode_symbol(Symbol::ENCODE_TAG); + + // wanted: Str, List whole_encoders_var -[clos]-> t' + // wanted: Str, List whole_encoders_var + let this_encode_tag_args_var_slice = VariableSubsSlice::insert_into_subs( + env.subs, + [Variable::STR, payload_encoders_list_var], + ); + let this_encode_tag_clos_var = env.subs.fresh_unnamed_flex_var(); // -[clos]-> + let this_encoder_var = env.subs.fresh_unnamed_flex_var(); // t' + let this_encode_tag_fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + this_encode_tag_args_var_slice, + this_encode_tag_clos_var, + this_encoder_var, + )), + ); + + // Str, List (Encoder fmt) -[uls]-> Encoder fmt | fmt has EncoderFormatting + // ~ Str, List whole_encoders_var -[clos]-> t' + env.unify(encode_tag_fn_var, this_encode_tag_fn_var); + + // Encode.tag : Str, List whole_encoders_var -[clos]-> Encoder fmt | fmt has EncoderFormatting + let encode_tag_fn = Box::new(( + this_encode_tag_fn_var, + Loc::at_zero(Var(Symbol::ENCODE_TAG)), + this_encode_tag_clos_var, + this_encoder_var, + )); + + // Encode.tag "A" [ Encode.toEncoder v1, Encode.toEncoder v2 ] + let encode_tag_call = Call( + encode_tag_fn, + vec![ + // (Str, "A") + (Variable::STR, Loc::at_zero(Str(tag_name.0.as_str().into()))), + // (List (Encoder fmt), [ Encode.toEncoder v1, Encode.toEncoder v2 ]) + ( + payload_encoders_list_var, + Loc::at_zero(payload_encoders_list), + ), + ], + CalledVia::Space, + ); + + // NOTE: must be done to unify the lambda sets under `encoder_var` + // Encode.tag "A" [ Encode.toEncoder v1, Encode.toEncoder v2 ] ~ whole_encoders + env.unify(this_encoder_var, whole_tag_encoders_var); + + WhenBranch { + patterns: vec![Loc::at_zero(pattern)], + value: Loc::at_zero(encode_tag_call), + guard: None, + redundant: RedundantMark::known_non_redundant(), + } + }) + .collect::>(); + + // when tag is + // A v1 v2 -> Encode.tag "A" [ Encode.toEncoder v1, Encode.toEncoder v2 ] + // B v3 -> Encode.tag "B" [ Encode.toEncoder v3 ] + let when_branches = When { + loc_cond: Box::new(Loc::at_zero(Var(tag_sym))), + cond_var: tag_union_var, + expr_var: whole_tag_encoders_var, + region: Region::zero(), + branches, + branches_cond_var: tag_union_var, + exhaustive: ExhaustiveMark::known_exhaustive(), + }; + + let fn_name = env.unique_symbol(); + let fn_name_labels = UnionLambdas::insert_into_subs(env.subs, once((fn_name, vec![]))); + // -[fn_name]-> + let fn_clos_var = synth_var( + env.subs, + Content::LambdaSet(LambdaSet { + solved: fn_name_labels, + recursion_var: OptVariable::NONE, + unspecialized: SubsSlice::default(), + }), + ); + // tag_union_var -[fn_name]-> whole_tag_encoders_var + let tag_union_var_slice = SubsSlice::insert_into_subs(env.subs, once(tag_union_var)); + let fn_var = synth_var( + env.subs, + Content::Structure(FlatType::Func( + tag_union_var_slice, + fn_clos_var, + whole_tag_encoders_var, + )), + ); + + // \tag -> when tag is + // A v1 v2 -> Encode.tag "A" [ Encode.toEncoder v1, Encode.toEncoder v2 ] + // B v3 -> Encode.tag "B" [ Encode.toEncoder v3 ] + Closure(ClosureData { + function_type: fn_var, + closure_type: fn_clos_var, + return_type: whole_tag_encoders_var, + name: fn_name, + captured_symbols: vec![], + recursive: Recursive::NotRecursive, + arguments: vec![( + tag_union_var, + AnnotatedMark::known_exhaustive(), + Loc::at_zero(Pattern::Identifier(tag_sym)), + )], + loc_body: Box::new(Loc::at_zero(when_branches)), + }) +} diff --git a/compiler/mono/src/derive/mod.rs b/compiler/mono/src/derive/mod.rs new file mode 100644 index 0000000000..531470625a --- /dev/null +++ b/compiler/mono/src/derive/mod.rs @@ -0,0 +1,18 @@ +//! Auto-derivers of builtin ability methods. + +use roc_types::subs::{Content, Descriptor, Mark, OptVariable, Rank, Subs, Variable}; + +pub fn synth_var(subs: &mut Subs, content: Content) -> Variable { + let descriptor = Descriptor { + content, + // NOTE: this is incorrect, but that is irrelevant - derivers may only be called during + // monomorphization (or later), at which point we do not care about variable + // generalization. Hence ranks should not matter. + rank: Rank::toplevel(), + mark: Mark::NONE, + copy: OptVariable::NONE, + }; + subs.fresh(descriptor) +} + +pub mod encoding; diff --git a/compiler/mono/src/layout.rs b/compiler/mono/src/layout.rs index 4606ba32ab..df89ee1049 100644 --- a/compiler/mono/src/layout.rs +++ b/compiler/mono/src/layout.rs @@ -2913,13 +2913,20 @@ pub fn list_layout_from_elem<'a>( pub struct LayoutId(u32); impl LayoutId { - // Returns something like "foo#1" when given a symbol that interns to "foo" + // Returns something like "#UserApp_foo_1" when given a symbol that interns to "foo" // and a LayoutId of 1. pub fn to_symbol_string(self, symbol: Symbol, interns: &Interns) -> String { let ident_string = symbol.as_str(interns); let module_string = interns.module_ids.get_name(symbol.module_id()).unwrap(); format!("{}_{}_{}", module_string, ident_string, self.0) } + + // Returns something like "roc__foo_1_exposed" when given a symbol that interns to "foo" + // and a LayoutId of 1. + pub fn to_exposed_symbol_string(self, symbol: Symbol, interns: &Interns) -> String { + let ident_string = symbol.as_str(interns); + format!("roc__{}_{}_exposed", ident_string, self.0) + } } struct IdsByLayout<'a> { diff --git a/compiler/mono/src/lib.rs b/compiler/mono/src/lib.rs index aa8dfba4c8..433509cdc7 100644 --- a/compiler/mono/src/lib.rs +++ b/compiler/mono/src/lib.rs @@ -5,6 +5,7 @@ pub mod borrow; pub mod code_gen_help; mod copy; +pub mod derive; pub mod inc_dec; pub mod ir; pub mod layout; diff --git a/compiler/solve/src/ability.rs b/compiler/solve/src/ability.rs index c9abaf2747..b0aeb8ab27 100644 --- a/compiler/solve/src/ability.rs +++ b/compiler/solve/src/ability.rs @@ -562,6 +562,27 @@ impl ObligationCache<'_> { return Err(var); } } + Alias( + Symbol::NUM_U8 + | Symbol::NUM_U16 + | Symbol::NUM_U32 + | Symbol::NUM_U64 + | Symbol::NUM_U128 + | Symbol::NUM_I8 + | Symbol::NUM_I16 + | Symbol::NUM_I32 + | Symbol::NUM_I64 + | Symbol::NUM_I128 + | Symbol::NUM_NAT + | Symbol::NUM_F32 + | Symbol::NUM_F64 + | Symbol::NUM_DEC, + _, + _, + _, + ) => { + // yes + } Alias(_, arguments, real_type_var, _) => { push_var_slice!(arguments.all_variables()); stack.push(*real_type_var); diff --git a/compiler/solve/src/solve.rs b/compiler/solve/src/solve.rs index 58550bb4ed..6b539bdefc 100644 --- a/compiler/solve/src/solve.rs +++ b/compiler/solve/src/solve.rs @@ -792,7 +792,9 @@ fn solve( let result = offenders.len(); if result > 0 { - dbg!(&subs, &offenders, &let_con.def_types); + eprintln!("subs = {:?}", &subs); + eprintln!("offenders = {:?}", &offenders); + eprintln!("let_con.def_types = {:?}", &let_con.def_types); } result @@ -1873,7 +1875,7 @@ fn compact_lambda_set( // Default has default : {} -> a | a has Default // // {a, b} = default {} - // # ^^^^^^^ {} -[{a: t1, b: t2}:default:1] + // # ^^^^^^^ {} -[{a: t1, b: t2}:default:1]-> {a: t1, b: t2} new_unspecialized.push(uls); continue; } diff --git a/compiler/solve/tests/solve_expr.rs b/compiler/solve/tests/solve_expr.rs index e5630ebcc1..a0daa3749e 100644 --- a/compiler/solve/tests/solve_expr.rs +++ b/compiler/solve/tests/solve_expr.rs @@ -6817,4 +6817,17 @@ mod solve_expr { ], ) } + + #[test] + fn list_of_lambdas() { + infer_queries!( + indoc!( + r#" + [\{} -> {}, \{} -> {}] + #^^^^^^^^^^^^^^^^^^^^^^{-1} + "# + ), + &[r#"[\{} -> {}, \{} -> {}] : List ({}* -[[1(1), 2(2)]]-> {})"#], + ) + } } diff --git a/compiler/test_derive/Cargo.toml b/compiler/test_derive/Cargo.toml new file mode 100644 index 0000000000..1633ef165d --- /dev/null +++ b/compiler/test_derive/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "test_derive" +version = "0.1.0" +authors = ["The Roc Contributors"] +license = "UPL-1.0" +edition = "2021" + +[[test]] +name = "test_derive" +path = "src/tests.rs" + +[dev-dependencies] +roc_collections = { path = "../collections" } +roc_module = { path = "../module" } +roc_builtins = { path = "../builtins" } +roc_load_internal = { path = "../load_internal" } +roc_can = { path = "../can" } +roc_derive_key = { path = "../derive_key" } +roc_mono = { path = "../mono" } +roc_target = { path = "../roc_target" } +roc_types = { path = "../types" } +roc_reporting = { path = "../../reporting" } +roc_constrain = { path = "../constrain" } +roc_region = { path = "../region" } +roc_solve = { path = "../solve" } +roc_debug_flags = { path = "../debug_flags" } +bumpalo = { version = "3.8.0", features = ["collections"] } +lazy_static = "1.4.0" +indoc = "1.0.3" +ven_pretty = { path = "../../vendor/pretty" } +pretty_assertions = "1.0.0" diff --git a/compiler/test_derive/src/encoding.rs b/compiler/test_derive/src/encoding.rs new file mode 100644 index 0000000000..4466ee29d5 --- /dev/null +++ b/compiler/test_derive/src/encoding.rs @@ -0,0 +1,597 @@ +#![cfg(test)] +// Even with #[allow(non_snake_case)] on individual idents, rust-analyzer issues diagnostics. +// See https://github.com/rust-lang/rust-analyzer/issues/6541. +// For the `v!` macro we use uppercase variables when constructing tag unions. +#![allow(non_snake_case)] + +use std::path::PathBuf; + +use bumpalo::Bump; +use indoc::indoc; +use pretty_assertions::assert_eq; +use ven_pretty::DocAllocator; + +use crate::pretty_print::{pretty_print, Ctx}; +use roc_can::{ + abilities::{AbilitiesStore, ResolvedSpecializations}, + constraint::Constraints, + expected::Expected, + expr::Expr, + module::RigidVariables, +}; +use roc_collections::VecSet; +use roc_constrain::{ + expr::constrain_expr, + module::{ExposedByModule, ExposedForModule, ExposedModuleTypes}, +}; +use roc_debug_flags::dbg_do; +use roc_derive_key::{encoding::FlatEncodableKey, Derived}; +use roc_load_internal::file::{add_imports, default_aliases, LoadedModule, Threading}; +use roc_module::{ + ident::{ModuleName, TagName}, + symbol::{IdentIds, Interns, ModuleId, Symbol}, +}; +use roc_mono::derive::{ + encoding::{self, Env}, + synth_var, +}; +use roc_region::all::{LineInfo, Region}; +use roc_reporting::report::{type_problem, RocDocAllocator}; +use roc_types::{ + pretty_print::{name_and_print_var, DebugPrint}, + subs::{ + AliasVariables, Content, ExposedTypesStorageSubs, FlatType, RecordFields, Subs, SubsIndex, + SubsSlice, UnionTags, Variable, + }, + types::{AliasKind, RecordField, Type}, +}; + +fn encode_path() -> PathBuf { + let repo_root = std::env::var("ROC_WORKSPACE_DIR").expect("are you running with `cargo test`?"); + PathBuf::from(repo_root) + .join("compiler") + .join("builtins") + .join("roc") + .join("Encode.roc") +} + +fn check_derived_typechecks( + derived: Expr, + test_module: ModuleId, + mut test_subs: Subs, + interns: &Interns, + exposed_encode_types: ExposedTypesStorageSubs, + encode_abilities_store: AbilitiesStore, + expected_type: &str, +) { + // constrain the derived + let mut constraints = Constraints::new(); + let mut env = roc_constrain::expr::Env { + rigids: Default::default(), + resolutions_to_make: Default::default(), + home: test_module, + }; + let real_type = test_subs.fresh_unnamed_flex_var(); + let constr = constrain_expr( + &mut constraints, + &mut env, + Region::zero(), + &derived, + Expected::NoExpectation(Type::Variable(real_type)), + ); + + // the derived depends on stuff from Encode, so + // - we need to add those dependencies as imported on the constraint + // - we need to add Encode ability info to a local abilities store + let encode_values_to_import = exposed_encode_types + .stored_vars_by_symbol + .keys() + .copied() + .collect::>(); + let pending_abilities = encode_abilities_store.closure_from_imported(&encode_values_to_import); + let mut exposed_by_module = ExposedByModule::default(); + exposed_by_module.insert( + ModuleId::ENCODE, + ExposedModuleTypes { + exposed_types_storage_subs: exposed_encode_types, + resolved_specializations: ResolvedSpecializations::default(), + }, + ); + let exposed_for_module = + ExposedForModule::new(encode_values_to_import.iter(), exposed_by_module); + let mut def_types = Default::default(); + let mut rigid_vars = Default::default(); + let (import_variables, abilities_store) = add_imports( + test_module, + &mut test_subs, + pending_abilities, + exposed_for_module, + &mut def_types, + &mut rigid_vars, + ); + let constr = + constraints.let_import_constraint(rigid_vars, def_types, constr, &import_variables); + + // run the solver, print and fail if we have errors + dbg_do!( + roc_debug_flags::ROC_PRINT_UNIFICATIONS_DERIVED, + std::env::set_var(roc_debug_flags::ROC_PRINT_UNIFICATIONS_DERIVED, "1") + ); + let (mut solved_subs, _, problems, _) = roc_solve::module::run_solve( + &constraints, + constr, + RigidVariables::default(), + test_subs, + default_aliases(), + abilities_store, + Default::default(), + ); + let subs = solved_subs.inner_mut(); + + if !problems.is_empty() { + let filename = PathBuf::from("Test.roc"); + let lines = LineInfo::new(" "); + let src_lines = vec![" "]; + let mut reports = Vec::new(); + let alloc = RocDocAllocator::new(&src_lines, test_module, interns); + + for problem in problems.into_iter() { + if let Some(report) = type_problem(&alloc, &lines, filename.clone(), problem.clone()) { + reports.push(report); + } + } + + let has_reports = !reports.is_empty(); + + let doc = alloc + .stack(reports.into_iter().map(|v| v.pretty(&alloc))) + .append(if has_reports { + alloc.line() + } else { + alloc.nil() + }); + + let mut buf = String::new(); + doc.1 + .render_raw(80, &mut roc_reporting::report::CiWrite::new(&mut buf)) + .unwrap(); + + panic!("Derived does not typecheck:\n{}", buf); + } + + let pretty_type = + name_and_print_var(real_type, subs, test_module, interns, DebugPrint::NOTHING); + + assert_eq!(expected_type, pretty_type); +} + +fn derive_test(synth_input: S, expected_type: &str, expected_source: &str) +where + S: FnOnce(&mut Subs) -> Variable, +{ + let arena = Bump::new(); + let source = roc_builtins::roc::module_source(ModuleId::ENCODE); + let target_info = roc_target::TargetInfo::default_x86_64(); + + let LoadedModule { + mut interns, + exposed_types_storage: mut exposed_encode_types, + abilities_store, + .. + } = roc_load_internal::file::load_and_typecheck_str( + &arena, + encode_path().file_name().unwrap().into(), + source, + encode_path().parent().unwrap(), + Default::default(), + target_info, + roc_reporting::report::RenderTarget::ColorTerminal, + Threading::AllAvailable, + ) + .unwrap(); + + let test_module = interns.module_id(&ModuleName::from("Test")); + let mut test_subs = Subs::new(); + interns + .all_ident_ids + .insert(test_module, IdentIds::default()); + + let signature_var = synth_input(&mut test_subs); + let key = get_key(&test_subs, signature_var).into(); + + let mut env = Env { + home: test_module, + arena: &arena, + subs: &mut test_subs, + ident_ids: interns.all_ident_ids.get_mut(&test_module).unwrap(), + exposed_encode_types: &mut exposed_encode_types, + }; + + let derived = encoding::derive_to_encoder(&mut env, key); + test_module.register_debug_idents(interns.all_ident_ids.get(&test_module).unwrap()); + + let ctx = Ctx { interns: &interns }; + let derived_program = pretty_print(&ctx, &derived); + assert_eq!(expected_source, derived_program); + + check_derived_typechecks( + derived, + test_module, + test_subs, + &interns, + exposed_encode_types, + abilities_store, + expected_type, + ); +} + +fn get_key(subs: &Subs, var: Variable) -> FlatEncodableKey { + match Derived::encoding(subs, var) { + Derived::Immediate(_) => unreachable!(), + Derived::Key(key) => key.repr, + } +} + +fn check_key(eq: bool, synth1: S1, synth2: S2) +where + S1: FnOnce(&mut Subs) -> Variable, + S2: FnOnce(&mut Subs) -> Variable, +{ + let mut subs = Subs::new(); + let var1 = synth1(&mut subs); + let var2 = synth2(&mut subs); + + let key1 = Derived::encoding(&subs, var1); + let key2 = Derived::encoding(&subs, var2); + + if eq { + assert_eq!(key1, key2); + } else { + assert_ne!(key1, key2); + } +} + +fn check_immediate(synth: S, immediate: Symbol) +where + S: FnOnce(&mut Subs) -> Variable, +{ + let mut subs = Subs::new(); + let var = synth(&mut subs); + + let key = Derived::encoding(&subs, var); + + assert_eq!(key, Derived::Immediate(immediate)); +} + +// Writing out the types into content is terrible, so let's use a DSL at least for testing +macro_rules! v { + ({ $($field:ident: $make_v:expr,)* $(?$opt_field:ident : $make_opt_v:expr,)* }) => { + |subs: &mut Subs| { + $(let $field = $make_v(subs);)* + $(let $opt_field = $make_opt_v(subs);)* + let fields = vec![ + $( (stringify!($field).into(), RecordField::Required($field)) ,)* + $( (stringify!($opt_field).into(), RecordField::Required($opt_field)) ,)* + ]; + let fields = RecordFields::insert_into_subs(subs, fields); + synth_var(subs, Content::Structure(FlatType::Record(fields, Variable::EMPTY_RECORD))) + } + }; + ([ $($tag:ident $($payload:expr)*),* ]) => { + |subs: &mut Subs| { + $( + let $tag = vec![ $( $payload(subs), )* ]; + )* + let tags = UnionTags::insert_into_subs::<_, Vec>(subs, vec![ $( (TagName(stringify!($tag).into()), $tag) ,)* ]); + synth_var(subs, Content::Structure(FlatType::TagUnion(tags, Variable::EMPTY_TAG_UNION))) + } + }; + ([ $($tag:ident $($payload:expr)*),* ] as $rec_var:ident) => { + |subs: &mut Subs| { + let $rec_var = subs.fresh_unnamed_flex_var(); + let rec_name_index = + SubsIndex::push_new(&mut subs.field_names, stringify!($rec).into()); + + $( + let $tag = vec![ $( $payload(subs), )* ]; + )* + let tags = UnionTags::insert_into_subs::<_, Vec>(subs, vec![ $( (TagName(stringify!($tag).into()), $tag) ,)* ]); + let tag_union_var = synth_var(subs, Content::Structure(FlatType::RecursiveTagUnion($rec_var, tags, Variable::EMPTY_TAG_UNION))); + + subs.set_content( + $rec_var, + Content::RecursionVar { + structure: tag_union_var, + opt_name: Some(rec_name_index), + }, + ); + tag_union_var + } + }; + (Symbol::$sym:ident $($arg:expr)*) => { + |subs: &mut Subs| { + let $sym = vec![ $( $arg(subs) ,)* ]; + let var_slice = SubsSlice::insert_into_subs(subs, $sym); + synth_var(subs, Content::Structure(FlatType::Apply(Symbol::$sym, var_slice))) + } + }; + (Symbol::$alias:ident $($arg:expr)* => $real_var:expr) => { + |subs: &mut Subs| { + let args = vec![$( $arg(subs) )*]; + let alias_variables = AliasVariables::insert_into_subs::, Vec<_>>(subs, args, vec![]); + let real_var = $real_var(subs); + synth_var(subs, Content::Alias(Symbol::$alias, alias_variables, real_var, AliasKind::Structural)) + } + }; + (@Symbol::$alias:ident $($arg:expr)* => $real_var:expr) => { + |subs: &mut Subs| { + let args = vec![$( $arg(subs) )*]; + let alias_variables = AliasVariables::insert_into_subs::, Vec<_>>(subs, args, vec![]); + let real_var = $real_var(subs); + synth_var(subs, Content::Alias(Symbol::$alias, alias_variables, real_var, AliasKind::Opaque)) + } + }; + (*$rec_var:ident) => { + |_: &mut Subs| { $rec_var } + }; + ($var:ident) => { + |_: &mut Subs| { Variable::$var } + }; +} + +macro_rules! test_hash_eq { + ($($name:ident: $synth1:expr, $synth2:expr)*) => {$( + #[test] + fn $name() { + check_key(true, $synth1, $synth2) + } + )*}; +} + +macro_rules! test_hash_neq { + ($($name:ident: $synth1:expr, $synth2:expr)*) => {$( + #[test] + fn $name() { + check_key(false, $synth1, $synth2) + } + )*}; +} + +// {{{ hash tests + +test_hash_eq! { + same_record: + v!({ a: v!(U8), }), v!({ a: v!(U8), }) + same_record_fields_diff_types: + v!({ a: v!(U8), }), v!({ a: v!(STR), }) + same_record_fields_any_order: + v!({ a: v!(U8), b: v!(U8), c: v!(U8), }), + v!({ c: v!(U8), a: v!(U8), b: v!(U8), }) + explicit_empty_record_and_implicit_empty_record: + v!(EMPTY_RECORD), v!({}) + same_record_fields_required_vs_optional: + v!({ a: v!(U8), b: v!(U8), }), + v!({ ?a: v!(U8), ?b: v!(U8), }) + + same_tag_union: + v!([ A v!(U8) v!(STR), B v!(STR) ]), v!([ A v!(U8) v!(STR), B v!(STR) ]) + same_tag_union_tags_diff_types: + v!([ A v!(U8) v!(U8), B v!(U8) ]), v!([ A v!(STR) v!(STR), B v!(STR) ]) + same_tag_union_tags_any_order: + v!([ A v!(U8) v!(U8), B v!(U8), C ]), v!([ C, B v!(STR), A v!(STR) v!(STR) ]) + explicit_empty_tag_union_and_implicit_empty_tag_union: + v!(EMPTY_TAG_UNION), v!([]) + + same_recursive_tag_union: + v!([ Nil, Cons v!(*lst)] as lst), v!([ Nil, Cons v!(*lst)] as lst) + same_tag_union_and_recursive_tag_union_fields: + v!([ Nil, Cons v!(STR)]), v!([ Nil, Cons v!(*lst)] as lst) + + list_list_diff_types: + v!(Symbol::LIST_LIST v!(STR)), v!(Symbol::LIST_LIST v!(U8)) + set_set_diff_types: + v!(Symbol::SET_SET v!(STR)), v!(Symbol::SET_SET v!(U8)) + dict_dict_diff_types: + v!(Symbol::DICT_DICT v!(STR) v!(STR)), v!(Symbol::DICT_DICT v!(U8) v!(U8)) + str_str: + v!(Symbol::STR_STR), v!(Symbol::STR_STR) + + alias_eq_real_type: + v!(Symbol::BOOL_BOOL => v!([ True, False ])), v!([False, True]) + diff_alias_same_real_type: + v!(Symbol::BOOL_BOOL => v!([ True, False ])), v!(Symbol::UNDERSCORE => v!([False, True])) + + opaque_eq_real_type: + v!(@Symbol::BOOL_BOOL => v!([ True, False ])), v!([False, True]) + diff_opaque_same_real_type: + v!(@Symbol::BOOL_BOOL => v!([ True, False ])), v!(@Symbol::UNDERSCORE => v!([False, True])) + + opaque_real_type_eq_alias_real_type: + v!(@Symbol::BOOL_BOOL => v!([ True, False ])), v!(Symbol::UNDERSCORE => v!([False, True])) +} + +test_hash_neq! { + different_record_fields: + v!({ a: v!(U8), }), v!({ b: v!(U8), }) + record_empty_vs_nonempty: + v!(EMPTY_RECORD), v!({ a: v!(U8), }) + + different_tag_union_tags: + v!([ A v!(U8) ]), v!([ B v!(U8) ]) + tag_union_empty_vs_nonempty: + v!(EMPTY_TAG_UNION), v!([ B v!(U8) ]) + different_recursive_tag_union_tags: + v!([ Nil, Cons v!(*lst) ] as lst), v!([ Nil, Next v!(*lst) ] as lst) + + same_alias_diff_real_type: + v!(Symbol::BOOL_BOOL => v!([ True, False ])), v!(Symbol::BOOL_BOOL => v!([ False, True, Maybe ])) + diff_alias_diff_real_type: + v!(Symbol::BOOL_BOOL => v!([ True, False ])), v!(Symbol::UNDERSCORE => v!([ False, True, Maybe ])) + + same_opaque_diff_real_type: + v!(@Symbol::BOOL_BOOL => v!([ True, False ])), v!(@Symbol::BOOL_BOOL => v!([ False, True, Maybe ])) + diff_opaque_diff_real_type: + v!(@Symbol::BOOL_BOOL => v!([ True, False ])), v!(@Symbol::UNDERSCORE => v!([ False, True, Maybe ])) +} + +// }}} hash tests + +// {{{ deriver tests + +#[test] +fn immediates() { + check_immediate(v!(U8), Symbol::ENCODE_U8); + check_immediate(v!(U16), Symbol::ENCODE_U16); + check_immediate(v!(U32), Symbol::ENCODE_U32); + check_immediate(v!(U64), Symbol::ENCODE_U64); + check_immediate(v!(U128), Symbol::ENCODE_U128); + check_immediate(v!(I8), Symbol::ENCODE_I8); + check_immediate(v!(I16), Symbol::ENCODE_I16); + check_immediate(v!(I32), Symbol::ENCODE_I32); + check_immediate(v!(I64), Symbol::ENCODE_I64); + check_immediate(v!(I128), Symbol::ENCODE_I128); + check_immediate(v!(DEC), Symbol::ENCODE_DEC); + check_immediate(v!(F32), Symbol::ENCODE_F32); + check_immediate(v!(F64), Symbol::ENCODE_F64); + check_immediate(v!(STR), Symbol::ENCODE_STRING); +} + +#[test] +fn empty_record() { + derive_test( + v!(EMPTY_RECORD), + "{} -> Encoder fmt | fmt has EncoderFormatting", + indoc!( + r#" + \Test.0 -> Encode.record [] + "# + ), + ) +} + +#[test] +fn zero_field_record() { + derive_test( + v!({}), + "{} -> Encoder fmt | fmt has EncoderFormatting", + indoc!( + r#" + \Test.0 -> Encode.record [] + "# + ), + ) +} + +#[test] +fn one_field_record() { + derive_test( + v!({ a: v!(U8), }), + "{ a : val } -> Encoder fmt | fmt has EncoderFormatting, val has Encoding", + indoc!( + r#" + \Test.0 -> Encode.record [{ value: Encode.toEncoder Test.0.a, key: "a", }] + "# + ), + ) +} + +#[test] +fn two_field_record() { + derive_test( + v!({ a: v!(U8), b: v!(STR), }), + "{ a : val, b : a } -> Encoder fmt | a has Encoding, fmt has EncoderFormatting, val has Encoding", + indoc!( + r#" + \Test.0 -> + Encode.record [ + { value: Encode.toEncoder Test.0.a, key: "a", }, + { value: Encode.toEncoder Test.0.b, key: "b", }, + ] + "# + ), + ) +} + +#[test] +#[ignore = "NOTE: this would never actually happen, because [] is uninhabited, and hence toEncoder can never be called with a value of []! +Rightfully it induces broken assertions in other parts of the compiler, so we ignore it."] +fn empty_tag_union() { + derive_test( + v!(EMPTY_TAG_UNION), + "[] -> Encoder fmt | fmt has EncoderFormatting", + indoc!( + r#" + \Test.0 -> when Test.0 is + "# + ), + ) +} + +#[test] +fn tag_one_label_zero_args() { + derive_test( + v!([A]), + "[A] -> Encoder fmt | fmt has EncoderFormatting", + indoc!( + r#" + \Test.0 -> when Test.0 is A -> Encode.tag "A" [] + "# + ), + ) +} + +#[test] +fn tag_one_label_two_args() { + derive_test( + v!([A v!(U8) v!(STR)]), + "[A val a] -> Encoder fmt | a has Encoding, fmt has EncoderFormatting, val has Encoding", + indoc!( + r#" + \Test.0 -> + when Test.0 is + A Test.1 Test.2 -> + Encode.tag "A" [Encode.toEncoder Test.1, Encode.toEncoder Test.2] + "# + ), + ) +} + +#[test] +fn tag_two_labels() { + derive_test( + v!([A v!(U8) v!(STR) v!(U16), B v!(STR)]), + "[A val a b, B c] -> Encoder fmt | a has Encoding, b has Encoding, c has Encoding, fmt has EncoderFormatting, val has Encoding", + indoc!( + r#" + \Test.0 -> + when Test.0 is + A Test.1 Test.2 Test.3 -> + Encode.tag "A" [ + Encode.toEncoder Test.1, + Encode.toEncoder Test.2, + Encode.toEncoder Test.3, + ] + B Test.4 -> Encode.tag "B" [Encode.toEncoder Test.4] + "# + ), + ) +} + +#[test] +fn recursive_tag_union() { + derive_test( + v!([Nil, Cons v!(U8) v!(*lst) ] as lst), + "[Cons val a, Nil] -> Encoder fmt | a has Encoding, fmt has EncoderFormatting, val has Encoding", + indoc!( + r#" + \Test.0 -> + when Test.0 is + Cons Test.1 Test.2 -> + Encode.tag "Cons" [Encode.toEncoder Test.1, Encode.toEncoder Test.2] + Nil -> Encode.tag "Nil" [] + "# + ), + ) +} + +// }}} deriver tests diff --git a/compiler/test_derive/src/pretty_print.rs b/compiler/test_derive/src/pretty_print.rs new file mode 100644 index 0000000000..c0065d7384 --- /dev/null +++ b/compiler/test_derive/src/pretty_print.rs @@ -0,0 +1,299 @@ +//! Pretty-prints the canonical AST back to check our work - do things look reasonable? + +use roc_can::expr::Expr::{self, *}; +use roc_can::expr::{ClosureData, WhenBranch}; +use roc_can::pattern::{Pattern, RecordDestruct}; + +use roc_module::symbol::Interns; +use ven_pretty::{Arena, DocAllocator, DocBuilder}; + +pub struct Ctx<'a> { + pub interns: &'a Interns, +} + +pub fn pretty_print(c: &Ctx, e: &Expr) -> String { + let f = Arena::new(); + expr(c, EPrec::Free, &f, e) + .append(f.hardline()) + .1 + .pretty(80) + .to_string() +} + +macro_rules! maybe_paren { + ($paren_if_above:expr, $my_prec:expr, $doc:expr) => { + if $my_prec > $paren_if_above { + $doc.parens().group() + } else { + $doc + } + }; +} + +#[derive(PartialEq, PartialOrd)] +enum EPrec { + Free, + CallArg, +} + +fn expr<'a>(c: &Ctx, p: EPrec, f: &'a Arena<'a>, e: &'a Expr) -> DocBuilder<'a, Arena<'a>> { + use EPrec::*; + match e { + Num(_, n, _, _) | Int(_, _, n, _, _) | Float(_, _, n, _, _) => f.text(&**n), + Str(s) => f.text(format!(r#""{}""#, s)), + SingleQuote(c) => f.text(format!("'{}'", c)), + List { + elem_var: _, + loc_elems, + } => f + .reflow("[") + .append( + f.concat(loc_elems.iter().map(|le| { + f.hardline() + .append(expr(c, Free, f, &le.value)) + .append(f.text(",")) + })) + .group() + .nest(2), + ) + .append(f.line_()) + .append("]") + .group() + .flat_alt( + f.reflow("[") + .append(f.intersperse( + loc_elems.iter().map(|le| expr(c, Free, f, &le.value)), + f.reflow(", "), + )) + .append(f.line_()) + .append("]") + .group(), + ), + Var(sym) | AbilityMember(sym, _, _) => f.text(format!( + "{}.{}", + sym.module_string(c.interns), + sym.as_str(c.interns), + )), + When { + loc_cond, branches, .. + } => f + .reflow("when ") + .append(expr(c, Free, f, &loc_cond.value)) + .append(f.text(" is")) + .append( + f.concat(branches.iter().map(|b| f.line().append(branch(c, f, b)))) + .group(), + ) + .nest(2) + .group(), + If { + branches, + final_else, + .. + } => f + .concat(branches.iter().enumerate().map(|(i, (cond, body))| { + let head = if i == 0 { "if " } else { "else if " }; + (f.reflow(head) + .append(expr(c, Free, f, &cond.value)) + .group() + .nest(2)) + .append(f.line()) + .append( + f.reflow("then") + .append(f.softline().append(expr(c, Free, f, &body.value))) + .group() + .nest(2), + ) + .append(f.line()) + })) + .append( + f.reflow("else ") + .append(expr(c, Free, f, &final_else.value)) + .group() + .nest(2), + ) + .group(), + LetRec(_, _, _) => todo!(), + LetNonRec(_, _) => todo!(), + Call(fun, args, _) => { + let (_, fun, _, _) = &**fun; + maybe_paren!( + Free, + p, + expr(c, CallArg, f, &fun.value) + .append(f.softline()) + .append(f.intersperse( + args.iter().map(|le| expr(c, CallArg, f, &le.1.value)), + f.softline() + )) + .group() + ) + } + RunLowLevel { .. } => todo!(), + ForeignCall { .. } => todo!(), + Closure(ClosureData { + arguments, + loc_body, + .. + }) => f + .text("\\") + .append( + f.intersperse( + arguments + .iter() + .map(|(_, _, arg)| pattern(c, PPrec::Free, f, &arg.value)), + f.text(", "), + ), + ) + .append(f.text(" ->")) + .append(f.line()) + .append(expr(c, Free, f, &loc_body.value)) + .nest(2) + .group(), + Record { fields, .. } => f + .reflow("{") + .append( + f.concat(fields.iter().map(|(name, field)| { + let field = f + .text(name.as_str()) + .append(f.reflow(": ")) + .append(expr(c, Free, f, &field.loc_expr.value)) + .nest(2) + .group(); + f.line().append(field).append(",") + })) + .nest(2) + .group(), + ) + .append(f.line()) + .append(f.text("}")) + .group(), + EmptyRecord => f.text("{}"), + Access { + loc_expr, field, .. + } => expr(c, CallArg, f, &loc_expr.value) + .append(f.text(format!(".{}", field.as_str()))) + .group(), + Accessor(_) => todo!(), + Update { .. } => todo!(), + Tag { .. } => todo!(), + ZeroArgumentTag { .. } => todo!(), + OpaqueRef { .. } => todo!(), + Expect { .. } => todo!(), + TypedHole(_) => todo!(), + RuntimeError(_) => todo!(), + } +} + +fn branch<'a>(c: &Ctx, f: &'a Arena<'a>, b: &'a WhenBranch) -> DocBuilder<'a, Arena<'a>> { + let WhenBranch { + patterns, + value, + guard, + redundant: _, + } = b; + + f.intersperse( + patterns + .iter() + .map(|lp| pattern(c, PPrec::Free, f, &lp.value)), + f.text(" | "), + ) + .append(match guard { + Some(e) => f.text("if ").append(expr(c, EPrec::Free, f, &e.value)), + None => f.nil(), + }) + .append(f.text(" ->")) + .append(f.line()) + .append(expr(c, EPrec::Free, f, &value.value)) + .nest(2) + .group() +} + +#[derive(PartialEq, PartialOrd)] +enum PPrec { + Free, + AppArg, +} + +fn pattern<'a>( + c: &Ctx, + prec: PPrec, + f: &'a Arena<'a>, + p: &'a Pattern, +) -> DocBuilder<'a, Arena<'a>> { + use PPrec::*; + use Pattern::*; + match p { + Identifier(sym) + | AbilityMemberSpecialization { + specializes: sym, .. + } => f.text(format!( + "{}.{}", + sym.module_string(c.interns), + sym.as_str(c.interns), + )), + AppliedTag { + tag_name, + arguments, + .. + } => maybe_paren!( + Free, + prec, + f.text(tag_name.0.as_str()) + .append(if arguments.is_empty() { + f.nil() + } else { + f.space() + }) + .append( + f.intersperse( + arguments + .iter() + .map(|(_, lp)| pattern(c, AppArg, f, &lp.value)), + f.space(), + ) + ) + .group() + ), + UnwrappedOpaque { + opaque, argument, .. + } => f + .text(format!("@{} ", opaque.module_string(c.interns))) + .append(pattern(c, Free, f, &argument.1.value)) + .group(), + RecordDestructure { destructs, .. } => f + .text("{") + .append( + f.intersperse( + destructs.iter().map(|l| &l.value).map( + |RecordDestruct { label, typ, .. }| match typ { + roc_can::pattern::DestructType::Required => f.text(label.as_str()), + roc_can::pattern::DestructType::Optional(_, e) => f + .text(label.as_str()) + .append(f.text(" ? ")) + .append(expr(c, EPrec::Free, f, &e.value)), + roc_can::pattern::DestructType::Guard(_, p) => f + .text(label.as_str()) + .append(f.text(": ")) + .append(pattern(c, Free, f, &p.value)), + }, + ), + f.text(", "), + ), + ) + .append(f.text("}")) + .group(), + NumLiteral(_, n, _, _) | IntLiteral(_, _, n, _, _) | FloatLiteral(_, _, n, _, _) => { + f.text(&**n) + } + StrLiteral(s) => f.text(format!(r#""{}""#, s)), + SingleQuote(c) => f.text(format!("'{}'", c)), + Underscore => f.text("_"), + + Shadowed(_, _, _) => todo!(), + OpaqueNotInScope(_) => todo!(), + UnsupportedPattern(_) => todo!(), + MalformedPattern(_, _) => todo!(), + } +} diff --git a/compiler/test_derive/src/tests.rs b/compiler/test_derive/src/tests.rs new file mode 100644 index 0000000000..3fdefff22f --- /dev/null +++ b/compiler/test_derive/src/tests.rs @@ -0,0 +1,5 @@ +#![cfg(test)] + +mod encoding; + +mod pretty_print; diff --git a/compiler/test_gen/src/helpers/dev.rs b/compiler/test_gen/src/helpers/dev.rs index 59f65b93e4..963c117a71 100644 --- a/compiler/test_gen/src/helpers/dev.rs +++ b/compiler/test_gen/src/helpers/dev.rs @@ -98,11 +98,9 @@ pub fn helper( let main_fn_layout = loaded.entry_point.layout; let mut layout_ids = roc_mono::layout::LayoutIds::default(); - let main_fn_name_base = layout_ids + let main_fn_name = layout_ids .get_toplevel(main_fn_symbol, &main_fn_layout) - .to_symbol_string(main_fn_symbol, &interns); - - let main_fn_name = format!("roc_{}_exposed", main_fn_name_base); + .to_exposed_symbol_string(main_fn_symbol, &interns); let mut lines = Vec::new(); // errors whose reporting we delay (so we can see that code gen generates runtime errors) diff --git a/compiler/types/src/subs.rs b/compiler/types/src/subs.rs index cafc5f02e3..925d1cd667 100644 --- a/compiler/types/src/subs.rs +++ b/compiler/types/src/subs.rs @@ -1078,14 +1078,6 @@ impl ExhaustiveMark { Self(Variable::EMPTY_TAG_UNION) } - pub fn variable_for_introduction(&self) -> Variable { - debug_assert!( - self.0 != Variable::EMPTY_TAG_UNION, - "Attempting to introduce known mark" - ); - self.0 - } - pub fn set_non_exhaustive(&self, subs: &mut Subs) { subs.set_content(self.0, Content::Error); } @@ -1110,14 +1102,6 @@ impl RedundantMark { Self(Variable::EMPTY_TAG_UNION) } - pub fn variable_for_introduction(&self) -> Variable { - debug_assert!( - self.0 != Variable::EMPTY_TAG_UNION, - "Attempting to introduce known mark" - ); - self.0 - } - pub fn set_redundant(&self, subs: &mut Subs) { subs.set_content(self.0, Content::Error); } @@ -1282,6 +1266,10 @@ define_const_var! { :pub F64, :pub DEC, + + // The following are abound in derived abilities, so we cache them. + :pub STR, + :pub LIST_U8, } impl Variable { @@ -1323,6 +1311,8 @@ impl Variable { Symbol::NUM_DEC => Some(Variable::DEC), + Symbol::STR_STR => Some(Variable::STR), + _ => None, } } @@ -1699,6 +1689,20 @@ impl Subs { ) }); + subs.set_content(Variable::STR, { + Content::Structure(FlatType::Apply( + Symbol::STR_STR, + VariableSubsSlice::default(), + )) + }); + + let u8_slice = + VariableSubsSlice::insert_into_subs(&mut subs, std::iter::once(Variable::U8)); + subs.set_content( + Variable::LIST_U8, + Content::Structure(FlatType::Apply(Symbol::LIST_LIST, u8_slice)), + ); + subs } @@ -1743,8 +1747,8 @@ impl Subs { /// Unions two keys without the possibility of failure. pub fn union(&mut self, left: Variable, right: Variable, desc: Descriptor) { - let l_root = self.utable.inlined_get_root_key(left); - let r_root = self.utable.inlined_get_root_key(right); + let l_root = self.utable.root_key(left); + let r_root = self.utable.root_key(right); // NOTE this swapping is intentional! most of our unifying commands are based on the elm // source, but unify_roots is from `ena`, not the elm source. Turns out that they have @@ -1799,7 +1803,7 @@ impl Subs { #[inline(always)] pub fn get_root_key(&mut self, key: Variable) -> Variable { - self.utable.inlined_get_root_key(key) + self.utable.root_key(key) } #[inline(always)] @@ -1809,7 +1813,7 @@ impl Subs { #[inline(always)] pub fn set(&mut self, key: Variable, r_value: Descriptor) { - let l_key = self.utable.inlined_get_root_key(key); + let l_key = self.utable.root_key(key); // self.utable.update_value(l_key, |node| node.value = r_value); self.utable.set_descriptor(l_key, r_value) @@ -4607,7 +4611,7 @@ struct CopyImportEnv<'a> { } pub fn copy_import_to( - source: &mut Subs, // mut to set the copy + source: &mut Subs, // mut to set the copy. TODO: use a separate copy table to avoid mut target: &mut Subs, var: Variable, rank: Rank, diff --git a/compiler/types/src/unification_table.rs b/compiler/types/src/unification_table.rs index 315e92d24b..12e10f4648 100644 --- a/compiler/types/src/unification_table.rs +++ b/compiler/types/src/unification_table.rs @@ -1,25 +1,46 @@ +use std::hint::unreachable_unchecked; + use crate::subs::{Content, Descriptor, Mark, OptVariable, Rank, Variable, VariableSubsSlice}; #[derive(Clone, Default)] pub struct UnificationTable { contents: Vec, - ranks: Vec, - marks: Vec, - copies: Vec, - redirects: Vec, + metadata: Vec, } pub(crate) struct Snapshot(UnificationTable); +#[derive(Debug, Clone, Copy)] +enum Combine { + Redirect(Variable), + Root(Root), +} + +#[derive(Debug, Clone, Copy)] +pub struct Root { + rank: Rank, + mark: Mark, + copy: OptVariable, +} + +impl Root { + const NONE: Self = Self { + rank: Rank::NONE, + mark: Mark::NONE, + copy: OptVariable::NONE, + }; +} + +impl Combine { + const NONE: Self = Self::Root(Root::NONE); +} + impl UnificationTable { #[allow(unused)] pub fn with_capacity(cap: usize) -> Self { Self { - contents: Vec::with_capacity(cap), // vec![Content::Error; cap], - ranks: Vec::with_capacity(cap), // vec![Rank::NONE; cap], - marks: Vec::with_capacity(cap), // vec![Mark::NONE; cap], - copies: Vec::with_capacity(cap), // vec![OptVariable::NONE; cap], - redirects: Vec::with_capacity(cap), // vec![OptVariable::NONE; cap], + contents: Vec::with_capacity(cap), + metadata: Vec::with_capacity(cap), } } @@ -38,12 +59,8 @@ impl UnificationTable { self.contents .extend(repeat(Content::FlexVar(None)).take(extra_length)); - self.ranks.extend(repeat(Rank::NONE).take(extra_length)); - self.marks.extend(repeat(Mark::NONE).take(extra_length)); - self.copies - .extend(repeat(OptVariable::NONE).take(extra_length)); - self.redirects - .extend(repeat(OptVariable::NONE).take(extra_length)); + self.metadata + .extend(repeat(Combine::NONE).take(extra_length)); VariableSubsSlice::new(start as _, extra_length as _) } @@ -58,10 +75,10 @@ impl UnificationTable { let variable = unsafe { Variable::from_index(self.len() as _) }; self.contents.push(content); - self.ranks.push(rank); - self.marks.push(mark); - self.copies.push(copy); - self.redirects.push(OptVariable::NONE); + + let combine = Combine::Root(Root { rank, mark, copy }); + + self.metadata.push(combine); variable } @@ -75,53 +92,78 @@ impl UnificationTable { mark: Mark, copy: OptVariable, ) { - let index = self.root_key(key).index() as usize; + let root = self.root_key(key); + self.set_unchecked(root, content, rank, mark, copy) + } + + pub fn set_unchecked( + &mut self, + key: Variable, + content: Content, + rank: Rank, + mark: Mark, + copy: OptVariable, + ) { + let index = key.index() as usize; self.contents[index] = content; - self.ranks[index] = rank; - self.marks[index] = mark; - self.copies[index] = copy; + + self.metadata[index] = Combine::Root(Root { rank, mark, copy }); } pub fn modify(&mut self, key: Variable, mapper: F) -> T where F: FnOnce(&mut Descriptor) -> T, { - let index = self.root_key(key).index() as usize; + let root_key = self.root_key(key); + let index = root_key.index() as usize; + + let root = self.get_root_unchecked(root_key); let mut desc = Descriptor { content: self.contents[index], - rank: self.ranks[index], - mark: self.marks[index], - copy: self.copies[index], + rank: root.rank, + mark: root.mark, + copy: root.copy, }; let result = mapper(&mut desc); - self.contents[index] = desc.content; - self.ranks[index] = desc.rank; - self.marks[index] = desc.mark; - self.copies[index] = desc.copy; + self.set_unchecked(root_key, desc.content, desc.rank, desc.mark, desc.copy); result } // GET UNCHECKED + #[inline(always)] + pub fn get_root_unchecked(&self, key: Variable) -> Root { + match self.metadata[key.index() as usize] { + Combine::Root(root) => root, + Combine::Redirect(_) => { + if cfg!(debug_assertions) { + unreachable!("unchecked access on non-root variable {:?}", key); + } else { + unsafe { unreachable_unchecked() } + } + } + } + } + #[inline(always)] pub fn get_rank_unchecked(&self, key: Variable) -> Rank { - self.ranks[key.index() as usize] + self.get_root_unchecked(key).rank } #[inline(always)] pub fn get_mark_unchecked(&self, key: Variable) -> Mark { - self.marks[key.index() as usize] + self.get_root_unchecked(key).mark } #[allow(unused)] #[inline(always)] pub fn get_copy_unchecked(&self, key: Variable) -> OptVariable { - self.copies[key.index() as usize] + self.get_root_unchecked(key).copy } #[inline(always)] @@ -131,20 +173,34 @@ impl UnificationTable { // GET CHECKED + #[inline(always)] + pub fn get_root(&self, key: Variable) -> Root { + let root_key = self.root_key_without_compacting(key); + match self.metadata[root_key.index() as usize] { + Combine::Root(root) => root, + Combine::Redirect(_) => { + if cfg!(debug_assertions) { + unreachable!("the root key {:?} is not actually a root key", root_key); + } else { + unsafe { unreachable_unchecked() } + } + } + } + } + #[inline(always)] pub fn get_rank(&self, key: Variable) -> Rank { - self.ranks[self.root_key_without_compacting(key).index() as usize] + self.get_root(key).rank } #[inline(always)] pub fn get_mark(&self, key: Variable) -> Mark { - self.marks[self.root_key_without_compacting(key).index() as usize] + self.get_root(key).mark } #[inline(always)] pub fn get_copy(&self, key: Variable) -> OptVariable { - let index = self.root_key_without_compacting(key).index() as usize; - self.copies[index] + self.get_root(key).copy } #[inline(always)] @@ -154,20 +210,37 @@ impl UnificationTable { // SET UNCHECKED + #[inline(always)] + pub fn modify_root_unchecked(&mut self, key: Variable, f: F) -> T + where + F: Fn(&mut Root) -> T, + { + match &mut self.metadata[key.index() as usize] { + Combine::Root(root) => f(root), + Combine::Redirect(_) => { + if cfg!(debug_assertions) { + unreachable!("unchecked access on non-root variable {:?}", key); + } else { + unsafe { unreachable_unchecked() } + } + } + } + } + #[inline(always)] pub fn set_rank_unchecked(&mut self, key: Variable, value: Rank) { - self.ranks[key.index() as usize] = value; + self.modify_root_unchecked(key, |root| root.rank = value) } #[inline(always)] pub fn set_mark_unchecked(&mut self, key: Variable, value: Mark) { - self.marks[key.index() as usize] = value; + self.modify_root_unchecked(key, |root| root.mark = value) } #[allow(unused)] #[inline(always)] pub fn set_copy_unchecked(&mut self, key: Variable, value: OptVariable) { - self.copies[key.index() as usize] = value; + self.modify_root_unchecked(key, |root| root.copy = value) } #[allow(unused)] @@ -180,20 +253,20 @@ impl UnificationTable { #[inline(always)] pub fn set_rank(&mut self, key: Variable, value: Rank) { - let index = self.root_key(key).index() as usize; - self.ranks[index] = value; + let root_key = self.root_key(key); + self.modify_root_unchecked(root_key, |root| root.rank = value) } #[inline(always)] pub fn set_mark(&mut self, key: Variable, value: Mark) { - let index = self.root_key(key).index() as usize; - self.marks[index] = value; + let root_key = self.root_key(key); + self.modify_root_unchecked(root_key, |root| root.mark = value) } #[inline(always)] pub fn set_copy(&mut self, key: Variable, value: OptVariable) { - let index = self.root_key(key).index() as usize; - self.copies[index] = value; + let root_key = self.root_key(key); + self.modify_root_unchecked(root_key, |root| root.copy = value) } #[inline(always)] @@ -206,22 +279,19 @@ impl UnificationTable { #[inline(always)] pub fn root_key(&mut self, mut key: Variable) -> Variable { - let index = key.index() as usize; + let root = self.root_key_without_compacting(key); - while let Some(redirect) = self.redirects[key.index() as usize].into_variable() { - key = redirect; + while let Combine::Redirect(redirect) = &mut self.metadata[key.index() as usize] { + key = *redirect; + *redirect = root; } - if index != key.index() as usize { - self.redirects[index] = OptVariable::from(key); - } - - key + root } #[inline(always)] pub fn root_key_without_compacting(&self, mut key: Variable) -> Variable { - while let Some(redirect) = self.redirects[key.index() as usize].into_variable() { + while let Combine::Redirect(redirect) = self.metadata[key.index() as usize] { key = redirect; } @@ -246,7 +316,7 @@ impl UnificationTable { } pub fn is_redirect(&self, key: Variable) -> bool { - self.redirects[key.index() as usize].is_some() + matches!(self.metadata[key.index() as usize], Combine::Redirect(_)) } pub fn unioned(&mut self, a: Variable, b: Variable) -> bool { @@ -256,17 +326,13 @@ impl UnificationTable { // custom very specific helpers #[inline(always)] pub fn get_rank_set_mark(&mut self, key: Variable, mark: Mark) -> Rank { - let index = self.root_key(key).index() as usize; + let root_key = self.root_key(key); - self.marks[index] = mark; + self.modify_root_unchecked(root_key, |root| { + root.mark = mark; - self.ranks[index] - } - - // TODO remove - #[inline(always)] - pub fn inlined_get_root_key(&mut self, key: Variable) -> Variable { - self.root_key(key) + root.rank + }) } /// NOTE: assumes variables are root @@ -276,34 +342,29 @@ impl UnificationTable { // redirect from -> to if from_index != to_index { - self.redirects[from_index] = OptVariable::from(to); + self.metadata[from_index] = Combine::Redirect(to) } // update to's Descriptor - self.contents[to_index] = desc.content; - self.ranks[to_index] = desc.rank; - self.marks[to_index] = desc.mark; - self.copies[to_index] = desc.copy; + self.set_unchecked(to, desc.content, desc.rank, desc.mark, desc.copy); } pub fn get_descriptor(&self, key: Variable) -> Descriptor { - let index = self.root_key_without_compacting(key).index() as usize; + let root_key = self.root_key_without_compacting(key); + let index = root_key.index() as usize; + + let root = self.get_root_unchecked(root_key); Descriptor { content: self.contents[index], - rank: self.ranks[index], - mark: self.marks[index], - copy: self.copies[index], + rank: root.rank, + mark: root.mark, + copy: root.copy, } } pub fn set_descriptor(&mut self, key: Variable, desc: Descriptor) { - let index = self.root_key(key).index() as usize; - - self.contents[index] = desc.content; - self.ranks[index] = desc.rank; - self.marks[index] = desc.mark; - self.copies[index] = desc.copy; + self.set(key, desc.content, desc.rank, desc.mark, desc.copy); } pub(crate) fn serialize( @@ -314,10 +375,37 @@ impl UnificationTable { use crate::subs::Subs; written = Subs::serialize_slice(&self.contents, writer, written)?; - written = Subs::serialize_slice(&self.ranks, writer, written)?; - written = Subs::serialize_slice(&self.marks, writer, written)?; - written = Subs::serialize_slice(&self.copies, writer, written)?; - written = Subs::serialize_slice(&self.redirects, writer, written)?; + + let mut ranks = Vec::new(); + let mut marks = Vec::new(); + let mut copies = Vec::new(); + let mut redirects = Vec::new(); + + for c in self.metadata.iter() { + match c { + Combine::Redirect(redirect) => { + let root = Root::NONE; + + ranks.push(root.rank); + marks.push(root.mark); + copies.push(root.copy); + + redirects.push(OptVariable::from(*redirect)); + } + Combine::Root(root) => { + ranks.push(root.rank); + marks.push(root.mark); + copies.push(root.copy); + + redirects.push(OptVariable::NONE); + } + } + } + + written = Subs::serialize_slice(&ranks, writer, written)?; + written = Subs::serialize_slice(&marks, writer, written)?; + written = Subs::serialize_slice(&copies, writer, written)?; + written = Subs::serialize_slice(&redirects, writer, written)?; Ok(written) } @@ -331,12 +419,29 @@ impl UnificationTable { let (copies, offset) = Subs::deserialize_slice::(bytes, length, offset); let (redirects, offset) = Subs::deserialize_slice::(bytes, length, offset); + let mut metadata = Vec::with_capacity(ranks.len()); + + let it = ranks + .iter() + .zip(marks.iter()) + .zip(copies.iter()) + .zip(redirects.iter()); + for (((rank, mark), copy), redirect) in it { + match redirect.into_variable() { + Some(redirect) => metadata.push(Combine::Redirect(redirect)), + None => { + metadata.push(Combine::Root(Root { + rank: *rank, + mark: *mark, + copy: *copy, + })); + } + } + } + let this = Self { contents: contents.to_vec(), - ranks: ranks.to_vec(), - marks: marks.to_vec(), - copies: copies.to_vec(), - redirects: redirects.to_vec(), + metadata, }; (this, offset) diff --git a/compiler/unify/src/unify.rs b/compiler/unify/src/unify.rs index 65b5571bb5..ca7cbdc3e5 100644 --- a/compiler/unify/src/unify.rs +++ b/compiler/unify/src/unify.rs @@ -528,7 +528,6 @@ fn unify_two_aliases( outcome } else { - dbg!(args.len(), other_args.len()); mismatch!("{:?}", _symbol) } } @@ -1130,6 +1129,8 @@ fn unify_shared_fields( let mut matching_fields = Vec::with_capacity(shared_fields.len()); let num_shared_fields = shared_fields.len(); + let mut whole_outcome = Outcome::default(); + for (name, (actual, expected)) in shared_fields { let local_outcome = unify_pool( subs, @@ -1163,6 +1164,7 @@ fn unify_shared_fields( }; matching_fields.push((name, actual)); + whole_outcome.union(local_outcome); } } @@ -1211,7 +1213,9 @@ fn unify_shared_fields( let flat_type = FlatType::Record(fields, new_ext_var); - merge(subs, ctx, Structure(flat_type)) + let merge_outcome = merge(subs, ctx, Structure(flat_type)); + whole_outcome.union(merge_outcome); + whole_outcome } else { mismatch!("in unify_shared_fields") } diff --git a/examples/README.md b/examples/README.md index 9399ba7fed..7097cb5397 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,20 +1,20 @@ # Examples -Took a look around in this folder; `examples/benchmarks/` contains some larger examples. - Run examples as follows: -1. Navigate to the examples directory +1. Navigate to this `examples` folder ```bash cd examples ``` -2. Run "Hello, World!" example +2. Run a particular example, such as Hello World: ```bash - cargo run hello-world/helloWorld.roc + roc run hello-world/main.roc ``` +`examples/benchmarks/` contains some larger examples. + Some examples like `examples/benchmarks/NQueens.roc` require input after running. For NQueens, input 10 in the terminal and press enter. diff --git a/examples/breakout/platform/Cargo.lock b/examples/breakout/platform/Cargo.lock index 8f8005c26d..a1a31eae48 100644 --- a/examples/breakout/platform/Cargo.lock +++ b/examples/breakout/platform/Cargo.lock @@ -2091,6 +2091,7 @@ checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157" name = "roc_std" version = "0.1.0" dependencies = [ + "arrayvec", "static_assertions 0.1.1", ] diff --git a/examples/false-interpreter/platform/Cargo.lock b/examples/false-interpreter/platform/Cargo.lock index 9132862791..1622b303ac 100644 --- a/examples/false-interpreter/platform/Cargo.lock +++ b/examples/false-interpreter/platform/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "host" version = "0.1.0" @@ -20,6 +26,7 @@ checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5" name = "roc_std" version = "0.1.0" dependencies = [ + "arrayvec", "static_assertions", ] diff --git a/examples/gui/platform/Cargo.lock b/examples/gui/platform/Cargo.lock index 8f8005c26d..a1a31eae48 100644 --- a/examples/gui/platform/Cargo.lock +++ b/examples/gui/platform/Cargo.lock @@ -2091,6 +2091,7 @@ checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157" name = "roc_std" version = "0.1.0" dependencies = [ + "arrayvec", "static_assertions 0.1.1", ] diff --git a/examples/hello-world/.gitignore b/examples/hello-world/.gitignore index 0f55df43a2..520f5f5dec 100644 --- a/examples/hello-world/.gitignore +++ b/examples/hello-world/.gitignore @@ -3,4 +3,5 @@ helloRust helloSwift helloWorld helloZig +hello *.wasm diff --git a/examples/hello-world/README.md b/examples/hello-world/README.md index bd5fbda39f..5a310c5746 100644 --- a/examples/hello-world/README.md +++ b/examples/hello-world/README.md @@ -1,44 +1,18 @@ # Hello, World! -To run, `cd` into this directory and run: +To run, `cd` into this directory and run this in your terminal: ```bash -cargo run helloWorld.roc +roc run ``` -To run in release mode instead, do: +This will run `main.roc` because, unless you explicitly give it a filename, `roc run` +defaults to running a file named `main.roc`. Other `roc` commands (like `roc build`, `roc test`, and so on) also default to `main.roc` unless you explicitly give them a filename. -```bash -cargo run --release helloWorld.roc -``` +# About this example -## Design Notes +This uses a very simple platform which does nothing more than printing the string you give it. -This demonstrates the basic design of hosts: Roc code gets compiled into a pure -function (in this case, a thunk that always returns `"Hello, World!\n"`) and -then the host calls that function. Fundamentally, that's the whole idea! The host -might not even have a `main` - it could be a library, a plugin, anything. -Everything else is built on this basic "hosts calling linked pure functions" design. +The line `main = "Hello, World!\n"` sets this string to be `"Hello, World!"` with a newline at the end, and the lines `packages { pf: "c-platform" }` and `provides [main] to pf` specify that the `c-platform/` directory contains this app's platform. -For example, things get more interesting when the compiled Roc function returns -a `Task` - that is, a tagged union data structure containing function pointers -to callback closures. This lets the Roc pure function describe arbitrary -chainable effects, which the host can interpret to perform I/O as requested by -the Roc program. (The tagged union `Task` would have a variant for each supported -I/O operation.) - -In this trivial example, it's very easy to line up the API between the host and -the Roc program. In a more involved host, this would be much trickier - especially -if the API were changing frequently during development. - -The idea there is to have a first-class concept of "glue code" which host authors -can write (it would be plain Roc code, but with some extra keywords that aren't -available in normal modules - kinda like `port module` in Elm), and which -describe both the Roc-host/C boundary as well as the Roc-host/Roc-app boundary. -Roc application authors only care about the Roc-host/Roc-app portion, and the -host author only cares about the Roc-host/C boundary when implementing the host. - -Using this glue code, the Roc compiler can generate C header files describing the -boundary. This not only gets us host compatibility with C compilers, but also -Rust FFI for free, because [`rust-bindgen`](https://github.com/rust-lang/rust-bindgen) -generates correct Rust FFI bindings from C headers. +This platform is called `c-platform` because its low-level code is written in C. There's also a `rust-platform`, `zig-platform`, and so on; if you like, you can try switching `pf: "c-platform"` to `pf: "zig-platform"` or `pf: "rust-platform"` to try one of those platforms instead. They all do the same thing, so the application won't look any different, but if you want to start building your own platforms, this Hello World example gives you some very simple platforms to use as starting points too. diff --git a/examples/hello-world/helloWorld.roc b/examples/hello-world/helloWorld.roc deleted file mode 100644 index 7f90a48811..0000000000 --- a/examples/hello-world/helloWorld.roc +++ /dev/null @@ -1,11 +0,0 @@ -app "helloWorld" - packages { pf: "c-platform" } - # To switch platforms, comment-out the line above and un-comment one below. - # packages { pf: "rust-platform" } - # packages { pf: "swift-platform" } - # packages { pf: "web-platform" } # See ./web-platform/README.md - # packages { pf: "zig-platform" } - imports [] - provides [main] to pf - -main = "Hello, World!\n" diff --git a/examples/hello-world/main.roc b/examples/hello-world/main.roc new file mode 100644 index 0000000000..18c447a3bc --- /dev/null +++ b/examples/hello-world/main.roc @@ -0,0 +1,6 @@ +app "hello" + packages { pf: "c-platform" } + imports [] + provides [main] to pf + +main = "Hello, World!\n" diff --git a/examples/hello-world/rust-platform/Cargo.lock b/examples/hello-world/rust-platform/Cargo.lock index ebd0a436cf..c6fa4fd219 100644 --- a/examples/hello-world/rust-platform/Cargo.lock +++ b/examples/hello-world/rust-platform/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "host" version = "0.1.0" @@ -20,6 +26,7 @@ checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" name = "roc_std" version = "0.1.0" dependencies = [ + "arrayvec", "static_assertions", ] diff --git a/examples/hello-world/web-platform/README.md b/examples/hello-world/web-platform/README.md index 9708431198..8ccf22ae00 100644 --- a/examples/hello-world/web-platform/README.md +++ b/examples/hello-world/web-platform/README.md @@ -7,7 +7,7 @@ To run this website, first compile either of these identical apps: cargo run -- build --target=wasm32 examples/hello-world/web-platform/helloWeb.roc # Option B: Compile helloWorld.roc with `pf: "web-platform"` and move the result -cargo run -- build --target=wasm32 examples/hello-world/helloWorld.roc +cargo run -- build --target=wasm32 examples/hello-world/main.roc (cd examples/hello-world && mv helloWorld.wasm web-platform/helloWeb.wasm) ``` diff --git a/highlight/tests/peg_grammar.rs b/highlight/tests/peg_grammar.rs index f8e052f22f..cb73e34d61 100644 --- a/highlight/tests/peg_grammar.rs +++ b/highlight/tests/peg_grammar.rs @@ -732,7 +732,7 @@ test1 = #[test] fn test_hello() { - let tokens = tokenize(&example_path("hello-world/helloWorld.roc")); + let tokens = tokenize(&example_path("hello-world/main.roc")); assert_eq!(tokenparser::module(&tokens), Ok(())); } @@ -965,7 +965,7 @@ test1 = r#"5 |> fun - + "#, ); @@ -1028,7 +1028,7 @@ test1 = fn test_deeper_pizza() { let tokens = tokenize( r#"5 - |> fun a + |> fun a |> fun b"#, ); @@ -1039,7 +1039,7 @@ test1 = fn test_deeper_indented_pizza_a() { let tokens = tokenize( r#"5 - |> fun a + |> fun a |> fun b"#, ); @@ -1050,7 +1050,7 @@ test1 = fn test_deeper_indented_pizza_b() { let tokens = tokenize( r#"5 - |> fun a + |> fun a |> fun b "#, ); @@ -1225,7 +1225,7 @@ test1 = e = mkExpr n 1 # comment unoptimized = eval e optimized = eval (constFolding (reassoc e)) - + optimized"#, ); @@ -1310,7 +1310,7 @@ balance = \color -> Node Red Node nColor -> - when key is + when key is GT -> balance nColor"#, ); @@ -1390,7 +1390,7 @@ balance = \color -> let tokens = tokenize( r#"with = \path -> handle <- withOpen - + 4"#, ); diff --git a/reporting/tests/test_reporting.rs b/reporting/tests/test_reporting.rs index fa1994f1eb..4d46fae593 100644 --- a/reporting/tests/test_reporting.rs +++ b/reporting/tests/test_reporting.rs @@ -399,7 +399,8 @@ mod test_reporting { assert_eq!(readable, expected_rendering); } - fn new_report_problem_as(subdir: &str, src: &str, expected_rendering: &str) { + /// Do not call this directly! Use the test_report macro below! + fn __new_report_problem_as(subdir: &str, src: &str, expected_rendering: &str) { let arena = Bump::new(); let finalize_render = |doc: RocDocBuilder<'_>, buf: &mut String| { @@ -420,6 +421,15 @@ mod test_reporting { assert_multiline_str_eq!(expected_rendering, buf.as_str()); } + macro_rules! test_report { + ($test_name:ident, $program:expr, $output:expr) => { + #[test] + fn $test_name() { + __new_report_problem_as(std::stringify!($test_name), $program, $output) + } + }; + } + fn human_readable(str: &str) -> String { str.replace(ANSI_STYLE_CODES.red, "") .replace(ANSI_STYLE_CODES.white, "") @@ -8821,49 +8831,44 @@ All branches in an `if` must have the same type! ) } - #[test] - fn ability_demands_not_indented_with_first() { - new_report_problem_as( - "ability_demands_not_indented_with_first", - indoc!( - r#" - Eq has - eq : a, a -> U64 | a has Eq - neq : a, a -> U64 | a has Eq + test_report!( + ability_demands_not_indented_with_first, + indoc!( + r#" + Eq has + eq : a, a -> U64 | a has Eq + neq : a, a -> U64 | a has Eq - 1 - "# - ), - indoc!( - r#" - ── UNFINISHED ABILITY ─── tmp/ability_demands_not_indented_with_first/Test.roc ─ + 1 + "# + ), + indoc!( + r#" + ── UNFINISHED ABILITY ─── tmp/ability_demands_not_indented_with_first/Test.roc ─ - I was partway through parsing an ability definition, but I got stuck - here: + I was partway through parsing an ability definition, but I got stuck + here: - 5│ eq : a, a -> U64 | a has Eq - 6│ neq : a, a -> U64 | a has Eq - ^ + 5│ eq : a, a -> U64 | a has Eq + 6│ neq : a, a -> U64 | a has Eq + ^ - I suspect this line is indented too much (by 4 spaces)"# - ), + I suspect this line is indented too much (by 4 spaces)"# ) - } + ); - #[test] - fn ability_demand_value_has_args() { - new_report_problem_as( - "ability_demand_value_has_args", - indoc!( - r#" + test_report!( + ability_demand_value_has_args, + indoc!( + r#" Eq has eq b c : a, a -> U64 | a has Eq 1 "# - ), - indoc!( - r#" + ), + indoc!( + r#" ── UNFINISHED ABILITY ───────────── tmp/ability_demand_value_has_args/Test.roc ─ I was partway through parsing an ability definition, but I got stuck @@ -8874,9 +8879,8 @@ All branches in an `if` must have the same type! I was expecting to see a : annotating the signature of this value next."# - ), ) - } + ); #[test] fn ability_non_signature_expression() { @@ -9038,722 +9042,668 @@ All branches in an `if` must have the same type! ) } - #[test] - fn ability_bad_type_parameter() { - new_report_problem_as( - "ability_bad_type_parameter", - indoc!( - r#" - app "test" provides [] to "./platform" + test_report!( + ability_bad_type_parameter, + indoc!( + r#" + app "test" provides [] to "./platform" - Hash a b c has - hash : a -> U64 | a has Hash - "# - ), - indoc!( - r#" - ── ABILITY HAS TYPE VARIABLES ──────────────────────────── /code/proj/Main.roc ─ + Hash a b c has + hash : a -> U64 | a has Hash + "# + ), + indoc!( + r#" + ── ABILITY HAS TYPE VARIABLES ──────────────────────────── /code/proj/Main.roc ─ - The definition of the `Hash` ability includes type variables: + The definition of the `Hash` ability includes type variables: - 3│ Hash a b c has - ^^^^^ + 3│ Hash a b c has + ^^^^^ - Abilities cannot depend on type variables, but their member values - can! + Abilities cannot depend on type variables, but their member values + can! - ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ - `Hash` is not used anywhere in your code. + `Hash` is not used anywhere in your code. - 3│ Hash a b c has - ^^^^ + 3│ Hash a b c has + ^^^^ - If you didn't intend on using `Hash` then remove it so future readers of - your code don't wonder why it is there. - "# - ), + If you didn't intend on using `Hash` then remove it so future readers of + your code don't wonder why it is there. + "# ) - } + ); - #[test] - fn alias_in_has_clause() { - new_report_problem_as( - "alias_in_has_clause", - indoc!( - r#" - app "test" provides [hash] to "./platform" + test_report!( + alias_in_has_clause, + indoc!( + r#" + app "test" provides [hash] to "./platform" - Hash has hash : a, b -> Num.U64 | a has Hash, b has Bool.Bool - "# - ), - indoc!( - r#" - ── HAS CLAUSE IS NOT AN ABILITY ────────────────────────── /code/proj/Main.roc ─ + Hash has hash : a, b -> Num.U64 | a has Hash, b has Bool.Bool + "# + ), + indoc!( + r#" + ── HAS CLAUSE IS NOT AN ABILITY ────────────────────────── /code/proj/Main.roc ─ - The type referenced in this "has" clause is not an ability: + The type referenced in this "has" clause is not an ability: - 3│ Hash has hash : a, b -> Num.U64 | a has Hash, b has Bool.Bool - ^^^^^^^^^ - "# - ), + 3│ Hash has hash : a, b -> Num.U64 | a has Hash, b has Bool.Bool + ^^^^^^^^^ + "# ) - } + ); - #[test] - fn shadowed_type_variable_in_has_clause() { - new_report_problem_as( - "shadowed_type_variable_in_has_clause", - indoc!( - r#" - app "test" provides [ab1] to "./platform" + test_report!( + shadowed_type_variable_in_has_clause, + indoc!( + r#" + app "test" provides [ab1] to "./platform" - Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1 - "# - ), - indoc!( - r#" - ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1 + "# + ), + indoc!( + r#" + ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ - The `a` name is first defined here: + The `a` name is first defined here: - 3│ Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1 - ^^^^^^^^^ + 3│ Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1 + ^^^^^^^^^ - But then it's defined a second time here: + But then it's defined a second time here: - 3│ Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1 - ^^^^^^^^^ + 3│ Ab1 has ab1 : a -> {} | a has Ab1, a has Ab1 + ^^^^^^^^^ - Since these variables have the same name, it's easy to use the wrong - one on accident. Give one of them a new name. - "# - ), + Since these variables have the same name, it's easy to use the wrong + one on accident. Give one of them a new name. + "# ) - } + ); - #[test] - fn ability_shadows_ability() { - new_report_problem_as( - "ability_shadows_ability", - indoc!( - r#" - app "test" provides [ab] to "./platform" + test_report!( + ability_shadows_ability, + indoc!( + r#" + app "test" provides [ab] to "./platform" - Ability has ab : a -> U64 | a has Ability + Ability has ab : a -> U64 | a has Ability - Ability has ab1 : a -> U64 | a has Ability - "# - ), - indoc!( - r#" - ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ + Ability has ab1 : a -> U64 | a has Ability + "# + ), + indoc!( + r#" + ── DUPLICATE NAME ──────────────────────────────────────── /code/proj/Main.roc ─ - The `Ability` name is first defined here: + The `Ability` name is first defined here: - 3│ Ability has ab : a -> U64 | a has Ability - ^^^^^^^ + 3│ Ability has ab : a -> U64 | a has Ability + ^^^^^^^ - But then it's defined a second time here: + But then it's defined a second time here: - 5│ Ability has ab1 : a -> U64 | a has Ability - ^^^^^^^ + 5│ Ability has ab1 : a -> U64 | a has Ability + ^^^^^^^ - Since these abilities have the same name, it's easy to use the wrong - one on accident. Give one of them a new name. - "# - ), + Since these abilities have the same name, it's easy to use the wrong + one on accident. Give one of them a new name. + "# ) - } + ); - #[test] - fn ability_member_does_not_bind_ability() { - new_report_problem_as( - "ability_member_does_not_bind_ability", - indoc!( - r#" - app "test" provides [] to "./platform" + test_report!( + ability_member_does_not_bind_ability, + indoc!( + r#" + app "test" provides [] to "./platform" - Ability has ab : {} -> {} - "# - ), - indoc!( - r#" - ── ABILITY MEMBER MISSING HAS CLAUSE ───────────────────── /code/proj/Main.roc ─ + Ability has ab : {} -> {} + "# + ), + indoc!( + r#" + ── ABILITY MEMBER MISSING HAS CLAUSE ───────────────────── /code/proj/Main.roc ─ - The definition of the ability member `ab` does not include a `has` clause - binding a type variable to the ability `Ability`: + The definition of the ability member `ab` does not include a `has` clause + binding a type variable to the ability `Ability`: - 3│ Ability has ab : {} -> {} - ^^ + 3│ Ability has ab : {} -> {} + ^^ - Ability members must include a `has` clause binding a type variable to - an ability, like + Ability members must include a `has` clause binding a type variable to + an ability, like - a has Ability + a has Ability - Otherwise, the function does not need to be part of the ability! + Otherwise, the function does not need to be part of the ability! - ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ + ── UNUSED DEFINITION ───────────────────────────────────── /code/proj/Main.roc ─ - `Ability` is not used anywhere in your code. + `Ability` is not used anywhere in your code. - 3│ Ability has ab : {} -> {} - ^^^^^^^ + 3│ Ability has ab : {} -> {} + ^^^^^^^ - If you didn't intend on using `Ability` then remove it so future readers - of your code don't wonder why it is there. - "# - ), + If you didn't intend on using `Ability` then remove it so future readers + of your code don't wonder why it is there. + "# ) - } + ); - #[test] - fn ability_member_binds_parent_twice() { - new_report_problem_as( - "ability_member_binds_parent_twice", - indoc!( - r#" - app "test" provides [] to "./platform" + test_report!( + ability_member_binds_parent_twice, + indoc!( + r#" + app "test" provides [] to "./platform" - Eq has eq : a, b -> Bool.Bool | a has Eq, b has Eq - "# - ), - indoc!( - r#" - ── ABILITY MEMBER BINDS MULTIPLE VARIABLES ─────────────── /code/proj/Main.roc ─ + Eq has eq : a, b -> Bool.Bool | a has Eq, b has Eq + "# + ), + indoc!( + r#" + ── ABILITY MEMBER BINDS MULTIPLE VARIABLES ─────────────── /code/proj/Main.roc ─ - The definition of the ability member `eq` includes multiple variables - bound to the `Eq`` ability:` + The definition of the ability member `eq` includes multiple variables + bound to the `Eq`` ability:` - 3│ Eq has eq : a, b -> Bool.Bool | a has Eq, b has Eq - ^^^^^^^^^^^^^^^^^^ + 3│ Eq has eq : a, b -> Bool.Bool | a has Eq, b has Eq + ^^^^^^^^^^^^^^^^^^ - Ability members can only bind one type variable to their parent - ability. Otherwise, I wouldn't know what type implements an ability by - looking at specializations! + Ability members can only bind one type variable to their parent + ability. Otherwise, I wouldn't know what type implements an ability by + looking at specializations! - Hint: Did you mean to only bind `a` to `Eq`? - "# - ), + Hint: Did you mean to only bind `a` to `Eq`? + "# ) - } + ); - #[test] - fn has_clause_not_on_toplevel() { - new_report_problem_as( - "has_clause_outside_of_ability", - indoc!( - r#" - app "test" provides [f] to "./platform" + test_report!( + has_clause_not_on_toplevel, + indoc!( + r#" + app "test" provides [f] to "./platform" - Hash has hash : (a | a has Hash) -> Num.U64 + Hash has hash : (a | a has Hash) -> Num.U64 - f : a -> Num.U64 | a has Hash - "# - ), - indoc!( - r#" - ── ILLEGAL HAS CLAUSE ──────────────────────────────────── /code/proj/Main.roc ─ + f : a -> Num.U64 | a has Hash + "# + ), + indoc!( + r#" + ── ILLEGAL HAS CLAUSE ──────────────────────────────────── /code/proj/Main.roc ─ - A `has` clause is not allowed here: + A `has` clause is not allowed here: - 3│ Hash has hash : (a | a has Hash) -> Num.U64 - ^^^^^^^^^^ + 3│ Hash has hash : (a | a has Hash) -> Num.U64 + ^^^^^^^^^^ - `has` clauses can only be specified on the top-level type annotations. + `has` clauses can only be specified on the top-level type annotations. - ── ABILITY MEMBER MISSING HAS CLAUSE ───────────────────── /code/proj/Main.roc ─ + ── ABILITY MEMBER MISSING HAS CLAUSE ───────────────────── /code/proj/Main.roc ─ - The definition of the ability member `hash` does not include a `has` - clause binding a type variable to the ability `Hash`: + The definition of the ability member `hash` does not include a `has` + clause binding a type variable to the ability `Hash`: - 3│ Hash has hash : (a | a has Hash) -> Num.U64 - ^^^^ + 3│ Hash has hash : (a | a has Hash) -> Num.U64 + ^^^^ - Ability members must include a `has` clause binding a type variable to - an ability, like + Ability members must include a `has` clause binding a type variable to + an ability, like - a has Hash + a has Hash - Otherwise, the function does not need to be part of the ability! - "# - ), + Otherwise, the function does not need to be part of the ability! + "# ) - } + ); - #[test] - fn ability_specialization_with_non_implementing_type() { - new_report_problem_as( - "ability_specialization_with_non_implementing_type", - indoc!( - r#" - app "test" provides [hash] to "./platform" + test_report!( + ability_specialization_with_non_implementing_type, + indoc!( + r#" + app "test" provides [hash] to "./platform" - Hash has hash : a -> Num.U64 | a has Hash + Hash has hash : a -> Num.U64 | a has Hash - hash = \{} -> 0u64 - "# - ), - indoc!( - r#" - ── ILLEGAL SPECIALIZATION ──────────────────────────────── /code/proj/Main.roc ─ + hash = \{} -> 0u64 + "# + ), + indoc!( + r#" + ── ILLEGAL SPECIALIZATION ──────────────────────────────── /code/proj/Main.roc ─ - This specialization of `hash` is for a non-opaque type: + This specialization of `hash` is for a non-opaque type: - 5│ hash = \{} -> 0u64 - ^^^^ + 5│ hash = \{} -> 0u64 + ^^^^ - It is specialized for + It is specialized for - {}a + {}a - but structural types can never specialize abilities! + but structural types can never specialize abilities! - Note: `hash` is a member of `#UserApp.Hash` - "# - ), + Note: `hash` is a member of `#UserApp.Hash` + "# ) - } + ); - #[test] - fn ability_specialization_does_not_match_type() { - new_report_problem_as( - "ability_specialization_does_not_match_type", - indoc!( - r#" - app "test" provides [hash] to "./platform" + test_report!( + ability_specialization_does_not_match_type, + indoc!( + r#" + app "test" provides [hash] to "./platform" - Hash has hash : a -> U64 | a has Hash + Hash has hash : a -> U64 | a has Hash - Id := U32 + Id := U32 - hash = \@Id n -> n - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + hash = \@Id n -> n + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - Something is off with this specialization of `hash`: + Something is off with this specialization of `hash`: - 7│ hash = \@Id n -> n - ^^^^ + 7│ hash = \@Id n -> n + ^^^^ - This value is a declared specialization of type: + This value is a declared specialization of type: - Id -> U32 + Id -> U32 - But the type annotation on `hash` says it must match: + But the type annotation on `hash` says it must match: - Id -> U64 - "# - ), + Id -> U64 + "# ) - } + ); - #[test] - fn ability_specialization_is_incomplete() { - new_report_problem_as( - "ability_specialization_is_incomplete", - indoc!( - r#" - app "test" provides [eq, le] to "./platform" + test_report!( + ability_specialization_is_incomplete, + indoc!( + r#" + app "test" provides [eq, le] to "./platform" - Eq has - eq : a, a -> Bool | a has Eq - le : a, a -> Bool | a has Eq + Eq has + eq : a, a -> Bool | a has Eq + le : a, a -> Bool | a has Eq - Id := U64 + Id := U64 - eq = \@Id m, @Id n -> m == n - "# - ), - indoc!( - r#" - ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + eq = \@Id m, @Id n -> m == n + "# + ), + indoc!( + r#" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ - The type `Id` does not fully implement the ability `Eq`. The following - specializations are missing: + The type `Id` does not fully implement the ability `Eq`. The following + specializations are missing: - A specialization for `le`, which is defined here: + A specialization for `le`, which is defined here: - 5│ le : a, a -> Bool | a has Eq - ^^ - "# - ), - ) - } - - #[test] - fn ability_specialization_overly_generalized() { - new_report_problem_as( - "ability_specialization_overly_generalized", - indoc!( - r#" - app "test" provides [hash] to "./platform" - - Hash has - hash : a -> U64 | a has Hash - - hash = \_ -> 0u64 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This specialization of `hash` is overly general: - - 6│ hash = \_ -> 0u64 - ^^^^ - - This value is a declared specialization of type: - - a -> U64 - - But the type annotation on `hash` says it must match: - - a -> U64 | a has Hash - - Note: The specialized type is too general, and does not provide a - concrete type where a type variable is bound to an ability. - - Specializations can only be made for concrete types. If you have a - generic implementation for this value, perhaps you don't need an - ability? - "# - ), - ) - } - - #[test] - fn ability_specialization_conflicting_specialization_types() { - new_report_problem_as( - "ability_specialization_conflicting_specialization_types", - indoc!( - r#" - app "test" provides [eq] to "./platform" - - Eq has - eq : a, a -> Bool | a has Eq - - You := {} - AndI := {} - - eq = \@You {}, @AndI {} -> False - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - Something is off with this specialization of `eq`: - - 9│ eq = \@You {}, @AndI {} -> False + 5│ le : a, a -> Bool | a has Eq ^^ - - This value is a declared specialization of type: - - You, AndI -> [False, True] - - But the type annotation on `eq` says it must match: - - You, You -> Bool - - Tip: Type comparisons between an opaque type are only ever equal if - both types are the same opaque type. Did you mean to create an opaque - type by wrapping it? If I have an opaque type Age := U32 I can create - an instance of this opaque type by doing @Age 23. - "# - ), + "# ) - } + ); - #[test] - fn ability_specialization_checked_against_annotation() { - new_report_problem_as( - "ability_specialization_checked_against_annotation", - indoc!( - r#" - app "test" provides [hash] to "./platform" + test_report!( + ability_specialization_overly_generalized, + indoc!( + r#" + app "test" provides [hash] to "./platform" - Hash has - hash : a -> U64 | a has Hash + Hash has + hash : a -> U64 | a has Hash - Id := U64 + hash = \_ -> 0u64 + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - hash : Id -> U32 - hash = \@Id n -> n - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + This specialization of `hash` is overly general: - Something is off with the body of this definition: + 6│ hash = \_ -> 0u64 + ^^^^ - 8│ hash : Id -> U32 - 9│ hash = \@Id n -> n - ^ + This value is a declared specialization of type: - This `n` value is a: + a -> U64 - U64 + But the type annotation on `hash` says it must match: - But the type annotation says it should be: + a -> U64 | a has Hash - U32 + Note: The specialized type is too general, and does not provide a + concrete type where a type variable is bound to an ability. - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + Specializations can only be made for concrete types. If you have a + generic implementation for this value, perhaps you don't need an + ability? + "# + ) + ); - Something is off with this specialization of `hash`: + test_report!( + ability_specialization_conflicting_specialization_types, + indoc!( + r#" + app "test" provides [eq] to "./platform" - 9│ hash = \@Id n -> n + Eq has + eq : a, a -> Bool | a has Eq + + You := {} + AndI := {} + + eq = \@You {}, @AndI {} -> False + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with this specialization of `eq`: + + 9│ eq = \@You {}, @AndI {} -> False + ^^ + + This value is a declared specialization of type: + + You, AndI -> [False, True] + + But the type annotation on `eq` says it must match: + + You, You -> Bool + + Tip: Type comparisons between an opaque type are only ever equal if + both types are the same opaque type. Did you mean to create an opaque + type by wrapping it? If I have an opaque type Age := U32 I can create + an instance of this opaque type by doing @Age 23. + "# + ) + ); + + test_report!( + ability_specialization_checked_against_annotation, + indoc!( + r#" + app "test" provides [hash] to "./platform" + + Hash has + hash : a -> U64 | a has Hash + + Id := U64 + + hash : Id -> U32 + hash = \@Id n -> n + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of this definition: + + 8│ hash : Id -> U32 + 9│ hash = \@Id n -> n + ^ + + This `n` value is a: + + U64 + + But the type annotation says it should be: + + U32 + + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with this specialization of `hash`: + + 9│ hash = \@Id n -> n + ^^^^ + + This value is a declared specialization of type: + + Id -> U32 + + But the type annotation on `hash` says it must match: + + Id -> U64 + "# + ) + ); + + test_report!( + ability_specialization_called_with_non_specializing, + indoc!( + r#" + app "test" provides [noGoodVeryBadTerrible] to "./platform" + + Hash has + hash : a -> U64 | a has Hash + + Id := U64 + + hash = \@Id n -> n + + User := {} + + noGoodVeryBadTerrible = + { + nope: hash (@User {}), + notYet: hash (A 1), + } + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 15│ notYet: hash (A 1), + ^^^ + + Roc can't generate an implementation of the `#UserApp.Hash` ability for + + [A (Num a)]b + + Only builtin abilities can have generated implementations! + + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 14│ nope: hash (@User {}), + ^^^^^^^^ + + The type `User` does not fully implement the ability `Hash`. The following + specializations are missing: + + A specialization for `hash`, which is defined here: + + 4│ hash : a -> U64 | a has Hash ^^^^ - - This value is a declared specialization of type: - - Id -> U32 - - But the type annotation on `hash` says it must match: - - Id -> U64 - "# - ), + "# ) - } + ); - #[test] - fn ability_specialization_called_with_non_specializing() { - new_report_problem_as( - "ability_specialization_called_with_non_specializing", - indoc!( - r#" - app "test" provides [noGoodVeryBadTerrible] to "./platform" + test_report!( + ability_not_on_toplevel, + indoc!( + r#" + app "test" provides [main] to "./platform" + main = Hash has hash : a -> U64 | a has Hash - Id := U64 + 123 + "# + ), + indoc!( + r#" + ── ABILITY NOT ON TOP-LEVEL ────────────────────────────── /code/proj/Main.roc ─ - hash = \@Id n -> n + This ability definition is not on the top-level of a module: - User := {} + 4│> Hash has + 5│> hash : a -> U64 | a has Hash - noGoodVeryBadTerrible = - { - nope: hash (@User {}), - notYet: hash (A 1), - } - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression has a type that does not implement the abilities it's expected to: - - 15│ notYet: hash (A 1), - ^^^ - - Roc can't generate an implementation of the `#UserApp.Hash` ability for - - [A (Num a)]b - - Only builtin abilities can have generated implementations! - - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression has a type that does not implement the abilities it's expected to: - - 14│ nope: hash (@User {}), - ^^^^^^^^ - - The type `User` does not fully implement the ability `Hash`. The following - specializations are missing: - - A specialization for `hash`, which is defined here: - - 4│ hash : a -> U64 | a has Hash - ^^^^ - "# - ), + Abilities can only be defined on the top-level of a Roc module. + "# ) - } + ); - #[test] - fn ability_not_on_toplevel() { - new_report_problem_as( - "ability_not_on_toplevel", - indoc!( - r#" - app "test" provides [main] to "./platform" + test_report!( + expression_generalization_to_ability_is_an_error, + indoc!( + r#" + app "test" provides [hash, hashable] to "./platform" - main = - Hash has - hash : a -> U64 | a has Hash + Hash has + hash : a -> U64 | a has Hash - 123 - "# - ), - indoc!( - r#" - ── ABILITY NOT ON TOP-LEVEL ────────────────────────────── /code/proj/Main.roc ─ + Id := U64 + hash = \@Id n -> n - This ability definition is not on the top-level of a module: + hashable : a | a has Hash + hashable = @Id 15 + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - 4│> Hash has - 5│> hash : a -> U64 | a has Hash + Something is off with the body of the `hashable` definition: - Abilities can only be defined on the top-level of a Roc module. - "# - ), + 9│ hashable : a | a has Hash + 10│ hashable = @Id 15 + ^^^^^^ + + This Id opaque wrapping has the type: + + Id + + But the type annotation on `hashable` says it should be: + + a | a has Hash + + Tip: The type annotation uses the type variable `a` to say that this + definition can produce any value implementing the `Hash` ability. But in + the body I see that it will only produce a `Id` value of a single + specific type. Maybe change the type annotation to be more specific? + Maybe change the code to be more general? + "# ) - } + ); - #[test] - fn expression_generalization_to_ability_is_an_error() { - new_report_problem_as( - "expression_generalization_to_ability_is_an_error", - indoc!( - r#" - app "test" provides [hash, hashable] to "./platform" + test_report!( + ability_value_annotations_are_an_error, + indoc!( + r#" + app "test" provides [result] to "./platform" - Hash has - hash : a -> U64 | a has Hash + Hash has + hash : a -> U64 | a has Hash - Id := U64 - hash = \@Id n -> n + mulHashes : Hash, Hash -> U64 + mulHashes = \x, y -> hash x * hash y - hashable : a | a has Hash - hashable = @Id 15 - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + Id := U64 + hash = \@Id n -> n - Something is off with the body of the `hashable` definition: + Three := {} + hash = \@Three _ -> 3 - 9│ hashable : a | a has Hash - 10│ hashable = @Id 15 - ^^^^^^ + result = mulHashes (@Id 100) (@Three {}) + "# + ), + indoc!( + r#" + ── ABILITY USED AS TYPE ────────────────────────────────── /code/proj/Main.roc ─ - This Id opaque wrapping has the type: + You are attempting to use the ability `Hash` as a type directly: - Id + 6│ mulHashes : Hash, Hash -> U64 + ^^^^ - But the type annotation on `hashable` says it should be: + Abilities can only be used in type annotations to constrain type + variables. - a | a has Hash + Hint: Perhaps you meant to include a `has` annotation, like - Tip: The type annotation uses the type variable `a` to say that this - definition can produce any value implementing the `Hash` ability. But in - the body I see that it will only produce a `Id` value of a single - specific type. Maybe change the type annotation to be more specific? - Maybe change the code to be more general? - "# - ), + a has Hash + + ── ABILITY USED AS TYPE ────────────────────────────────── /code/proj/Main.roc ─ + + You are attempting to use the ability `Hash` as a type directly: + + 6│ mulHashes : Hash, Hash -> U64 + ^^^^ + + Abilities can only be used in type annotations to constrain type + variables. + + Hint: Perhaps you meant to include a `has` annotation, like + + b has Hash + "# ) - } + ); - #[test] - fn ability_value_annotations_are_an_error() { - new_report_problem_as( - "ability_value_annotations_are_an_error", - indoc!( - r#" - app "test" provides [result] to "./platform" + test_report!( + branches_have_more_cases_than_condition, + indoc!( + r#" + foo : Bool -> Str + foo = \bool -> + when bool is + True -> "true" + False -> "false" + Wat -> "surprise!" + foo + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - Hash has - hash : a -> U64 | a has Hash + The branches of this `when` expression don't match the condition: - mulHashes : Hash, Hash -> U64 - mulHashes = \x, y -> hash x * hash y + 6│> when bool is + 7│ True -> "true" + 8│ False -> "false" + 9│ Wat -> "surprise!" - Id := U64 - hash = \@Id n -> n + This `bool` value is a: - Three := {} - hash = \@Three _ -> 3 + Bool - result = mulHashes (@Id 100) (@Three {}) - "# - ), - indoc!( - r#" - ── ABILITY USED AS TYPE ────────────────────────────────── /code/proj/Main.roc ─ + But the branch patterns have type: - You are attempting to use the ability `Hash` as a type directly: + [False, True, Wat] - 6│ mulHashes : Hash, Hash -> U64 - ^^^^ - - Abilities can only be used in type annotations to constrain type - variables. - - Hint: Perhaps you meant to include a `has` annotation, like - - a has Hash - - ── ABILITY USED AS TYPE ────────────────────────────────── /code/proj/Main.roc ─ - - You are attempting to use the ability `Hash` as a type directly: - - 6│ mulHashes : Hash, Hash -> U64 - ^^^^ - - Abilities can only be used in type annotations to constrain type - variables. - - Hint: Perhaps you meant to include a `has` annotation, like - - b has Hash - "# - ), + The branches must be cases of the `when` condition's type! + "# ) - } - - #[test] - fn branches_have_more_cases_than_condition() { - new_report_problem_as( - "branches_have_more_cases_than_condition", - indoc!( - r#" - foo : Bool -> Str - foo = \bool -> - when bool is - True -> "true" - False -> "false" - Wat -> "surprise!" - foo - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The branches of this `when` expression don't match the condition: - - 6│> when bool is - 7│ True -> "true" - 8│ False -> "false" - 9│ Wat -> "surprise!" - - This `bool` value is a: - - Bool - - But the branch patterns have type: - - [False, True, Wat] - - The branches must be cases of the `when` condition's type! - "# - ), - ) - } + ); #[test] fn always_function() { @@ -9774,578 +9724,527 @@ All branches in an `if` must have the same type! ) } - #[test] - fn imports_missing_comma() { - new_report_problem_as( - "imports_missing_comma", - indoc!( - r#" - app "test-missing-comma" - packages { pf: "platform" } - imports [pf.Task Base64] - provides [main, @Foo] to pf - "# - ), - indoc!( - r#" - ── WEIRD IMPORTS ────────────────────────── tmp/imports_missing_comma/Test.roc ─ + test_report!( + imports_missing_comma, + indoc!( + r#" + app "test-missing-comma" + packages { pf: "platform" } + imports [pf.Task Base64] + provides [main, @Foo] to pf + "# + ), + indoc!( + r#" + ── WEIRD IMPORTS ────────────────────────── tmp/imports_missing_comma/Test.roc ─ - I am partway through parsing a imports list, but I got stuck here: + I am partway through parsing a imports list, but I got stuck here: - 2│ packages { pf: "platform" } - 3│ imports [pf.Task Base64] - ^ + 2│ packages { pf: "platform" } + 3│ imports [pf.Task Base64] + ^ - I am expecting a comma or end of list, like + I am expecting a comma or end of list, like - imports [Shape, Vector]"# - ), + imports [Shape, Vector]"# ) - } + ); - #[test] - fn not_enough_cases_for_open_union() { - new_report_problem_as( - "branches_have_more_cases_than_condition", - indoc!( - r#" - foo : [A, B]a -> Str - foo = \it -> - when it is - A -> "" - foo - "# - ), - indoc!( - r#" - ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ + test_report!( + not_enough_cases_for_open_union, + indoc!( + r#" + foo : [A, B]a -> Str + foo = \it -> + when it is + A -> "" + foo + "# + ), + indoc!( + r#" + ── UNSAFE PATTERN ──────────────────────────────────────── /code/proj/Main.roc ─ - This `when` does not cover all the possibilities: + This `when` does not cover all the possibilities: - 6│> when it is - 7│> A -> "" + 6│> when it is + 7│> A -> "" - Other possibilities include: + Other possibilities include: - B - _ + B + _ - I would have to crash if I saw one of those! Add branches for them! - "# - ), + I would have to crash if I saw one of those! Add branches for them! + "# ) - } + ); - #[test] - fn issue_2778_specialization_is_not_a_redundant_pattern() { - new_report_problem_as( - "issue_2778_specialization_is_not_a_redundant_pattern", - indoc!( - r#" - formatColor = \color -> - when color is - Red -> "red" - Yellow -> "yellow" - _ -> "unknown" + test_report!( + issue_2778_specialization_is_not_a_redundant_pattern, + indoc!( + r#" + formatColor = \color -> + when color is + Red -> "red" + Yellow -> "yellow" + _ -> "unknown" - Red |> formatColor |> Str.concat (formatColor Orange) - "# - ), - "", // no problem - ) - } + Red |> formatColor |> Str.concat (formatColor Orange) + "# + ), + "" // no problem + ); - #[test] - fn nested_specialization() { - new_report_problem_as( - "nested_specialization", - indoc!( - r#" - app "test" provides [main] to "./platform" + test_report!( + nested_specialization, + indoc!( + r#" + app "test" provides [main] to "./platform" - Default has default : {} -> a | a has Default - - main = - A := {} - default = \{} -> @A {} - default {} - "# - ), - indoc!( - r#" - ── SPECIALIZATION NOT ON TOP-LEVEL ─────────────────────── /code/proj/Main.roc ─ - - This specialization of the `default` ability member is in a nested - scope: - - 7│ default = \{} -> @A {} - ^^^^^^^ - - Specializations can only be defined on the top-level of a module. - "# - ), - ) - } - - #[test] - fn recursion_var_specialization_error() { - new_report_problem_as( - "recursion_var_specialization_error", - indoc!( - r#" - Job a : [Job (List (Job a))] - - job : Job Str - - when job is - Job lst -> lst == "" - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 2nd argument to `isEq` is not what I expect: - - 9│ Job lst -> lst == "" - ^^ - - This argument is a string of type: - - Str - - But `isEq` needs the 2nd argument to be: - - List [Job ∞] as ∞ - "# - ), - ) - } - - #[test] - fn type_error_in_apply_is_circular() { - new_report_problem_as( - "type_error_in_apply_is_circular", - indoc!( - r#" - app "test" provides [go] to "./platform" - - S a : { set : Set a } - - go : a, S a -> Result (List a) * - go = \goal, model -> - if goal == goal - then Ok [] - else - new = { model & set : Set.remove goal model.set } - go goal new - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - The 1st argument to `remove` is not what I expect: - - 10│ new = { model & set : Set.remove goal model.set } - ^^^^ - - This `goal` value is a: - - a - - But `remove` needs the 1st argument to be: - - Set a - - Tip: The type annotation uses the type variable `a` to say that this - definition can produce any type of value. But in the body I see that - it will only produce a `Set` value of a single specific type. Maybe - change the type annotation to be more specific? Maybe change the code - to be more general? - - ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ - - I'm inferring a weird self-referential type for `new`: - - 10│ new = { model & set : Set.remove goal model.set } - ^^^ - - Here is my best effort at writing down the type. You will see ∞ for - parts of the type that repeat something already printed out - infinitely. - - { set : Set ∞ } - - ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ - - I'm inferring a weird self-referential type for `goal`: - - 6│ go = \goal, model -> - ^^^^ - - Here is my best effort at writing down the type. You will see ∞ for - parts of the type that repeat something already printed out - infinitely. - - Set ∞ - "# - ), - ) - } - - #[test] - fn cycle_through_non_function() { - new_report_problem_as( - "cycle_through_non_function", - indoc!( - r#" - force : ({} -> I64) -> I64 - force = \eval -> eval {} - - t1 = \_ -> force (\_ -> t2) - - t2 = t1 {} - - t2 - "# - ), - indoc!( - r#" - ── CIRCULAR DEFINITION ─────────────────────────────────── /code/proj/Main.roc ─ - - The `t1` definition is causing a very tricky infinite loop: - - 7│ t1 = \_ -> force (\_ -> t2) - ^^ - - The `t1` value depends on itself through the following chain of - definitions: - - ┌─────┐ - │ t1 - │ ↓ - │ t2 - └─────┘ - "# - ), - ) - } - - #[test] - fn function_does_not_implement_encoding() { - new_report_problem_as( - "function_does_not_implement_encoding", - indoc!( - r#" - app "test" imports [Encode] provides [main] to "./platform" - - main = Encode.toEncoder \x -> x - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression has a type that does not implement the abilities it's expected to: - - 3│ main = Encode.toEncoder \x -> x - ^^^^^^^ - - Roc can't generate an implementation of the `Encode.Encoding` ability - for - - a -> a - - Note: `Encoding` cannot be generated for functions. - "# - ), - ) - } - - #[test] - fn unbound_type_in_record_does_not_implement_encoding() { - new_report_problem_as( - "unbound_type_in_record_does_not_implement_encoding", - indoc!( - r#" - app "test" imports [Encode] provides [main] to "./platform" - - main = \x -> Encode.toEncoder { x: x } - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - - This expression has a type that does not implement the abilities it's expected to: - - 3│ main = \x -> Encode.toEncoder { x: x } - ^^^^^^^^ - - Roc can't generate an implementation of the `Encode.Encoding` ability - for - - { x : a } - - In particular, an implementation for - - a - - cannot be generated. - - Tip: This type variable is not bound to `Encoding`. Consider adding a - `has` clause to bind the type variable, like `| a has Encode.Encoding` - "# - ), - ) - } - - #[test] - fn nested_opaque_does_not_implement_encoding() { - new_report_problem_as( - "nested_opaque_does_not_implement_encoding", - indoc!( - r#" - app "test" imports [Encode] provides [main] to "./platform" + Default has default : {} -> a | a has Default + main = A := {} - main = Encode.toEncoder { x: @A {} } - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + default = \{} -> @A {} + default {} + "# + ), + indoc!( + r#" + ── SPECIALIZATION NOT ON TOP-LEVEL ─────────────────────── /code/proj/Main.roc ─ - This expression has a type that does not implement the abilities it's expected to: + This specialization of the `default` ability member is in a nested + scope: - 4│ main = Encode.toEncoder { x: @A {} } - ^^^^^^^^^^^^ + 7│ default = \{} -> @A {} + ^^^^^^^ - Roc can't generate an implementation of the `Encode.Encoding` ability - for - - { x : A } - - In particular, an implementation for - - A - - cannot be generated. - - Tip: `A` does not implement `Encoding`. Consider adding a custom - implementation or `has Encode.Encoding` to the definition of `A`. - "# - ), + Specializations can only be defined on the top-level of a module. + "# ) - } + ); - #[test] - fn derive_non_builtin_ability() { - new_report_problem_as( - "derive_non_builtin_ability", - indoc!( - r#" - app "test" provides [A] to "./platform" + test_report!( + recursion_var_specialization_error, + indoc!( + r#" + Job a : [Job (List (Job a))] - Ab has ab : a -> a | a has Ab + job : Job Str - A := {} has [Ab] - "# - ), - indoc!( - r#" - ── ILLEGAL DERIVE ──────────────────────────────────────── /code/proj/Main.roc ─ + when job is + Job lst -> lst == "" + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - This ability cannot be derived: + The 2nd argument to `isEq` is not what I expect: - 5│ A := {} has [Ab] - ^^ + 9│ Job lst -> lst == "" + ^^ - Only builtin abilities can be derived. + This argument is a string of type: - Note: The builtin abilities are `Encode.Encoding` - "# - ), + Str + + But `isEq` needs the 2nd argument to be: + + List [Job ∞] as ∞ + "# ) - } + ); - #[test] - fn has_encoding_for_function() { - new_report_problem_as( - "has_encoding_for_function", - indoc!( - r#" - app "test" imports [Encode] provides [A] to "./platform" + test_report!( + type_error_in_apply_is_circular, + indoc!( + r#" + app "test" provides [go] to "./platform" - A a := a -> a has [Encode.Encoding] - "# - ), - indoc!( - r#" - ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + S a : { set : Set a } - Roc can't derive an implementation of the `Encode.Encoding` for `A`: + go : a, S a -> Result (List a) * + go = \goal, model -> + if goal == goal + then Ok [] + else + new = { model & set : Set.remove goal model.set } + go goal new + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - 3│ A a := a -> a has [Encode.Encoding] - ^^^^^^^^^^^^^^^ + The 1st argument to `remove` is not what I expect: - Note: `Encoding` cannot be generated for functions. + 10│ new = { model & set : Set.remove goal model.set } + ^^^^ - Tip: You can define a custom implementation of `Encode.Encoding` for `A`. - "# - ), + This `goal` value is a: + + a + + But `remove` needs the 1st argument to be: + + Set a + + Tip: The type annotation uses the type variable `a` to say that this + definition can produce any type of value. But in the body I see that + it will only produce a `Set` value of a single specific type. Maybe + change the type annotation to be more specific? Maybe change the code + to be more general? + + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `new`: + + 10│ new = { model & set : Set.remove goal model.set } + ^^^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + { set : Set ∞ } + + ── CIRCULAR TYPE ───────────────────────────────────────── /code/proj/Main.roc ─ + + I'm inferring a weird self-referential type for `goal`: + + 6│ go = \goal, model -> + ^^^^ + + Here is my best effort at writing down the type. You will see ∞ for + parts of the type that repeat something already printed out + infinitely. + + Set ∞ + "# ) - } + ); - #[test] - fn has_encoding_for_non_encoding_alias() { - new_report_problem_as( - "has_encoding_for_non_encoding_alias", - indoc!( - r#" - app "test" imports [Encode] provides [A] to "./platform" + test_report!( + cycle_through_non_function, + indoc!( + r#" + force : ({} -> I64) -> I64 + force = \eval -> eval {} - A := B has [Encode.Encoding] + t1 = \_ -> force (\_ -> t2) - B := {} - "# - ), - indoc!( - r#" - ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + t2 = t1 {} - Roc can't derive an implementation of the `Encode.Encoding` for `A`: + t2 + "# + ), + indoc!( + r#" + ── CIRCULAR DEFINITION ─────────────────────────────────── /code/proj/Main.roc ─ - 3│ A := B has [Encode.Encoding] - ^^^^^^^^^^^^^^^ + The `t1` definition is causing a very tricky infinite loop: - Tip: `B` does not implement `Encoding`. Consider adding a custom - implementation or `has Encode.Encoding` to the definition of `B`. + 7│ t1 = \_ -> force (\_ -> t2) + ^^ - Tip: You can define a custom implementation of `Encode.Encoding` for `A`. - "# - ), + The `t1` value depends on itself through the following chain of + definitions: + + ┌─────┐ + │ t1 + │ ↓ + │ t2 + └─────┘ + "# ) - } + ); - #[test] - fn has_encoding_for_other_has_encoding() { - new_report_problem_as( - "has_encoding_for_other_has_encoding", - indoc!( - r#" - app "test" imports [Encode] provides [A] to "./platform" + test_report!( + function_does_not_implement_encoding, + indoc!( + r#" + app "test" imports [Encode] provides [main] to "./platform" - A := B has [Encode.Encoding] + main = Encode.toEncoder \x -> x + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - B := {} has [Encode.Encoding] - "# - ), - indoc!(""), // no error + This expression has a type that does not implement the abilities it's expected to: + + 3│ main = Encode.toEncoder \x -> x + ^^^^^^^ + + Roc can't generate an implementation of the `Encode.Encoding` ability + for + + a -> a + + Note: `Encoding` cannot be generated for functions. + "# ) - } + ); - #[test] - fn has_encoding_for_recursive_deriving() { - new_report_problem_as( - "has_encoding_for_recursive_deriving", - indoc!( - r#" - app "test" imports [Encode] provides [MyNat] to "./platform" + test_report!( + unbound_type_in_record_does_not_implement_encoding, + indoc!( + r#" + app "test" imports [Encode] provides [main] to "./platform" - MyNat := [S MyNat, Z] has [Encode.Encoding] - "# - ), - indoc!(""), // no error + main = \x -> Encode.toEncoder { x: x } + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + This expression has a type that does not implement the abilities it's expected to: + + 3│ main = \x -> Encode.toEncoder { x: x } + ^^^^^^^^ + + Roc can't generate an implementation of the `Encode.Encoding` ability + for + + { x : a } + + In particular, an implementation for + + a + + cannot be generated. + + Tip: This type variable is not bound to `Encoding`. Consider adding a + `has` clause to bind the type variable, like `| a has Encode.Encoding` + "# ) - } + ); - #[test] - fn has_encoding_dominated_by_custom() { - new_report_problem_as( - "has_encoding_dominated_by_custom", - indoc!( - r#" - app "test" imports [Encode.{ Encoding, toEncoder, custom }] provides [A] to "./platform" + test_report!( + nested_opaque_does_not_implement_encoding, + indoc!( + r#" + app "test" imports [Encode] provides [main] to "./platform" - A := {} has [Encode.Encoding] + A := {} + main = Encode.toEncoder { x: @A {} } + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ - toEncoder = \@A {} -> custom \l, _ -> l - "# - ), - indoc!( - r#" - ── CONFLICTING DERIVE AND IMPLEMENTATION ───────────────── /code/proj/Main.roc ─ + This expression has a type that does not implement the abilities it's expected to: - `A` both derives and custom-implements `Encode.Encoding`. We found the - derive here: + 4│ main = Encode.toEncoder { x: @A {} } + ^^^^^^^^^^^^ - 3│ A := {} has [Encode.Encoding] - ^^^^^^^^^^^^^^^ + Roc can't generate an implementation of the `Encode.Encoding` ability + for - and one custom implementation of `Encode.Encoding` here: + { x : A } - 5│ toEncoder = \@A {} -> custom \l, _ -> l - ^^^^^^^^^ + In particular, an implementation for - Derived and custom implementations can conflict, so one of them needs - to be removed! + A - Note: We'll try to compile your program using the custom - implementation first, and fall-back on the derived implementation if - needed. Make sure to disambiguate which one you want! - "# - ), + cannot be generated. + + Tip: `A` does not implement `Encoding`. Consider adding a custom + implementation or `has Encode.Encoding` to the definition of `A`. + "# ) - } + ); - #[test] - fn issue_1755() { - new_report_problem_as( - "issue_1755", - indoc!( - r#" - Handle := {} + test_report!( + derive_non_builtin_ability, + indoc!( + r#" + app "test" provides [A] to "./platform" - await : Result a err, (a -> Result b err) -> Result b err - open : {} -> Result Handle * - close : Handle -> Result {} * + Ab has ab : a -> a | a has Ab - withOpen : (Handle -> Result {} *) -> Result {} * - withOpen = \callback -> - handle <- await (open {}) - {} <- await (callback handle) - close handle + A := {} has [Ab] + "# + ), + indoc!( + r#" + ── ILLEGAL DERIVE ──────────────────────────────────────── /code/proj/Main.roc ─ - withOpen - "# - ), - indoc!( - r#" - ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + This ability cannot be derived: - Something is off with the body of the `withOpen` definition: + 5│ A := {} has [Ab] + ^^ - 10│ withOpen : (Handle -> Result {} *) -> Result {} * - 11│ withOpen = \callback -> - 12│> handle <- await (open {}) - 13│> {} <- await (callback handle) - 14│> close handle + Only builtin abilities can be derived. - The type annotation on `withOpen` says this `await` call should have the - type: - - Result {} * - - However, the type of this `await` call is connected to another type in a - way that isn't reflected in this annotation. - - Tip: Any connection between types must use a named type variable, not - a `*`! Maybe the annotation on `withOpen` should have a named type - variable in place of the `*`? - "# - ), + Note: The builtin abilities are `Encode.Encoding` + "# ) - } + ); + + test_report!( + has_encoding_for_function, + indoc!( + r#" + app "test" imports [Encode] provides [A] to "./platform" + + A a := a -> a has [Encode.Encoding] + "# + ), + indoc!( + r#" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + Roc can't derive an implementation of the `Encode.Encoding` for `A`: + + 3│ A a := a -> a has [Encode.Encoding] + ^^^^^^^^^^^^^^^ + + Note: `Encoding` cannot be generated for functions. + + Tip: You can define a custom implementation of `Encode.Encoding` for `A`. + "# + ) + ); + + test_report!( + has_encoding_for_non_encoding_alias, + indoc!( + r#" + app "test" imports [Encode] provides [A] to "./platform" + + A := B has [Encode.Encoding] + + B := {} + "# + ), + indoc!( + r#" + ── INCOMPLETE ABILITY IMPLEMENTATION ───────────────────── /code/proj/Main.roc ─ + + Roc can't derive an implementation of the `Encode.Encoding` for `A`: + + 3│ A := B has [Encode.Encoding] + ^^^^^^^^^^^^^^^ + + Tip: `B` does not implement `Encoding`. Consider adding a custom + implementation or `has Encode.Encoding` to the definition of `B`. + + Tip: You can define a custom implementation of `Encode.Encoding` for `A`. + "# + ) + ); + + test_report!( + has_encoding_for_other_has_encoding, + indoc!( + r#" + app "test" imports [Encode] provides [A] to "./platform" + + A := B has [Encode.Encoding] + + B := {} has [Encode.Encoding] + "# + ), + indoc!("") // no error + ); + + test_report!( + has_encoding_for_recursive_deriving, + indoc!( + r#" + app "test" imports [Encode] provides [MyNat] to "./platform" + + MyNat := [S MyNat, Z] has [Encode.Encoding] + "# + ), + indoc!("") // no error + ); + + test_report!( + has_encoding_dominated_by_custom, + indoc!( + r#" + app "test" imports [Encode.{ Encoding, toEncoder, custom }] provides [A] to "./platform" + + A := {} has [Encode.Encoding] + + toEncoder = \@A {} -> custom \l, _ -> l + "# + ), + indoc!( + r#" + ── CONFLICTING DERIVE AND IMPLEMENTATION ───────────────── /code/proj/Main.roc ─ + + `A` both derives and custom-implements `Encode.Encoding`. We found the + derive here: + + 3│ A := {} has [Encode.Encoding] + ^^^^^^^^^^^^^^^ + + and one custom implementation of `Encode.Encoding` here: + + 5│ toEncoder = \@A {} -> custom \l, _ -> l + ^^^^^^^^^ + + Derived and custom implementations can conflict, so one of them needs + to be removed! + + Note: We'll try to compile your program using the custom + implementation first, and fall-back on the derived implementation if + needed. Make sure to disambiguate which one you want! + "# + ) + ); + + test_report!( + issue_1755, + indoc!( + r#" + Handle := {} + + await : Result a err, (a -> Result b err) -> Result b err + open : {} -> Result Handle * + close : Handle -> Result {} * + + withOpen : (Handle -> Result {} *) -> Result {} * + withOpen = \callback -> + handle <- await (open {}) + {} <- await (callback handle) + close handle + + withOpen + "# + ), + indoc!( + r#" + ── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─ + + Something is off with the body of the `withOpen` definition: + + 10│ withOpen : (Handle -> Result {} *) -> Result {} * + 11│ withOpen = \callback -> + 12│> handle <- await (open {}) + 13│> {} <- await (callback handle) + 14│> close handle + + The type annotation on `withOpen` says this `await` call should have the + type: + + Result {} * + + However, the type of this `await` call is connected to another type in a + way that isn't reflected in this annotation. + + Tip: Any connection between types must use a named type variable, not + a `*`! Maybe the annotation on `withOpen` should have a named type + variable in place of the `*`? + "# + ) + ); }