#[macro_use] extern crate pretty_assertions; #[macro_use] extern crate indoc; extern crate bumpalo; mod helpers; #[cfg(test)] mod solve_expr { use crate::helpers::with_larger_debug_stack; use lazy_static::lazy_static; use regex::Regex; use roc_can::{ abilities::ImplKey, traverse::{find_ability_member_and_owning_type_at, find_type_at}, }; use roc_load::LoadedModule; use roc_module::symbol::{Interns, ModuleId}; use roc_packaging::cache::RocCacheDir; use roc_problem::can::Problem; use roc_region::all::{LineColumn, LineColumnRegion, LineInfo, Region}; use roc_reporting::report::{can_problem, type_problem, RocDocAllocator}; use roc_solve_problem::TypeError; use roc_types::{ pretty_print::{name_and_print_var, DebugPrint}, types::MemberImpl, }; use std::path::PathBuf; // HELPERS lazy_static! { static ref RE_TYPE_QUERY: Regex = Regex::new(r#"(?P\^+)(?:\{-(?P\d+)\})?"#).unwrap(); } #[derive(Debug, Clone, Copy)] struct TypeQuery(Region); fn parse_queries(src: &str) -> Vec { let line_info = LineInfo::new(src); let mut queries = vec![]; let mut consecutive_query_lines = 0; for (i, line) in src.lines().enumerate() { let mut queries_on_line = RE_TYPE_QUERY.captures_iter(line).into_iter().peekable(); if queries_on_line.peek().is_none() { consecutive_query_lines = 0; continue; } else { consecutive_query_lines += 1; } for capture in queries_on_line { let wher = capture.name("where").unwrap(); let subtract_col = capture .name("sub") .and_then(|m| str::parse(m.as_str()).ok()) .unwrap_or(0); let (start, end) = (wher.start() as u32, wher.end() as u32); let (start, end) = (start - subtract_col, end - subtract_col); let last_line = i as u32 - consecutive_query_lines; let start_lc = LineColumn { line: last_line, column: start, }; let end_lc = LineColumn { line: last_line, column: end, }; let lc_region = LineColumnRegion::new(start_lc, end_lc); let region = line_info.convert_line_column_region(lc_region); queries.push(TypeQuery(region)); } } queries } fn run_load_and_infer(src: &str) -> Result<(LoadedModule, String), std::io::Error> { use bumpalo::Bump; use tempfile::tempdir; let arena = &Bump::new(); let module_src; let temp; if src.starts_with("app") { // this is already a module module_src = src; } else { // this is an expression, promote it to a module temp = promote_expr_to_module(src); module_src = &temp; } let loaded = { let dir = tempdir()?; let filename = PathBuf::from("Test.roc"); let file_path = dir.path().join(filename); let result = roc_load::load_and_typecheck_str( arena, file_path, module_src, dir.path().to_path_buf(), roc_target::TargetInfo::default_x86_64(), roc_reporting::report::RenderTarget::Generic, RocCacheDir::Disallowed, roc_reporting::report::DEFAULT_PALETTE, ); dir.close()?; result }; let loaded = loaded.expect("failed to load module"); Ok((loaded, module_src.to_string())) } fn format_problems( src: &str, home: ModuleId, interns: &Interns, can_problems: Vec, type_problems: Vec, ) -> (String, String) { let filename = PathBuf::from("test.roc"); let src_lines: Vec<&str> = src.split('\n').collect(); let lines = LineInfo::new(src); let alloc = RocDocAllocator::new(&src_lines, home, interns); let mut can_reports = vec![]; let mut type_reports = vec![]; for problem in can_problems { let report = can_problem(&alloc, &lines, filename.clone(), problem.clone()); can_reports.push(report.pretty(&alloc)); } for problem in type_problems { if let Some(report) = type_problem(&alloc, &lines, filename.clone(), problem.clone()) { type_reports.push(report.pretty(&alloc)); } } let mut can_reports_buf = String::new(); let mut type_reports_buf = String::new(); use roc_reporting::report::CiWrite; alloc .stack(can_reports) .1 .render_raw(70, &mut CiWrite::new(&mut can_reports_buf)) .unwrap(); alloc .stack(type_reports) .1 .render_raw(70, &mut CiWrite::new(&mut type_reports_buf)) .unwrap(); (can_reports_buf, type_reports_buf) } fn infer_eq_help(src: &str) -> Result<(String, String, String), std::io::Error> { let ( LoadedModule { module_id: home, mut can_problems, mut type_problems, interns, mut solved, mut exposed_to_host, abilities_store, .. }, src, ) = run_load_and_infer(src)?; let mut can_problems = can_problems.remove(&home).unwrap_or_default(); let type_problems = type_problems.remove(&home).unwrap_or_default(); // Disregard UnusedDef problems, because those are unavoidable when // returning a function from the test expression. can_problems.retain(|prob| { !matches!( prob, roc_problem::can::Problem::UnusedDef(_, _) | roc_problem::can::Problem::UnusedBranchDef(..) ) }); let (can_problems, type_problems) = format_problems(&src, home, &interns, can_problems, type_problems); let subs = solved.inner_mut(); exposed_to_host.retain(|s, _| !abilities_store.is_specialization_name(*s)); debug_assert!(exposed_to_host.len() == 1, "{:?}", exposed_to_host); let (_symbol, variable) = exposed_to_host.into_iter().next().unwrap(); let actual_str = name_and_print_var(variable, subs, home, &interns, DebugPrint::NOTHING); Ok((type_problems, can_problems, actual_str)) } fn promote_expr_to_module(src: &str) -> String { let mut buffer = String::from(indoc!( r#" app "test" imports [] provides [main] to "./platform" main = "# )); for line in src.lines() { // indent the body! buffer.push_str(" "); buffer.push_str(line); buffer.push('\n'); } buffer } fn infer_eq(src: &str, expected: &str) { let (_, can_problems, actual) = infer_eq_help(src).unwrap(); assert!( can_problems.is_empty(), "Canonicalization problems: {}", can_problems ); assert_eq!(actual, expected.to_string()); } fn infer_eq_without_problem(src: &str, expected: &str) { let (type_problems, can_problems, actual) = infer_eq_help(src).unwrap(); assert!( can_problems.is_empty(), "Canonicalization problems: {}", can_problems ); if !type_problems.is_empty() { // fail with an assert, but print the problems normally so rust doesn't try to diff // an empty vec with the problems. panic!( "expected:\n{:?}\ninferred:\n{:?}\nproblems:\n{}", expected, actual, type_problems, ); } assert_eq!(actual, expected.to_string()); } #[derive(Default)] struct InferOptions { print_can_decls: bool, print_only_under_alias: bool, allow_errors: bool, } fn infer_queries_help(src: &str, expected: impl FnOnce(&str), options: InferOptions) { let ( LoadedModule { module_id: home, mut can_problems, mut type_problems, mut declarations_by_id, mut solved, interns, abilities_store, .. }, src, ) = run_load_and_infer(src).unwrap(); let decls = declarations_by_id.remove(&home).unwrap(); let subs = solved.inner_mut(); let can_problems = can_problems.remove(&home).unwrap_or_default(); let type_problems = type_problems.remove(&home).unwrap_or_default(); let (can_problems, type_problems) = format_problems(&src, home, &interns, can_problems, type_problems); if !options.allow_errors { assert!( can_problems.is_empty(), "Canonicalization problems: {}", can_problems ); assert!(type_problems.is_empty(), "Type problems: {}", type_problems); } let queries = parse_queries(&src); assert!(!queries.is_empty(), "No queries provided!"); let mut output_parts = Vec::with_capacity(queries.len() + 2); if options.print_can_decls { use roc_can::debug::{pretty_print_declarations, PPCtx}; let ctx = PPCtx { home, interns: &interns, print_lambda_names: true, }; let pretty_decls = pretty_print_declarations(&ctx, &decls); output_parts.push(pretty_decls); output_parts.push("\n".to_owned()); } for TypeQuery(region) in queries.into_iter() { let start = region.start().offset; let end = region.end().offset; let text = &src[start as usize..end as usize]; let var = find_type_at(region, &decls) .unwrap_or_else(|| panic!("No type for {:?} ({:?})!", &text, region)); let snapshot = subs.snapshot(); let actual_str = name_and_print_var( var, subs, home, &interns, DebugPrint { print_lambda_sets: true, print_only_under_alias: options.print_only_under_alias, ignore_polarity: true, print_weakened_vars: true, }, ); subs.rollback_to(snapshot); let elaborated = match find_ability_member_and_owning_type_at(region, &decls, &abilities_store) { Some((spec_type, spec_symbol)) => { format!( "{}#{}({}) : {}", spec_type.as_str(&interns), text, spec_symbol.ident_id().index(), actual_str ) } None => { format!("{} : {}", text, actual_str) } }; output_parts.push(elaborated); } let pretty_output = output_parts.join("\n"); expected(&pretty_output); } macro_rules! infer_queries { ($program:expr, @$queries:literal $($option:ident: $value:expr)*) => { infer_queries_help($program, |golden| insta::assert_snapshot!(golden, @$queries), InferOptions { $($option: $value,)* ..InferOptions::default() }) }; } fn check_inferred_abilities<'a, I>(src: &'a str, expected_specializations: I) where I: IntoIterator, { let LoadedModule { module_id: home, mut can_problems, mut type_problems, interns, abilities_store, .. } = run_load_and_infer(src).unwrap().0; let can_problems = can_problems.remove(&home).unwrap_or_default(); let type_problems = type_problems.remove(&home).unwrap_or_default(); assert_eq!(can_problems, Vec::new(), "Canonicalization problems: "); if !type_problems.is_empty() { eprintln!("{:?}", type_problems); panic!(); } let known_specializations = abilities_store.iter_declared_implementations().filter_map( |(impl_key, member_impl)| match member_impl { MemberImpl::Impl(impl_symbol) => { let specialization = abilities_store.specialization_info(*impl_symbol).expect( "declared implementations should be resolved conclusively after solving", ); Some((impl_key, specialization.clone())) } MemberImpl::Error => None, }, ); use std::collections::HashSet; let pretty_specializations = known_specializations .into_iter() .map(|(impl_key, _)| { let ImplKey { opaque, ability_member, } = impl_key; let member_data = abilities_store.member_def(ability_member).unwrap(); let member_str = ability_member.as_str(&interns); let ability_str = member_data.parent_ability.as_str(&interns); ( format!("{}:{}", ability_str, member_str), opaque.as_str(&interns), ) }) .collect::>(); for (parent, specialization) in expected_specializations.into_iter() { let has_the_one = pretty_specializations .iter() // references are annoying so we do this .any(|(p, s)| p == parent && s == &specialization); assert!( has_the_one, "{:#?} not in {:#?}", (parent, specialization), pretty_specializations, ); } } #[test] fn int_literal() { infer_eq("5", "Num *"); } #[test] fn float_literal() { infer_eq("0.5", "Float *"); } #[test] fn dec_literal() { infer_eq( indoc!( r#" val : Dec val = 1.2 val "# ), "Dec", ); } #[test] fn string_literal() { infer_eq( indoc!( r#" "type inference!" "# ), "Str", ); } #[test] fn empty_string() { infer_eq( indoc!( r#" "" "# ), "Str", ); } #[test] fn string_starts_with() { infer_eq_without_problem( indoc!( r#" Str.startsWith "# ), "Str, Str -> Bool", ); } #[test] fn string_from_int() { infer_eq_without_problem( indoc!( r#" Num.toStr "# ), "Num * -> Str", ); } #[test] fn string_from_utf8() { infer_eq_without_problem( indoc!( r#" Str.fromUtf8 "# ), "List U8 -> Result Str [BadUtf8 Utf8ByteProblem Nat]", ); } #[test] fn choose_correct_recursion_var_under_record() { infer_queries!( indoc!( r#" Parser : [ Specialize Parser, Record (List {parser: Parser}), ] printCombinatorParser : Parser -> Str printCombinatorParser = \parser -> when parser is # ^^^^^^ Specialize p -> printed = printCombinatorParser p if Bool.false then printed else "foo" Record fields -> fields |> List.map \f -> printed = printCombinatorParser f.parser if Bool.false then printed else "foo" |> List.first |> Result.withDefault ("foo") printCombinatorParser (Record []) "# ), @r###" parser : [Record (List { parser : a }), Specialize a] as a "### print_only_under_alias: true ); } // #[test] // fn block_string_literal() { // infer_eq( // indoc!( // r#" // """type // inference!""" // "# // ), // "Str", // ); // } // LIST #[test] fn empty_list() { infer_eq( indoc!( r#" [] "# ), "List *", ); } #[test] fn list_of_lists() { infer_eq( indoc!( r#" [[]] "# ), "List (List *)", ); } #[test] fn triple_nested_list() { infer_eq( indoc!( r#" [[[]]] "# ), "List (List (List *))", ); } #[test] fn nested_empty_list() { infer_eq( indoc!( r#" [[], [[]]] "# ), "List (List (List *))", ); } #[test] fn list_of_one_int() { infer_eq( indoc!( r#" [42] "# ), "List (Num *)", ); } #[test] fn triple_nested_int_list() { infer_eq( indoc!( r#" [[[5]]] "# ), "List (List (List (Num *)))", ); } #[test] fn list_of_ints() { infer_eq( indoc!( r#" [1, 2, 3] "# ), "List (Num *)", ); } #[test] fn nested_list_of_ints() { infer_eq( indoc!( r#" [[1], [2, 3]] "# ), "List (List (Num *))", ); } #[test] fn list_of_one_string() { infer_eq( indoc!( r#" ["cowabunga"] "# ), "List Str", ); } #[test] fn triple_nested_string_list() { infer_eq( indoc!( r#" [[["foo"]]] "# ), "List (List (List Str))", ); } #[test] fn list_of_strings() { infer_eq( indoc!( r#" ["foo", "bar"] "# ), "List Str", ); } // INTERPOLATED STRING #[test] fn infer_interpolated_string() { infer_eq( indoc!( r#" whatItIs = "great" "type inference is \(whatItIs)!" "# ), "Str", ); } #[test] fn infer_interpolated_var() { infer_eq( indoc!( r#" whatItIs = "great" str = "type inference is \(whatItIs)!" whatItIs "# ), "Str", ); } #[test] fn infer_interpolated_field() { infer_eq( indoc!( r#" rec = { whatItIs: "great" } str = "type inference is \(rec.whatItIs)!" rec "# ), "{ whatItIs : Str }", ); } // LIST MISMATCH #[test] fn mismatch_heterogeneous_list() { infer_eq( indoc!( r#" ["foo", 5] "# ), "List ", ); } #[test] fn mismatch_heterogeneous_nested_list() { infer_eq( indoc!( r#" [["foo", 5]] "# ), "List (List )", ); } #[test] fn mismatch_heterogeneous_nested_empty_list() { infer_eq( indoc!( r#" [[1], [[]]] "# ), "List ", ); } // CLOSURE #[test] fn always_return_empty_record() { infer_eq( indoc!( r#" \_ -> {} "# ), "* -> {}", ); } #[test] fn two_arg_return_int() { infer_eq( indoc!( r#" \_, _ -> 42 "# ), "*, * -> Num *", ); } #[test] fn three_arg_return_string() { infer_eq( indoc!( r#" \_, _, _ -> "test!" "# ), "*, *, * -> Str", ); } // DEF #[test] fn def_empty_record() { infer_eq( indoc!( r#" foo = {} foo "# ), "{}", ); } #[test] fn def_string() { infer_eq( indoc!( r#" str = "thing" str "# ), "Str", ); } #[test] fn def_1_arg_closure() { infer_eq( indoc!( r#" fn = \_ -> {} fn "# ), "* -> {}", ); } #[test] fn applied_tag() { infer_eq_without_problem( indoc!( r#" List.map ["a", "b"] \elem -> Foo elem "# ), "List [Foo Str]", ) } // Tests (TagUnion, Func) #[test] fn applied_tag_function() { infer_eq_without_problem( indoc!( r#" foo = Foo foo "hi" "# ), "[Foo Str]", ) } // Tests (TagUnion, Func) #[test] fn applied_tag_function_list_map() { infer_eq_without_problem( indoc!( r#" List.map ["a", "b"] Foo "# ), "List [Foo Str]", ) } // Tests (TagUnion, Func) #[test] fn applied_tag_function_list() { infer_eq_without_problem( indoc!( r#" [\x -> Bar x, Foo] "# ), "List (a -> [Bar a, Foo a])", ) } // Tests (Func, TagUnion) #[test] fn applied_tag_function_list_other_way() { infer_eq_without_problem( indoc!( r#" [Foo, \x -> Bar x] "# ), "List (a -> [Bar a, Foo a])", ) } // Tests (Func, TagUnion) #[test] fn applied_tag_function_record() { infer_eq_without_problem( indoc!( r#" foo0 = Foo foo1 = Foo foo2 = Foo { x: [foo0, Foo], y: [foo1, \x -> Foo x], z: [foo2, \x,y -> Foo x y] } "# ), "{ x : List [Foo], y : List (a -> [Foo a]), z : List (b, c -> [Foo b c]) }", ) } // Tests (TagUnion, Func) #[test] fn applied_tag_function_with_annotation() { infer_eq_without_problem( indoc!( r#" x : List [Foo I64] x = List.map [1, 2] Foo x "# ), "List [Foo I64]", ) } #[test] fn def_2_arg_closure() { infer_eq( indoc!( r#" func = \_, _ -> 42 func "# ), "*, * -> Num *", ); } #[test] fn def_3_arg_closure() { infer_eq( indoc!( r#" f = \_, _, _ -> "test!" f "# ), "*, *, * -> Str", ); } #[test] fn def_multiple_functions() { infer_eq( indoc!( r#" a = \_, _, _ -> "test!" b = a b "# ), "*, *, * -> Str", ); } #[test] fn def_multiple_strings() { infer_eq( indoc!( r#" a = "test!" b = a b "# ), "Str", ); } #[test] fn def_multiple_ints() { infer_eq( indoc!( r#" c = b b = a a = 42 c "# ), "Num *", ); } #[test] fn def_returning_closure() { infer_eq( indoc!( r#" f = \z -> z g = \z -> z (\x -> a = f x b = g x x ) "# ), "a -> a", ); } // CALLING FUNCTIONS #[test] fn call_returns_int() { infer_eq( indoc!( r#" alwaysFive = \_ -> 5 alwaysFive "stuff" "# ), "Num *", ); } #[test] fn identity_returns_given_type() { infer_eq( indoc!( r#" identity = \a -> a identity "hi" "# ), "Str", ); } #[test] fn identity_infers_principal_type() { infer_eq( indoc!( r#" identity = \x -> x y = identity 5 identity "# ), "a -> a", ); } #[test] fn identity_works_on_incompatible_types() { infer_eq( indoc!( r#" identity = \a -> a x = identity 5 y = identity "hi" x "# ), "Num *", ); } #[test] fn call_returns_list() { infer_eq( indoc!( r#" enlist = \val -> [val] enlist 5 "# ), "List (Num *)", ); } #[test] fn indirect_always() { infer_eq( indoc!( r#" always = \val -> (\_ -> val) alwaysFoo = always "foo" alwaysFoo 42 "# ), "Str", ); } #[test] fn pizza_desugar() { infer_eq( indoc!( r#" 1 |> (\a -> a) "# ), "Num *", ); } #[test] fn pizza_desugar_two_arguments() { infer_eq( indoc!( r#" always2 = \a, _ -> a 1 |> always2 "foo" "# ), "Num *", ); } #[test] fn anonymous_identity() { infer_eq( indoc!( r#" (\a -> a) 3.14 "# ), "Float *", ); } #[test] fn identity_of_identity() { infer_eq( indoc!( r#" (\val -> val) (\val -> val) "# ), "a -> a", ); } #[test] fn recursive_identity() { infer_eq( indoc!( r#" identity = \val -> val identity identity "# ), "a -> a", ); } #[test] fn identity_function() { infer_eq( indoc!( r#" \val -> val "# ), "a -> a", ); } #[test] fn use_apply() { infer_eq( indoc!( r#" identity = \a -> a apply = \f, x -> f x apply identity 5 "# ), "Num *", ); } #[test] fn apply_function() { infer_eq( indoc!( r#" \f, x -> f x "# ), "(a -> b), a -> b", ); } // #[test] // TODO FIXME this should pass, but instead fails to canonicalize // fn use_flip() { // infer_eq( // indoc!( // r#" // flip = \f -> (\a b -> f b a) // neverendingInt = \f int -> f int // x = neverendingInt (\a -> a) 5 // flip neverendingInt // "# // ), // "(Num *, (a -> a)) -> Num *", // ); // } #[test] fn flip_function() { infer_eq( indoc!( r#" \f -> (\a, b -> f b a) "# ), "(a, b -> c) -> (b, a -> c)", ); } #[test] fn always_function() { infer_eq( indoc!( r#" \val -> \_ -> val "# ), "a -> (* -> a)", ); } #[test] fn pass_a_function() { infer_eq( indoc!( r#" \f -> f {} "# ), "({} -> a) -> a", ); } // OPERATORS // #[test] // fn div_operator() { // infer_eq( // indoc!( // r#" // \l r -> l / r // "# // ), // "F64, F64 -> F64", // ); // } // #[test] // fn basic_float_division() { // infer_eq( // indoc!( // r#" // 1 / 2 // "# // ), // "F64", // ); // } // #[test] // fn basic_int_division() { // infer_eq( // indoc!( // r#" // 1 // 2 // "# // ), // "Num *", // ); // } // #[test] // fn basic_addition() { // infer_eq( // indoc!( // r#" // 1 + 2 // "# // ), // "Num *", // ); // } // #[test] // fn basic_circular_type() { // infer_eq( // indoc!( // r#" // \x -> x x // "# // ), // "", // ); // } // #[test] // fn y_combinator_has_circular_type() { // assert_eq!( // infer(indoc!(r#" // \f -> (\x -> f x x) (\x -> f x x) // "#)), // Erroneous(Problem::CircularType) // ); // } // #[test] // fn no_higher_ranked_types() { // // This should error because it can't type of alwaysFive // infer_eq( // indoc!( // r#" // \always -> [always [], always ""] // "# // ), // "", // ); // } #[test] fn always_with_list() { infer_eq( indoc!( r#" alwaysFive = \_ -> 5 [alwaysFive "foo", alwaysFive []] "# ), "List (Num *)", ); } #[test] fn if_with_int_literals() { infer_eq( indoc!( r#" if Bool.true then 42 else 24 "# ), "Num *", ); } #[test] fn when_with_int_literals() { infer_eq( indoc!( r#" when 1 is 1 -> 2 3 -> 4 "# ), "Num *", ); } // RECORDS #[test] fn empty_record() { infer_eq("{}", "{}"); } #[test] fn one_field_record() { infer_eq("{ x: 5 }", "{ x : Num * }"); } #[test] fn two_field_record() { infer_eq("{ x: 5, y : 3.14 }", "{ x : Num *, y : Float * }"); } #[test] fn record_literal_accessor() { infer_eq("{ x: 5, y : 3.14 }.x", "Num *"); } #[test] fn record_literal_accessor_function() { infer_eq(".x { x: 5, y : 3.14 }", "Num *"); } #[test] fn tuple_literal_accessor() { infer_eq("(5, 3.14 ).0", "Num *"); } #[test] fn tuple_literal_accessor_function() { infer_eq(".0 (5, 3.14 )", "Num *"); } #[test] fn tuple_literal_ty() { infer_eq("(5, 3.14 )", "( Num *, Float * )*"); } #[test] fn tuple_literal_accessor_ty() { infer_eq(".0", "( a )* -> a"); infer_eq(".4", "( _, _, _, _, a )* -> a"); infer_eq(".5", "( ... 5 omitted, a )* -> a"); infer_eq(".200", "( ... 200 omitted, a )* -> a"); } #[test] fn tuple_accessor_generalization() { infer_eq( indoc!( r#" get0 = .0 { a: get0 (1, 2), b: get0 ("a", "b", "c") } "# ), "{ a : Num *, b : Str }", ); } #[test] fn record_arg() { infer_eq("\\rec -> rec.x", "{ x : a }* -> a"); } #[test] fn record_with_bound_var() { infer_eq( indoc!( r#" fn = \rec -> x = rec.x rec fn "# ), "{ x : a }b -> { x : a }b", ); } #[test] fn using_type_signature() { infer_eq( indoc!( r#" bar : custom -> custom bar = \x -> x bar "# ), "custom -> custom", ); } #[test] fn type_signature_without_body() { infer_eq( indoc!( r#" foo: Str -> {} foo "hi" "# ), "{}", ); } #[test] fn type_signature_without_body_rigid() { infer_eq( indoc!( r#" foo : Num * -> custom foo 2 "# ), "custom", ); } #[test] fn accessor_function() { infer_eq(".foo", "{ foo : a }* -> a"); } #[test] fn type_signature_without_body_record() { infer_eq( indoc!( r#" { x, y } : { x : ({} -> custom), y : {} } x "# ), "{} -> custom", ); } #[test] fn empty_record_pattern() { infer_eq( indoc!( r#" # technically, an empty record can be destructured thunk = \{} -> 42 xEmpty = if thunk {} == 42 then { x: {} } else { x: {} } when xEmpty is { x: {} } -> {} "# ), "{}", ); } #[test] fn record_type_annotation() { // check that a closed record remains closed infer_eq( indoc!( r#" foo : { x : custom } -> custom foo = \{ x } -> x foo "# ), "{ x : custom } -> custom", ); } #[test] fn record_update() { infer_eq( indoc!( r#" user = { year: "foo", name: "Sam" } { user & year: "foo" } "# ), "{ name : Str, year : Str }", ); } #[test] fn bare_tag() { infer_eq( indoc!( r#" Foo "# ), "[Foo]", ); } #[test] fn single_tag_pattern() { infer_eq( indoc!( r#" \Foo -> 42 "# ), "[Foo] -> Num *", ); } #[test] fn two_tag_pattern() { infer_eq( indoc!( r#" \x -> when x is True -> 1 False -> 0 "# ), "[False, True] -> Num *", ); } #[test] fn tag_application() { infer_eq( indoc!( r#" Foo "happy" 12 "# ), "[Foo Str (Num *)]", ); } #[test] fn record_extraction() { infer_eq( indoc!( r#" f = \x -> when x is { a, b: _ } -> a f "# ), "{ a : a, b : * }* -> a", ); } #[test] fn record_field_pattern_match_with_guard() { infer_eq( indoc!( r#" when { x: 5 } is { x: 4 } -> 4 "# ), "Num *", ); } #[test] fn tag_union_pattern_match() { infer_eq( indoc!( r#" \Foo x -> Foo x "# ), "[Foo a] -> [Foo a]", ); } #[test] fn tag_union_pattern_match_ignored_field() { infer_eq( indoc!( r#" \Foo x _ -> Foo x "y" "# ), "[Foo a *] -> [Foo a Str]", ); } #[test] fn tag_with_field() { infer_eq( indoc!( r#" when Foo "blah" is Foo x -> x "# ), "Str", ); } #[test] fn qualified_annotation_num_integer() { infer_eq( indoc!( r#" int : Num.Num (Num.Integer Num.Signed64) int "# ), "I64", ); } #[test] fn qualified_annotated_num_integer() { infer_eq( indoc!( r#" int : Num.Num (Num.Integer Num.Signed64) int = 5 int "# ), "I64", ); } #[test] fn annotation_num_integer() { infer_eq( indoc!( r#" int : Num (Integer Signed64) int "# ), "I64", ); } #[test] fn annotated_num_integer() { infer_eq( indoc!( r#" int : Num (Integer Signed64) int = 5 int "# ), "I64", ); } #[test] fn qualified_annotation_using_i128() { infer_eq( indoc!( r#" int : Num.I128 int "# ), "I128", ); } #[test] fn qualified_annotated_using_i128() { infer_eq( indoc!( r#" int : Num.I128 int = 5 int "# ), "I128", ); } #[test] fn annotation_using_i128() { infer_eq( indoc!( r#" int : I128 int "# ), "I128", ); } #[test] fn annotated_using_i128() { infer_eq( indoc!( r#" int : I128 int = 5 int "# ), "I128", ); } #[test] fn qualified_annotation_using_u128() { infer_eq( indoc!( r#" int : Num.U128 int "# ), "U128", ); } #[test] fn qualified_annotated_using_u128() { infer_eq( indoc!( r#" int : Num.U128 int = 5 int "# ), "U128", ); } #[test] fn annotation_using_u128() { infer_eq( indoc!( r#" int : U128 int "# ), "U128", ); } #[test] fn annotated_using_u128() { infer_eq( indoc!( r#" int : U128 int = 5 int "# ), "U128", ); } #[test] fn qualified_annotation_using_i64() { infer_eq( indoc!( r#" int : Num.I64 int "# ), "I64", ); } #[test] fn qualified_annotated_using_i64() { infer_eq( indoc!( r#" int : Num.I64 int = 5 int "# ), "I64", ); } #[test] fn annotation_using_i64() { infer_eq( indoc!( r#" int : I64 int "# ), "I64", ); } #[test] fn annotated_using_i64() { infer_eq( indoc!( r#" int : I64 int = 5 int "# ), "I64", ); } #[test] fn qualified_annotation_using_u64() { infer_eq( indoc!( r#" int : Num.U64 int "# ), "U64", ); } #[test] fn qualified_annotated_using_u64() { infer_eq( indoc!( r#" int : Num.U64 int = 5 int "# ), "U64", ); } #[test] fn annotation_using_u64() { infer_eq( indoc!( r#" int : U64 int "# ), "U64", ); } #[test] fn annotated_using_u64() { infer_eq( indoc!( r#" int : U64 int = 5 int "# ), "U64", ); } #[test] fn qualified_annotation_using_i32() { infer_eq( indoc!( r#" int : Num.I32 int "# ), "I32", ); } #[test] fn qualified_annotated_using_i32() { infer_eq( indoc!( r#" int : Num.I32 int = 5 int "# ), "I32", ); } #[test] fn annotation_using_i32() { infer_eq( indoc!( r#" int : I32 int "# ), "I32", ); } #[test] fn annotated_using_i32() { infer_eq( indoc!( r#" int : I32 int = 5 int "# ), "I32", ); } #[test] fn qualified_annotation_using_u32() { infer_eq( indoc!( r#" int : Num.U32 int "# ), "U32", ); } #[test] fn qualified_annotated_using_u32() { infer_eq( indoc!( r#" int : Num.U32 int = 5 int "# ), "U32", ); } #[test] fn annotation_using_u32() { infer_eq( indoc!( r#" int : U32 int "# ), "U32", ); } #[test] fn annotated_using_u32() { infer_eq( indoc!( r#" int : U32 int = 5 int "# ), "U32", ); } #[test] fn qualified_annotation_using_i16() { infer_eq( indoc!( r#" int : Num.I16 int "# ), "I16", ); } #[test] fn qualified_annotated_using_i16() { infer_eq( indoc!( r#" int : Num.I16 int = 5 int "# ), "I16", ); } #[test] fn annotation_using_i16() { infer_eq( indoc!( r#" int : I16 int "# ), "I16", ); } #[test] fn annotated_using_i16() { infer_eq( indoc!( r#" int : I16 int = 5 int "# ), "I16", ); } #[test] fn qualified_annotation_using_u16() { infer_eq( indoc!( r#" int : Num.U16 int "# ), "U16", ); } #[test] fn qualified_annotated_using_u16() { infer_eq( indoc!( r#" int : Num.U16 int = 5 int "# ), "U16", ); } #[test] fn annotation_using_u16() { infer_eq( indoc!( r#" int : U16 int "# ), "U16", ); } #[test] fn annotated_using_u16() { infer_eq( indoc!( r#" int : U16 int = 5 int "# ), "U16", ); } #[test] fn qualified_annotation_using_i8() { infer_eq( indoc!( r#" int : Num.I8 int "# ), "I8", ); } #[test] fn qualified_annotated_using_i8() { infer_eq( indoc!( r#" int : Num.I8 int = 5 int "# ), "I8", ); } #[test] fn annotation_using_i8() { infer_eq( indoc!( r#" int : I8 int "# ), "I8", ); } #[test] fn annotated_using_i8() { infer_eq( indoc!( r#" int : I8 int = 5 int "# ), "I8", ); } #[test] fn qualified_annotation_using_u8() { infer_eq( indoc!( r#" int : Num.U8 int "# ), "U8", ); } #[test] fn qualified_annotated_using_u8() { infer_eq( indoc!( r#" int : Num.U8 int = 5 int "# ), "U8", ); } #[test] fn annotation_using_u8() { infer_eq( indoc!( r#" int : U8 int "# ), "U8", ); } #[test] fn annotated_using_u8() { infer_eq( indoc!( r#" int : U8 int = 5 int "# ), "U8", ); } #[test] fn qualified_annotation_num_floatingpoint() { infer_eq( indoc!( r#" float : Num.Num (Num.FloatingPoint Num.Binary64) float "# ), "F64", ); } #[test] fn qualified_annotated_num_floatingpoint() { infer_eq( indoc!( r#" float : Num.Num (Num.FloatingPoint Num.Binary64) float = 5.5 float "# ), "F64", ); } #[test] fn annotation_num_floatingpoint() { infer_eq( indoc!( r#" float : Num (FloatingPoint Binary64) float "# ), "F64", ); } #[test] fn annotated_num_floatingpoint() { infer_eq( indoc!( r#" float : Num (FloatingPoint Binary64) float = 5.5 float "# ), "F64", ); } #[test] fn qualified_annotation_f64() { infer_eq( indoc!( r#" float : Num.F64 float "# ), "F64", ); } #[test] fn qualified_annotated_f64() { infer_eq( indoc!( r#" float : Num.F64 float = 5.5 float "# ), "F64", ); } #[test] fn annotation_f64() { infer_eq( indoc!( r#" float : F64 float "# ), "F64", ); } #[test] fn annotated_f64() { infer_eq( indoc!( r#" float : F64 float = 5.5 float "# ), "F64", ); } #[test] fn qualified_annotation_f32() { infer_eq( indoc!( r#" float : Num.F32 float "# ), "F32", ); } #[test] fn qualified_annotated_f32() { infer_eq( indoc!( r#" float : Num.F32 float = 5.5 float "# ), "F32", ); } #[test] fn annotation_f32() { infer_eq( indoc!( r#" float : F32 float "# ), "F32", ); } #[test] fn annotated_f32() { infer_eq( indoc!( r#" float : F32 float = 5.5 float "# ), "F32", ); } #[test] fn fake_result_ok() { infer_eq( indoc!( r#" Res a e : [Okay a, Error e] ok : Res I64 * ok = Okay 5 ok "# ), "Res I64 *", ); } #[test] fn fake_result_err() { infer_eq( indoc!( r#" Res a e : [Okay a, Error e] err : Res * Str err = Error "blah" err "# ), "Res * Str", ); } #[test] fn basic_result_ok() { infer_eq( indoc!( r#" ok : Result I64 * ok = Ok 5 ok "# ), "Result I64 *", ); } #[test] fn basic_result_err() { infer_eq( indoc!( r#" err : Result * Str err = Err "blah" err "# ), "Result * Str", ); } #[test] fn basic_result_conditional() { infer_eq( indoc!( r#" ok : Result I64 _ ok = Ok 5 err : Result _ Str err = Err "blah" if 1 > 0 then ok else err "# ), "Result I64 Str", ); } // #[test] // fn annotation_using_num_used() { // // There was a problem where `I64`, because it is only an annotation // // wasn't added to the vars_by_symbol. // infer_eq_without_problem( // indoc!( // r#" // int : I64 // p = (\x -> x) int // p // "# // ), // "I64", // ); // } #[test] fn num_identity() { infer_eq_without_problem( indoc!( r#" numIdentity : Num.Num a -> Num.Num a numIdentity = \x -> x y = numIdentity 3.14 { numIdentity, x : numIdentity 42, y } "# ), "{ numIdentity : Num a -> Num a, x : Num *, y : Float * }", ); } #[test] fn when_with_annotation() { infer_eq_without_problem( indoc!( r#" x : Num.Num (Num.Integer Num.Signed64) x = when 2 is 3 -> 4 _ -> 5 x "# ), "I64", ); } // TODO add more realistic function when able #[test] fn integer_sum() { infer_eq_without_problem( indoc!( r#" f = \n -> when n is 0 -> 0 _ -> f n f "# ), "Num * -> Num *", ); } #[test] fn identity_map() { infer_eq_without_problem( indoc!( r#" map : (a -> b), [Identity a] -> [Identity b] map = \f, identity -> when identity is Identity v -> Identity (f v) map "# ), "(a -> b), [Identity a] -> [Identity b]", ); } #[test] fn to_bit() { infer_eq_without_problem( indoc!( r#" toBit = \bool -> when bool is True -> 1 False -> 0 toBit "# ), "[False, True] -> Num *", ); } // this test is related to a bug where ext_var would have an incorrect rank. // This match has duplicate cases, but we ignore that. #[test] fn to_bit_record() { infer_eq( indoc!( r#" foo = \rec -> when rec is { x: _ } -> "1" { y: _ } -> "2" foo "# ), "{ x : *, y : * }* -> Str", ); } #[test] fn from_bit() { infer_eq_without_problem( indoc!( r#" fromBit = \int -> when int is 0 -> False _ -> True fromBit "# ), "Num * -> [False, True]", ); } #[test] fn result_map_explicit() { infer_eq_without_problem( indoc!( r#" map : (a -> b), [Err e, Ok a] -> [Err e, Ok b] map = \f, result -> when result is Ok v -> Ok (f v) Err e -> Err e map "# ), "(a -> b), [Err e, Ok a] -> [Err e, Ok b]", ); } #[test] fn result_map_alias() { infer_eq_without_problem( indoc!( r#" Res e a : [Ok a, Err e] map : (a -> b), Res e a -> Res e b map = \f, result -> when result is Ok v -> Ok (f v) Err e -> Err e map "# ), "(a -> b), Res e a -> Res e b", ); } #[test] fn record_from_load() { infer_eq_without_problem( indoc!( r#" foo = \{ x } -> x foo { x: 5 } "# ), "Num *", ); } #[test] fn defs_from_load() { infer_eq_without_problem( indoc!( r#" alwaysThreePointZero = \_ -> 3.0 answer = 42 identity = \a -> a threePointZero = identity (alwaysThreePointZero {}) threePointZero "# ), "Float *", ); } #[test] fn use_as_in_signature() { infer_eq_without_problem( indoc!( r#" foo : Str.Str as Foo -> Foo foo = \_ -> "foo" foo "# ), "Foo -> Foo", ); } #[test] fn use_alias_in_let() { infer_eq_without_problem( indoc!( r#" Foo : Str.Str foo : Foo -> Foo foo = \_ -> "foo" foo "# ), "Foo -> Foo", ); } #[test] fn use_alias_with_argument_in_let() { infer_eq_without_problem( indoc!( r#" Foo a : { foo : a } v : Foo (Num.Num (Num.Integer Num.Signed64)) v = { foo: 42 } v "# ), "Foo I64", ); } #[test] fn identity_alias() { infer_eq_without_problem( indoc!( r#" Foo a : { foo : a } id : Foo a -> Foo a id = \x -> x id "# ), "Foo a -> Foo a", ); } #[test] fn linked_list_empty() { infer_eq_without_problem( indoc!( r#" empty : [Cons a (ConsList a), Nil] as ConsList a empty = Nil empty "# ), "ConsList a", ); } #[test] fn linked_list_singleton() { infer_eq_without_problem( indoc!( r#" singleton : a -> [Cons a (ConsList a), Nil] as ConsList a singleton = \x -> Cons x Nil singleton "# ), "a -> ConsList a", ); } #[test] fn peano_length() { infer_eq_without_problem( indoc!( r#" Peano : [S Peano, Z] length : Peano -> Num.Num (Num.Integer Num.Signed64) length = \peano -> when peano is Z -> 0 S v -> length v length "# ), "Peano -> I64", ); } #[test] fn peano_map() { infer_eq_without_problem( indoc!( r#" map : [S Peano, Z] as Peano -> Peano map = \peano -> when peano is Z -> Z S v -> S (map v) map "# ), "Peano -> Peano", ); } #[test] fn infer_linked_list_map() { infer_eq_without_problem( indoc!( r#" map = \f, list -> when list is Nil -> Nil Cons x xs -> a = f x b = map f xs Cons a b map "# ), "(a -> b), [Cons a c, Nil] as c -> [Cons b d, Nil] as d", ); } #[test] fn typecheck_linked_list_map() { infer_eq_without_problem( indoc!( r#" ConsList a : [Cons a (ConsList a), Nil] map : (a -> b), ConsList a -> ConsList b map = \f, list -> when list is Nil -> Nil Cons x xs -> Cons (f x) (map f xs) map "# ), "(a -> b), ConsList a -> ConsList b", ); } #[test] fn mismatch_in_alias_args_gets_reported() { infer_eq( indoc!( r#" Foo a : a r : Foo {} r = {} s : Foo Str.Str s = "bar" when {} is _ -> s _ -> r "# ), "", ); } #[test] fn mismatch_in_apply_gets_reported() { infer_eq( indoc!( r#" r : { x : (Num.Num (Num.Integer Signed64)) } r = { x : 1 } s : { left : { x : Num.Num (Num.FloatingPoint Num.Binary64) } } s = { left: { x : 3.14 } } when 0 is 1 -> s.left 0 -> r "# ), "", ); } #[test] fn mismatch_in_tag_gets_reported() { infer_eq( indoc!( r#" r : [Ok Str.Str] r = Ok 1 s : { left: [Ok {}] } s = { left: Ok 3.14 } when 0 is 1 -> s.left 0 -> r "# ), "", ); } // TODO As intended, this fails, but it fails with the wrong error! // // #[test] // fn nums() { // infer_eq_without_problem( // indoc!( // r#" // s : Num * // s = 3.1 // s // "# // ), // "", // ); // } #[test] fn peano_map_alias() { infer_eq( indoc!( r#" app "test" provides [main] to "./platform" Peano : [S Peano, Z] map : Peano -> Peano map = \peano -> when peano is Z -> Z S rest -> S (map rest) main = map "# ), "Peano -> Peano", ); } #[test] fn unit_alias() { infer_eq( indoc!( r#" Unit : [Unit] unit : Unit unit = Unit unit "# ), "Unit", ); } #[test] fn rigid_in_letnonrec() { infer_eq_without_problem( indoc!( r#" ConsList a : [Cons a (ConsList a), Nil] toEmpty : ConsList a -> ConsList a toEmpty = \_ -> result : ConsList a result = Nil result toEmpty "# ), "ConsList a -> ConsList a", ); } #[test] fn rigid_in_letrec_ignored() { infer_eq_without_problem( indoc!( r#" ConsList a : [Cons a (ConsList a), Nil] toEmpty : ConsList a -> ConsList a toEmpty = \_ -> result : ConsList _ # TODO to enable using `a` we need scoped variables result = Nil toEmpty result toEmpty "# ), "ConsList a -> ConsList a", ); } #[test] fn rigid_in_letrec() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" ConsList a : [Cons a (ConsList a), Nil] toEmpty : ConsList a -> ConsList a toEmpty = \_ -> result : ConsList _ # TODO to enable using `a` we need scoped variables result = Nil toEmpty result main = toEmpty "# ), "ConsList a -> ConsList a", ); } #[test] fn let_record_pattern_with_annotation() { infer_eq_without_problem( indoc!( r#" { x, y } : { x : Str.Str, y : Num.Num (Num.FloatingPoint Num.Binary64) } { x, y } = { x : "foo", y : 3.14 } x "# ), "Str", ); } #[test] fn let_record_pattern_with_annotation_alias() { infer_eq( indoc!( r#" Foo : { x : Str.Str, y : Num.Num (Num.FloatingPoint Num.Binary64) } { x, y } : Foo { x, y } = { x : "foo", y : 3.14 } x "# ), "Str", ); } #[test] fn peano_map_infer() { infer_eq( indoc!( r#" app "test" provides [main] to "./platform" map = \peano -> when peano is Z -> Z S rest -> map rest |> S main = map "# ), "[S a, Z] as a -> [S b, Z] as b", ); } #[test] fn peano_map_infer_nested() { infer_eq( indoc!( r#" map = \peano -> when peano is Z -> Z S rest -> map rest |> S map "# ), "[S a, Z] as a -> [S b, Z] as b", ); } #[test] fn let_record_pattern_with_alias_annotation() { infer_eq_without_problem( indoc!( r#" Foo : { x : Str.Str, y : Num.Num (Num.FloatingPoint Num.Binary64) } { x, y } : Foo { x, y } = { x : "foo", y : 3.14 } x "# ), "Str", ); } #[test] fn let_tag_pattern_with_annotation() { infer_eq_without_problem( indoc!( r#" UserId x : [UserId I64] UserId x = UserId 42 x "# ), "I64", ); } #[test] fn typecheck_record_linked_list_map() { infer_eq_without_problem( indoc!( r#" ConsList q : [Cons { x: q, xs: ConsList q }, Nil] map : (a -> b), ConsList a -> ConsList b map = \f, list -> when list is Nil -> Nil Cons { x, xs } -> Cons { x: f x, xs : map f xs } map "# ), "(a -> b), ConsList a -> ConsList b", ); } #[test] fn infer_record_linked_list_map() { infer_eq_without_problem( indoc!( r#" map = \f, list -> when list is Nil -> Nil Cons { x, xs } -> Cons { x: f x, xs : map f xs } map "# ), "(a -> b), [Cons { x : a, xs : c }*, Nil] as c -> [Cons { x : b, xs : d }, Nil] as d", ); } #[test] fn typecheck_mutually_recursive_tag_union_2() { infer_eq_without_problem( indoc!( r#" ListA a b : [Cons a (ListB b a), Nil] ListB a b : [Cons a (ListA b a), Nil] ConsList q : [Cons q (ConsList q), Nil] toAs : (b -> a), ListA a b -> ConsList a toAs = \f, lista -> when lista is Nil -> Nil Cons a listb -> when listb is Nil -> Nil Cons b newLista -> Cons a (Cons (f b) (toAs f newLista)) toAs "# ), "(b -> a), ListA a b -> ConsList a", ); } #[test] fn typecheck_mutually_recursive_tag_union_listabc() { infer_eq_without_problem( indoc!( r#" ListA a : [Cons a (ListB a)] ListB a : [Cons a (ListC a)] ListC a : [Cons a (ListA a), Nil] val : ListC Num.I64 val = Cons 1 (Cons 2 (Cons 3 Nil)) val "# ), "ListC I64", ); } #[test] fn infer_mutually_recursive_tag_union() { infer_eq_without_problem( indoc!( r#" toAs = \f, lista -> when lista is Nil -> Nil Cons a listb -> when listb is Nil -> Nil Cons b newLista -> Cons a (Cons (f b) (toAs f newLista)) toAs "# ), "(a -> b), [Cons c [Cons a d, Nil], Nil] as d -> [Cons c [Cons b e], Nil] as e", ); } #[test] fn solve_list_get() { infer_eq_without_problem( indoc!( r#" List.get ["a"] 0 "# ), "Result Str [OutOfBounds]", ); } #[test] fn type_more_general_than_signature() { infer_eq_without_problem( indoc!( r#" partition : Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))] partition = \low, high, initialList -> when List.get initialList high is Ok _ -> Pair 0 [] Err _ -> Pair (low - 1) initialList partition "# ), "Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))]", ); } #[test] fn quicksort_partition() { with_larger_debug_stack(|| { infer_eq_without_problem( indoc!( r#" swap : Nat, Nat, List a -> List a swap = \i, j, list -> when Pair (List.get list i) (List.get list j) is Pair (Ok atI) (Ok atJ) -> list |> List.set i atJ |> List.set j atI _ -> list partition : Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))] partition = \low, high, initialList -> when List.get initialList high is Ok pivot -> go = \i, j, list -> if j < high then when List.get list j is Ok value -> if value <= pivot then go (i + 1) (j + 1) (swap (i + 1) j list) else go i (j + 1) list Err _ -> Pair i list else Pair i list when go (low - 1) low initialList is Pair newI newList -> Pair (newI + 1) (swap (newI + 1) high newList) Err _ -> Pair (low - 1) initialList partition "# ), "Nat, Nat, List (Int a) -> [Pair Nat (List (Int a))]", ); }); } #[test] fn identity_list() { infer_eq_without_problem( indoc!( r#" idList : List a -> List a idList = \list -> list foo : List I64 -> List I64 foo = \initialList -> idList initialList foo "# ), "List I64 -> List I64", ); } #[test] fn list_get() { infer_eq_without_problem( indoc!( r#" List.get [10, 9, 8, 7] 1 "# ), "Result (Num *) [OutOfBounds]", ); infer_eq_without_problem( indoc!( r#" List.get "# ), "List a, Nat -> Result a [OutOfBounds]", ); } #[test] fn use_rigid_twice() { infer_eq_without_problem( indoc!( r#" id1 : q -> q id1 = \x -> x id2 : q -> q id2 = \x -> x { id1, id2 } "# ), "{ id1 : q -> q, id2 : q1 -> q1 }", ); } #[test] fn map_insert() { infer_eq_without_problem( indoc!( r#" Dict.insert "# ), "Dict k v, k, v -> Dict k v | k has Hash & Eq", ); } #[test] fn num_to_frac() { infer_eq_without_problem( indoc!( r#" Num.toFrac "# ), "Num * -> Float a", ); } #[test] fn pow() { infer_eq_without_problem( indoc!( r#" Num.pow "# ), "Float a, Float a -> Float a", ); } #[test] fn ceiling() { infer_eq_without_problem( indoc!( r#" Num.ceiling "# ), "Float * -> Int a", ); } #[test] fn floor() { infer_eq_without_problem( indoc!( r#" Num.floor "# ), "Float * -> Int a", ); } #[test] fn div() { infer_eq_without_problem( indoc!( r#" Num.div "# ), "Float a, Float a -> Float a", ) } #[test] fn div_checked() { infer_eq_without_problem( indoc!( r#" Num.divChecked "# ), "Float a, Float a -> Result (Float a) [DivByZero]", ) } #[test] fn div_ceil() { infer_eq_without_problem( indoc!( r#" Num.divCeil "# ), "Int a, Int a -> Int a", ); } #[test] fn div_ceil_checked() { infer_eq_without_problem( indoc!( r#" Num.divCeilChecked "# ), "Int a, Int a -> Result (Int a) [DivByZero]", ); } #[test] fn div_trunc() { infer_eq_without_problem( indoc!( r#" Num.divTrunc "# ), "Int a, Int a -> Int a", ); } #[test] fn div_trunc_checked() { infer_eq_without_problem( indoc!( r#" Num.divTruncChecked "# ), "Int a, Int a -> Result (Int a) [DivByZero]", ); } #[test] fn atan() { infer_eq_without_problem( indoc!( r#" Num.atan "# ), "Float a -> Float a", ); } #[test] fn min_i128() { infer_eq_without_problem( indoc!( r#" Num.minI128 "# ), "I128", ); } #[test] fn max_i128() { infer_eq_without_problem( indoc!( r#" Num.maxI128 "# ), "I128", ); } #[test] fn min_i64() { infer_eq_without_problem( indoc!( r#" Num.minI64 "# ), "I64", ); } #[test] fn max_i64() { infer_eq_without_problem( indoc!( r#" Num.maxI64 "# ), "I64", ); } #[test] fn min_u64() { infer_eq_without_problem( indoc!( r#" Num.minU64 "# ), "U64", ); } #[test] fn max_u64() { infer_eq_without_problem( indoc!( r#" Num.maxU64 "# ), "U64", ); } #[test] fn min_i32() { infer_eq_without_problem( indoc!( r#" Num.minI32 "# ), "I32", ); } #[test] fn max_i32() { infer_eq_without_problem( indoc!( r#" Num.maxI32 "# ), "I32", ); } #[test] fn min_u32() { infer_eq_without_problem( indoc!( r#" Num.minU32 "# ), "U32", ); } #[test] fn max_u32() { infer_eq_without_problem( indoc!( r#" Num.maxU32 "# ), "U32", ); } #[test] fn reconstruct_path() { infer_eq_without_problem( indoc!( r#" reconstructPath : Dict position position, position -> List position | position has Hash & Eq reconstructPath = \cameFrom, goal -> when Dict.get cameFrom goal is Err KeyNotFound -> [] Ok next -> List.append (reconstructPath cameFrom next) goal reconstructPath "# ), "Dict position position, position -> List position | position has Hash & Eq", ); } #[test] fn use_correct_ext_record() { // Related to a bug solved in 81fbab0b3fe4765bc6948727e603fc2d49590b1c infer_eq_without_problem( indoc!( r#" f = \r -> g = r.q h = r.p 42 f "# ), "{ p : *, q : * }* -> Num *", ); } #[test] fn use_correct_ext_tag_union() { // related to a bug solved in 08c82bf151a85e62bce02beeed1e14444381069f infer_eq_without_problem( indoc!( r#" app "test" imports [Result.{ Result }] provides [main] to "./platform" boom = \_ -> boom {} Model position : { openSet : Set position } cheapestOpen : Model position -> Result position [KeyNotFound] | position has Hash & Eq cheapestOpen = \model -> folder = \resSmallestSoFar, position -> when resSmallestSoFar is Err _ -> resSmallestSoFar Ok smallestSoFar -> if position == smallestSoFar.position then resSmallestSoFar else Ok { position, cost: 0.0 } Set.walk model.openSet (Ok { position: boom {}, cost: 0.0 }) folder |> Result.map (\x -> x.position) astar : Model position -> Result position [KeyNotFound] | position has Hash & Eq astar = \model -> cheapestOpen model main = astar "# ), "Model position -> Result position [KeyNotFound] | position has Hash & Eq", ); } #[test] fn when_with_or_pattern_and_guard() { infer_eq_without_problem( indoc!( r#" \x -> when x is 2 | 3 -> 0 a if a < 20 -> 1 3 | 4 if Bool.false -> 2 _ -> 3 "# ), "Num * -> Num *", ); } #[test] fn sorting() { // based on https://github.com/elm/compiler/issues/2057 // Roc seems to do this correctly, tracking to make sure it stays that way infer_eq_without_problem( indoc!( r#" sort : ConsList cm -> ConsList cm sort = \xs -> f : cm, cm -> Order f = \_, _ -> LT sortWith f xs sortBy : (x -> cmpl), ConsList x -> ConsList x sortBy = \_, list -> cmp : x, x -> Order cmp = \_, _ -> LT sortWith cmp list always = \x, _ -> x sortWith : (foobar, foobar -> Order), ConsList foobar -> ConsList foobar sortWith = \_, list -> f = \arg -> g arg g = \bs -> when bs is bx -> f bx always Nil (f list) Order : [LT, GT, EQ] ConsList a : [Nil, Cons a (ConsList a)] { x: sortWith, y: sort, z: sortBy } "# ), "{ x : (foobar, foobar -> Order), ConsList foobar -> ConsList foobar, y : ConsList cm -> ConsList cm, z : (x -> cmpl), ConsList x -> ConsList x }" ); } // Like in elm, this test now fails. Polymorphic recursion (even with an explicit signature) // yields a type error. // // We should at some point investigate why that is. Elm did support polymorphic recursion in // earlier versions. // // #[test] // fn wrapper() { // // based on https://github.com/elm/compiler/issues/1964 // // Roc seems to do this correctly, tracking to make sure it stays that way // infer_eq_without_problem( // indoc!( // r#" // Type a : [TypeCtor (Type (Wrapper a))] // // Wrapper a : [Wrapper a] // // Opaque : [Opaque] // // encodeType1 : Type a -> Opaque // encodeType1 = \thing -> // when thing is // TypeCtor v0 -> // encodeType1 v0 // // encodeType1 // "# // ), // "Type a -> Opaque", // ); // } #[test] fn rigids() { infer_eq_without_problem( indoc!( r#" f : List a -> List a f = \input -> # let-polymorphism at work x : {} -> List b x = \{} -> [] when List.get input 0 is Ok val -> List.append (x {}) val Err _ -> input f "# ), "List a -> List a", ); } #[cfg(debug_assertions)] #[test] #[should_panic] fn rigid_record_quantification() { // the ext here is qualified on the outside (because we have rank 1 types, not rank 2). // That means e.g. `f : { bar : String, foo : I64 } -> Bool }` is a valid argument, but // that function could not be applied to the `{ foo : I64 }` list. Therefore, this function // is not allowed. // // should hit a debug_assert! in debug mode, and produce a type error in release mode infer_eq_without_problem( indoc!( r#" test : ({ foo : I64 }ext -> Bool), { foo : I64 } -> Bool test = \fn, a -> fn a test "# ), "should fail", ); } // OPTIONAL RECORD FIELDS #[test] fn optional_field_unifies_with_missing() { infer_eq_without_problem( indoc!( r#" negatePoint : { x : I64, y : I64, z ? Num c } -> { x : I64, y : I64, z : Num c } negatePoint { x: 1, y: 2 } "# ), "{ x : I64, y : I64, z : Num c }", ); } #[test] fn open_optional_field_unifies_with_missing() { infer_eq_without_problem( indoc!( r#" negatePoint : { x : I64, y : I64, z ? Num c }r -> { x : I64, y : I64, z : Num c }r a = negatePoint { x: 1, y: 2 } b = negatePoint { x: 1, y: 2, blah : "hi" } { a, b } "# ), "{ a : { x : I64, y : I64, z : Num c }, b : { blah : Str, x : I64, y : I64, z : Num c1 } }", ); } #[test] fn optional_field_unifies_with_present() { infer_eq_without_problem( indoc!( r#" negatePoint : { x : Num a, y : Num b, z ? c } -> { x : Num a, y : Num b, z : c } negatePoint { x: 1, y: 2.1, z: 0x3 } "# ), "{ x : Num *, y : Float *, z : Int * }", ); } #[test] fn open_optional_field_unifies_with_present() { infer_eq_without_problem( indoc!( r#" negatePoint : { x : Num a, y : Num b, z ? c }r -> { x : Num a, y : Num b, z : c }r a = negatePoint { x: 1, y: 2.1 } b = negatePoint { x: 1, y: 2.1, blah : "hi" } { a, b } "# ), "{ a : { x : Num *, y : Float *, z : c }, b : { blah : Str, x : Num *, y : Float *, z : c1 } }", ); } #[test] fn optional_field_function() { infer_eq_without_problem( indoc!( r#" \{ x, y ? 0 } -> x + y "# ), "{ x : Num a, y ? Num a }* -> Num a", ); } #[test] fn optional_field_let() { infer_eq_without_problem( indoc!( r#" { x, y ? 0 } = { x: 32 } x + y "# ), "Num *", ); } #[test] fn optional_field_when() { infer_eq_without_problem( indoc!( r#" \r -> when r is { x, y ? 0 } -> x + y "# ), "{ x : Num a, y ? Num a }* -> Num a", ); } #[test] fn optional_field_let_with_signature() { infer_eq_without_problem( indoc!( r#" \rec -> { x, y } : { x : I64, y ? Bool }* { x, y ? Bool.false } = rec { x, y } "# ), "{ x : I64, y ? Bool }* -> { x : I64, y : Bool }", ); } #[test] fn list_walk_backwards() { infer_eq_without_problem( indoc!( r#" List.walkBackwards "# ), "List elem, state, (state, elem -> state) -> state", ); } #[test] fn list_walk_backwards_example() { infer_eq_without_problem( indoc!( r#" empty : List I64 empty = [] List.walkBackwards empty 0 (\a, b -> a + b) "# ), "I64", ); } #[test] fn list_drop_at() { infer_eq_without_problem( indoc!( r#" List.dropAt "# ), "List elem, Nat -> List elem", ); } #[test] fn str_trim() { infer_eq_without_problem( indoc!( r#" Str.trim "# ), "Str -> Str", ); } #[test] fn str_trim_left() { infer_eq_without_problem( indoc!( r#" Str.trimLeft "# ), "Str -> Str", ); } #[test] fn list_take_first() { infer_eq_without_problem( indoc!( r#" List.takeFirst "# ), "List elem, Nat -> List elem", ); } #[test] fn list_take_last() { infer_eq_without_problem( indoc!( r#" List.takeLast "# ), "List elem, Nat -> List elem", ); } #[test] fn list_sublist() { infer_eq_without_problem( indoc!( r#" List.sublist "# ), "List elem, { len : Nat, start : Nat } -> List elem", ); } #[test] fn list_split() { infer_eq_without_problem( indoc!("List.split"), "List elem, Nat -> { before : List elem, others : List elem }", ); } #[test] fn list_drop_last() { infer_eq_without_problem( indoc!( r#" List.dropLast "# ), "List elem -> List elem", ); } #[test] fn list_intersperse() { infer_eq_without_problem( indoc!( r#" List.intersperse "# ), "List elem, elem -> List elem", ); } #[test] fn function_that_captures_nothing_is_not_captured() { // we should make sure that a function that doesn't capture anything it not itself captured // such functions will be lifted to the top-level, and are thus globally available! infer_eq_without_problem( indoc!( r#" f = \x -> x + 1 g = \y -> f y g "# ), "Num a -> Num a", ); } #[test] fn double_named_rigids() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" main : List x main = empty : List x empty = [] empty "# ), "List x", ); } #[test] fn double_tag_application() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" main = if 1 == 1 then Foo (Bar) 1 else Foo Bar 1 "# ), "[Foo [Bar] (Num *)]", ); infer_eq_without_problem("Foo Bar 1", "[Foo [Bar] (Num *)]"); } #[test] fn double_tag_application_pattern() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" Bar : [Bar] Foo : [Foo Bar I64, Empty] foo : Foo foo = Foo Bar 1 main = when foo is Foo Bar 1 -> Foo Bar 2 x -> x "# ), "[Empty, Foo [Bar] I64]", ); } #[test] fn recursive_function_with_rigid() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" State a : { count : I64, x : a } foo : State a -> I64 foo = \state -> if state.count == 0 then 0 else 1 + foo { count: state.count - 1, x: state.x } main : I64 main = foo { count: 3, x: {} } "# ), "I64", ); } #[test] fn rbtree_empty() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" # The color of a node. Leaves are considered Black. NodeColor : [Red, Black] RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] # Create an empty dictionary. empty : {} -> RBTree k v empty = \{} -> Empty foo : RBTree I64 I64 foo = empty {} main : RBTree I64 I64 main = foo "# ), "RBTree I64 I64", ); } #[test] fn rbtree_insert() { // exposed an issue where pattern variables were not introduced // at the correct level in the constraint // // see 22592eff805511fbe1da63849771ee5f367a6a16 infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" RBTree k : [Node k (RBTree k), Empty] balance : RBTree k -> RBTree k balance = \left -> when left is Node _ Empty -> Empty _ -> Empty main : RBTree {} main = balance Empty "# ), "RBTree {}", ); } #[test] fn rbtree_full_remove_min() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" NodeColor : [Red, Black] RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] moveRedLeft : RBTree k v -> RBTree k v moveRedLeft = \dict -> when dict is # Node clr k v (Node lClr lK lV lLeft lRight) (Node rClr rK rV ((Node Red rlK rlV rlL rlR) as rLeft) rRight) -> # Node clr k v (Node lClr lK lV lLeft lRight) (Node rClr rK rV rLeft rRight) -> Node clr k v (Node _ lK lV lLeft lRight) (Node _ rK rV rLeft rRight) -> when rLeft is Node Red rlK rlV rlL rlR -> Node Red rlK rlV (Node Black k v (Node Red lK lV lLeft lRight) rlL) (Node Black rK rV rlR rRight) _ -> when clr is Black -> Node Black k v (Node Red lK lV lLeft lRight) (Node Red rK rV rLeft rRight) Red -> Node Black k v (Node Red lK lV lLeft lRight) (Node Red rK rV rLeft rRight) _ -> dict balance : NodeColor, k, v, RBTree k v, RBTree k v -> RBTree k v balance = \color, key, value, left, right -> when right is Node Red rK rV rLeft rRight -> when left is Node Red lK lV lLeft lRight -> Node Red key value (Node Black lK lV lLeft lRight) (Node Black rK rV rLeft rRight) _ -> Node color rK rV (Node Red key value left rLeft) rRight _ -> when left is Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> Node Red lK lV (Node Black llK llV llLeft llRight) (Node Black key value lRight right) _ -> Node color key value left right Key k : Num k removeHelpEQGT : Key k, RBTree (Key k) v -> RBTree (Key k) v | k has Hash & Eq removeHelpEQGT = \targetKey, dict -> when dict is Node color key value left right -> if targetKey == key then when getMin right is Node _ minKey minValue _ _ -> balance color minKey minValue left (removeMin right) Empty -> Empty else balance color key value left (removeHelp targetKey right) Empty -> Empty getMin : RBTree k v -> RBTree k v getMin = \dict -> when dict is # Node _ _ _ ((Node _ _ _ _ _) as left) _ -> Node _ _ _ left _ -> when left is Node _ _ _ _ _ -> getMin left _ -> dict _ -> dict moveRedRight : RBTree k v -> RBTree k v moveRedRight = \dict -> when dict is Node clr k v (Node lClr lK lV (Node Red llK llV llLeft llRight) lRight) (Node rClr rK rV rLeft rRight) -> Node Red lK lV (Node Black llK llV llLeft llRight) (Node Black k v lRight (Node Red rK rV rLeft rRight)) Node clr k v (Node lClr lK lV lLeft lRight) (Node rClr rK rV rLeft rRight) -> when clr is Black -> Node Black k v (Node Red lK lV lLeft lRight) (Node Red rK rV rLeft rRight) Red -> Node Black k v (Node Red lK lV lLeft lRight) (Node Red rK rV rLeft rRight) _ -> dict removeHelpPrepEQGT : Key k, RBTree (Key k) v, NodeColor, (Key k), v, RBTree (Key k) v, RBTree (Key k) v -> RBTree (Key k) v removeHelpPrepEQGT = \_, dict, color, key, value, left, right -> when left is Node Red lK lV lLeft lRight -> Node color lK lV lLeft (Node Red key value lRight right) _ -> when right is Node Black _ _ (Node Black _ _ _ _) _ -> moveRedRight dict Node Black _ _ Empty _ -> moveRedRight dict _ -> dict removeMin : RBTree k v -> RBTree k v removeMin = \dict -> when dict is Node color key value left right -> when left is Node lColor _ _ lLeft _ -> when lColor is Black -> when lLeft is Node Red _ _ _ _ -> Node color key value (removeMin left) right _ -> when moveRedLeft dict is # here 1 Node nColor nKey nValue nLeft nRight -> balance nColor nKey nValue (removeMin nLeft) nRight Empty -> Empty _ -> Node color key value (removeMin left) right _ -> Empty _ -> Empty removeHelp : Key k, RBTree (Key k) v -> RBTree (Key k) v | k has Hash & Eq removeHelp = \targetKey, dict -> when dict is Empty -> Empty Node color key value left right -> if targetKey < key then when left is Node Black _ _ lLeft _ -> when lLeft is Node Red _ _ _ _ -> Node color key value (removeHelp targetKey left) right _ -> when moveRedLeft dict is # here 2 Node nColor nKey nValue nLeft nRight -> balance nColor nKey nValue (removeHelp targetKey nLeft) nRight Empty -> Empty _ -> Node color key value (removeHelp targetKey left) right else removeHelpEQGT targetKey (removeHelpPrepEQGT targetKey dict color key value left right) main : RBTree I64 I64 main = removeHelp 1i64 Empty "# ), "RBTree (Key (Integer Signed64)) I64", ); } #[test] fn rbtree_remove_min_1() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" RBTree k : [Node k (RBTree k) (RBTree k), Empty] removeHelp : Num k, RBTree (Num k) -> RBTree (Num k) removeHelp = \targetKey, dict -> when dict is Empty -> Empty Node key left right -> if targetKey < key then when left is Node _ lLeft _ -> when lLeft is Node _ _ _ -> Empty _ -> Empty _ -> Node key (removeHelp targetKey left) right else Empty main : RBTree I64 main = removeHelp 1 Empty "# ), "RBTree I64", ); } #[test] fn rbtree_foobar() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" NodeColor : [Red, Black] RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] removeHelp : Num k, RBTree (Num k) v -> RBTree (Num k) v | k has Hash & Eq removeHelp = \targetKey, dict -> when dict is Empty -> Empty Node color key value left right -> if targetKey < key then when left is Node Black _ _ lLeft _ -> when lLeft is Node Red _ _ _ _ -> Node color key value (removeHelp targetKey left) right _ -> when moveRedLeft dict is # here 2 Node nColor nKey nValue nLeft nRight -> balance nColor nKey nValue (removeHelp targetKey nLeft) nRight Empty -> Empty _ -> Node color key value (removeHelp targetKey left) right else removeHelpEQGT targetKey (removeHelpPrepEQGT targetKey dict color key value left right) Key k : Num k balance : NodeColor, k, v, RBTree k v, RBTree k v -> RBTree k v moveRedLeft : RBTree k v -> RBTree k v removeHelpPrepEQGT : Key k, RBTree (Key k) v, NodeColor, (Key k), v, RBTree (Key k) v, RBTree (Key k) v -> RBTree (Key k) v removeHelpEQGT : Key k, RBTree (Key k) v -> RBTree (Key k) v | k has Hash & Eq removeHelpEQGT = \targetKey, dict -> when dict is Node color key value left right -> if targetKey == key then when getMin right is Node _ minKey minValue _ _ -> balance color minKey minValue left (removeMin right) Empty -> Empty else balance color key value left (removeHelp targetKey right) Empty -> Empty getMin : RBTree k v -> RBTree k v removeMin : RBTree k v -> RBTree k v main : RBTree I64 I64 main = removeHelp 1i64 Empty "# ), "RBTree I64 I64", ); } #[test] fn quicksort_partition_help() { infer_eq_without_problem( indoc!( r#" app "test" provides [partitionHelp] to "./platform" swap : Nat, Nat, List a -> List a swap = \i, j, list -> when Pair (List.get list i) (List.get list j) is Pair (Ok atI) (Ok atJ) -> list |> List.set i atJ |> List.set j atI _ -> [] partitionHelp : Nat, Nat, List (Num a), Nat, (Num a) -> [Pair Nat (List (Num a))] partitionHelp = \i, j, list, high, pivot -> if j < high then when List.get list j is Ok value -> if value <= pivot then partitionHelp (i + 1) (j + 1) (swap (i + 1) j list) high pivot else partitionHelp i (j + 1) list high pivot Err _ -> Pair i list else Pair i list "# ), "Nat, Nat, List (Num a), Nat, Num a -> [Pair Nat (List (Num a))]", ); } #[test] fn rbtree_old_balance_simplified() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" RBTree k : [Node k (RBTree k) (RBTree k), Empty] balance : k, RBTree k -> RBTree k balance = \key, left -> Node key left Empty main : RBTree I64 main = balance 0 Empty "# ), "RBTree I64", ); } #[test] fn rbtree_balance_simplified() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" RBTree k : [Node k (RBTree k) (RBTree k), Empty] node = \x,y,z -> Node x y z balance : k, RBTree k -> RBTree k balance = \key, left -> node key left Empty main : RBTree I64 main = balance 0 Empty "# ), "RBTree I64", ); } #[test] fn rbtree_balance() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" NodeColor : [Red, Black] RBTree k v : [Node NodeColor k v (RBTree k v) (RBTree k v), Empty] balance : NodeColor, k, v, RBTree k v, RBTree k v -> RBTree k v balance = \color, key, value, left, right -> when right is Node Red rK rV rLeft rRight -> when left is Node Red lK lV lLeft lRight -> Node Red key value (Node Black lK lV lLeft lRight) (Node Black rK rV rLeft rRight) _ -> Node color rK rV (Node Red key value left rLeft) rRight _ -> when left is Node Red lK lV (Node Red llK llV llLeft llRight) lRight -> Node Red lK lV (Node Black llK llV llLeft llRight) (Node Black key value lRight right) _ -> Node color key value left right main : RBTree I64 I64 main = balance Red 0 0 Empty Empty "# ), "RBTree I64 I64", ); } #[test] fn pattern_rigid_problem() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" RBTree k : [Node k (RBTree k) (RBTree k), Empty] balance : k, RBTree k -> RBTree k balance = \key, left -> when left is Node _ _ lRight -> Node key lRight Empty _ -> Empty main : RBTree I64 main = balance 0 Empty "# ), "RBTree I64", ); } #[test] fn expr_to_str() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" Expr : [Add Expr Expr, Val I64, Var I64] printExpr : Expr -> Str printExpr = \e -> when e is Add a b -> "Add (" |> Str.concat (printExpr a) |> Str.concat ") (" |> Str.concat (printExpr b) |> Str.concat ")" Val v -> Num.toStr v Var v -> "Var " |> Str.concat (Num.toStr v) main : Str main = printExpr (Var 3) "# ), "Str", ); } #[test] fn int_type_let_polymorphism() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" x = 4 f : U8 -> U32 f = \z -> Num.intCast z y = f x main = x "# ), "Num *", ); } #[test] fn rigid_type_variable_problem() { // see https://github.com/roc-lang/roc/issues/1162 infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" RBTree k : [Node k (RBTree k) (RBTree k), Empty] balance : a, RBTree a -> RBTree a balance = \key, left -> when left is Node _ _ lRight -> Node key lRight Empty _ -> Empty main : RBTree {} main = balance {} Empty "# ), "RBTree {}", ); } #[test] fn inference_var_inside_arrow() { infer_eq_without_problem( indoc!( r#" id : _ -> _ id = \x -> x id "# ), "a -> a", ) } #[test] fn inference_var_inside_ctor() { infer_eq_without_problem( indoc!( r#" canIGo : _ -> Result.Result _ _ canIGo = \color -> when color is "green" -> Ok "go!" "yellow" -> Err (SlowIt "whoa, let's slow down!") "red" -> Err (StopIt "absolutely not") _ -> Err (UnknownColor "this is a weird stoplight") canIGo "# ), "Str -> Result Str [SlowIt Str, StopIt Str, UnknownColor Str]", ) } #[test] fn inference_var_inside_ctor_linked() { infer_eq_without_problem( indoc!( r#" swapRcd: {x: _, y: _} -> {x: _, y: _} swapRcd = \{x, y} -> {x: y, y: x} swapRcd "# ), "{ x : a, y : b } -> { x : b, y : a }", ) } #[test] fn inference_var_link_with_rigid() { infer_eq_without_problem( indoc!( r#" swapRcd: {x: tx, y: ty} -> {x: _, y: _} swapRcd = \{x, y} -> {x: y, y: x} swapRcd "# ), "{ x : tx, y : ty } -> { x : ty, y : tx }", ) } #[test] fn inference_var_inside_tag_ctor() { infer_eq_without_problem( indoc!( r#" badComics: [True, False] -> [CowTools _, Thagomizer _] badComics = \c -> when c is True -> CowTools "The Far Side" False -> Thagomizer "The Far Side" badComics "# ), "[False, True] -> [CowTools Str, Thagomizer Str]", ) } #[test] fn inference_var_tag_union_ext() { // TODO: we should really be inferring [Blue, Orange]a -> [Lavender, Peach]a here. // See https://github.com/roc-lang/roc/issues/2053 infer_eq_without_problem( indoc!( r#" pastelize: _ -> [Lavender, Peach]_ pastelize = \color -> when color is Blue -> Lavender Orange -> Peach col -> col pastelize "# ), "[Blue, Lavender, Orange, Peach]a -> [Blue, Lavender, Orange, Peach]a", ) } #[test] fn inference_var_rcd_union_ext() { infer_eq_without_problem( indoc!( r#" setRocEmail : _ -> { name: Str, email: Str }_ setRocEmail = \person -> { person & email: "\(person.name)@roclang.com" } setRocEmail "# ), "{ email : Str, name : Str }a -> { email : Str, name : Str }a", ) } #[test] fn issue_2217() { infer_eq_without_problem( indoc!( r#" LinkedList elem : [Empty, Prepend (LinkedList elem) elem] fromList : List elem -> LinkedList elem fromList = \elems -> List.walk elems Empty Prepend fromList "# ), "List elem -> LinkedList elem", ) } #[test] fn issue_2217_inlined() { infer_eq_without_problem( indoc!( r#" fromList : List elem -> [Empty, Prepend (LinkedList elem) elem] as LinkedList elem fromList = \elems -> List.walk elems Empty Prepend fromList "# ), "List elem -> LinkedList elem", ) } #[test] fn infer_union_input_position1() { infer_eq_without_problem( indoc!( r#" \tag -> when tag is A -> X B -> Y "# ), "[A, B] -> [X, Y]", ) } #[test] fn infer_union_input_position2() { infer_eq_without_problem( indoc!( r#" \tag -> when tag is A -> X B -> Y _ -> Z "# ), "[A, B]* -> [X, Y, Z]", ) } #[test] fn infer_union_input_position3() { infer_eq_without_problem( indoc!( r#" \tag -> when tag is A M -> X A N -> Y "# ), "[A [M, N]] -> [X, Y]", ) } #[test] fn infer_union_input_position4() { infer_eq_without_problem( indoc!( r#" \tag -> when tag is A M -> X A N -> Y A _ -> Z "# ), "[A [M, N]*] -> [X, Y, Z]", ) } #[test] fn infer_union_input_position5() { infer_eq_without_problem( indoc!( r#" \tag -> when tag is A (M J) -> X A (N K) -> X "# ), "[A [M [J], N [K]]] -> [X]", ) } #[test] fn infer_union_input_position6() { infer_eq_without_problem( indoc!( r#" \tag -> when tag is A M -> X B -> X A N -> X "# ), "[A [M, N], B] -> [X]", ) } #[test] fn infer_union_input_position7() { infer_eq_without_problem( indoc!( r#" \tag -> when tag is A -> X t -> t "# ), "[A, X]a -> [A, X]a", ) } #[test] fn infer_union_input_position8() { infer_eq_without_problem( indoc!( r#" \opt -> when opt is Some ({tag: A}) -> 1 Some ({tag: B}) -> 1 None -> 0 "# ), "[None, Some { tag : [A, B] }*] -> Num *", ) } #[test] fn infer_union_input_position9() { infer_eq_without_problem( indoc!( r#" opt : [Some Str, None] opt = Some "" rcd = { opt } when rcd is { opt: Some s } -> s { opt: None } -> "?" "# ), "Str", ) } #[test] fn infer_union_input_position10() { infer_eq_without_problem( indoc!( r#" \r -> when r is { x: Blue, y ? 3 } -> y { x: Red, y ? 5 } -> y "# ), "{ x : [Blue, Red], y ? Num a }* -> Num a", ) } #[test] // Issue #2299 fn infer_union_argument_position() { infer_eq_without_problem( indoc!( r#" \UserId id -> id + 1 "# ), "[UserId (Num a)] -> Num a", ) } #[test] fn infer_union_def_position() { infer_eq_without_problem( indoc!( r#" \email -> Email str = email Str.isEmpty str "# ), "[Email Str] -> Bool", ) } #[test] fn numeric_literal_suffixes() { infer_eq_without_problem( indoc!( r#" { u8: 123u8, u16: 123u16, u32: 123u32, u64: 123u64, u128: 123u128, i8: 123i8, i16: 123i16, i32: 123i32, i64: 123i64, i128: 123i128, nat: 123nat, bu8: 0b11u8, bu16: 0b11u16, bu32: 0b11u32, bu64: 0b11u64, bu128: 0b11u128, bi8: 0b11i8, bi16: 0b11i16, bi32: 0b11i32, bi64: 0b11i64, bi128: 0b11i128, bnat: 0b11nat, dec: 123.0dec, f32: 123.0f32, f64: 123.0f64, fdec: 123dec, ff32: 123f32, ff64: 123f64, } "# ), r#"{ bi128 : I128, bi16 : I16, bi32 : I32, bi64 : I64, bi8 : I8, bnat : Nat, bu128 : U128, bu16 : U16, bu32 : U32, bu64 : U64, bu8 : U8, dec : Dec, f32 : F32, f64 : F64, fdec : Dec, ff32 : F32, ff64 : F64, i128 : I128, i16 : I16, i32 : I32, i64 : I64, i8 : I8, nat : Nat, u128 : U128, u16 : U16, u32 : U32, u64 : U64, u8 : U8 }"#, ) } #[test] fn numeric_literal_suffixes_in_pattern() { infer_eq_without_problem( indoc!( r#" { u8: (\n -> when n is 123u8 -> n _ -> n), u16: (\n -> when n is 123u16 -> n _ -> n), u32: (\n -> when n is 123u32 -> n _ -> n), u64: (\n -> when n is 123u64 -> n _ -> n), u128: (\n -> when n is 123u128 -> n _ -> n), i8: (\n -> when n is 123i8 -> n _ -> n), i16: (\n -> when n is 123i16 -> n _ -> n), i32: (\n -> when n is 123i32 -> n _ -> n), i64: (\n -> when n is 123i64 -> n _ -> n), i128: (\n -> when n is 123i128 -> n _ -> n), nat: (\n -> when n is 123nat -> n _ -> n), bu8: (\n -> when n is 0b11u8 -> n _ -> n), bu16: (\n -> when n is 0b11u16 -> n _ -> n), bu32: (\n -> when n is 0b11u32 -> n _ -> n), bu64: (\n -> when n is 0b11u64 -> n _ -> n), bu128: (\n -> when n is 0b11u128 -> n _ -> n), bi8: (\n -> when n is 0b11i8 -> n _ -> n), bi16: (\n -> when n is 0b11i16 -> n _ -> n), bi32: (\n -> when n is 0b11i32 -> n _ -> n), bi64: (\n -> when n is 0b11i64 -> n _ -> n), bi128: (\n -> when n is 0b11i128 -> n _ -> n), bnat: (\n -> when n is 0b11nat -> n _ -> n), dec: (\n -> when n is 123.0dec -> n _ -> n), f32: (\n -> when n is 123.0f32 -> n _ -> n), f64: (\n -> when n is 123.0f64 -> n _ -> n), fdec: (\n -> when n is 123dec -> n _ -> n), ff32: (\n -> when n is 123f32 -> n _ -> n), ff64: (\n -> when n is 123f64 -> n _ -> n), } "# ), r#"{ bi128 : I128 -> I128, bi16 : I16 -> I16, bi32 : I32 -> I32, bi64 : I64 -> I64, bi8 : I8 -> I8, bnat : Nat -> Nat, bu128 : U128 -> U128, bu16 : U16 -> U16, bu32 : U32 -> U32, bu64 : U64 -> U64, bu8 : U8 -> U8, dec : Dec -> Dec, f32 : F32 -> F32, f64 : F64 -> F64, fdec : Dec -> Dec, ff32 : F32 -> F32, ff64 : F64 -> F64, i128 : I128 -> I128, i16 : I16 -> I16, i32 : I32 -> I32, i64 : I64 -> I64, i8 : I8 -> I8, nat : Nat -> Nat, u128 : U128 -> U128, u16 : U16 -> U16, u32 : U32 -> U32, u64 : U64 -> U64, u8 : U8 -> U8 }"#, ) } #[test] fn issue_2458() { infer_eq_without_problem( indoc!( r#" Foo a : [Blah (Result (Bar a) { val: a })] Bar a : Foo a v : Bar U8 v = Blah (Ok (Blah (Err { val: 1 }))) v "# ), "Bar U8", ) } #[test] fn issue_2458_swapped_order() { infer_eq_without_problem( indoc!( r#" Bar a : Foo a Foo a : [Blah (Result (Bar a) { val: a })] v : Bar U8 v = Blah (Ok (Blah (Err { val: 1 }))) v "# ), "Bar U8", ) } // https://github.com/roc-lang/roc/issues/2379 #[test] fn copy_vars_referencing_copied_vars() { infer_eq_without_problem( indoc!( r#" Job : [Job [Command] (List Job)] job : Job job "# ), "Job", ) } #[test] fn generalize_and_specialize_recursion_var() { infer_eq_without_problem( indoc!( r#" Job a : [Job (List (Job a)) a] job : Job Str when job is Job lst s -> P lst s "# ), "[P (List ([Job (List a) Str] as a)) Str]", ) } #[test] fn to_int() { infer_eq_without_problem( indoc!( r#" { toI8: Num.toI8, toI16: Num.toI16, toI32: Num.toI32, toI64: Num.toI64, toI128: Num.toI128, toNat: Num.toNat, toU8: Num.toU8, toU16: Num.toU16, toU32: Num.toU32, toU64: Num.toU64, toU128: Num.toU128, } "# ), r#"{ toI128 : Int * -> I128, toI16 : Int a -> I16, toI32 : Int b -> I32, toI64 : Int c -> I64, toI8 : Int d -> I8, toNat : Int e -> Nat, toU128 : Int f -> U128, toU16 : Int g -> U16, toU32 : Int h -> U32, toU64 : Int i -> U64, toU8 : Int j -> U8 }"#, ) } #[test] fn to_float() { infer_eq_without_problem( indoc!( r#" { toF32: Num.toF32, toF64: Num.toF64, } "# ), r#"{ toF32 : Num * -> F32, toF64 : Num a -> F64 }"#, ) } #[test] fn opaque_wrap_infer() { infer_eq_without_problem( indoc!( r#" Age := U32 @Age 21 "# ), r#"Age"#, ) } #[test] fn opaque_wrap_check() { infer_eq_without_problem( indoc!( r#" Age := U32 a : Age a = @Age 21 a "# ), r#"Age"#, ) } #[test] fn opaque_wrap_polymorphic_infer() { infer_eq_without_problem( indoc!( r#" Id n := [Id U32 n] @Id (Id 21 "sasha") "# ), r#"Id Str"#, ) } #[test] fn opaque_wrap_polymorphic_check() { infer_eq_without_problem( indoc!( r#" Id n := [Id U32 n] a : Id Str a = @Id (Id 21 "sasha") a "# ), r#"Id Str"#, ) } #[test] fn opaque_wrap_polymorphic_from_multiple_branches_infer() { infer_eq_without_problem( indoc!( r#" Id n := [Id U32 n] condition : Bool if condition then @Id (Id 21 (Y "sasha")) else @Id (Id 21 (Z "felix")) "# ), r#"Id [Y Str, Z Str]"#, ) } #[test] fn opaque_wrap_polymorphic_from_multiple_branches_check() { infer_eq_without_problem( indoc!( r#" Id n := [Id U32 n] condition : Bool v : Id [Y Str, Z Str] v = if condition then @Id (Id 21 (Y "sasha")) else @Id (Id 21 (Z "felix")) v "# ), r#"Id [Y Str, Z Str]"#, ) } #[test] fn opaque_unwrap_infer() { infer_eq_without_problem( indoc!( r#" Age := U32 \@Age n -> n "# ), r#"Age -> U32"#, ) } #[test] fn opaque_unwrap_check() { infer_eq_without_problem( indoc!( r#" Age := U32 v : Age -> U32 v = \@Age n -> n v "# ), r#"Age -> U32"#, ) } #[test] fn opaque_unwrap_polymorphic_infer() { infer_eq_without_problem( indoc!( r#" Id n := [Id U32 n] \@Id (Id _ n) -> n "# ), r#"Id a -> a"#, ) } #[test] fn opaque_unwrap_polymorphic_check() { infer_eq_without_problem( indoc!( r#" Id n := [Id U32 n] v : Id a -> a v = \@Id (Id _ n) -> n v "# ), r#"Id a -> a"#, ) } #[test] fn opaque_unwrap_polymorphic_specialized_infer() { infer_eq_without_problem( indoc!( r#" Id n := [Id U32 n] strToBool : Str -> Bool \@Id (Id _ n) -> strToBool n "# ), r#"Id Str -> Bool"#, ) } #[test] fn opaque_unwrap_polymorphic_specialized_check() { infer_eq_without_problem( indoc!( r#" Id n := [Id U32 n] strToBool : Str -> Bool v : Id Str -> Bool v = \@Id (Id _ n) -> strToBool n v "# ), r#"Id Str -> Bool"#, ) } #[test] fn opaque_unwrap_polymorphic_from_multiple_branches_infer() { infer_eq_without_problem( indoc!( r#" Id n := [Id U32 n] \id -> when id is @Id (Id _ A) -> "" @Id (Id _ B) -> "" @Id (Id _ (C { a: "" })) -> "" @Id (Id _ (C { a: _ })) -> "" # any other string, for exhautiveness "# ), r#"Id [A, B, C { a : Str }*] -> Str"#, ) } #[test] fn opaque_unwrap_polymorphic_from_multiple_branches_check() { infer_eq_without_problem( indoc!( r#" Id n := [Id U32 n] f : Id [A, B, C { a : Str }e] -> Str f = \id -> when id is @Id (Id _ A) -> "" @Id (Id _ B) -> "" @Id (Id _ (C { a: "" })) -> "" @Id (Id _ (C { a: _ })) -> "" # any other string, for exhautiveness f "# ), r#"Id [A, B, C { a : Str }e] -> Str"#, ) } #[test] fn lambda_set_within_alias_is_quantified() { infer_eq_without_problem( indoc!( r#" app "test" provides [effectAlways] to "./platform" Effect a := {} -> a effectAlways : a -> Effect a effectAlways = \x -> inner = \{} -> x @Effect inner "# ), r#"a -> Effect a"#, ) } #[test] fn generalized_accessor_function_applied() { infer_eq_without_problem( indoc!( r#" returnFoo = .foo returnFoo { foo: "foo" } "# ), "Str", ) } #[test] fn record_extension_variable_is_alias() { infer_eq_without_problem( indoc!( r#" Other a b : { y: a, z: b } f : { x : Str }(Other Str Str) f "# ), r#"{ x : Str, y : Str, z : Str }"#, ) } #[test] fn tag_extension_variable_is_alias() { infer_eq_without_problem( indoc!( r#" Other : [B, C] f : [A]Other f "# ), r#"[A, B, C]"#, ) } #[test] // https://github.com/roc-lang/roc/issues/2702 fn tag_inclusion_behind_opaque() { infer_eq_without_problem( indoc!( r#" Outer k := [Empty, Wrapped k] insert : Outer k, k -> Outer k insert = \m, var -> when m is @Outer Empty -> @Outer (Wrapped var) @Outer (Wrapped _) -> @Outer (Wrapped var) insert "# ), r#"Outer k, k -> Outer k"#, ) } #[test] fn tag_inclusion_behind_opaque_infer() { infer_eq_without_problem( indoc!( r#" Outer k := [Empty, Wrapped k] when (@Outer Empty) is @Outer Empty -> @Outer (Wrapped "") @Outer (Wrapped k) -> @Outer (Wrapped k) "# ), r#"Outer Str"#, ) } #[test] fn tag_inclusion_behind_opaque_infer_single_ctor() { infer_eq_without_problem( indoc!( r#" Outer := [A, B] when (@Outer A) is @Outer A -> @Outer A @Outer B -> @Outer B "# ), r#"Outer"#, ) } #[test] fn issue_2583_specialize_errors_behind_unified_branches() { infer_eq_without_problem( indoc!( r#" if Bool.true then List.first [] else Str.toI64 "" "# ), "Result I64 [InvalidNumStr, ListWasEmpty]", ) } #[test] fn lots_of_type_variables() { infer_eq_without_problem( indoc!( r#" fun = \a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,aa,bb -> {a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,aa,bb} fun "# ), "a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, aa, bb -> { a : a, aa : aa, b : b, bb : bb, c : c, d : d, e : e, f : f, g : g, h : h, i : i, j : j, k : k, l : l, m : m, n : n, o : o, p : p, q : q, r : r, s : s, t : t, u : u, v : v, w : w, x : x, y : y, z : z }", ) } #[test] fn exposed_ability_name() { infer_eq_without_problem( indoc!( r#" app "test" provides [hash] to "./platform" MHash has hash : a -> U64 | a has MHash "# ), "a -> U64 | a has MHash", ) } #[test] fn single_ability_single_member_specializations() { check_inferred_abilities( indoc!( r#" app "test" provides [hash] to "./platform" MHash has hash : a -> U64 | a has MHash Id := U64 has [MHash {hash}] hash = \@Id n -> n "# ), [("MHash:hash", "Id")], ) } #[test] fn single_ability_multiple_members_specializations() { check_inferred_abilities( indoc!( r#" app "test" provides [hash, hash32] to "./platform" MHash has hash : a -> U64 | a has MHash hash32 : a -> U32 | a has MHash Id := U64 has [MHash {hash, hash32}] hash = \@Id n -> n hash32 = \@Id n -> Num.toU32 n "# ), [("MHash:hash", "Id"), ("MHash:hash32", "Id")], ) } #[test] fn multiple_abilities_multiple_members_specializations() { check_inferred_abilities( indoc!( r#" app "test" provides [hash, hash32, eq, le] to "./platform" MHash has hash : a -> U64 | a has MHash hash32 : a -> U32 | a has MHash Ord has eq : a, a -> Bool | a has Ord le : a, a -> Bool | a has Ord Id := U64 has [MHash {hash, hash32}, Ord {eq, le}] hash = \@Id n -> n hash32 = \@Id n -> Num.toU32 n eq = \@Id m, @Id n -> m == n le = \@Id m, @Id n -> m < n "# ), [ ("MHash:hash", "Id"), ("MHash:hash32", "Id"), ("Ord:eq", "Id"), ("Ord:le", "Id"), ], ) } #[test] fn ability_checked_specialization_with_typed_body() { check_inferred_abilities( indoc!( r#" app "test" provides [hash] to "./platform" MHash has hash : a -> U64 | a has MHash Id := U64 has [MHash {hash}] hash : Id -> U64 hash = \@Id n -> n "# ), [("MHash:hash", "Id")], ) } #[test] fn ability_checked_specialization_with_annotation_only() { check_inferred_abilities( indoc!( r#" app "test" provides [hash] to "./platform" MHash has hash : a -> U64 | a has MHash Id := U64 has [MHash {hash}] hash : Id -> U64 "# ), [("MHash:hash", "Id")], ) } #[test] fn ability_specialization_called() { infer_eq_without_problem( indoc!( r#" app "test" provides [zero] to "./platform" MHash has hash : a -> U64 | a has MHash Id := U64 has [MHash {hash}] hash = \@Id n -> n zero = hash (@Id 0) "# ), "U64", ) } #[test] fn alias_ability_member() { infer_eq_without_problem( indoc!( r#" app "test" provides [thething] to "./platform" MHash has hash : a -> U64 | a has MHash thething = itis = hash itis "# ), "a -> U64 | a has MHash", ) } #[test] fn when_branch_and_body_flipflop() { infer_eq_without_problem( indoc!( r#" func = \record -> when record.tag is A -> { record & tag: B } B -> { record & tag: A } func "# ), "{ tag : [A, B] }a -> { tag : [A, B] }a", ) } #[test] fn ability_constrained_in_non_member_check() { infer_eq_without_problem( indoc!( r#" app "test" provides [hashEq] to "./platform" MHash has hash : a -> U64 | a has MHash hashEq : a, a -> Bool | a has MHash hashEq = \x, y -> hash x == hash y "# ), "a, a -> Bool | a has MHash", ) } #[test] fn ability_constrained_in_non_member_infer() { infer_eq_without_problem( indoc!( r#" app "test" provides [hashEq] to "./platform" MHash has hash : a -> U64 | a has MHash hashEq = \x, y -> hash x == hash y "# ), "a, a1 -> Bool | a has MHash, a1 has MHash", ) } #[test] fn ability_constrained_in_non_member_infer_usage() { infer_eq_without_problem( indoc!( r#" app "test" provides [result] to "./platform" MHash has hash : a -> U64 | a has MHash hashEq = \x, y -> hash x == hash y Id := U64 has [MHash {hash}] hash = \@Id n -> n result = hashEq (@Id 100) (@Id 101) "# ), "Bool", ) } #[test] fn ability_constrained_in_non_member_multiple_specializations() { infer_eq_without_problem( indoc!( r#" app "test" provides [result] to "./platform" MHash has hash : a -> U64 | a has MHash mulMHashes = \x, y -> hash x * hash y Id := U64 has [MHash { hash: hashId }] hashId = \@Id n -> n Three := {} has [MHash { hash: hashThree }] hashThree = \@Three _ -> 3 result = mulMHashes (@Id 100) (@Three {}) "# ), "U64", ) } #[test] fn intermediate_branch_types() { infer_queries!( indoc!( r#" app "test" provides [foo] to "./platform" foo : [True, False] -> Str foo = \ob -> # ^^ when ob is # ^^ True -> "A" # ^^^^ False -> "B" # ^^^^^ "# ), @r###" ob : [False, True] ob : [False, True] True : [False, True] False : [False, True] "### ) } #[test] fn nested_open_tag_union() { infer_eq_without_problem( indoc!( r#" app "test" provides [go] to "./platform" Expr : [ Wrap Expr, Val I64, ] go : Expr -> Expr go = \e -> when P e is P (Wrap (Val _)) -> Wrap e # This branch should force the first argument to `P` and # the first argument to `Wrap` to be an open tag union. # This tests checks that we don't regress on that. P y1 -> Wrap y1 "# ), indoc!(r#"Expr -> Expr"#), ) } #[test] fn opaque_and_alias_unify() { infer_eq_without_problem( indoc!( r#" app "test" provides [always] to "./platform" Effect a := {} -> a Task a err : Effect (Result a err) always : a -> Task a * always = \x -> @Effect (\{} -> Ok x) "# ), "a -> Task a *", ); } #[test] fn export_rigid_to_lower_rank() { infer_eq_without_problem( indoc!( r#" app "test" provides [foo] to "./platform" F a : { foo : a } foo = \arg -> x : F b x = arg x.foo "# ), "F b -> b", ); } #[test] fn alias_in_opaque() { infer_eq_without_problem( indoc!( r#" app "test" provides [foo] to "./platform" MyError : [Error] MyResult := Result U8 MyError foo = @MyResult (Err Error) "# ), "MyResult", ) } #[test] fn alias_propagates_able_var() { infer_eq_without_problem( indoc!( r#" app "test" provides [zeroEncoder] to "./platform" MEncoder fmt := List U8, fmt -> List U8 | fmt has Format Format has it : fmt -> {} | fmt has Format zeroEncoder = @MEncoder \lst, _ -> lst "# ), "MEncoder a | a has Format", ) } #[test] fn encoder() { infer_queries!( indoc!( r#" app "test" provides [myU8Bytes] to "./platform" MEncoder fmt := List U8, fmt -> List U8 | fmt has Format MEncoding has toEncoder : val -> MEncoder fmt | val has MEncoding, fmt has Format Format has u8 : U8 -> MEncoder fmt | fmt has Format appendWith : List U8, MEncoder fmt, fmt -> List U8 | fmt has Format appendWith = \lst, (@MEncoder doFormat), fmt -> doFormat lst fmt toBytes : val, fmt -> List U8 | val has MEncoding, fmt has Format toBytes = \val, fmt -> appendWith [] (toEncoder val) fmt Linear := {} has [Format {u8}] u8 = \n -> @MEncoder (\lst, @Linear {} -> List.append lst n) #^^{-1} MyU8 := U8 has [MEncoding {toEncoder}] toEncoder = \@MyU8 n -> u8 n #^^^^^^^^^{-1} myU8Bytes = toBytes (@MyU8 15) (@Linear {}) #^^^^^^^^^{-1} "# ), @r###" Linear#u8(10) : U8 -[[u8(10)]]-> MEncoder Linear MyU8#toEncoder(11) : MyU8 -[[toEncoder(11)]]-> MEncoder fmt | fmt has Format myU8Bytes : List U8 "### ) } #[test] fn decoder() { infer_queries!( indoc!( r#" app "test" provides [myU8] to "./platform" MDecodeError : [TooShort, Leftover (List U8)] MDecoder val fmt := List U8, fmt -> { result: Result val MDecodeError, rest: List U8 } | fmt has MDecoderFormatting MDecoding has decoder : MDecoder val fmt | val has MDecoding, fmt has MDecoderFormatting MDecoderFormatting has u8 : MDecoder U8 fmt | fmt has MDecoderFormatting decodeWith : List U8, MDecoder val fmt, fmt -> { result: Result val MDecodeError, rest: List U8 } | fmt has MDecoderFormatting decodeWith = \lst, (@MDecoder doDecode), fmt -> doDecode lst fmt fromBytes : List U8, fmt -> Result val MDecodeError | fmt has MDecoderFormatting, val has MDecoding fromBytes = \lst, fmt -> when decodeWith lst decoder fmt is { result, rest } -> when result is Ok val -> if List.isEmpty rest then Ok val else Err (Leftover rest) Err e -> Err e Linear := {} has [MDecoderFormatting {u8}] u8 = @MDecoder \lst, @Linear {} -> #^^{-1} when List.first lst is Ok n -> { result: Ok n, rest: List.dropFirst lst } Err _ -> { result: Err TooShort, rest: [] } MyU8 := U8 has [MDecoding {decoder}] decoder = @MDecoder \lst, fmt -> #^^^^^^^{-1} when decodeWith lst u8 fmt is { result, rest } -> { result: Result.map result (\n -> @MyU8 n), rest } myU8 : Result MyU8 _ myU8 = fromBytes [15] (@Linear {}) #^^^^{-1} "# ), @r###" Linear#u8(11) : MDecoder U8 Linear MyU8#decoder(12) : MDecoder MyU8 fmt | fmt has MDecoderFormatting myU8 : Result MyU8 MDecodeError "### ) } #[test] fn task_wildcard_wildcard() { infer_eq_without_problem( indoc!( r#" app "test" provides [tforever] to "./platform" Effect a := {} -> a eforever : Effect a -> Effect b Task a err : Effect (Result a err) tforever : Task val err -> Task * * tforever = \task -> eforever task "# ), "Task val err -> Task * *", ); } #[test] fn static_specialization() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Default has default : {} -> a | a has Default A := {} has [Default {default}] default = \{} -> @A {} main = a : A a = default {} # ^^^^^^^ a "# ), @"A#default(4) : {} -[[default(4)]]-> A" ) } #[test] fn stdlib_encode_json() { infer_eq_without_problem( indoc!( r#" app "test" imports [Json] provides [main] to "./platform" HelloWorld := {} has [Encoding {toEncoder}] toEncoder = \@HelloWorld {} -> Encode.custom \bytes, fmt -> bytes |> Encode.appendWith (Encode.string "Hello, World!\n") fmt main = when Str.fromUtf8 (Encode.toBytes (@HelloWorld {}) Json.toUtf8) is Ok s -> s _ -> "" "# ), "Str", ) } #[test] fn encode_record() { infer_queries!( indoc!( r#" app "test" imports [Encode.{ toEncoder }] provides [main] to "./platform" main = toEncoder { a: "" } # ^^^^^^^^^ "# ), @"Encoding#toEncoder(2) : { a : Str } -[[#Derived.toEncoder_{a}(0)]]-> Encoder fmt | fmt has EncoderFormatting" ) } #[test] fn encode_record_with_nested_custom_impl() { infer_queries!( indoc!( r#" app "test" imports [Encode.{ toEncoder, custom }] provides [main] to "./platform" A := {} has [Encoding {toEncoder}] toEncoder = \@A _ -> custom \b, _ -> b main = toEncoder { a: @A {} } # ^^^^^^^^^ "# ), @"Encoding#toEncoder(2) : { a : A } -[[#Derived.toEncoder_{a}(0)]]-> Encoder fmt | fmt has EncoderFormatting" ) } #[test] fn resolve_lambda_set_generalized_ability_alias() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Id has id : a -> a | a has Id A := {} has [Id {id}] id = \@A {} -> @A {} #^^{-1} main = alias1 = \x -> id x # ^^ alias2 = \x -> alias1 x # ^^^^^^ a : A a = alias2 (@A {}) # ^^^^^^ a "# ), @r###" A#id(4) : A -[[id(4)]]-> A Id#id(2) : a -[[] + a:id(2):1]-> a | a has Id alias1 : a -[[alias1(6)]]-> a | a has Id alias2 : A -[[alias2(7)]]-> A "### ) } #[test] fn resolve_lambda_set_weakened_ability_alias() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Id has id : a -> a | a has Id A := {} has [Id {id}] id = \@A {} -> @A {} #^^{-1} main = # Both alias1, alias2 should get weakened alias1 = id # ^^ alias2 = alias1 # ^^^^^^ a : A a = alias2 (@A {}) # ^^^^^^ a "# ), @r###" A#id(4) : A -[[id(4)]]-> A Id#id(2) : A -[[id(4)]]-> A alias1 : A -[[id(4)]]-> A alias2 : A -[[id(4)]]-> A "### ) } #[test] fn resolve_lambda_set_ability_chain() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Id1 has id1 : a -> a | a has Id1 Id2 has id2 : a -> a | a has Id2 A := {} has [Id1 {id1}, Id2 {id2}] id1 = \@A {} -> @A {} #^^^{-1} id2 = \@A {} -> id1 (@A {}) #^^^{-1} ^^^ main = a : A a = id2 (@A {}) # ^^^ a "# ), @r###" A#id1(6) : A -[[id1(6)]]-> A A#id2(7) : A -[[id2(7)]]-> A A#id1(6) : A -[[id1(6)]]-> A A#id2(7) : A -[[id2(7)]]-> A "### ) } #[test] fn resolve_lambda_set_branches_ability_vs_non_ability() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Id has id : a -> a | a has Id A := {} has [Id {id}] id = \@A {} -> @A {} #^^{-1} idNotAbility = \x -> x #^^^^^^^^^^^^{-1} main = choice : [T, U] # Should not get generalized idChoice = #^^^^^^^^{-1} when choice is T -> id U -> idNotAbility idChoice (@A {}) #^^^^^^^^{-1} "# ), @r###" A#id(4) : A -[[id(4)]]-> A idNotAbility : a -[[idNotAbility(5)]]-> a idChoice : A -[[id(4), idNotAbility(5)]]-> A idChoice : A -[[id(4), idNotAbility(5)]]-> A "### ) } #[test] fn resolve_lambda_set_branches_same_ability() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Id has id : a -> a | a has Id A := {} has [Id {id}] id = \@A {} -> @A {} #^^{-1} main = choice : [T, U] # Should not get generalized idChoice = #^^^^^^^^{-1} when choice is T -> id U -> id idChoice (@A {}) #^^^^^^^^{-1} "# ), @r#" A#id(4) : A -[[id(4)]]-> A idChoice : A -[[id(4)]]-> A idChoice : A -[[id(4)]]-> A "# ) } #[test] fn resolve_unspecialized_lambda_set_behind_alias() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Thunk a : {} -> a Id has id : a -> Thunk a | a has Id A := {} has [Id {id}] id = \@A {} -> \{} -> @A {} #^^{-1} main = alias = \x -> id x # ^^ a : A a = (alias (@A {})) {} # ^^^^^ a "# ), @r#" A#id(5) : {} -[[id(5)]]-> ({} -[[8]]-> {}) Id#id(3) : a -[[] + a:id(3):1]-> ({} -[[] + a:id(3):2]-> a) | a has Id alias : {} -[[alias(9)]]-> ({} -[[8]]-> {}) "# print_only_under_alias: true ) } #[test] fn resolve_unspecialized_lambda_set_behind_opaque() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Thunk a := {} -> a Id has id : a -> Thunk a | a has Id A := {} has [Id {id}] id = \@A {} -> @Thunk (\{} -> @A {}) #^^{-1} main = thunk = id (@A {}) @Thunk it = thunk it {} #^^{-1} "# ), @r#" A#id(5) : {} -[[id(5)]]-> ({} -[[8]]-> {}) it : {} -[[8]]-> {} "# print_only_under_alias: true ) } #[test] fn resolve_two_unspecialized_lambda_sets_in_one_lambda_set() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Thunk a : {} -> a Id has id : a -> Thunk a | a has Id A := {} has [Id {id}] id = \@A {} -> \{} -> @A {} #^^{-1} main = a : A a = (id (@A {})) {} # ^^ a "# ), @r#" A#id(5) : {} -[[id(5)]]-> ({} -[[8]]-> {}) A#id(5) : {} -[[id(5)]]-> ({} -[[8]]-> {}) "# print_only_under_alias: true ) } #[test] fn resolve_recursive_ability_lambda_set() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Diverge has diverge : a -> a | a has Diverge A := {} has [Diverge {diverge}] diverge : A -> A diverge = \@A {} -> diverge (@A {}) #^^^^^^^{-1} ^^^^^^^ main = a : A a = diverge (@A {}) # ^^^^^^^ a "# ), @r###" A#diverge(4) : A -[[diverge(4)]]-> A A#diverge(4) : A -[[diverge(4)]]-> A A#diverge(4) : A -[[diverge(4)]]-> A "### ) } #[test] fn resolve_mutually_recursive_ability_lambda_sets() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Bounce has ping : a -> a | a has Bounce pong : a -> a | a has Bounce A := {} has [Bounce {ping: pingA, pong: pongA}] pingA = \@A {} -> pong (@A {}) #^^^^^{-1} ^^^^ pongA = \@A {} -> ping (@A {}) #^^^^^{-1} ^^^^ main = a : A a = ping (@A {}) # ^^^^ a "# ), @r###" pingA : A -[[pingA(5)]]-> A A#pong(6) : A -[[pongA(6)]]-> A pongA : A -[[pongA(6)]]-> A A#ping(5) : A -[[pingA(5)]]-> A A#ping(5) : A -[[pingA(5)]]-> A "### ) } #[test] fn resolve_mutually_recursive_ability_lambda_sets_inferred() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Bounce has ping : a -> a | a has Bounce pong : a -> a | a has Bounce A := {} has [Bounce {ping, pong}] ping = \@A {} -> pong (@A {}) #^^^^{-1} ^^^^ pong = \@A {} -> ping (@A {}) #^^^^{-1} ^^^^ main = a : A a = ping (@A {}) # ^^^^ a "# ), @r###" A#ping(5) : A -[[ping(5)]]-> A A#pong(6) : A -[[pong(6)]]-> A A#pong(6) : A -[[pong(6)]]-> A A#ping(5) : A -[[ping(5)]]-> A A#ping(5) : A -[[ping(5)]]-> A "### ) } #[test] fn list_of_lambdas() { infer_queries!( indoc!( r#" [\{} -> {}, \{} -> {}] #^^^^^^^^^^^^^^^^^^^^^^{-1} "# ), @r###"[\{} -> {}, \{} -> {}] : List ({}w_a -[[1, 2]]-> {})"### ) } #[test] fn self_recursion_with_inference_var() { infer_eq_without_problem( indoc!( r#" f : _ -> _ f = \_ -> if Bool.false then "" else f "" f "# ), "Str -> Str", ) } #[test] fn mutual_recursion_with_inference_var() { infer_eq_without_problem( indoc!( r#" f : _ -> Str f = \s -> g s g = \s -> if Bool.true then s else f s g "# ), "Str -> Str", ) } #[test] fn issue_3261() { infer_queries!( indoc!( r#" Named : [Named Str (List Named)] foo : Named foo = Named "outer" [Named "inner" []] #^^^{-1} Named name outerList = foo #^^^^^^^^^^^^^^^^^^^^{-1} # ^^^^ ^^^^^^^^^ {name, outerList} "# ), @r#" foo : [Named Str (List a)] as a Named name outerList : [Named Str (List a)] as a name : Str outerList : List ([Named Str (List a)] as a) "# print_only_under_alias: true ) } #[test] fn function_alias_in_signature() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" Parser a : List U8 -> List [Pair a (List U8)] any: Parser U8 any = \inp -> when List.first inp is Ok u -> [Pair u (List.drop inp 1)] _ -> [] main = any "# ), "Parser U8", ); } #[test] fn infer_variables_in_value_def_signature() { infer_eq_without_problem( indoc!( r#" app "test" provides [a] to "./platform" a : {a: _} a = {a: ""} "# ), "{ a : Str }", ); } #[test] fn infer_variables_in_destructure_def_signature() { infer_eq_without_problem( indoc!( r#" app "test" provides [a] to "./platform" {a} : {a: _} {a} = {a: ""} "# ), "Str", ) } #[test] fn lambda_sets_collide_with_captured_var() { infer_queries!( indoc!( r#" capture : a -> ({} -> Str) capture = \val -> thunk = \{} -> when val is _ -> "" thunk x : [True, False] fun = when x is True -> capture "" # ^^^^^^^ False -> capture {} # ^^^^^^^ fun #^^^{-1} "# ), @r#" capture : Str -[[capture(1)]]-> ({} -[[thunk(5) {}, thunk(5) Str]]-> Str) capture : {} -[[capture(1)]]-> ({} -[[thunk(5) {}, thunk(5) Str]]-> Str) fun : {} -[[thunk(5) {}, thunk(5) Str]]-> Str "# ); } #[test] fn lambda_sets_collide_with_captured_function() { infer_queries!( indoc!( r#" Lazy a : {} -> a after : Lazy a, (a -> Lazy b) -> Lazy b after = \effect, map -> thunk = \{} -> when map (effect {}) is b -> b {} thunk f = \_ -> \_ -> "" g = \{ s1 } -> \_ -> s1 x : [True, False] fun = when x is True -> after (\{} -> "") f False -> after (\{} -> {s1: "s1"}) g fun #^^^{-1} "# ), @r#"fun : {} -[[thunk(9) (({} -[[15]]-> { s1 : Str })) ({ s1 : Str } -[[g(4)]]-> ({} -[[13 Str]]-> Str)), thunk(9) (({} -[[14]]-> Str)) (Str -[[f(3)]]-> ({} -[[11]]-> Str))]]-> Str"# print_only_under_alias: true ); } #[test] fn lambda_set_niche_same_layout_different_constructor() { infer_queries!( indoc!( r#" capture : a -> ({} -> Str) capture = \val -> thunk = \{} -> when val is _ -> "" thunk x : [True, False] fun = when x is True -> capture {a: ""} False -> capture (A "") fun #^^^{-1} "# ), @r#"fun : {} -[[thunk(5) [A Str]w_a, thunk(5) { a : Str }]]-> Str"# ); } #[test] fn check_phantom_type() { infer_eq_without_problem( indoc!( r#" F a b := b foo : F Str Str -> F U8 Str x : F Str Str foo x "# ), "F U8 Str", ) } #[test] fn infer_phantom_type_flow() { infer_eq_without_problem( indoc!( r#" F a b := b foo : _ -> F U8 Str foo = \it -> it foo "# ), "F U8 Str -> F U8 Str", ) } #[test] fn infer_unbound_phantom_type_star() { infer_eq_without_problem( indoc!( r#" F a b := b foo = \@F {} -> @F "" foo "# ), "F * {}* -> F * Str", ) } #[test] fn polymorphic_lambda_set_specialization() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" F has f : a -> (b -> {}) | a has F, b has G G has g : b -> {} | b has G Fo := {} has [F {f}] f = \@Fo {} -> g #^{-1} Go := {} has [G {g}] g = \@Go {} -> {} #^{-1} main = (f (@Fo {})) (@Go {}) # ^ # ^^^^^^^^^^ "# ), @r###" Fo#f(7) : Fo -[[f(7)]]-> (b -[[] + b:g(4):1]-> {}) | b has G Go#g(8) : Go -[[g(8)]]-> {} Fo#f(7) : Fo -[[f(7)]]-> (Go -[[g(8)]]-> {}) f (@Fo {}) : Go -[[g(8)]]-> {} "### ); } #[test] fn polymorphic_lambda_set_specialization_bound_output() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" F has f : a -> ({} -> b) | a has F, b has G G has g : {} -> b | b has G Fo := {} has [F {f}] f = \@Fo {} -> g #^{-1} Go := {} has [G {g}] g = \{} -> @Go {} #^{-1} main = foo = 1 @Go it = (f (@Fo {})) {} # ^ # ^^^^^^^^^^ {foo, it} "# ), @r###" Fo#f(7) : Fo -[[f(7)]]-> ({} -[[] + b:g(4):1]-> b) | b has G Go#g(8) : {} -[[g(8)]]-> Go Fo#f(7) : Fo -[[f(7)]]-> ({} -[[g(8)]]-> Go) f (@Fo {}) : {} -[[g(8)]]-> Go "### ); } #[test] fn polymorphic_lambda_set_specialization_with_let_weakened() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" F has f : a -> (b -> {}) | a has F, b has G G has g : b -> {} | b has G Fo := {} has [F {f}] f = \@Fo {} -> g #^{-1} Go := {} has [G {g}] g = \@Go {} -> {} #^{-1} main = # h should get weakened h = f (@Fo {}) # ^ ^ h (@Go {}) # ^ "# ), @r###" Fo#f(7) : Fo -[[f(7)]]-> (b -[[] + b:g(4):1]-> {}) | b has G Go#g(8) : Go -[[g(8)]]-> {} h : Go -[[g(8)]]-> {} Fo#f(7) : Fo -[[f(7)]]-> (Go -[[g(8)]]-> {}) h : Go -[[g(8)]]-> {} "### ); } #[test] fn polymorphic_lambda_set_specialization_with_let_weakened_unapplied() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" F has f : a -> (b -> {}) | a has F, b has G G has g : b -> {} | b has G Fo := {} has [F {f}] f = \@Fo {} -> g #^{-1} Go := {} has [G {g}] g = \@Go {} -> {} #^{-1} main = #^^^^{-1} h = f (@Fo {}) # ^ ^ h "# ), @r###" Fo#f(7) : Fo -[[f(7)]]-> (b -[[] + b:g(4):1]-> {}) | b has G Go#g(8) : Go -[[g(8)]]-> {} main : b -[[] + b:g(4):1]-> {} | b has G h : b -[[] + b:g(4):1]-> {} | b has G Fo#f(7) : Fo -[[f(7)]]-> (b -[[] + b:g(4):1]-> {}) | b has G "### ); } #[test] fn polymorphic_lambda_set_specialization_with_deep_specialization_and_capture() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" F has f : a, b -> ({} -> ({} -> {})) | a has F, b has G G has g : b -> ({} -> {}) | b has G Fo := {} has [F {f}] f = \@Fo {}, b -> \{} -> g b #^{-1} Go := {} has [G {g}] g = \@Go {} -> \{} -> {} #^{-1} main = (f (@Fo {}) (@Go {})) {} # ^ "# ), @r###" Fo#f(7) : Fo, b -[[f(7)]]-> ({} -[[13 b]]-> ({} -[[] + b:g(4):2]-> {})) | b has G Go#g(8) : Go -[[g(8)]]-> ({} -[[14]]-> {}) Fo#f(7) : Fo, Go -[[f(7)]]-> ({} -[[13 Go]]-> ({} -[[14]]-> {})) "### ); } #[test] fn polymorphic_lambda_set_specialization_varying_over_multiple_variables() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" J has j : j -> (k -> {}) | j has J, k has K K has k : k -> {} | k has K C := {} has [J {j: jC}] jC = \@C _ -> k #^^{-1} D := {} has [J {j: jD}] jD = \@D _ -> k #^^{-1} E := {} has [K {k}] k = \@E _ -> {} #^{-1} f = \flag, a, b -> # ^ ^ it = # ^^ when flag is A -> j a # ^ B -> j b # ^ it # ^^ main = (f A (@C {}) (@D {})) (@E {}) # ^ # ^^^^^^^^^^^^^^^^^^^ #^^^^{-1} "# ), @r###" jC : C -[[jC(8)]]-> (k -[[] + k:k(4):1]-> {}) | k has K jD : D -[[jD(9)]]-> (k -[[] + k:k(4):1]-> {}) | k has K E#k(10) : E -[[k(10)]]-> {} a : j | j has J b : j | j has J it : k -[[] + j:j(2):2 + j1:j(2):2]-> {} | j has J, j1 has J, k has K J#j(2) : j -[[] + j:j(2):1]-> (k -[[] + j:j(2):2 + j1:j(2):2]-> {}) | j has J, j1 has J, k has K J#j(2) : j -[[] + j:j(2):1]-> (k -[[] + j1:j(2):2 + j:j(2):2]-> {}) | j has J, j1 has J, k has K it : k -[[] + j:j(2):2 + j1:j(2):2]-> {} | j has J, j1 has J, k has K f : [A, B], C, D -[[f(11)]]-> (E -[[k(10)]]-> {}) f A (@C {}) (@D {}) : E -[[k(10)]]-> {} main : {} "### ); } #[test] fn polymorphic_lambda_set_specialization_varying_over_multiple_variables_two_results() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" J has j : j -> (k -> {}) | j has J, k has K K has k : k -> {} | k has K C := {} has [J {j: jC}] jC = \@C _ -> k #^^{-1} D := {} has [J {j: jD}] jD = \@D _ -> k #^^{-1} E := {} has [K {k: kE}] kE = \@E _ -> {} #^^{-1} F := {} has [K {k: kF}] kF = \@F _ -> {} #^^{-1} f = \flag, a, b -> # ^ ^ it = # ^^ when flag is A -> j a # ^ B -> j b # ^ it # ^^ main = #^^^^{-1} it = \x -> # ^^ (f A (@C {}) (@D {})) x # ^ if Bool.true then it (@E {}) # ^^ else it (@F {}) # ^^ "# ), @r###" jC : C -[[jC(9)]]-> (k -[[] + k:k(4):1]-> {}) | k has K jD : D -[[jD(10)]]-> (k -[[] + k:k(4):1]-> {}) | k has K kE : E -[[kE(11)]]-> {} kF : F -[[kF(12)]]-> {} a : j | j has J b : j | j has J it : k -[[] + j:j(2):2 + j1:j(2):2]-> {} | j has J, j1 has J, k has K J#j(2) : j -[[] + j:j(2):1]-> (k -[[] + j:j(2):2 + j1:j(2):2]-> {}) | j has J, j1 has J, k has K J#j(2) : j -[[] + j:j(2):1]-> (k -[[] + j1:j(2):2 + j:j(2):2]-> {}) | j has J, j1 has J, k has K it : k -[[] + j:j(2):2 + j1:j(2):2]-> {} | j has J, j1 has J, k has K main : {} it : k -[[it(21)]]-> {} | k has K f : [A, B], C, D -[[f(13)]]-> (k -[[] + k:k(4):1]-> {}) | k has K it : E -[[it(21)]]-> {} it : F -[[it(21)]]-> {} "### ); } #[test] fn polymorphic_lambda_set_specialization_branching_over_single_variable() { infer_queries!( indoc!( r#" app "test" provides [f] to "./platform" J has j : j -> (k -> {}) | j has J, k has K K has k : k -> {} | k has K C := {} has [J {j: jC}] jC = \@C _ -> k D := {} has [J {j: jD}] jD = \@D _ -> k E := {} has [K {k}] k = \@E _ -> {} f = \flag, a, c -> it = when flag is A -> j a B -> j a it c # ^^ ^ "# ), @r###" it : k -[[] + j:j(2):2]-> {} | j has J, k has K c : k | k has K "### ); } #[test] fn wrap_recursive_opaque_negative_position() { infer_eq_without_problem( indoc!( r#" OList := [Nil, Cons {} OList] lst : [Cons {} OList] olist : OList olist = (\l -> @OList l) lst olist "# ), "OList", ); } #[test] fn wrap_recursive_opaque_positive_position() { infer_eq_without_problem( indoc!( r#" OList := [Nil, Cons {} OList] lst : [Cons {} OList] olist : OList olist = @OList lst olist "# ), "OList", ); } #[test] fn rosetree_with_result_is_legal_recursive_type() { infer_eq_without_problem( indoc!( r#" Rose a : [Rose (Result (List (Rose a)) I64)] x : Rose I64 x = Rose (Ok []) x "# ), "Rose I64", ); } #[test] fn opaque_wrap_function() { infer_eq_without_problem( indoc!( r#" A := U8 List.map [1, 2, 3] @A "# ), "List A", ); } #[test] fn opaque_wrap_function_with_inferred_arg() { infer_eq_without_problem( indoc!( r#" A a := a List.map [1u8, 2u8, 3u8] @A "# ), "List (A U8)", ); } #[test] fn shared_pattern_variable_in_when_patterns() { infer_queries!( indoc!( r#" when A "" is # ^^^^ A x | B x -> x # ^ ^ ^ "# ), @r###" A "" : [A Str, B Str] x : Str x : Str x : Str "### ); } #[test] fn shared_pattern_variable_in_multiple_branch_when_patterns() { infer_queries!( indoc!( r#" when A "" is # ^^^^ A x | B x -> x # ^ ^ ^ C x | D x -> x # ^ ^ ^ "# ), @r###" A "" : [A Str, B Str, C Str, D Str] x : Str x : Str x : Str x : Str x : Str x : Str "### ); } #[test] fn catchall_branch_for_pattern_not_last() { infer_queries!( indoc!( r#" \x -> when x is #^ A B _ -> "" A _ C -> "" "# ), @r#"x : [A [B]w_a [C]w_b]"# allow_errors: true ); } #[test] fn catchall_branch_walk_into_nested_types() { infer_queries!( indoc!( r#" \x -> when x is #^ { a: A { b: B } } -> "" _ -> "" "# ), @"x : { a : [A { b : [B]w_a }*]w_b }*" ); } #[test] fn infer_type_with_underscore_destructure_assignment() { infer_eq_without_problem( indoc!( r#" Pair x _ = Pair 0 1 x "# ), "Num *", ); } #[test] fn issue_3444() { infer_queries!( indoc!( r#" compose = \f, g -> closCompose = \x -> g (f x) closCompose const = \x -> closConst = \_ -> x closConst list = [] res : Str -> Str res = List.walk list (const "z") (\c1, c2 -> compose c1 c2) # ^^^^^ ^^^^^^^ # ^^^^^^^^^^^^^^^^^^^^^^^^ #^^^{-1} res "hello" #^^^{-1} "# ), @r###" const : Str -[[const(2)]]-> (Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str) compose : (Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str), (Str -[[]]-> Str) -[[compose(1)]]-> (Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str) \c1, c2 -> compose c1 c2 : (Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str), (Str -[[]]-> Str) -[[11]]-> (Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str) res : Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str res : Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str "### ); } #[test] fn transient_captures() { infer_queries!( indoc!( r#" x = "abc" getX = \{} -> x h = \{} -> (getX {}) #^{-1} h {} "# ), @"h : {}* -[[h(3) Str]]-> Str" ); } #[test] fn transient_captures_after_def_ordering() { infer_queries!( indoc!( r#" h = \{} -> (getX {}) #^{-1} getX = \{} -> x x = "abc" h {} "# ), @"h : {}* -[[h(1) Str]]-> Str" ); } #[test] fn mutually_recursive_captures() { infer_queries!( indoc!( r#" x = Bool.true y = Bool.false a = "foo" b = "bar" foo = \{} -> if x then a else bar {} #^^^{-1} bar = \{} -> if y then b else foo {} #^^^{-1} bar {} "# ), @r###" foo : {} -[[foo(5) Bool Bool Str Str]]-> Str bar : {} -[[bar(6) Bool Bool Str Str]]-> Str "### ); } #[test] fn unify_optional_record_fields_in_two_closed_records() { infer_eq_without_problem( indoc!( r#" f : { x ? Str, y ? Str } -> {} f {x : ""} "# ), "{}", ); } #[test] fn match_on_result_with_uninhabited_error_branch() { infer_eq_without_problem( indoc!( r#" x : Result Str [] x = Ok "abc" when x is Ok s -> s "# ), "Str", ); } #[test] fn match_on_result_with_uninhabited_error_destructuring() { infer_eq_without_problem( indoc!( r#" x : Result Str [] x = Ok "abc" Ok str = x str "# ), "Str", ); } #[test] fn match_on_result_with_uninhabited_error_destructuring_in_lambda_syntax() { infer_eq_without_problem( indoc!( r#" x : Result Str [] -> Str x = \Ok s -> s x "# ), "Result Str [] -> Str", ); } #[test] fn custom_implement_hash() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" Noop := {} has [Hash {hash}] hash = \hasher, @Noop {} -> hasher main = \hasher -> hash hasher (@Noop {}) "# ), "hasher -> hasher | hasher has Hasher", ); } #[test] fn dispatch_tag_union_function_inferred() { infer_eq_without_problem( indoc!( r#" g = if Bool.true then A else B g "" "# ), "[A Str, B Str]", ); } #[test] fn check_char_as_u8() { infer_eq_without_problem( indoc!( r#" x : U8 x = '.' x "# ), "U8", ); } #[test] fn check_char_as_u16() { infer_eq_without_problem( indoc!( r#" x : U16 x = '.' x "# ), "U16", ); } #[test] fn check_char_as_u32() { infer_eq_without_problem( indoc!( r#" x : U32 x = '.' x "# ), "U32", ); } #[test] fn check_char_pattern_as_u8() { infer_eq_without_problem( indoc!( r#" f : U8 -> _ f = \c -> when c is '.' -> 'A' c1 -> c1 f "# ), "U8 -> U8", ); } #[test] fn check_char_pattern_as_u16() { infer_eq_without_problem( indoc!( r#" f : U16 -> _ f = \c -> when c is '.' -> 'A' c1 -> c1 f "# ), "U16 -> U16", ); } #[test] fn check_char_pattern_as_u32() { infer_eq_without_problem( indoc!( r#" f : U32 -> _ f = \c -> when c is '.' -> 'A' c1 -> c1 f "# ), "U32 -> U32", ); } #[test] fn issue_4246_admit_recursion_between_opaque_functions() { infer_eq_without_problem( indoc!( r#" app "test" provides [b] to "./platform" O := {} -> {} a = @O \{} -> ((\@O f -> f {}) b) b = a "# ), "O", ); } #[test] fn custom_implement_eq() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" Trivial := {} has [Eq {isEq}] isEq = \@Trivial {}, @Trivial {} -> Bool.true main = Bool.isEq (@Trivial {}) (@Trivial {}) "# ), "Bool", ); } #[test] fn expand_able_variables_in_type_alias() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" F a : a | a has Hash main : F a -> F a #^^^^{-1} "# ), @"main : a -[[main(0)]]-> a | a has Hash" print_only_under_alias: true ); } #[test] fn self_recursive_function_not_syntactically_a_function() { infer_eq_without_problem( indoc!( r#" app "test" provides [fx] to "./platform" after : ({} -> a), ({} -> b) -> ({} -> b) fx = after (\{} -> {}) \{} -> if Bool.true then fx {} else {} "# ), "{} -> {}", ); } #[test] fn self_recursive_function_not_syntactically_a_function_nested() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" main = after : ({} -> a), ({} -> b) -> ({} -> b) fx = after (\{} -> {}) \{} -> if Bool.true then fx {} else {} fx "# ), "{} -> {}", ); } #[test] fn derive_to_encoder_for_opaque() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" N := U8 has [Encoding] main = Encode.toEncoder (@N 15) # ^^^^^^^^^^^^^^^^ "# ), @"N#Encode.toEncoder(3) : N -[[#N_toEncoder(3)]]-> Encoder fmt | fmt has EncoderFormatting" ); } #[test] fn derive_decoder_for_opaque() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" N := U8 has [Decoding] main : Decoder N _ main = Decode.custom \bytes, fmt -> Decode.decodeWith bytes Decode.decoder fmt # ^^^^^^^^^^^^^^ "# ), @"N#Decode.decoder(3) : List U8, fmt -[[7]]-> { rest : List U8, result : [Err [TooShort], Ok U8] } | fmt has DecoderFormatting" print_only_under_alias: true ); } #[test] fn derive_hash_for_opaque() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" N := U8 has [Hash] main = \hasher, @N n -> Hash.hash hasher (@N n) # ^^^^^^^^^ "# ), @"N#Hash.hash(3) : a, N -[[#N_hash(3)]]-> a | a has Hasher" ); } #[test] fn derive_eq_for_opaque() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" N := U8 has [Eq] main = Bool.isEq (@N 15) (@N 23) # ^^^^^^^^^ "# ), @"N#Bool.isEq(3) : N, N -[[#N_isEq(3)]]-> Bool" ); } #[test] fn multiple_variables_bound_to_an_ability_from_type_def() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" F a : a | a has Hash & Eq & Decoding main : F a -> F a #^^^^{-1} "# ), @"main : a -[[main(0)]]-> a | a has Hash & Decoding & Eq" print_only_under_alias: true ); } #[test] fn rigid_able_bounds_are_superset_of_flex_bounds_admitted() { infer_eq_without_problem( indoc!( r#" app "test" provides [main] to "./platform" f : x -> x | x has Hash g : x -> x | x has Decoding & Encoding main : x -> x | x has Hash & Decoding & Encoding main = \x -> x |> f |> g "# ), "x -> x | x has Hash & Encoding & Decoding", ); } #[test] fn extend_uninhabited_without_opening_union() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" walkHelp : {} -> [Continue {}, Break []] main = when walkHelp {} is # ^^^^^^^^^^^ Continue {} -> {} "# ), @"walkHelp {} : [Break [], Continue {}]" ); } #[test] fn contextual_openness_for_type_alias() { infer_queries!( indoc!( r#" app "test" provides [accum] to "./platform" Q : [Green, Blue] f : Q -> Q f = \q -> when q is #^{-1} Green -> Green Blue -> Blue accum = \q -> when q is #^^^^^{-1} A -> f Green B -> Yellow C -> Orange "# ), @r###" f : Q -[[f(2)]]-> Q accum : [A, B, C] -[[accum(0)]]-> [Blue, Green, Orange, Yellow]* "### ); } #[test] fn inferred_fixed_fixpoints() { infer_queries!( indoc!( r#" app "test" provides [job] to "./platform" F : [Bar, FromG G] G : [G {lst : List F}] job : { lst : List F } -> G job = \config -> G config #^^^{-1} # ^^^^^^ ^^^^^^^^ "# ), @r###" job : { lst : List [Bar, FromG ([G { lst : List [Bar, FromG a] }] as a)] } -[[job(0)]]-> [G { lst : List [Bar, FromG a] }] as a config : { lst : List [Bar, FromG ([G { lst : List [Bar, FromG a] }] as a)] } G config : [G { lst : List [Bar, FromG a] }] as a "### print_only_under_alias: true ); } #[test] fn fix_recursion_under_alias_issue_4368() { infer_eq_without_problem( indoc!( r#" app "test" provides [doIt] to "./platform" Effect : [ DoIt {} ({} -> Effect), ] Task := ({} -> Effect) -> Effect doIt : {} -> Task doIt = \{} -> @Task \toNext -> DoIt {} \{} -> (toNext {}) "# ), "{} -> Task", ); } #[test] fn choose_ranged_num_for_hash() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" main = \h -> Hash.hash h 7 # ^^^^^^^^^ "# ), @"Hash#Hash.hash(1) : a, I64 -[[Hash.hashI64(12)]]-> a | a has Hasher" ) } #[test] fn generalize_inferred_opaque_variable_bound_to_ability_issue_4408() { infer_eq_without_problem( indoc!( r#" app "test" provides [top] to "./platform" MDict u := (List u) | u has Hash & Eq bot : MDict k -> MDict k bot = \@MDict data -> when {} is {} -> @MDict data top : MDict v -> MDict v top = \x -> bot x "# ), "MDict v -> MDict v | v has Hash & Eq", ); } #[test] fn unify_types_with_fixed_fixpoints_outside_fixing_region() { infer_queries!(indoc!( r#" app "test" provides [main] to "./platform" Input := [ FromJob Job ] Job := [ Job (List Input) ] job : List Input -> Job job = \inputs -> @Job (Job inputs) helloWorld : Job helloWorld = @Job ( Job [ @Input (FromJob greeting) ] ) # ^^^^^^^^^^^^^^^^^^^^^^^^^ greeting : Job greeting = job [] main = (\_ -> "Which platform am I running on now?\n") helloWorld "# ), @r###" @Input (FromJob greeting) : [FromJob ([Job (List [FromJob a])] as a)] "### print_only_under_alias: true ) } #[test] fn impl_ability_for_opaque_with_lambda_sets() { infer_queries!( indoc!( r#" app "test" provides [isEqQ] to "./platform" Q := [ F (Str -> Str), G ] has [Eq { isEq: isEqQ }] isEqQ = \@Q q1, @Q q2 -> when T q1 q2 is #^^^^^{-1} T (F _) (F _) -> Bool.true T G G -> Bool.true _ -> Bool.false "# ), @"isEqQ : Q, Q -[[isEqQ(0)]]-> Bool" ); } #[test] fn impl_ability_for_opaque_with_lambda_sets_material() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Q := ({} -> Str) has [Eq {isEq: isEqQ}] isEqQ = \@Q f1, @Q f2 -> (f1 {} == f2 {}) #^^^^^{-1} main = isEqQ (@Q \{} -> "a") (@Q \{} -> "a") # ^^^^^ "# ), @r###" isEqQ : ({} -[[]]-> Str), ({} -[[]]-> Str) -[[isEqQ(2)]]-> [False, True] isEqQ : ({} -[[6, 7]]-> Str), ({} -[[6, 7]]-> Str) -[[isEqQ(2)]]-> [False, True] "### print_only_under_alias: true ); } #[test] fn infer_concrete_type_with_inference_var() { infer_queries!(indoc!( r#" app "test" provides [f] to "./platform" f : _ -> {} f = \_ -> f {} #^{-1} "# ), @r###" f : {} -[[f(0)]]-> {} "### ) } #[test] fn solve_inference_var_in_annotation_requiring_recursion_fix() { infer_queries!(indoc!( r#" app "test" provides [translateStatic] to "./platform" translateStatic : _ -> _ translateStatic = \Element c -> #^^^^^^^^^^^^^^^{-1} Element (List.map c translateStatic) "# ), @"translateStatic : [Element (List a)] as a -[[translateStatic(0)]]-> [Element (List b)]* as b" ) } #[test] fn infer_contextual_crash() { infer_eq_without_problem( indoc!( r#" app "test" provides [getInfallible] to "./platform" getInfallible = \result -> when result is Ok x -> x _ -> crash "turns out this was fallible" "# ), "[Ok a]* -> a", ); } #[test] fn resolve_eq_for_float_forces_dec() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" n : Num * main = n == 1. # ^ "# ), @"n : Dec" ); } #[test] fn resolve_set_eq_issue_4671() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" main = s1 : Set U8 s1 = Set.empty {} s2 : Set Str s2 = Set.empty {} Bool.isEq s1 s1 && Bool.isEq s2 s2 # ^^^^^^^^^ ^^^^^^^^^ "# ), @r###" Set#Bool.isEq(17) : Set U8, Set U8 -[[Set.isEq(17)]]-> Bool Set#Bool.isEq(17) : Set Str, Set Str -[[Set.isEq(17)]]-> Bool "### ); } #[test] fn disjoint_nested_lambdas_result_in_disjoint_parents_issue_4712() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" Parser a : {} -> a v1 : {} v1 = {} v2 : Str v2 = "" apply : Parser (a -> Str), a -> Parser Str apply = \fnParser, valParser -> \{} -> (fnParser {}) (valParser) map : a, (a -> Str) -> Parser Str map = \simpleParser, transform -> apply (\{} -> transform) simpleParser parseInput = \{} -> when [ map v1 (\{} -> ""), map v2 (\s -> s) ] is # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ _ -> "" main = parseInput {} == "" "# ), @r###" v1 = {} v2 = "" apply = \fnParser, valParser-> \{} -[9]-> (fnParser {}) valParser map = \simpleParser, transform-> apply \{} -[12]-> transform simpleParser parseInput = \{}-> when [ map v1 \{} -[13]-> "", map v2 \s -[14]-> s, ] is _ -> "" main = Bool.isEq (parseInput {}) "" [ map v1 (\{} -> ""), map v2 (\s -> s) ] : List (({} -[[9 (({} -[[12 (Str -[[14]]-> Str)]]-> (Str -[[14]]-> Str))) Str, 9 (({} -[[12 ({} -[[13]]-> Str)]]-> ({} -[[13]]-> Str))) {}]]-> Str)) "### print_only_under_alias: true print_can_decls: true ); } #[test] fn constrain_dbg_flex_var() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" polyDbg = \x -> #^^^^^^^{-1} dbg x x main = polyDbg "" "# ), @"polyDbg : a -[[polyDbg(1)]]-> a" ); } #[test] fn pattern_as_uses_inferred_type() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" main = when A "foo" is A _ as a -> a # ^ b -> b # ^ "# ), @r###" a : [A Str]w_a b : [A Str]w_a "### ); } #[test] fn pattern_as_does_not_narrow() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" input : [A Str, B Str] input = A "foo" drop : a -> {} drop = \_ -> {} main = when input is # ^^^^^ A _ as a -> drop a # ^ B _ as b -> drop b # ^ "# ), @r###" input : [A Str, B Str] a : [A Str, B Str] b : [A Str, B Str] "### ); } #[test] fn pattern_as_list() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" input : List Str input = [ "foo", "bar" ] main = when input is # ^^^^^ [ _first, .. as rest ] -> 1 + List.len rest # ^^^^ [] -> 0 "# ), @r###" input : List Str rest : List Str "### ); } #[test] fn rank_no_overgeneralization() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" main = #^^^^{-1} \x -> y = \z -> x z y "# ), @"main : (a -[[]]-> b) -[[main(0)]]-> (a -[[y(2) (a -[[]]-> b)]]-> b)" ); } #[test] fn when_branch_variables_not_generalized() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" main = \{} -> when Red is #^^^^{-1} x -> y : [Red]_ y = x z : [Red, Green]_ z = x {y, z} "# ), @"main : {}* -[[main(0)]]-> { y : [Green, Red]a, z : [Green, Red]a }" ); } #[test] fn weakened_list() { infer_queries!( indoc!( r#" app "test" provides [main] to "./platform" main = [] #^^^^{-1} "# ), @"main : List w_a" ); } }