mirror of
https://github.com/roc-lang/roc.git
synced 2025-08-03 03:42:17 +00:00
Merge remote-tracking branch 'remote/main' into builtin-json
This commit is contained in:
commit
e7e6978b0b
18 changed files with 859 additions and 245 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3770,6 +3770,7 @@ dependencies = [
|
|||
"distance",
|
||||
"indoc",
|
||||
"insta",
|
||||
"itertools 0.10.5",
|
||||
"pretty_assertions",
|
||||
"roc_builtins",
|
||||
"roc_can",
|
||||
|
|
|
@ -8,7 +8,7 @@ Roc is not ready for a 0.1 release yet, but we do have:
|
|||
- [frequently asked questions](https://github.com/roc-lang/roc/blob/main/FAQ.md)
|
||||
- [Zulip chat](https://roc.zulipchat.com) for help, questions and discussions
|
||||
|
||||
If you'd like to get involved in contributing to the language, the Zulip chat is also the best place to get help with [good first issues](https://github.com/roc-lang/roc/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
|
||||
If you'd like to contribute, check out [good first issues](https://github.com/roc-lang/roc/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). Don't hesitate to ask for help on our [Zulip chat](https://roc.zulipchat.com), we're friendly!
|
||||
|
||||
## Sponsors
|
||||
|
||||
|
|
|
@ -35,7 +35,10 @@ pub fn pretty_print_declarations(c: &Ctx, declarations: &Declarations) -> String
|
|||
DeclarationTag::Expectation => todo!(),
|
||||
DeclarationTag::ExpectationFx => todo!(),
|
||||
DeclarationTag::Destructure(_) => todo!(),
|
||||
DeclarationTag::MutualRecursion { .. } => todo!(),
|
||||
DeclarationTag::MutualRecursion { .. } => {
|
||||
// the defs will be printed next
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
defs.push(def);
|
||||
|
@ -123,9 +126,10 @@ fn toplevel_function<'a>(
|
|||
.append(f.line())
|
||||
.append(f.text("\\"))
|
||||
.append(f.intersperse(args, f.text(", ")))
|
||||
.append(f.text("->"))
|
||||
.append(f.text(" ->"))
|
||||
.group()
|
||||
.append(f.line())
|
||||
.append(expr(c, EPrec::Free, f, body))
|
||||
.append(expr(c, EPrec::Free, f, body).group())
|
||||
.nest(2)
|
||||
.group()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
pub use roc_ident::IdentStr;
|
||||
use std::fmt;
|
||||
use std::fmt::{self, Debug};
|
||||
|
||||
use crate::symbol::PQModuleName;
|
||||
|
||||
|
@ -74,7 +74,7 @@ pub type TagIdIntType = u16;
|
|||
/// If tags had a Symbol representation, then each module would have to
|
||||
/// deal with contention on a global mutex around translating tag strings
|
||||
/// into integers. (Record field labels work the same way, for the same reason.)
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct TagName(pub Uppercase);
|
||||
|
||||
roc_error_macros::assert_sizeof_non_wasm!(TagName, 16);
|
||||
|
@ -86,6 +86,12 @@ impl TagName {
|
|||
}
|
||||
}
|
||||
|
||||
impl Debug for TagName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Debug::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TagName {
|
||||
fn from(string: &str) -> Self {
|
||||
Self(string.into())
|
||||
|
@ -161,7 +167,7 @@ impl From<ModuleName> for Box<str> {
|
|||
|
||||
impl fmt::Display for ModuleName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -298,7 +304,7 @@ impl From<Ident> for Box<str> {
|
|||
|
||||
impl fmt::Display for Ident {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,7 +323,7 @@ impl fmt::Debug for Lowercase {
|
|||
|
||||
impl fmt::Display for Lowercase {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -336,6 +342,6 @@ impl fmt::Debug for Uppercase {
|
|||
|
||||
impl fmt::Display for Uppercase {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3506,6 +3506,7 @@ fn specialize_proc_help<'a>(
|
|||
UnionLayout::NonRecursive(_)
|
||||
| UnionLayout::Recursive(_)
|
||||
| UnionLayout::NullableUnwrapped { .. }
|
||||
| UnionLayout::NullableWrapped { .. }
|
||||
));
|
||||
debug_assert_eq!(field_layouts.len(), captured.len());
|
||||
|
||||
|
|
|
@ -1621,11 +1621,27 @@ impl<'a> LambdaSet<'a> {
|
|||
union_layout: union,
|
||||
}
|
||||
}
|
||||
UnionLayout::NonNullableUnwrapped(_) => todo!("recursive closures"),
|
||||
UnionLayout::NullableWrapped {
|
||||
nullable_id: _,
|
||||
other_tags: _,
|
||||
} => todo!("recursive closures"),
|
||||
} => {
|
||||
let (index, (name, fields)) = self
|
||||
.set
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, (s, layouts))| comparator(*s, layouts))
|
||||
.unwrap();
|
||||
|
||||
let closure_name = *name;
|
||||
|
||||
ClosureRepresentation::Union {
|
||||
tag_id: index as TagIdIntType,
|
||||
alphabetic_order_fields: fields,
|
||||
closure_name,
|
||||
union_layout: union,
|
||||
}
|
||||
}
|
||||
UnionLayout::NonNullableUnwrapped(_) => internal_error!("I thought a non-nullable-unwrapped variant for a lambda set was impossible: how could such a lambda set be created without a base case?"),
|
||||
}
|
||||
}
|
||||
Layout::Struct { .. } => {
|
||||
|
|
|
@ -8608,12 +8608,11 @@ mod solve_expr {
|
|||
|
||||
v2 = ""
|
||||
|
||||
apply = \fnParser, valParser-> \{} -[9]-> (fnParser {}) valParser
|
||||
apply = \fnParser, valParser -> \{} -[9]-> (fnParser {}) valParser
|
||||
|
||||
map = \simpleParser, transform-> apply \{} -[12]-> transform simpleParser
|
||||
map = \simpleParser, transform -> apply \{} -[12]-> transform simpleParser
|
||||
|
||||
parseInput =
|
||||
\{}->
|
||||
parseInput = \{} ->
|
||||
when [
|
||||
map v1 \{} -[13]-> "",
|
||||
map v2 \s -[14]-> s,
|
||||
|
|
|
@ -3911,6 +3911,34 @@ fn compose_recursive_lambda_set_productive_inferred() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
|
||||
fn compose_recursive_lambda_set_productive_nullable_wrapped() {
|
||||
assert_evals_to!(
|
||||
indoc!(
|
||||
r#"
|
||||
app "test" provides [main] to "./platform"
|
||||
|
||||
compose = \forward -> \f, g ->
|
||||
if forward
|
||||
then \x -> g (f x)
|
||||
else \x -> f (g x)
|
||||
|
||||
identity = \x -> x
|
||||
exclame = \s -> "\(s)!"
|
||||
whisper = \s -> "(\(s))"
|
||||
|
||||
main =
|
||||
res: Str -> Str
|
||||
res = List.walk [ exclame, whisper ] identity (compose Bool.false)
|
||||
res "hello"
|
||||
"#
|
||||
),
|
||||
RocStr::from("(hello)!"),
|
||||
RocStr
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
|
||||
fn local_binding_aliases_function() {
|
||||
|
@ -4335,3 +4363,26 @@ fn when_guard_appears_multiple_times_in_compiled_decision_tree_issue_5176() {
|
|||
u8
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
|
||||
fn recursive_lambda_set_resolved_only_upon_specialization() {
|
||||
assert_evals_to!(
|
||||
indoc!(
|
||||
r#"
|
||||
app "test" provides [main] to "./platform"
|
||||
|
||||
factCPS = \n, cont ->
|
||||
if n == 0 then
|
||||
cont 1
|
||||
else
|
||||
factCPS (n - 1) \value -> cont (n * value)
|
||||
|
||||
main =
|
||||
factCPS 5u64 \x -> x
|
||||
"#
|
||||
),
|
||||
120,
|
||||
u64
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
procedure Bool.2 ():
|
||||
let Bool.23 : Int1 = true;
|
||||
ret Bool.23;
|
||||
|
||||
procedure List.139 (List.140, List.141, List.138):
|
||||
let List.513 : [<rnw><null>, C *self Int1, C *self Int1] = CallByName Test.6 List.140 List.141 List.138;
|
||||
ret List.513;
|
||||
|
||||
procedure List.18 (List.136, List.137, List.138):
|
||||
let List.494 : [<rnw><null>, C *self Int1, C *self Int1] = CallByName List.92 List.136 List.137 List.138;
|
||||
ret List.494;
|
||||
|
||||
procedure List.6 (#Attr.2):
|
||||
let List.511 : U64 = lowlevel ListLen #Attr.2;
|
||||
ret List.511;
|
||||
|
||||
procedure List.66 (#Attr.2, #Attr.3):
|
||||
let List.510 : Int1 = lowlevel ListGetUnsafe #Attr.2 #Attr.3;
|
||||
ret List.510;
|
||||
|
||||
procedure List.80 (List.517, List.518, List.519, List.520, List.521):
|
||||
joinpoint List.500 List.433 List.434 List.435 List.436 List.437:
|
||||
let List.502 : Int1 = CallByName Num.22 List.436 List.437;
|
||||
if List.502 then
|
||||
let List.509 : Int1 = CallByName List.66 List.433 List.436;
|
||||
let List.503 : [<rnw><null>, C *self Int1, C *self Int1] = CallByName List.139 List.434 List.509 List.435;
|
||||
let List.506 : U64 = 1i64;
|
||||
let List.505 : U64 = CallByName Num.19 List.436 List.506;
|
||||
jump List.500 List.433 List.503 List.435 List.505 List.437;
|
||||
else
|
||||
ret List.434;
|
||||
in
|
||||
jump List.500 List.517 List.518 List.519 List.520 List.521;
|
||||
|
||||
procedure List.92 (List.430, List.431, List.432):
|
||||
let List.498 : U64 = 0i64;
|
||||
let List.499 : U64 = CallByName List.6 List.430;
|
||||
let List.497 : [<rnw><null>, C *self Int1, C *self Int1] = CallByName List.80 List.430 List.431 List.432 List.498 List.499;
|
||||
ret List.497;
|
||||
|
||||
procedure Num.19 (#Attr.2, #Attr.3):
|
||||
let Num.275 : U64 = lowlevel NumAdd #Attr.2 #Attr.3;
|
||||
ret Num.275;
|
||||
|
||||
procedure Num.22 (#Attr.2, #Attr.3):
|
||||
let Num.276 : Int1 = lowlevel NumLt #Attr.2 #Attr.3;
|
||||
ret Num.276;
|
||||
|
||||
procedure Str.3 (#Attr.2, #Attr.3):
|
||||
let Str.268 : Str = lowlevel StrConcat #Attr.2 #Attr.3;
|
||||
ret Str.268;
|
||||
|
||||
procedure Test.1 (Test.5):
|
||||
ret Test.5;
|
||||
|
||||
procedure Test.11 (Test.53, Test.54):
|
||||
joinpoint Test.27 Test.12 #Attr.12:
|
||||
let Test.8 : Int1 = UnionAtIndex (Id 2) (Index 1) #Attr.12;
|
||||
let Test.7 : [<rnw><null>, C *self Int1, C *self Int1] = UnionAtIndex (Id 2) (Index 0) #Attr.12;
|
||||
inc Test.7;
|
||||
dec #Attr.12;
|
||||
joinpoint Test.31 Test.29:
|
||||
let Test.30 : U8 = GetTagId Test.7;
|
||||
switch Test.30:
|
||||
case 0:
|
||||
dec Test.7;
|
||||
let Test.28 : Str = CallByName Test.2 Test.29;
|
||||
dec Test.29;
|
||||
ret Test.28;
|
||||
|
||||
case 1:
|
||||
let Test.28 : Str = CallByName Test.9 Test.29 Test.7;
|
||||
ret Test.28;
|
||||
|
||||
default:
|
||||
jump Test.27 Test.29 Test.7;
|
||||
|
||||
in
|
||||
switch Test.8:
|
||||
case 0:
|
||||
let Test.32 : Str = CallByName Test.3 Test.12;
|
||||
jump Test.31 Test.32;
|
||||
|
||||
default:
|
||||
let Test.32 : Str = CallByName Test.4 Test.12;
|
||||
jump Test.31 Test.32;
|
||||
|
||||
in
|
||||
jump Test.27 Test.53 Test.54;
|
||||
|
||||
procedure Test.2 (Test.13):
|
||||
inc Test.13;
|
||||
ret Test.13;
|
||||
|
||||
procedure Test.3 (Test.14):
|
||||
let Test.48 : Str = "!";
|
||||
let Test.47 : Str = CallByName Str.3 Test.14 Test.48;
|
||||
dec Test.48;
|
||||
ret Test.47;
|
||||
|
||||
procedure Test.4 (Test.15):
|
||||
let Test.44 : Str = "(";
|
||||
let Test.46 : Str = ")";
|
||||
let Test.45 : Str = CallByName Str.3 Test.15 Test.46;
|
||||
dec Test.46;
|
||||
let Test.43 : Str = CallByName Str.3 Test.44 Test.45;
|
||||
dec Test.45;
|
||||
ret Test.43;
|
||||
|
||||
procedure Test.6 (Test.7, Test.8, Test.5):
|
||||
if Test.5 then
|
||||
let Test.33 : [<rnw><null>, C *self Int1, C *self Int1] = TagId(1) Test.7 Test.8;
|
||||
ret Test.33;
|
||||
else
|
||||
let Test.26 : [<rnw><null>, C *self Int1, C *self Int1] = TagId(2) Test.7 Test.8;
|
||||
ret Test.26;
|
||||
|
||||
procedure Test.9 (Test.10, #Attr.12):
|
||||
let Test.8 : Int1 = UnionAtIndex (Id 1) (Index 1) #Attr.12;
|
||||
let Test.7 : [<rnw><null>, C *self Int1, C *self Int1] = UnionAtIndex (Id 1) (Index 0) #Attr.12;
|
||||
inc Test.7;
|
||||
dec #Attr.12;
|
||||
let Test.37 : U8 = GetTagId Test.7;
|
||||
joinpoint Test.38 Test.36:
|
||||
switch Test.8:
|
||||
case 0:
|
||||
let Test.35 : Str = CallByName Test.3 Test.36;
|
||||
ret Test.35;
|
||||
|
||||
default:
|
||||
let Test.35 : Str = CallByName Test.4 Test.36;
|
||||
ret Test.35;
|
||||
|
||||
in
|
||||
switch Test.37:
|
||||
case 0:
|
||||
dec Test.7;
|
||||
let Test.39 : Str = CallByName Test.2 Test.10;
|
||||
dec Test.10;
|
||||
jump Test.38 Test.39;
|
||||
|
||||
case 1:
|
||||
let Test.39 : Str = CallByName Test.9 Test.10 Test.7;
|
||||
jump Test.38 Test.39;
|
||||
|
||||
default:
|
||||
let Test.39 : Str = CallByName Test.11 Test.10 Test.7;
|
||||
jump Test.38 Test.39;
|
||||
|
||||
|
||||
procedure Test.0 ():
|
||||
let Test.41 : Int1 = false;
|
||||
let Test.42 : Int1 = true;
|
||||
let Test.20 : List Int1 = Array [Test.41, Test.42];
|
||||
let Test.21 : [<rnw><null>, C *self Int1, C *self Int1] = TagId(0) ;
|
||||
let Test.23 : Int1 = CallByName Bool.2;
|
||||
let Test.22 : Int1 = CallByName Test.1 Test.23;
|
||||
let Test.16 : [<rnw><null>, C *self Int1, C *self Int1] = CallByName List.18 Test.20 Test.21 Test.22;
|
||||
dec Test.20;
|
||||
let Test.18 : Str = "hello";
|
||||
let Test.19 : U8 = GetTagId Test.16;
|
||||
switch Test.19:
|
||||
case 0:
|
||||
dec Test.16;
|
||||
let Test.17 : Str = CallByName Test.2 Test.18;
|
||||
dec Test.18;
|
||||
ret Test.17;
|
||||
|
||||
case 1:
|
||||
let Test.17 : Str = CallByName Test.9 Test.18 Test.16;
|
||||
ret Test.17;
|
||||
|
||||
default:
|
||||
let Test.17 : Str = CallByName Test.11 Test.18 Test.16;
|
||||
ret Test.17;
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
procedure Bool.11 (#Attr.2, #Attr.3):
|
||||
let Bool.23 : Int1 = lowlevel Eq #Attr.2 #Attr.3;
|
||||
ret Bool.23;
|
||||
|
||||
procedure Num.20 (#Attr.2, #Attr.3):
|
||||
let Num.276 : U8 = lowlevel NumSub #Attr.2 #Attr.3;
|
||||
ret Num.276;
|
||||
|
||||
procedure Num.21 (#Attr.2, #Attr.3):
|
||||
let Num.275 : U8 = lowlevel NumMul #Attr.2 #Attr.3;
|
||||
ret Num.275;
|
||||
|
||||
procedure Test.1 (Test.26, Test.27):
|
||||
joinpoint Test.11 Test.2 Test.3:
|
||||
let Test.24 : U8 = 0i64;
|
||||
let Test.20 : Int1 = CallByName Bool.11 Test.2 Test.24;
|
||||
if Test.20 then
|
||||
let Test.22 : U8 = 1i64;
|
||||
let Test.23 : U8 = GetTagId Test.3;
|
||||
switch Test.23:
|
||||
case 0:
|
||||
let Test.21 : U8 = CallByName Test.4 Test.22 Test.3;
|
||||
ret Test.21;
|
||||
|
||||
default:
|
||||
dec Test.3;
|
||||
let Test.21 : U8 = CallByName Test.6 Test.22;
|
||||
ret Test.21;
|
||||
|
||||
else
|
||||
let Test.19 : U8 = 1i64;
|
||||
let Test.13 : U8 = CallByName Num.20 Test.2 Test.19;
|
||||
let Test.14 : [<rnu><null>, C *self U8] = TagId(0) Test.3 Test.2;
|
||||
jump Test.11 Test.13 Test.14;
|
||||
in
|
||||
jump Test.11 Test.26 Test.27;
|
||||
|
||||
procedure Test.4 (Test.28, Test.29):
|
||||
joinpoint Test.15 Test.5 #Attr.12:
|
||||
let Test.2 : U8 = UnionAtIndex (Id 0) (Index 1) #Attr.12;
|
||||
let Test.3 : [<rnu><null>, C *self U8] = UnionAtIndex (Id 0) (Index 0) #Attr.12;
|
||||
inc Test.3;
|
||||
dec #Attr.12;
|
||||
let Test.17 : U8 = CallByName Num.21 Test.2 Test.5;
|
||||
let Test.18 : U8 = GetTagId Test.3;
|
||||
switch Test.18:
|
||||
case 0:
|
||||
jump Test.15 Test.17 Test.3;
|
||||
|
||||
default:
|
||||
dec Test.3;
|
||||
let Test.16 : U8 = CallByName Test.6 Test.17;
|
||||
ret Test.16;
|
||||
|
||||
in
|
||||
jump Test.15 Test.28 Test.29;
|
||||
|
||||
procedure Test.6 (Test.7):
|
||||
ret Test.7;
|
||||
|
||||
procedure Test.0 ():
|
||||
let Test.9 : U8 = 5i64;
|
||||
let Test.10 : [<rnu><null>, C *self U8] = TagId(1) ;
|
||||
let Test.8 : U8 = CallByName Test.1 Test.9 Test.10;
|
||||
ret Test.8;
|
|
@ -2804,3 +2804,44 @@ fn when_guard_appears_multiple_times_in_compiled_decision_tree_issue_5176() {
|
|||
"#
|
||||
)
|
||||
}
|
||||
|
||||
#[mono_test]
|
||||
fn recursive_lambda_set_resolved_only_upon_specialization() {
|
||||
indoc!(
|
||||
r#"
|
||||
app "test" provides [main] to "./platform"
|
||||
|
||||
factCPS = \n, cont ->
|
||||
if n == 0u8 then
|
||||
cont 1u8
|
||||
else
|
||||
factCPS (n - 1) \value -> cont (n * value)
|
||||
|
||||
main =
|
||||
factCPS 5 \x -> x
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
#[mono_test]
|
||||
fn compose_recursive_lambda_set_productive_nullable_wrapped() {
|
||||
indoc!(
|
||||
r#"
|
||||
app "test" provides [main] to "./platform"
|
||||
|
||||
compose = \forward -> \f, g ->
|
||||
if forward
|
||||
then \x -> g (f x)
|
||||
else \x -> f (g x)
|
||||
|
||||
identity = \x -> x
|
||||
exclame = \s -> "\(s)!"
|
||||
whisper = \s -> "(\(s))"
|
||||
|
||||
main =
|
||||
res: Str -> Str
|
||||
res = List.walk [ exclame, whisper ] identity (compose Bool.true)
|
||||
res "hello"
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3778,30 +3778,38 @@ fn unify_recursion<M: MetaCollector>(
|
|||
structure: Variable,
|
||||
other: &Content,
|
||||
) -> Outcome<M> {
|
||||
if !matches!(other, RecursionVar { .. }) {
|
||||
if env.seen_recursion_pair(ctx.first, ctx.second) {
|
||||
return Default::default();
|
||||
}
|
||||
|
||||
env.add_recursion_pair(ctx.first, ctx.second);
|
||||
if env.seen_recursion_pair(ctx.first, ctx.second) {
|
||||
return Default::default();
|
||||
}
|
||||
|
||||
env.add_recursion_pair(ctx.first, ctx.second);
|
||||
|
||||
let outcome = match other {
|
||||
RecursionVar {
|
||||
opt_name: other_opt_name,
|
||||
structure: _other_structure,
|
||||
structure: other_structure,
|
||||
} => {
|
||||
// NOTE: structure and other_structure may not be unified yet, but will be
|
||||
// we should not do that here, it would create an infinite loop!
|
||||
// We haven't seen these two recursion vars yet, so go and unify their structures.
|
||||
// We need to do this before we merge the two recursion vars, since the unification of
|
||||
// the structures may be material.
|
||||
|
||||
let mut outcome = unify_pool(env, pool, structure, *other_structure, ctx.mode);
|
||||
if !outcome.mismatches.is_empty() {
|
||||
return outcome;
|
||||
}
|
||||
|
||||
let name = (*opt_name).or(*other_opt_name);
|
||||
merge(
|
||||
let merge_outcome = merge(
|
||||
env,
|
||||
ctx,
|
||||
RecursionVar {
|
||||
opt_name: name,
|
||||
structure,
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
outcome.union(merge_outcome);
|
||||
outcome
|
||||
}
|
||||
|
||||
Structure(_) => {
|
||||
|
@ -3863,9 +3871,7 @@ fn unify_recursion<M: MetaCollector>(
|
|||
Error => merge(env, ctx, Error),
|
||||
};
|
||||
|
||||
if !matches!(other, RecursionVar { .. }) {
|
||||
env.remove_recursion_pair(ctx.first, ctx.second);
|
||||
}
|
||||
env.remove_recursion_pair(ctx.first, ctx.second);
|
||||
|
||||
outcome
|
||||
}
|
||||
|
|
|
@ -540,6 +540,7 @@ fn type_annotation_to_html(
|
|||
type_annotation_to_html(indent_level, buf, extension, true);
|
||||
}
|
||||
TypeAnnotation::Function { args, output } => {
|
||||
let mut paren_is_open = false;
|
||||
let mut peekable_args = args.iter().peekable();
|
||||
while let Some(arg) = peekable_args.next() {
|
||||
if is_multiline {
|
||||
|
@ -548,8 +549,14 @@ fn type_annotation_to_html(
|
|||
}
|
||||
indent(buf, indent_level + 1);
|
||||
}
|
||||
if needs_parens && !paren_is_open {
|
||||
buf.push('(');
|
||||
paren_is_open = true;
|
||||
}
|
||||
|
||||
type_annotation_to_html(indent_level, buf, arg, false);
|
||||
let child_needs_parens =
|
||||
matches!(arg, TypeAnnotation::Function { args: _, output: _ });
|
||||
type_annotation_to_html(indent_level, buf, arg, child_needs_parens);
|
||||
|
||||
if peekable_args.peek().is_some() {
|
||||
buf.push_str(", ");
|
||||
|
@ -570,6 +577,9 @@ fn type_annotation_to_html(
|
|||
}
|
||||
|
||||
type_annotation_to_html(next_indent_level, buf, output, false);
|
||||
if needs_parens && paren_is_open {
|
||||
buf.push(')');
|
||||
}
|
||||
}
|
||||
TypeAnnotation::Ability { members: _ } => {
|
||||
// TODO(abilities): fill me in
|
||||
|
|
|
@ -666,10 +666,16 @@ mod test {
|
|||
|
||||
When it failed, these variables had these values:
|
||||
|
||||
a : [Err Str, Ok Str]
|
||||
a : [
|
||||
Err Str,
|
||||
Ok Str,
|
||||
]
|
||||
a = Ok "Astra mortemque praestare gradatim"
|
||||
|
||||
b : [Err Str, Ok Str]
|
||||
b : [
|
||||
Err Str,
|
||||
Ok Str,
|
||||
]
|
||||
b = Err "Profundum et fundamentum"
|
||||
"#
|
||||
),
|
||||
|
|
|
@ -21,6 +21,7 @@ roc_solve_problem = { path = "../compiler/solve_problem" }
|
|||
roc_std = { path = "../roc_std" }
|
||||
roc_types = { path = "../compiler/types" }
|
||||
ven_pretty = { path = "../vendor/pretty" }
|
||||
itertools = "0.10.5"
|
||||
|
||||
bumpalo.workspace = true
|
||||
distance.workspace = true
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
use crate::error::canonicalize::{to_circular_def_doc, CIRCULAR_DEF};
|
||||
use crate::report::{Annotation, Report, RocDocAllocator, RocDocBuilder};
|
||||
use itertools::EitherOrBoth;
|
||||
use itertools::Itertools;
|
||||
use roc_can::expected::{Expected, PExpected};
|
||||
use roc_collections::all::{HumanIndex, MutSet, SendMap};
|
||||
use roc_collections::VecMap;
|
||||
|
@ -2492,6 +2494,8 @@ fn to_doc_help<'b>(
|
|||
.map(|(k, v)| (alloc.tag_name(k), v))
|
||||
.collect(),
|
||||
tag_ext_to_doc(alloc, pol, gen_usages, ext),
|
||||
0, // zero tags omitted, since this isn't a diff
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2511,13 +2515,16 @@ fn to_doc_help<'b>(
|
|||
.collect::<Vec<_>>();
|
||||
tags.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
|
||||
report_text::recursive_tag_union(
|
||||
let rec_doc = to_doc_help(ctx, gen_usages, alloc, Parens::Unnecessary, *rec_var);
|
||||
|
||||
report_text::tag_union(
|
||||
alloc,
|
||||
to_doc_help(ctx, gen_usages, alloc, Parens::Unnecessary, *rec_var),
|
||||
tags.into_iter()
|
||||
.map(|(k, v)| (alloc.tag_name(k), v))
|
||||
.collect(),
|
||||
tag_ext_to_doc(alloc, pol, gen_usages, ext),
|
||||
0, // zero tags omitted, since this isn't a diff
|
||||
Some(rec_doc),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2885,21 +2892,11 @@ fn to_diff<'b>(
|
|||
}
|
||||
|
||||
(TagUnion(tags1, ext1, pol), TagUnion(tags2, ext2, _)) => {
|
||||
diff_tag_union(alloc, pol, &tags1, ext1, &tags2, ext2)
|
||||
diff_tag_union(alloc, pol, tags1, ext1, None, tags2, ext2, None)
|
||||
}
|
||||
|
||||
(RecursiveTagUnion(_rec1, _tags1, _ext1, _), RecursiveTagUnion(_rec2, _tags2, _ext2, _)) => {
|
||||
// TODO do a better job here
|
||||
let (left, left_able) = to_doc(alloc, Parens::Unnecessary, type1);
|
||||
let (right, right_able) = to_doc(alloc, Parens::Unnecessary, type2);
|
||||
|
||||
Diff {
|
||||
left,
|
||||
right,
|
||||
status: Status::Similar,
|
||||
left_able,
|
||||
right_able,
|
||||
}
|
||||
(RecursiveTagUnion(rec1, tags1, ext1, pol), RecursiveTagUnion(rec2, tags2, ext2, _)) => {
|
||||
diff_tag_union(alloc, pol, tags1, ext1, Some(*rec1), tags2, ext2, Some(*rec2))
|
||||
}
|
||||
|
||||
pair => {
|
||||
|
@ -3225,7 +3222,11 @@ fn should_show_diff(t1: &ErrorType, t2: &ErrorType) -> bool {
|
|||
.any(|(t1, t2)| should_show_diff(t1, t2))
|
||||
}
|
||||
(Infinite, Infinite) | (Error, Error) => false,
|
||||
(FlexVar(v1), FlexVar(v2)) | (RigidVar(v1), RigidVar(v2)) => v1 != v2,
|
||||
(RigidVar(v1), RigidVar(v2)) => v1 != v2,
|
||||
(FlexVar(_), _) | (_, FlexVar(_)) => {
|
||||
// If either is flex, it will unify to the other type; no diff is needed.
|
||||
false
|
||||
}
|
||||
(FlexAbleVar(v1, _set1), FlexAbleVar(v2, _set2))
|
||||
| (RigidAbleVar(v1, _set1), RigidAbleVar(v2, _set2)) => {
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -3243,14 +3244,53 @@ fn should_show_diff(t1: &ErrorType, t2: &ErrorType) -> bool {
|
|||
v1 != v2
|
||||
}
|
||||
(Record(fields1, ext1), Record(fields2, ext2)) => {
|
||||
if fields1.len() != fields2.len() || ext1 != ext2 {
|
||||
let is_1_open = matches!(ext1, TypeExt::FlexOpen(_));
|
||||
let is_2_open = matches!(ext2, TypeExt::FlexOpen(_));
|
||||
|
||||
if !is_1_open && !is_2_open && fields1.len() != fields2.len() {
|
||||
return true;
|
||||
}
|
||||
|
||||
fields1
|
||||
.iter()
|
||||
.zip(fields2.iter())
|
||||
.any(|((name1, f1), (name2, f2))| name1 != name2 || should_show_field_diff(f1, f2))
|
||||
// Check for diffs in any of the fields1 fields.
|
||||
for (name, f1) in fields1.iter() {
|
||||
match fields2.get(name) {
|
||||
Some(f2) => {
|
||||
// If the field is on both records, and the diff should be
|
||||
// shown, then we should show a diff for the whole record.
|
||||
if should_show_field_diff(f1, f2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// (If the field is on both records, but no diff should be
|
||||
// shown between those fields, continue checking other fields.)
|
||||
}
|
||||
None => {
|
||||
// It's fine for 1 to have a field that the other doesn't have,
|
||||
// so long as the other one is open.
|
||||
if !is_2_open {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this point we've checked all the fields that are in both records,
|
||||
// as well as all the fields that are in 1 but not 2.
|
||||
// All that remains is to check the fields that are in 2 but not 1,
|
||||
// which we don't care about if 1 is open (because then it's fine
|
||||
// for the other record to have fields it doesn't).
|
||||
if !is_1_open {
|
||||
for name in fields2.keys() {
|
||||
if !fields1.contains_key(name) {
|
||||
// fields1 is missing this field, and fields1 is not open,
|
||||
// therefore this is a relevant diff.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We haven't early-returned true yet, so we didn't find any relevant diffs!
|
||||
false
|
||||
}
|
||||
(Tuple(elems1, ext1), Tuple(elems2, ext2)) => {
|
||||
if elems1.len() != elems2.len() || ext1 != ext2 {
|
||||
|
@ -3361,8 +3401,6 @@ fn should_show_diff(t1: &ErrorType, t2: &ErrorType) -> bool {
|
|||
| (_, Error)
|
||||
| (Type(_, _), _)
|
||||
| (_, Type(_, _))
|
||||
| (FlexVar(_), _)
|
||||
| (_, FlexVar(_))
|
||||
| (RigidVar(_), _)
|
||||
| (_, RigidVar(_))
|
||||
| (FlexAbleVar(_, _), _)
|
||||
|
@ -3429,64 +3467,94 @@ fn should_show_field_diff(
|
|||
fn same_tag_name_overlap_diff<'b>(
|
||||
alloc: &'b RocDocAllocator<'b>,
|
||||
field: TagName,
|
||||
args1: Vec<ErrorType>,
|
||||
args2: Vec<ErrorType>,
|
||||
payload_vals1: Vec<ErrorType>,
|
||||
payload_vals2: Vec<ErrorType>,
|
||||
) -> Diff<(TagName, RocDocBuilder<'b>, Vec<RocDocBuilder<'b>>)> {
|
||||
if args1.len() == args2.len() {
|
||||
let diff = diff_args(alloc, Parens::InTypeParam, args1, args2);
|
||||
// Render ellipses wherever the payload slots have the same type.
|
||||
let mut left_doc = Vec::with_capacity(payload_vals1.len());
|
||||
let mut left_able = Vec::new();
|
||||
let mut right_doc = Vec::with_capacity(payload_vals2.len());
|
||||
let mut right_able = Vec::new();
|
||||
|
||||
Diff {
|
||||
left: (field.clone(), alloc.tag_name(field.clone()), diff.left),
|
||||
right: (field.clone(), alloc.tag_name(field), diff.right),
|
||||
status: diff.status,
|
||||
left_able: diff.left_able,
|
||||
right_able: diff.right_able,
|
||||
}
|
||||
} else {
|
||||
let (left_doc, left_able): (_, Vec<AbleVariables>) = args1
|
||||
.into_iter()
|
||||
.map(|arg| to_doc(alloc, Parens::InTypeParam, arg))
|
||||
.unzip();
|
||||
let (right_doc, right_able): (_, Vec<AbleVariables>) = args2
|
||||
.into_iter()
|
||||
.map(|arg| to_doc(alloc, Parens::InTypeParam, arg))
|
||||
.unzip();
|
||||
// itertools::zip_longest is a zip that can continue past the end of one Vec.
|
||||
// If they both have payload values in a given slot, and both are the same type,
|
||||
// we render ellipsis instead of the actual type - since there's no diff between them.
|
||||
// If one of them doesn't have a payload value in that slot, we always render its type.
|
||||
for either_or_both in payload_vals1
|
||||
.into_iter()
|
||||
.zip_longest(payload_vals2.into_iter())
|
||||
{
|
||||
match either_or_both {
|
||||
// Both tag unions have a payload value in this slot
|
||||
EitherOrBoth::Both(t1, t2) => {
|
||||
if should_show_diff(&t1, &t2) {
|
||||
{
|
||||
let (doc, able) = to_doc(alloc, Parens::InTypeParam, t1);
|
||||
left_doc.push(doc);
|
||||
left_able.extend(able);
|
||||
}
|
||||
|
||||
Diff {
|
||||
left: (field.clone(), alloc.tag_name(field.clone()), left_doc),
|
||||
right: (field.clone(), alloc.tag_name(field), right_doc),
|
||||
status: Status::Similar,
|
||||
left_able: left_able.into_iter().flatten().collect(),
|
||||
right_able: right_able.into_iter().flatten().collect(),
|
||||
{
|
||||
let (doc, able) = to_doc(alloc, Parens::InTypeParam, t2);
|
||||
right_doc.push(doc);
|
||||
right_able.extend(able);
|
||||
}
|
||||
} else {
|
||||
left_doc.push(alloc.ellipsis());
|
||||
right_doc.push(alloc.ellipsis());
|
||||
}
|
||||
}
|
||||
// Only the left tag union has a payload value in this slot
|
||||
EitherOrBoth::Left(t1) => {
|
||||
let (doc, able) = to_doc(alloc, Parens::InTypeParam, t1);
|
||||
left_doc.push(doc);
|
||||
left_able.extend(able);
|
||||
}
|
||||
// Only the right tag union has a payload value in this slot
|
||||
EitherOrBoth::Right(t2) => {
|
||||
let (doc, able) = to_doc(alloc, Parens::InTypeParam, t2);
|
||||
right_doc.push(doc);
|
||||
right_able.extend(able);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Diff {
|
||||
left: (field.clone(), alloc.tag_name(field.clone()), left_doc),
|
||||
right: (field.clone(), alloc.tag_name(field), right_doc),
|
||||
status: Status::Similar,
|
||||
left_able,
|
||||
right_able,
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_tag_union<'b>(
|
||||
alloc: &'b RocDocAllocator<'b>,
|
||||
pol: Polarity,
|
||||
fields1: &SendMap<TagName, Vec<ErrorType>>,
|
||||
tags1: SendMap<TagName, Vec<ErrorType>>,
|
||||
ext1: TypeExt,
|
||||
fields2: &SendMap<TagName, Vec<ErrorType>>,
|
||||
rec1: Option<ErrorType>,
|
||||
mut tags2: SendMap<TagName, Vec<ErrorType>>,
|
||||
ext2: TypeExt,
|
||||
rec2: Option<ErrorType>,
|
||||
) -> Diff<RocDocBuilder<'b>> {
|
||||
let gen_usages1 = {
|
||||
let mut usages = VecMap::default();
|
||||
count_generated_name_usages(&mut usages, fields1.values().flatten());
|
||||
count_generated_name_usages(&mut usages, tags1.values().flatten());
|
||||
count_generated_name_usages_in_exts(&mut usages, [(&ext1, false)]);
|
||||
usages
|
||||
};
|
||||
let gen_usages2 = {
|
||||
let mut usages = VecMap::default();
|
||||
count_generated_name_usages(&mut usages, fields2.values().flatten());
|
||||
count_generated_name_usages(&mut usages, tags2.values().flatten());
|
||||
count_generated_name_usages_in_exts(&mut usages, [(&ext2, false)]);
|
||||
usages
|
||||
};
|
||||
|
||||
let to_overlap_docs = |(field, (t1, t2)): (TagName, (Vec<ErrorType>, Vec<ErrorType>))| {
|
||||
same_tag_name_overlap_diff(alloc, field, t1, t2)
|
||||
let to_overlap_docs = |(tag_name, (t1, t2)): (TagName, (Vec<ErrorType>, Vec<ErrorType>))| {
|
||||
same_tag_name_overlap_diff(alloc, tag_name, t1, t2)
|
||||
};
|
||||
let to_unknown_docs = |(field, args): (&TagName, &Vec<ErrorType>)| -> (
|
||||
let to_unknown_docs = |(tag_name, args): (&TagName, &Vec<ErrorType>)| -> (
|
||||
TagName,
|
||||
RocDocBuilder<'b>,
|
||||
Vec<RocDocBuilder<'b>>,
|
||||
|
@ -3498,103 +3566,221 @@ fn diff_tag_union<'b>(
|
|||
.map(|arg| to_doc(alloc, Parens::InTypeParam, arg.clone()))
|
||||
.unzip();
|
||||
(
|
||||
field.clone(),
|
||||
alloc.tag_name(field.clone()),
|
||||
tag_name.clone(),
|
||||
alloc.tag_name(tag_name.clone()),
|
||||
args,
|
||||
able.into_iter().flatten().collect(),
|
||||
)
|
||||
};
|
||||
let shared_keys = fields1
|
||||
.clone()
|
||||
.intersection_with(fields2.clone(), |v1, v2| (v1, v2));
|
||||
let mut same_tags_different_payloads = VecMap::default();
|
||||
let mut tags_in_left_only = Vec::default();
|
||||
let mut same_tags_same_payloads = 0;
|
||||
|
||||
let left_keys = fields1.clone().relative_complement(fields2.clone());
|
||||
let right_keys = fields2.clone().relative_complement(fields1.clone());
|
||||
for (k1, v1) in tags1.into_iter() {
|
||||
match tags2.remove(&k1) {
|
||||
Some(v2) if should_show_payload_diff(&v1, &v2) => {
|
||||
// The tag names are the same but the payload types are different
|
||||
// (or at least should be rendered as different)
|
||||
same_tags_different_payloads.insert(k1.clone(), (v1.clone(), v2));
|
||||
}
|
||||
Some(_) => {
|
||||
// They both have the same tag name as well as the same payload types
|
||||
same_tags_same_payloads += 1;
|
||||
}
|
||||
None => {
|
||||
// Only tags1 has this tag.
|
||||
tags_in_left_only.push((k1, v1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let both = shared_keys.into_iter().map(to_overlap_docs);
|
||||
let mut left = left_keys.iter().map(to_unknown_docs).peekable();
|
||||
let mut right = right_keys.iter().map(to_unknown_docs).peekable();
|
||||
// We've removed all the tags that they had in common, so the remaining entries in tags2
|
||||
// are ones that appear on the right only.
|
||||
let tags_in_right_only = tags2;
|
||||
let no_tags_in_common = same_tags_different_payloads.is_empty() && same_tags_same_payloads == 0;
|
||||
let both = same_tags_different_payloads
|
||||
.into_iter()
|
||||
.map(to_overlap_docs);
|
||||
|
||||
let all_fields_shared = left.peek().is_none() && right.peek().is_none();
|
||||
let any_tags_on_one_side_only = !tags_in_left_only.is_empty() || !tags_in_right_only.is_empty();
|
||||
|
||||
let mut left = tags_in_left_only
|
||||
.iter()
|
||||
.map(|(k, v)| to_unknown_docs((k, v)))
|
||||
.peekable();
|
||||
let mut right = tags_in_right_only.iter().map(to_unknown_docs).peekable();
|
||||
|
||||
let status = match (ext_has_fixed_fields(&ext1), ext_has_fixed_fields(&ext2)) {
|
||||
(false, false) => Status::Similar,
|
||||
_ => match (left.peek(), right.peek()) {
|
||||
// At least one tag appeared only on the left, and also
|
||||
// at least one tag appeared only on the right. There's a chance this is
|
||||
// because of a typo, so we'll suggest that as a hint.
|
||||
(Some((f, _, _, _)), Some(_)) => Status::Different(vec![Problem::TagTypo(
|
||||
f.clone(),
|
||||
fields2.keys().cloned().collect(),
|
||||
tags_in_right_only.keys().cloned().collect(),
|
||||
)]),
|
||||
(Some(_), None) => {
|
||||
let status =
|
||||
Status::Different(vec![Problem::TagsMissing(left.map(|v| v.0).collect())]);
|
||||
left = left_keys.iter().map(to_unknown_docs).peekable();
|
||||
status
|
||||
}
|
||||
// At least one tag appeared only on the left, but all of the tags
|
||||
// on the right also appeared on the left. So at least one tag is missing.
|
||||
(Some(_), None) => Status::Different(vec![Problem::TagsMissing(
|
||||
left.clone().map(|v| v.0).collect(),
|
||||
)]),
|
||||
// At least one tag appeared only on the right, but all of the tags
|
||||
// on the left also appeared on the right. So at least one tag is missing.
|
||||
(None, Some(_)) => {
|
||||
let status =
|
||||
Status::Different(vec![Problem::TagsMissing(right.map(|v| v.0).collect())]);
|
||||
right = right_keys.iter().map(to_unknown_docs).peekable();
|
||||
right = tags_in_right_only.iter().map(to_unknown_docs).peekable();
|
||||
status
|
||||
}
|
||||
// Left and right have the same set of tag names (but may have different payloads).
|
||||
(None, None) => Status::Similar,
|
||||
},
|
||||
};
|
||||
|
||||
let ext1_is_open = matches!(&ext1, TypeExt::FlexOpen(_));
|
||||
let ext2_is_open = matches!(&ext2, TypeExt::FlexOpen(_));
|
||||
let ext_diff = tag_ext_to_diff(alloc, pol, ext1, ext2, &gen_usages1, &gen_usages2);
|
||||
|
||||
let mut fields_diff: Diff<Vec<(TagName, RocDocBuilder<'b>, Vec<RocDocBuilder<'b>>)>> = Diff {
|
||||
left: vec![],
|
||||
right: vec![],
|
||||
let mut tags_diff: Diff<Vec<(TagName, RocDocBuilder<'b>, Vec<RocDocBuilder<'b>>)>> = Diff {
|
||||
status: Status::Similar,
|
||||
left_able: vec![],
|
||||
right_able: vec![],
|
||||
left: Vec::new(),
|
||||
right: Vec::new(),
|
||||
left_able: Vec::new(),
|
||||
right_able: Vec::new(),
|
||||
};
|
||||
|
||||
for diff in both {
|
||||
fields_diff.left.push(diff.left);
|
||||
fields_diff.right.push(diff.right);
|
||||
fields_diff.status.merge(diff.status);
|
||||
fields_diff.left_able.extend(diff.left_able);
|
||||
fields_diff.right_able.extend(diff.right_able);
|
||||
tags_diff.status.merge(diff.status);
|
||||
tags_diff.left.push(diff.left);
|
||||
tags_diff.right.push(diff.right);
|
||||
tags_diff.left_able.extend(diff.left_able);
|
||||
tags_diff.right_able.extend(diff.right_able);
|
||||
}
|
||||
|
||||
if !all_fields_shared {
|
||||
for (tag, tag_doc, args, able) in left {
|
||||
fields_diff.left.push((tag, tag_doc, args));
|
||||
fields_diff.left_able.extend(able);
|
||||
let left_tags_omitted;
|
||||
let right_tags_omitted;
|
||||
|
||||
if no_tags_in_common {
|
||||
// If they have no tags in common, we shouldn't omit any tags,
|
||||
// because that would result in an unhelpful diff of
|
||||
// […] on one side and another […] on the other side!
|
||||
|
||||
left_tags_omitted = 0;
|
||||
right_tags_omitted = 0;
|
||||
|
||||
for (tag, tag_doc, payload_vals, able) in left {
|
||||
tags_diff.left.push((tag, tag_doc, payload_vals));
|
||||
tags_diff.left_able.extend(able);
|
||||
}
|
||||
for (tag, tag_doc, args, able) in right {
|
||||
fields_diff.right.push((tag, tag_doc, args));
|
||||
fields_diff.right_able.extend(able);
|
||||
|
||||
for (tag, tag_doc, payload_vals, able) in right {
|
||||
tags_diff.right.push((tag, tag_doc, payload_vals));
|
||||
tags_diff.right_able.extend(able);
|
||||
}
|
||||
fields_diff.status.merge(Status::Different(vec![]));
|
||||
|
||||
tags_diff.status.merge(Status::Different(Vec::new()));
|
||||
} else if any_tags_on_one_side_only {
|
||||
// If either tag union is open but the other is not, then omit the tags in the other.
|
||||
//
|
||||
// In other words, if one tag union is a pattern match which has _ ->,
|
||||
// don't list the tags which fall under that catch-all pattern because
|
||||
// they won't be helpful. By omitting them, we'll only show the tags that
|
||||
// are actually matched.
|
||||
//
|
||||
// We shouldn't do this if they're both open though,
|
||||
// because that would result in an unhelpful diff of
|
||||
// […] on one side and another […] on the other side!
|
||||
if ext2_is_open && !ext1_is_open {
|
||||
left_tags_omitted = same_tags_same_payloads + left.len();
|
||||
} else {
|
||||
left_tags_omitted = same_tags_same_payloads;
|
||||
|
||||
for (tag, tag_doc, args, able) in left {
|
||||
tags_diff.left.push((tag, tag_doc, args));
|
||||
tags_diff.left_able.extend(able);
|
||||
}
|
||||
}
|
||||
|
||||
if ext1_is_open && !ext2_is_open {
|
||||
right_tags_omitted = same_tags_same_payloads + right.len();
|
||||
} else {
|
||||
right_tags_omitted = same_tags_same_payloads;
|
||||
|
||||
for (tag, tag_doc, args, able) in right {
|
||||
tags_diff.right.push((tag, tag_doc, args));
|
||||
tags_diff.right_able.extend(able);
|
||||
}
|
||||
}
|
||||
|
||||
tags_diff.status.merge(Status::Different(Vec::new()));
|
||||
} else {
|
||||
left_tags_omitted = same_tags_same_payloads;
|
||||
right_tags_omitted = same_tags_same_payloads;
|
||||
}
|
||||
|
||||
fields_diff.left.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
fields_diff.right.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
tags_diff.left.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
tags_diff.right.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let lefts = fields_diff
|
||||
.left
|
||||
.into_iter()
|
||||
.map(|(_, a, b)| (a, b))
|
||||
.collect();
|
||||
let rights = fields_diff
|
||||
let lefts = tags_diff.left.into_iter().map(|(_, a, b)| (a, b)).collect();
|
||||
let rights = tags_diff
|
||||
.right
|
||||
.into_iter()
|
||||
.map(|(_, a, b)| (a, b))
|
||||
.collect();
|
||||
|
||||
let doc1 = report_text::tag_union(alloc, lefts, ext_diff.left);
|
||||
let doc2 = report_text::tag_union(alloc, rights, ext_diff.right);
|
||||
let doc1 = match rec1 {
|
||||
None => report_text::tag_union(alloc, lefts, ext_diff.left, left_tags_omitted, None),
|
||||
Some(rec) => {
|
||||
let (rec_doc, able) = to_doc(alloc, Parens::Unnecessary, rec);
|
||||
|
||||
fields_diff.status.merge(status);
|
||||
tags_diff.left_able.extend(able);
|
||||
|
||||
report_text::tag_union(
|
||||
alloc,
|
||||
lefts,
|
||||
ext_diff.left,
|
||||
left_tags_omitted,
|
||||
Some(rec_doc),
|
||||
)
|
||||
}
|
||||
};
|
||||
let doc2 = match rec2 {
|
||||
None => report_text::tag_union(alloc, rights, ext_diff.right, right_tags_omitted, None),
|
||||
Some(rec) => {
|
||||
let (rec_doc, able) = to_doc(alloc, Parens::Unnecessary, rec);
|
||||
|
||||
tags_diff.right_able.extend(able);
|
||||
|
||||
report_text::tag_union(
|
||||
alloc,
|
||||
rights,
|
||||
ext_diff.right,
|
||||
right_tags_omitted,
|
||||
Some(rec_doc),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
tags_diff.status.merge(status);
|
||||
|
||||
Diff {
|
||||
left: doc1,
|
||||
right: doc2,
|
||||
status: fields_diff.status,
|
||||
left_able: fields_diff.left_able,
|
||||
right_able: fields_diff.right_able,
|
||||
status: tags_diff.status,
|
||||
left_able: tags_diff.left_able,
|
||||
right_able: tags_diff.right_able,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show_payload_diff(errs1: &[ErrorType], errs2: &[ErrorType]) -> bool {
|
||||
if errs1.len() == errs2.len() {
|
||||
errs1
|
||||
.iter()
|
||||
.zip(errs2.iter())
|
||||
.any(|(err1, err2)| should_show_diff(err1, err2))
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3615,16 +3801,15 @@ fn tag_ext_to_diff<'b>(
|
|||
left: ext_doc_1,
|
||||
right: ext_doc_2,
|
||||
status,
|
||||
left_able: vec![],
|
||||
right_able: vec![],
|
||||
left_able: Vec::new(),
|
||||
right_able: Vec::new(),
|
||||
},
|
||||
Status::Different(_) => Diff {
|
||||
// NOTE elm colors these differently at this point
|
||||
left: ext_doc_1,
|
||||
right: ext_doc_2,
|
||||
status,
|
||||
left_able: vec![],
|
||||
right_able: vec![],
|
||||
left_able: Vec::new(),
|
||||
right_able: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -3945,6 +4130,8 @@ mod report_text {
|
|||
alloc: &'b RocDocAllocator<'b>,
|
||||
entries: Vec<(RocDocBuilder<'b>, Vec<RocDocBuilder<'b>>)>,
|
||||
opt_ext: Option<RocDocBuilder<'b>>,
|
||||
tags_omitted: usize,
|
||||
opt_rec: Option<RocDocBuilder<'b>>,
|
||||
) -> RocDocBuilder<'b> {
|
||||
let ext_doc = if let Some(t) = opt_ext {
|
||||
t
|
||||
|
@ -3952,73 +4139,67 @@ mod report_text {
|
|||
alloc.nil()
|
||||
};
|
||||
|
||||
if entries.is_empty() {
|
||||
alloc.text("[]")
|
||||
} else {
|
||||
let entry_to_doc = |(tag_name, arguments): (RocDocBuilder<'b>, Vec<_>)| {
|
||||
if arguments.is_empty() {
|
||||
tag_name
|
||||
} else {
|
||||
tag_name
|
||||
.append(alloc.space())
|
||||
.append(alloc.intersperse(arguments, alloc.space()))
|
||||
}
|
||||
};
|
||||
|
||||
let starts =
|
||||
std::iter::once(alloc.reflow("[")).chain(std::iter::repeat(alloc.reflow(", ")));
|
||||
|
||||
let entries_doc = alloc.concat(
|
||||
entries
|
||||
.into_iter()
|
||||
.zip(starts)
|
||||
.map(|(entry, start)| start.append(entry_to_doc(entry))),
|
||||
);
|
||||
|
||||
entries_doc.append(alloc.reflow("]")).append(ext_doc)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recursive_tag_union<'b>(
|
||||
alloc: &'b RocDocAllocator<'b>,
|
||||
rec_var: RocDocBuilder<'b>,
|
||||
entries: Vec<(RocDocBuilder<'b>, Vec<RocDocBuilder<'b>>)>,
|
||||
opt_ext: Option<RocDocBuilder<'b>>,
|
||||
) -> RocDocBuilder<'b> {
|
||||
let ext_doc = if let Some(t) = opt_ext {
|
||||
t
|
||||
} else {
|
||||
alloc.nil()
|
||||
let entry_to_doc = |(tag_name, arguments): (RocDocBuilder<'b>, Vec<_>)| {
|
||||
if arguments.is_empty() {
|
||||
tag_name
|
||||
} else {
|
||||
tag_name
|
||||
.append(alloc.space())
|
||||
.append(alloc.intersperse(arguments, alloc.space()))
|
||||
}
|
||||
};
|
||||
|
||||
if entries.is_empty() {
|
||||
alloc.text("[]")
|
||||
} else {
|
||||
let entry_to_doc = |(tag_name, arguments): (RocDocBuilder<'b>, Vec<_>)| {
|
||||
if arguments.is_empty() {
|
||||
tag_name
|
||||
let without_rec = if entries.is_empty() {
|
||||
if tags_omitted == 0 {
|
||||
alloc.text("[]")
|
||||
} else {
|
||||
alloc
|
||||
.text("[")
|
||||
.append(alloc.ellipsis().append(alloc.text("]")))
|
||||
}
|
||||
.append(ext_doc)
|
||||
} else if entries.len() == 1 {
|
||||
// Single-tag unions get printed on one line; multi-tag unions get multiple lines
|
||||
alloc
|
||||
.text("[")
|
||||
.append(entry_to_doc(entries.into_iter().next().unwrap()))
|
||||
.append(if tags_omitted == 0 {
|
||||
alloc.text("")
|
||||
} else {
|
||||
tag_name
|
||||
.append(alloc.space())
|
||||
.append(alloc.intersperse(arguments, alloc.space()))
|
||||
}
|
||||
};
|
||||
|
||||
let starts =
|
||||
std::iter::once(alloc.reflow("[")).chain(std::iter::repeat(alloc.reflow(", ")));
|
||||
|
||||
let entries_doc = alloc.concat(
|
||||
entries
|
||||
.into_iter()
|
||||
.zip(starts)
|
||||
.map(|(entry, start)| start.append(entry_to_doc(entry))),
|
||||
);
|
||||
|
||||
entries_doc
|
||||
.append(alloc.reflow("]"))
|
||||
alloc.text(", ").append(alloc.ellipsis())
|
||||
})
|
||||
.append(alloc.text("]"))
|
||||
.append(ext_doc)
|
||||
.append(alloc.text(" as "))
|
||||
.append(rec_var)
|
||||
} else {
|
||||
let ending = if tags_omitted == 0 {
|
||||
alloc.reflow("]")
|
||||
} else {
|
||||
alloc.vcat([
|
||||
alloc.ellipsis().indent(super::TAG_INDENT),
|
||||
alloc.reflow("]"),
|
||||
])
|
||||
};
|
||||
|
||||
// Multi-tag unions get printed on multiple lines, as do tag unions with tags omitted.
|
||||
alloc
|
||||
.vcat(
|
||||
std::iter::once(alloc.reflow("[")).chain(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
entry_to_doc(entry)
|
||||
.indent(super::TAG_INDENT)
|
||||
.append(alloc.reflow(","))
|
||||
})
|
||||
.chain(std::iter::once(ending)),
|
||||
),
|
||||
)
|
||||
.append(ext_doc)
|
||||
};
|
||||
|
||||
match opt_rec {
|
||||
Some(rec) => without_rec.append(alloc.text(" as ")).append(rec),
|
||||
None => without_rec,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4753,6 +4934,7 @@ fn exhaustive_pattern_to_doc<'b>(
|
|||
}
|
||||
|
||||
const AFTER_TAG_INDENT: &str = " ";
|
||||
const TAG_INDENT: usize = 4;
|
||||
const RECORD_FIELD_INDENT: usize = 4;
|
||||
|
||||
fn pattern_to_doc_help<'b>(
|
||||
|
|
|
@ -1461,7 +1461,10 @@ mod test_reporting {
|
|||
|
||||
But `f` needs its 1st argument to be:
|
||||
|
||||
[Green, Red]
|
||||
[
|
||||
Green,
|
||||
Red,
|
||||
]
|
||||
|
||||
Tip: Seems like a tag typo. Maybe `Blue` should be `Red`?
|
||||
|
||||
|
@ -1495,7 +1498,10 @@ mod test_reporting {
|
|||
|
||||
But `f` needs its 1st argument to be:
|
||||
|
||||
[Green Str, Red (Int *)]
|
||||
[
|
||||
Green Str,
|
||||
Red (Int *),
|
||||
]
|
||||
|
||||
Tip: Seems like a tag typo. Maybe `Blue` should be `Red`?
|
||||
|
||||
|
@ -2528,11 +2534,11 @@ mod test_reporting {
|
|||
|
||||
This `a` value is a:
|
||||
|
||||
[A]
|
||||
[…]
|
||||
|
||||
But the type annotation on `f` says it should be:
|
||||
|
||||
[A, B]
|
||||
[B, …]
|
||||
|
||||
Tip: Looks like a closed tag union does not have the `B` tag.
|
||||
|
||||
|
@ -2562,11 +2568,15 @@ mod test_reporting {
|
|||
|
||||
This `a` value is a:
|
||||
|
||||
[A]
|
||||
[…]
|
||||
|
||||
But the type annotation on `f` says it should be:
|
||||
|
||||
[A, B, C]
|
||||
[
|
||||
B,
|
||||
C,
|
||||
…
|
||||
]
|
||||
|
||||
Tip: Looks like a closed tag union does not have the `B` and `C` tags.
|
||||
|
||||
|
@ -2631,11 +2641,11 @@ mod test_reporting {
|
|||
|
||||
This `x` value is a:
|
||||
|
||||
[Left {}, Right Str]
|
||||
[Right Str, …]
|
||||
|
||||
But you are trying to use it as:
|
||||
|
||||
[Left *]
|
||||
[…]
|
||||
|
||||
Tip: Looks like a closed tag union does not have the `Right` tag.
|
||||
|
||||
|
@ -3380,11 +3390,23 @@ mod test_reporting {
|
|||
|
||||
This `Cons` tag application has the type:
|
||||
|
||||
[Cons {} [Cons Str [Cons {} a, Nil]b as a, Nil]b, Nil]b
|
||||
[
|
||||
Cons {} [
|
||||
Cons Str [
|
||||
Cons {} a,
|
||||
Nil,
|
||||
]b as a,
|
||||
Nil,
|
||||
]b,
|
||||
Nil,
|
||||
]b
|
||||
|
||||
But the type annotation on `x` says it should be:
|
||||
|
||||
[Cons {} a, Nil] as a
|
||||
[
|
||||
Cons {} a,
|
||||
Nil,
|
||||
] as a
|
||||
"###
|
||||
);
|
||||
|
||||
|
@ -3417,12 +3439,29 @@ mod test_reporting {
|
|||
|
||||
This `ACons` tag application has the type:
|
||||
|
||||
[ACons (Int Signed64) [BCons (Int Signed64) [ACons Str [BCons I64 [ACons I64 (BList I64 I64),
|
||||
ANil]b as ∞, BNil]c, ANil]b, BNil]c, ANil]b
|
||||
[
|
||||
ACons (Int Signed64) [
|
||||
BCons (Int Signed64) [
|
||||
ACons Str [
|
||||
BCons I64 [
|
||||
ACons I64 (BList I64 I64),
|
||||
ANil,
|
||||
]b as ∞,
|
||||
BNil,
|
||||
]c,
|
||||
ANil,
|
||||
]b,
|
||||
BNil,
|
||||
]c,
|
||||
ANil,
|
||||
]b
|
||||
|
||||
But the type annotation on `x` says it should be:
|
||||
|
||||
[ACons I64 (BList I64 I64), ANil] as a
|
||||
[
|
||||
ACons I64 (BList I64 I64),
|
||||
ANil,
|
||||
] as a
|
||||
"###
|
||||
);
|
||||
|
||||
|
@ -7946,11 +7985,11 @@ In roc, functions are always written as a lambda, like{}
|
|||
|
||||
This `v` value is a:
|
||||
|
||||
F [A, B, C]
|
||||
F [C, …]
|
||||
|
||||
But the branch patterns have type:
|
||||
|
||||
F [A, B]
|
||||
F […]
|
||||
|
||||
The branches must be cases of the `when` condition's type!
|
||||
|
||||
|
@ -8976,26 +9015,30 @@ In roc, functions are always written as a lambda, like{}
|
|||
foo
|
||||
"#
|
||||
),
|
||||
@r#"
|
||||
── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─
|
||||
@r###"
|
||||
── TYPE MISMATCH ───────────────────────────────────────── /code/proj/Main.roc ─
|
||||
|
||||
The branches of this `when` expression don't match the condition:
|
||||
The branches of this `when` expression don't match the condition:
|
||||
|
||||
6│> when bool is
|
||||
7│ True -> "true"
|
||||
8│ False -> "false"
|
||||
9│ Wat -> "surprise!"
|
||||
6│> when bool is
|
||||
7│ True -> "true"
|
||||
8│ False -> "false"
|
||||
9│ Wat -> "surprise!"
|
||||
|
||||
This `bool` value is a:
|
||||
This `bool` value is a:
|
||||
|
||||
Bool
|
||||
Bool
|
||||
|
||||
But the branch patterns have type:
|
||||
But the branch patterns have type:
|
||||
|
||||
[False, True, Wat]
|
||||
[
|
||||
False,
|
||||
True,
|
||||
Wat,
|
||||
]
|
||||
|
||||
The branches must be cases of the `when` condition's type!
|
||||
"#
|
||||
The branches must be cases of the `when` condition's type!
|
||||
"###
|
||||
);
|
||||
|
||||
// from https://github.com/roc-lang/roc/commit/1372737f5e53ee5bb96d7e1b9593985e5537023a
|
||||
|
@ -9976,7 +10019,10 @@ In roc, functions are always written as a lambda, like{}
|
|||
|
||||
This `lst` value is a:
|
||||
|
||||
[Cons {} ∞, Nil] as ∞
|
||||
[
|
||||
Cons {} ∞,
|
||||
Nil,
|
||||
] as ∞
|
||||
|
||||
But the type annotation on `olist` says it should be:
|
||||
|
||||
|
@ -10543,11 +10589,11 @@ I recommend using camelCase. It's the standard style in Roc code!
|
|||
|
||||
This `u8` value is a:
|
||||
|
||||
[Bad [DecodeProblem], Good (List U8)]
|
||||
[Good …, …]
|
||||
|
||||
But the branch patterns have type:
|
||||
|
||||
[Bad [DecodeProblem], Good (List U8) *]
|
||||
[Good … *, …]
|
||||
|
||||
The branches must be cases of the `when` condition's type!
|
||||
"###
|
||||
|
@ -12119,7 +12165,10 @@ I recommend using camelCase. It's the standard style in Roc code!
|
|||
|
||||
The `when` condition is a list of type:
|
||||
|
||||
List [A, B]
|
||||
List [
|
||||
A,
|
||||
B,
|
||||
]
|
||||
|
||||
But the branch patterns have type:
|
||||
|
||||
|
@ -13102,11 +13151,11 @@ I recommend using camelCase. It's the standard style in Roc code!
|
|||
|
||||
This `map` call produces:
|
||||
|
||||
List [One, Two]
|
||||
List [Two, …]
|
||||
|
||||
But the type annotation on `main` says it should be:
|
||||
|
||||
List [One]
|
||||
List […]
|
||||
"###
|
||||
);
|
||||
|
||||
|
@ -13140,11 +13189,11 @@ I recommend using camelCase. It's the standard style in Roc code!
|
|||
|
||||
This `map` call produces:
|
||||
|
||||
List [One, Two]
|
||||
List [Two, …]
|
||||
|
||||
But the type annotation on `main` says it should be:
|
||||
|
||||
List [One]
|
||||
List […]
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
|
@ -1313,7 +1313,7 @@ Some differences to note:
|
|||
- `List` refers to something more like Elm's `Array`, as noted earlier.
|
||||
- No `Char`. This is by design. What most people think of as a "character" is a rendered glyph. However, rendered glyphs are comprised of [grapheme clusters](https://stackoverflow.com/a/27331885), which are a variable number of Unicode code points - and there's no upper bound on how many code points there can be in a single cluster. In a world of emoji, I think this makes `Char` error-prone and it's better to have `Str` be the only first-class unit. For convenience when working with unicode code points (e.g. for performance-critical tasks like parsing), the single-quote syntax is sugar for the corresponding `U32` code point - for example, writing `'鹏'` is exactly the same as writing `40527`. Like Rust, you get a compiler error if you put something in single quotes that's not a valid [Unicode scalar value](http://www.unicode.org/glossary/#unicode_scalar_value).
|
||||
- No `Basics`. You use everything from the standard library fully-qualified; e.g. `Bool.not` or `Num.negate` or `Num.ceiling`. There is no `Never` because `[]` already serves that purpose. (Roc's standard library doesn't include an equivalent of `Basics.never`, but it's one line of code and anyone can implement it: `never = \a -> never a`.)
|
||||
- No `Tuple`. Roc doesn't have tuple syntax. As a convention, `Pair` can be used to represent tuples (e.g. `List.zip : List a, List b -> List [Pair a b]*`), but this comes up infrequently compared to languages that have dedicated syntax for it.
|
||||
- No `Tuple` module, but there is syntax support for tuples which allows not only destructuring (like in Elm) but also direct field access - which looks like record field access, but with numbered indices instead of named fields. For example, the Elm code `Tuple.first ( "a", "b" )` and `Tuple.second ( "a", "b" )` could be written in Roc as `("a", "b").0` and `("a", "b").1`. Roc tuples can also have more than two fields.
|
||||
- No `Task`. By design, platform authors implement `Task` (or don't; it's up to them) - it's not something that really *could* be usefully present in Roc's standard library.
|
||||
- No `Process`, `Platform`, `Cmd`, or `Sub` - similarly to `Task`, these are things platform authors would include, or not.
|
||||
- No `Debug`. Roc has a [built-in `dbg` keyword](https://www.roc-lang.org/tutorial#debugging) instead of `Debug.log` and a [`crash` keyword](https://www.roc-lang.org/tutorial#crashing) instead of `Debug.todo`.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue