diff --git a/Cargo.lock b/Cargo.lock index 407db63a00..a1247c17c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4797,6 +4797,28 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test_derivers" +version = "0.1.0" +dependencies = [ + "bumpalo", + "indoc", + "lazy_static", + "roc_builtins", + "roc_can", + "roc_collections", + "roc_constrain", + "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..8559892118 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "compiler/mono", "compiler/alias_analysis", "compiler/test_mono", + "compiler/test_derivers", "compiler/load", "compiler/load_internal", "compiler/gen_llvm", 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/load_internal/src/file.rs b/compiler/load_internal/src/file.rs index 13bdbe0d22..d95a07a841 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, @@ -1404,6 +1406,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 +1424,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 +2213,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, @@ -2663,6 +2668,7 @@ fn finish( 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 +2703,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 +3745,7 @@ impl<'a> BuildTask<'a> { } } -fn add_imports( +pub fn add_imports( my_module: ModuleId, subs: &mut Subs, mut pending_abilities: PendingAbilitiesStore, @@ -5013,7 +5020,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/test_derivers/Cargo.toml b/compiler/test_derivers/Cargo.toml new file mode 100644 index 0000000000..52b52143d1 --- /dev/null +++ b/compiler/test_derivers/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "test_derivers" +version = "0.1.0" +authors = ["The Roc Contributors"] +license = "UPL-1.0" +edition = "2021" + +[[test]] +name = "test_derives" +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_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" } +bumpalo = { version = "3.8.0", features = ["collections"] } +lazy_static = "1.4.0" +indoc = "1.0.3" +ven_pretty = { path = "../../vendor/pretty" } diff --git a/compiler/test_derivers/src/encoding.rs b/compiler/test_derivers/src/encoding.rs new file mode 100644 index 0000000000..c6897785f2 --- /dev/null +++ b/compiler/test_derivers/src/encoding.rs @@ -0,0 +1,198 @@ +#![cfg(test)] + +use std::path::PathBuf; + +use bumpalo::Bump; +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_load_internal::file::{add_imports, default_aliases, LoadedModule, Threading}; +use roc_module::{ + ident::ModuleName, + symbol::{IdentIds, Interns, ModuleId}, +}; +use roc_mono::derivers::encoding::Env; +use roc_region::all::{LineInfo, Region}; +use roc_reporting::report::{type_problem, RocDocAllocator}; +use roc_types::{ + subs::{Content, ExposedTypesStorageSubs, FlatType, RecordFields, Subs, Variable}, + types::{RecordField, Type}, +}; +use ven_pretty::DocAllocator; + +use roc_mono::derivers::{encoding, synth_var}; + +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, +) { + let mut constraints = Constraints::new(); + let mut env = roc_constrain::expr::Env { + rigids: Default::default(), + resolutions_to_make: Default::default(), + home: test_module, + }; + let constr = constrain_expr( + &mut constraints, + &mut env, + Region::zero(), + &derived, + Expected::NoExpectation(Type::Variable(test_subs.fresh_unnamed_flex_var())), + ); + 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); + + let (_solved_subs, _, problems, _) = roc_solve::module::run_solve( + &constraints, + constr, + RigidVariables::default(), + test_subs, + default_aliases(), + abilities_store, + Default::default(), + ); + + 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); + } +} + +fn derive_test(synth_input: S) +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(); + let mut test_ident_ids = IdentIds::default(); + + let mut env = Env { + home: test_module, + arena: &arena, + subs: &mut test_subs, + ident_ids: &mut test_ident_ids, + exposed_encode_types: &mut exposed_encode_types, + }; + + let signature_var = synth_input(env.subs); + + let derived = encoding::derive_to_encoder(&mut env, signature_var); + + check_derived_typechecks( + derived, + test_module, + test_subs, + &interns, + exposed_encode_types, + abilities_store, + ); +} + +macro_rules! synth { + (rcd{ $($field:literal: $typ:expr),* }) => { + |subs| { + let fields = RecordFields::insert_into_subs(subs, vec![ $( ($field.into(), RecordField::Required($typ)) ,)* ]); + synth_var(subs, Content::Structure(FlatType::Record(fields, Variable::EMPTY_RECORD))) + } + } +} + +#[test] +fn one_field_record() { + derive_test(synth!(rcd{ "a": Variable::U8 })) +} diff --git a/compiler/test_derivers/src/tests.rs b/compiler/test_derivers/src/tests.rs new file mode 100644 index 0000000000..bd5082c3df --- /dev/null +++ b/compiler/test_derivers/src/tests.rs @@ -0,0 +1,3 @@ +#![cfg(test)] + +mod encoding;