From cfe7c8e5efbefa61cbdf11326e528370eb16bfd3 Mon Sep 17 00:00:00 2001 From: Ayaz Hafiz Date: Wed, 26 Oct 2022 10:49:19 -0500 Subject: [PATCH] Warn on the presence of unnecessary wildcards in output positions --- crates/cli/tests/cli_run.rs | 4 +- .../benchmarks/Base64.roc | 8 ++-- .../cli_testing_examples/benchmarks/Deriv.roc | 2 +- crates/compiler/builtins/roc/Dict.roc | 2 +- crates/compiler/builtins/roc/List.roc | 22 ++++----- crates/compiler/builtins/roc/Num.roc | 44 +++++++++--------- crates/compiler/builtins/roc/Str.roc | 46 +++++++++---------- crates/compiler/can/src/annotation.rs | 13 +++++- crates/compiler/problem/src/can.rs | 3 ++ crates/compiler/solve/tests/solve_expr.rs | 24 +++++----- crates/compiler/test_gen/src/gen_tags.rs | 8 ++-- crates/repl_test/src/tests.rs | 38 +++++++-------- crates/reporting/src/error/canonicalize.rs | 9 ++++ crates/reporting/tests/test_reporting.rs | 22 +++++++++ examples/cli/cli-platform/Arg.roc | 2 +- examples/cli/cli-platform/Stdout.roc | 4 +- examples/cli/false-interpreter/Context.roc | 6 +-- 17 files changed, 151 insertions(+), 106 deletions(-) diff --git a/crates/cli/tests/cli_run.rs b/crates/cli/tests/cli_run.rs index 538dd0cb5a..d585e6fa4a 100644 --- a/crates/cli/tests/cli_run.rs +++ b/crates/cli/tests/cli_run.rs @@ -991,7 +991,7 @@ mod cli_run { This #UserApp.main value is a: - Task.Task {} * [Write [Stdout]*]* ? + Task.Task {} * [Write [Stdout]a]b ? But the type annotation on main says it should be: @@ -1012,7 +1012,7 @@ mod cli_run { This #UserApp.main value is a: - Task.Task {} * [Write [Stdout]*]* ? + Task.Task {} * [Write [Stdout]a]b ? But toEffect needs its 1st argument to be: diff --git a/crates/cli_testing_examples/benchmarks/Base64.roc b/crates/cli_testing_examples/benchmarks/Base64.roc index 930e08968b..792af7ca31 100644 --- a/crates/cli_testing_examples/benchmarks/Base64.roc +++ b/crates/cli_testing_examples/benchmarks/Base64.roc @@ -1,7 +1,7 @@ interface Base64 exposes [fromBytes, fromStr, toBytes, toStr] imports [Base64.Decode, Base64.Encode] # base 64 encoding from a sequence of bytes -fromBytes : List U8 -> Result Str [InvalidInput]* +fromBytes : List U8 -> Result Str [InvalidInput] fromBytes = \bytes -> when Base64.Decode.fromBytes bytes is Ok v -> @@ -11,16 +11,16 @@ fromBytes = \bytes -> Err InvalidInput # base 64 encoding from a string -fromStr : Str -> Result Str [InvalidInput]* +fromStr : Str -> Result Str [InvalidInput] fromStr = \str -> fromBytes (Str.toUtf8 str) # base64-encode bytes to the original -toBytes : Str -> Result (List U8) [InvalidInput]* +toBytes : Str -> Result (List U8) [InvalidInput] toBytes = \str -> Ok (Base64.Encode.toBytes str) -toStr : Str -> Result Str [InvalidInput]* +toStr : Str -> Result Str [InvalidInput] toStr = \str -> when toBytes str is Ok bytes -> diff --git a/crates/cli_testing_examples/benchmarks/Deriv.roc b/crates/cli_testing_examples/benchmarks/Deriv.roc index 4109666c3e..055dbf5c23 100644 --- a/crates/cli_testing_examples/benchmarks/Deriv.roc +++ b/crates/cli_testing_examples/benchmarks/Deriv.roc @@ -36,7 +36,7 @@ nestHelp = \{ s, f, m, x } -> Expr : [Val I64, Var Str, Add Expr Expr, Mul Expr Expr, Pow Expr Expr, Ln Expr] -divmod : I64, I64 -> Result { div : I64, mod : I64 } [DivByZero]* +divmod : I64, I64 -> Result { div : I64, mod : I64 } [DivByZero] divmod = \l, r -> when Pair (Num.divTruncChecked l r) (Num.remChecked l r) is Pair (Ok div) (Ok mod) -> Ok { div, mod } diff --git a/crates/compiler/builtins/roc/Dict.roc b/crates/compiler/builtins/roc/Dict.roc index 7641b2aade..cb83d06b7c 100644 --- a/crates/compiler/builtins/roc/Dict.roc +++ b/crates/compiler/builtins/roc/Dict.roc @@ -105,7 +105,7 @@ withCapacity = \n -> @Dict (List.withCapacity n) ## ## expect Dict.get dictionary 1 == Ok "Apple" ## expect Dict.get dictionary 2000 == Err KeyNotFound -get : Dict k v, k -> Result v [KeyNotFound]* | k has Eq +get : Dict k v, k -> Result v [KeyNotFound] | k has Eq get = \@Dict list, needle -> when List.findFirst list (\Pair key _ -> key == needle) is Ok (Pair _ v) -> diff --git a/crates/compiler/builtins/roc/List.roc b/crates/compiler/builtins/roc/List.roc index 22177f1458..0e06253a22 100644 --- a/crates/compiler/builtins/roc/List.roc +++ b/crates/compiler/builtins/roc/List.roc @@ -219,7 +219,7 @@ isEmpty = \list -> # but will cause a reference count increment on the value it got out of the list getUnsafe : List a, Nat -> a -get : List a, Nat -> Result a [OutOfBounds]* +get : List a, Nat -> Result a [OutOfBounds] get = \list, index -> if index < List.len list then Ok (List.getUnsafe list index) @@ -298,7 +298,7 @@ reserve : List a, Nat -> List a concat : List a, List a -> List a ## Returns the last element in the list, or `ListWasEmpty` if it was empty. -last : List a -> Result a [ListWasEmpty]* +last : List a -> Result a [ListWasEmpty] last = \list -> when List.get list (Num.subSaturated (List.len list) 1) is Ok v -> Ok v @@ -683,7 +683,7 @@ sortDesc = \list -> List.sortWith list (\a, b -> Num.compare b a) swap : List a, Nat, Nat -> List a ## Returns the first element in the list, or `ListWasEmpty` if it was empty. -first : List a -> Result a [ListWasEmpty]* +first : List a -> Result a [ListWasEmpty] first = \list -> when List.get list 0 is Ok v -> Ok v @@ -776,7 +776,7 @@ drop = \list, n -> ## To replace the element at a given index, instead of dropping it, see [List.set]. dropAt : List elem, Nat -> List elem -min : List (Num a) -> Result (Num a) [ListWasEmpty]* +min : List (Num a) -> Result (Num a) [ListWasEmpty] min = \list -> when List.first list is Ok initial -> @@ -793,7 +793,7 @@ minHelp = \list, initial -> else bestSoFar -max : List (Num a) -> Result (Num a) [ListWasEmpty]* +max : List (Num a) -> Result (Num a) [ListWasEmpty] max = \list -> when List.first list is Ok initial -> @@ -820,7 +820,7 @@ joinMap = \list, mapper -> ## Returns the first element of the list satisfying a predicate function. ## If no satisfying element is found, an `Err NotFound` is returned. -findFirst : List elem, (elem -> Bool) -> Result elem [NotFound]* +findFirst : List elem, (elem -> Bool) -> Result elem [NotFound] findFirst = \list, pred -> callback = \_, elem -> if pred elem then @@ -834,7 +834,7 @@ findFirst = \list, pred -> ## Returns the last element of the list satisfying a predicate function. ## If no satisfying element is found, an `Err NotFound` is returned. -findLast : List elem, (elem -> Bool) -> Result elem [NotFound]* +findLast : List elem, (elem -> Bool) -> Result elem [NotFound] findLast = \list, pred -> callback = \_, elem -> if pred elem then @@ -849,7 +849,7 @@ findLast = \list, pred -> ## Returns the index at which the first element in the list ## satisfying a predicate function can be found. ## If no satisfying element is found, an `Err NotFound` is returned. -findFirstIndex : List elem, (elem -> Bool) -> Result Nat [NotFound]* +findFirstIndex : List elem, (elem -> Bool) -> Result Nat [NotFound] findFirstIndex = \list, matcher -> foundIndex = List.iterate list 0 \index, elem -> if matcher elem then @@ -864,7 +864,7 @@ findFirstIndex = \list, matcher -> ## Returns the last index at which the first element in the list ## satisfying a predicate function can be found. ## If no satisfying element is found, an `Err NotFound` is returned. -findLastIndex : List elem, (elem -> Bool) -> Result Nat [NotFound]* +findLastIndex : List elem, (elem -> Bool) -> Result Nat [NotFound] findLastIndex = \list, matches -> foundIndex = List.iterateBackwards list (List.len list) \prevIndex, elem -> if matches elem then @@ -962,7 +962,7 @@ split = \elements, userSplitIndex -> ## remaining elements after that occurrence. If the delimiter is not found, returns `Err`. ## ## List.splitFirst [Foo, Z, Bar, Z, Baz] Z == Ok { before: [Foo], after: [Bar, Baz] } -splitFirst : List elem, elem -> Result { before : List elem, after : List elem } [NotFound]* | elem has Eq +splitFirst : List elem, elem -> Result { before : List elem, after : List elem } [NotFound] | elem has Eq splitFirst = \list, delimiter -> when List.findFirstIndex list (\elem -> elem == delimiter) is Ok index -> @@ -977,7 +977,7 @@ splitFirst = \list, delimiter -> ## remaining elements after that occurrence. If the delimiter is not found, returns `Err`. ## ## List.splitLast [Foo, Z, Bar, Z, Baz] Z == Ok { before: [Foo, Bar], after: [Baz] } -splitLast : List elem, elem -> Result { before : List elem, after : List elem } [NotFound]* | elem has Eq +splitLast : List elem, elem -> Result { before : List elem, after : List elem } [NotFound] | elem has Eq splitLast = \list, delimiter -> when List.findLastIndex list (\elem -> elem == delimiter) is Ok index -> diff --git a/crates/compiler/builtins/roc/Num.roc b/crates/compiler/builtins/roc/Num.roc index 6ec5df4a32..4fbd87512d 100644 --- a/crates/compiler/builtins/roc/Num.roc +++ b/crates/compiler/builtins/roc/Num.roc @@ -743,7 +743,7 @@ atan : Frac a -> Frac a ## >>> Num.sqrt -4.0f64 sqrt : Frac a -> Frac a -sqrtChecked : Frac a -> Result (Frac a) [SqrtOfNegative]* +sqrtChecked : Frac a -> Result (Frac a) [SqrtOfNegative] sqrtChecked = \x -> if x < 0.0 then Err SqrtOfNegative @@ -752,7 +752,7 @@ sqrtChecked = \x -> log : Frac a -> Frac a -logChecked : Frac a -> Result (Frac a) [LogNeedsPositive]* +logChecked : Frac a -> Result (Frac a) [LogNeedsPositive] logChecked = \x -> if x <= 0.0 then Err LogNeedsPositive @@ -791,7 +791,7 @@ logChecked = \x -> ## >>> |> Num.div 2.0 div : Frac a, Frac a -> Frac a -divChecked : Frac a, Frac a -> Result (Frac a) [DivByZero]* +divChecked : Frac a, Frac a -> Result (Frac a) [DivByZero] divChecked = \a, b -> if Num.isZero b then Err DivByZero @@ -800,7 +800,7 @@ divChecked = \a, b -> divCeil : Int a, Int a -> Int a -divCeilChecked : Int a, Int a -> Result (Int a) [DivByZero]* +divCeilChecked : Int a, Int a -> Result (Int a) [DivByZero] divCeilChecked = \a, b -> if Num.isZero b then Err DivByZero @@ -825,7 +825,7 @@ divCeilChecked = \a, b -> ## divTrunc : Int a, Int a -> Int a -divTruncChecked : Int a, Int a -> Result (Int a) [DivByZero]* +divTruncChecked : Int a, Int a -> Result (Int a) [DivByZero] divTruncChecked = \a, b -> if Num.isZero b then Err DivByZero @@ -845,7 +845,7 @@ divTruncChecked = \a, b -> ## >>> Num.rem -8 -3 rem : Int a, Int a -> Int a -remChecked : Int a, Int a -> Result (Int a) [DivByZero]* +remChecked : Int a, Int a -> Result (Int a) [DivByZero] remChecked = \a, b -> if Num.isZero b then Err DivByZero @@ -944,7 +944,7 @@ addSaturated : Num a, Num a -> Num a ## ## This is the same as [Num.add] except if the operation overflows, instead of ## panicking or returning ∞ or -∞, it will return `Err Overflow`. -addChecked : Num a, Num a -> Result (Num a) [Overflow]* +addChecked : Num a, Num a -> Result (Num a) [Overflow] addChecked = \a, b -> result = addCheckedLowlevel a b @@ -970,7 +970,7 @@ subSaturated : Num a, Num a -> Num a ## ## This is the same as [Num.sub] except if the operation overflows, instead of ## panicking or returning ∞ or -∞, it will return `Err Overflow`. -subChecked : Num a, Num a -> Result (Num a) [Overflow]* +subChecked : Num a, Num a -> Result (Num a) [Overflow] subChecked = \a, b -> result = subCheckedLowlevel a b @@ -994,7 +994,7 @@ mulSaturated : Num a, Num a -> Num a ## ## This is the same as [Num.mul] except if the operation overflows, instead of ## panicking or returning ∞ or -∞, it will return `Err Overflow`. -mulChecked : Num a, Num a -> Result (Num a) [Overflow]* +mulChecked : Num a, Num a -> Result (Num a) [Overflow] mulChecked = \a, b -> result = mulCheckedLowlevel a b @@ -1250,19 +1250,19 @@ toF64 : Num * -> F64 ## Converts a [Int] to an [I8]. ## If the given integer can't be precisely represented in an [I8], returns ## `Err OutOfBounds`. -toI8Checked : Int * -> Result I8 [OutOfBounds]* -toI16Checked : Int * -> Result I16 [OutOfBounds]* -toI32Checked : Int * -> Result I32 [OutOfBounds]* -toI64Checked : Int * -> Result I64 [OutOfBounds]* -toI128Checked : Int * -> Result I128 [OutOfBounds]* -toU8Checked : Int * -> Result U8 [OutOfBounds]* -toU16Checked : Int * -> Result U16 [OutOfBounds]* -toU32Checked : Int * -> Result U32 [OutOfBounds]* -toU64Checked : Int * -> Result U64 [OutOfBounds]* -toU128Checked : Int * -> Result U128 [OutOfBounds]* -toNatChecked : Int * -> Result Nat [OutOfBounds]* -toF32Checked : Num * -> Result F32 [OutOfBounds]* -toF64Checked : Num * -> Result F64 [OutOfBounds]* +toI8Checked : Int * -> Result I8 [OutOfBounds] +toI16Checked : Int * -> Result I16 [OutOfBounds] +toI32Checked : Int * -> Result I32 [OutOfBounds] +toI64Checked : Int * -> Result I64 [OutOfBounds] +toI128Checked : Int * -> Result I128 [OutOfBounds] +toU8Checked : Int * -> Result U8 [OutOfBounds] +toU16Checked : Int * -> Result U16 [OutOfBounds] +toU32Checked : Int * -> Result U32 [OutOfBounds] +toU64Checked : Int * -> Result U64 [OutOfBounds] +toU128Checked : Int * -> Result U128 [OutOfBounds] +toNatChecked : Int * -> Result Nat [OutOfBounds] +toF32Checked : Num * -> Result F32 [OutOfBounds] +toF64Checked : Num * -> Result F64 [OutOfBounds] # Special Floating-Point operations ## When given a [F64] or [F32] value, returns `Bool.false` if that value is diff --git a/crates/compiler/builtins/roc/Str.roc b/crates/compiler/builtins/roc/Str.roc index 24925a45c2..1ebb83ca0a 100644 --- a/crates/compiler/builtins/roc/Str.roc +++ b/crates/compiler/builtins/roc/Str.roc @@ -220,7 +220,7 @@ toUtf8 : Str -> List U8 ## ## expect Str.fromUtf8 [233, 185, 143] == Ok "鹏" ## expect Str.fromUtf8 [0xb0] == Err (BadUtf8 InvalidStartByte 0) -fromUtf8 : List U8 -> Result Str [BadUtf8 Utf8ByteProblem Nat]* +fromUtf8 : List U8 -> Result Str [BadUtf8 Utf8ByteProblem Nat] fromUtf8 = \bytes -> result = fromUtf8RangeLowlevel bytes 0 (List.len bytes) @@ -233,7 +233,7 @@ fromUtf8 = \bytes -> ## into a [Str] ## ## expect Str.fromUtf8Range [72, 105, 80, 103] { start : 0, count : 2 } == Ok "Hi" -fromUtf8Range : List U8, { start : Nat, count : Nat } -> Result Str [BadUtf8 Utf8ByteProblem Nat, OutOfBounds]* +fromUtf8Range : List U8, { start : Nat, count : Nat } -> Result Str [BadUtf8 Utf8ByteProblem Nat, OutOfBounds] fromUtf8Range = \bytes, config -> if config.start + config.count <= List.len bytes then result = fromUtf8RangeLowlevel bytes config.start config.count @@ -288,7 +288,7 @@ trimRight : Str -> Str ## expect Str.toDec "10" == Ok 10dec ## expect Str.toDec "-0.25" == Ok -0.25dec ## expect Str.toDec "not a number" == Err InvalidNumStr -toDec : Str -> Result Dec [InvalidNumStr]* +toDec : Str -> Result Dec [InvalidNumStr] toDec = \string -> strToNumHelp string ## Encode a [Str] to a [F64]. A [F64] value is a 64-bit @@ -297,7 +297,7 @@ toDec = \string -> strToNumHelp string ## ## expect Str.toF64 "0.10" == Ok 0.10f64 ## expect Str.toF64 "not a number" == Err InvalidNumStr -toF64 : Str -> Result F64 [InvalidNumStr]* +toF64 : Str -> Result F64 [InvalidNumStr] toF64 = \string -> strToNumHelp string ## Encode a [Str] to a [F32].A [F32] value is a 32-bit @@ -306,7 +306,7 @@ toF64 = \string -> strToNumHelp string ## ## expect Str.toF32 "0.10" == Ok 0.10f32 ## expect Str.toF32 "not a number" == Err InvalidNumStr -toF32 : Str -> Result F32 [InvalidNumStr]* +toF32 : Str -> Result F32 [InvalidNumStr] toF32 = \string -> strToNumHelp string ## Convert a [Str] to a [Nat]. If the given number doesn't fit in [Nat], it will be [truncated](https://www.ualberta.ca/computing-science/media-library/teaching-resources/java/truncation-rounding.html). @@ -324,7 +324,7 @@ toF32 = \string -> strToNumHelp string ## ## expect Str.toNat "9_000_000_000" == Ok 9000000000 ## expect Str.toNat "not a number" == Err InvalidNumStr -toNat : Str -> Result Nat [InvalidNumStr]* +toNat : Str -> Result Nat [InvalidNumStr] toNat = \string -> strToNumHelp string ## Encode a [Str] to an unsigned [U128] integer. A [U128] value can hold numbers @@ -335,7 +335,7 @@ toNat = \string -> strToNumHelp string ## expect Str.toU128 "0.1" == Err InvalidNumStr ## expect Str.toU128 "-1" == Err InvalidNumStr ## expect Str.toU128 "not a number" == Err InvalidNumStr -toU128 : Str -> Result U128 [InvalidNumStr]* +toU128 : Str -> Result U128 [InvalidNumStr] toU128 = \string -> strToNumHelp string ## Encode a [Str] to a signed [I128] integer. A [I128] value can hold numbers @@ -347,7 +347,7 @@ toU128 = \string -> strToNumHelp string ## expect Str.toI128 "-1" == Ok -1i128 ## expect Str.toI128 "0.1" == Err InvalidNumStr ## expect Str.toI128 "not a number" == Err InvalidNumStr -toI128 : Str -> Result I128 [InvalidNumStr]* +toI128 : Str -> Result I128 [InvalidNumStr] toI128 = \string -> strToNumHelp string ## Encode a [Str] to an unsigned [U64] integer. A [U64] value can hold numbers @@ -358,7 +358,7 @@ toI128 = \string -> strToNumHelp string ## expect Str.toU64 "0.1" == Err InvalidNumStr ## expect Str.toU64 "-1" == Err InvalidNumStr ## expect Str.toU64 "not a number" == Err InvalidNumStr -toU64 : Str -> Result U64 [InvalidNumStr]* +toU64 : Str -> Result U64 [InvalidNumStr] toU64 = \string -> strToNumHelp string ## Encode a [Str] to a signed [I64] integer. A [I64] value can hold numbers @@ -369,7 +369,7 @@ toU64 = \string -> strToNumHelp string ## expect Str.toI64 "-1" == Ok -1i64 ## expect Str.toI64 "0.1" == Err InvalidNumStr ## expect Str.toI64 "not a number" == Err InvalidNumStr -toI64 : Str -> Result I64 [InvalidNumStr]* +toI64 : Str -> Result I64 [InvalidNumStr] toI64 = \string -> strToNumHelp string ## Encode a [Str] to an unsigned [U32] integer. A [U32] value can hold numbers @@ -380,7 +380,7 @@ toI64 = \string -> strToNumHelp string ## expect Str.toU32 "0.1" == Err InvalidNumStr ## expect Str.toU32 "-1" == Err InvalidNumStr ## expect Str.toU32 "not a number" == Err InvalidNumStr -toU32 : Str -> Result U32 [InvalidNumStr]* +toU32 : Str -> Result U32 [InvalidNumStr] toU32 = \string -> strToNumHelp string ## Encode a [Str] to a signed [I32] integer. A [I32] value can hold numbers @@ -391,7 +391,7 @@ toU32 = \string -> strToNumHelp string ## expect Str.toI32 "-1" == Ok -1i32 ## expect Str.toI32 "0.1" == Err InvalidNumStr ## expect Str.toI32 "not a number" == Err InvalidNumStr -toI32 : Str -> Result I32 [InvalidNumStr]* +toI32 : Str -> Result I32 [InvalidNumStr] toI32 = \string -> strToNumHelp string ## Encode a [Str] to an unsigned [U16] integer. A [U16] value can hold numbers @@ -401,7 +401,7 @@ toI32 = \string -> strToNumHelp string ## expect Str.toU16 "0.1" == Err InvalidNumStr ## expect Str.toU16 "-1" == Err InvalidNumStr ## expect Str.toU16 "not a number" == Err InvalidNumStr -toU16 : Str -> Result U16 [InvalidNumStr]* +toU16 : Str -> Result U16 [InvalidNumStr] toU16 = \string -> strToNumHelp string ## Encode a [Str] to a signed [I16] integer. A [I16] value can hold numbers @@ -412,7 +412,7 @@ toU16 = \string -> strToNumHelp string ## expect Str.toI16 "-1" == Ok -1i16 ## expect Str.toI16 "0.1" == Err InvalidNumStr ## expect Str.toI16 "not a number" == Err InvalidNumStr -toI16 : Str -> Result I16 [InvalidNumStr]* +toI16 : Str -> Result I16 [InvalidNumStr] toI16 = \string -> strToNumHelp string ## Encode a [Str] to an unsigned [U8] integer. A [U8] value can hold numbers @@ -422,7 +422,7 @@ toI16 = \string -> strToNumHelp string ## expect Str.toU8 "-0.1" == Err InvalidNumStr ## expect Str.toU8 "not a number" == Err InvalidNumStr ## expect Str.toU8 "1500" == Err InvalidNumStr -toU8 : Str -> Result U8 [InvalidNumStr]* +toU8 : Str -> Result U8 [InvalidNumStr] toU8 = \string -> strToNumHelp string ## Encode a [Str] to a signed [I8] integer. A [I8] value can hold numbers @@ -432,7 +432,7 @@ toU8 = \string -> strToNumHelp string ## expect Str.toI8 "-15" == Ok -15i8 ## expect Str.toI8 "150.00" == Err InvalidNumStr ## expect Str.toI8 "not a number" == Err InvalidNumStr -toI8 : Str -> Result I8 [InvalidNumStr]* +toI8 : Str -> Result I8 [InvalidNumStr] toI8 = \string -> strToNumHelp string ## Get the byte at the given index, without performing a bounds check. @@ -451,7 +451,7 @@ substringUnsafe : Str, Nat, Nat -> Str ## ## expect Str.replaceEach "foo/bar/baz" "/" "_" == Ok "foo_bar_baz" ## expect Str.replaceEach "not here" "/" "_" == Err NotFound -replaceEach : Str, Str, Str -> Result Str [NotFound]* +replaceEach : Str, Str, Str -> Result Str [NotFound] replaceEach = \haystack, needle, flower -> when splitFirst haystack needle is Ok { before, after } -> @@ -483,7 +483,7 @@ expect Str.replaceEach "abXdeXghi" "X" "_" == Ok "ab_de_ghi" ## ## expect Str.replaceFirst "foo/bar/baz" "/" "_" == Ok "foo_bar/baz" ## expect Str.replaceFirst "no slashes here" "/" "_" == Err NotFound -replaceFirst : Str, Str, Str -> Result Str [NotFound]* +replaceFirst : Str, Str, Str -> Result Str [NotFound] replaceFirst = \haystack, needle, flower -> when splitFirst haystack needle is Ok { before, after } -> @@ -498,7 +498,7 @@ expect Str.replaceFirst "abXdeXghi" "X" "_" == Ok "ab_deXghi" ## ## expect Str.replaceLast "foo/bar/baz" "/" "_" == Ok "foo/bar_baz" ## expect Str.replaceLast "no slashes here" "/" "_" == Err NotFound -replaceLast : Str, Str, Str -> Result Str [NotFound]* +replaceLast : Str, Str, Str -> Result Str [NotFound] replaceLast = \haystack, needle, flower -> when splitLast haystack needle is Ok { before, after } -> @@ -514,7 +514,7 @@ expect Str.replaceLast "abXdeXghi" "X" "_" == Ok "abXde_ghi" ## ## expect Str.splitFirst "foo/bar/baz" "/" == Ok { before: "foo", after: "bar/baz" } ## expect Str.splitFirst "no slashes here" "/" == Err NotFound -splitFirst : Str, Str -> Result { before : Str, after : Str } [NotFound]* +splitFirst : Str, Str -> Result { before : Str, after : Str } [NotFound] splitFirst = \haystack, needle -> when firstMatch haystack needle is Some index -> @@ -567,7 +567,7 @@ firstMatchHelp = \haystack, needle, index, lastPossible -> ## ## expect Str.splitLast "foo/bar/baz" "/" == Ok { before: "foo/bar", after: "baz" } ## expect Str.splitLast "no slashes here" "/" == Err NotFound -splitLast : Str, Str -> Result { before : Str, after : Str } [NotFound]* +splitLast : Str, Str -> Result { before : Str, after : Str } [NotFound] splitLast = \haystack, needle -> when lastMatch haystack needle is Some index -> @@ -684,7 +684,7 @@ appendScalarUnsafe : Str, U32 -> Str ## ## expect Str.appendScalar "H" 105 == Ok "Hi" ## expect Str.appendScalar "😢" 0xabcdef == Err InvalidScalar -appendScalar : Str, U32 -> Result Str [InvalidScalar]* +appendScalar : Str, U32 -> Result Str [InvalidScalar] appendScalar = \string, scalar -> if isValidScalar scalar then Ok (appendScalarUnsafe string scalar) @@ -749,7 +749,7 @@ walkScalarsUntilHelp = \string, state, step, index, length -> strToNum : Str -> { berrorcode : U8, aresult : Num * } -strToNumHelp : Str -> Result (Num a) [InvalidNumStr]* +strToNumHelp : Str -> Result (Num a) [InvalidNumStr] strToNumHelp = \string -> result : { berrorcode : U8, aresult : Num a } result = strToNum string diff --git a/crates/compiler/can/src/annotation.rs b/crates/compiler/can/src/annotation.rs index 9ddcaa750f..cf41a18a6c 100644 --- a/crates/compiler/can/src/annotation.rs +++ b/crates/compiler/can/src/annotation.rs @@ -334,7 +334,7 @@ pub(crate) fn canonicalize_annotation( } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] enum CanPolarity { /// Polarity should be disregarded for now; relevant in aliaes + opaques. Disregard, @@ -1081,6 +1081,17 @@ fn can_extension_type<'a>( references, ); if valid_extension_type(shallow_dealias_with_scope(scope, &ext_type)) { + if matches!(loc_ann.extract_spaces().item, TypeAnnotation::Wildcard) + && matches!(ext_problem_kind, ExtensionTypeKind::TagUnion) + && pol == CanPolarity::Pos + { + // Wildcards are redundant in positive positions, since they will always be + // inferred as necessary there! + env.problem(roc_problem::can::Problem::UnnecessaryOutputWildcard { + region: loc_ann.region, + }) + } + ext_type } else { // Report an error but mark the extension variable to be inferred diff --git a/crates/compiler/problem/src/can.rs b/crates/compiler/problem/src/can.rs index ca0d50fd4c..0a37c87814 100644 --- a/crates/compiler/problem/src/can.rs +++ b/crates/compiler/problem/src/can.rs @@ -182,6 +182,9 @@ pub enum Problem { original_opaque: Symbol, ability_member: Symbol, }, + UnnecessaryOutputWildcard { + region: Region, + }, } #[derive(Clone, Debug, PartialEq)] diff --git a/crates/compiler/solve/tests/solve_expr.rs b/crates/compiler/solve/tests/solve_expr.rs index 196a0f3d10..b4cbfff155 100644 --- a/crates/compiler/solve/tests/solve_expr.rs +++ b/crates/compiler/solve/tests/solve_expr.rs @@ -502,7 +502,7 @@ mod solve_expr { Str.fromUtf8 "# ), - "List U8 -> Result Str [BadUtf8 Utf8ByteProblem Nat]*", + "List U8 -> Result Str [BadUtf8 Utf8ByteProblem Nat]", ); } @@ -3330,7 +3330,7 @@ mod solve_expr { List.get ["a"] 0 "# ), - "Result Str [OutOfBounds]*", + "Result Str [OutOfBounds]", ); } @@ -3432,7 +3432,7 @@ mod solve_expr { List.get [10, 9, 8, 7] 1 "# ), - "Result (Num *) [OutOfBounds]*", + "Result (Num *) [OutOfBounds]", ); infer_eq_without_problem( @@ -3441,7 +3441,7 @@ mod solve_expr { List.get "# ), - "List a, Nat -> Result a [OutOfBounds]*", + "List a, Nat -> Result a [OutOfBounds]", ); } @@ -3543,7 +3543,7 @@ mod solve_expr { Num.divChecked "# ), - "Float a, Float a -> Result (Float a) [DivByZero]*", + "Float a, Float a -> Result (Float a) [DivByZero]", ) } @@ -3567,7 +3567,7 @@ mod solve_expr { Num.divCeilChecked "# ), - "Int a, Int a -> Result (Int a) [DivByZero]*", + "Int a, Int a -> Result (Int a) [DivByZero]", ); } @@ -3591,7 +3591,7 @@ mod solve_expr { Num.divTruncChecked "# ), - "Int a, Int a -> Result (Int a) [DivByZero]*", + "Int a, Int a -> Result (Int a) [DivByZero]", ); } @@ -3779,7 +3779,7 @@ mod solve_expr { Model position : { openSet : Set position } - cheapestOpen : Model position -> Result position [KeyNotFound]* | position has Eq + cheapestOpen : Model position -> Result position [KeyNotFound] | position has Eq cheapestOpen = \model -> folder = \resSmallestSoFar, position -> @@ -3794,14 +3794,14 @@ mod solve_expr { Set.walk model.openSet (Ok { position: boom {}, cost: 0.0 }) folder |> Result.map (\x -> x.position) - astar : Model position -> Result position [KeyNotFound]* | position has Eq + astar : Model position -> Result position [KeyNotFound] | position has Eq astar = \model -> cheapestOpen model main = astar "# ), - "Model position -> Result position [KeyNotFound]* | position has Eq", + "Model position -> Result position [KeyNotFound] | position has Eq", ); } @@ -7498,7 +7498,7 @@ mod solve_expr { r#" OList := [Nil, Cons {} OList] - lst : [Cons {} OList]* + lst : [Cons {} OList] olist : OList olist = (\l -> @OList l) lst @@ -7517,7 +7517,7 @@ mod solve_expr { r#" OList := [Nil, Cons {} OList] - lst : [Cons {} OList]* + lst : [Cons {} OList] olist : OList olist = @OList lst diff --git a/crates/compiler/test_gen/src/gen_tags.rs b/crates/compiler/test_gen/src/gen_tags.rs index c28a38a50d..01683b86ec 100644 --- a/crates/compiler/test_gen/src/gen_tags.rs +++ b/crates/compiler/test_gen/src/gen_tags.rs @@ -1531,7 +1531,7 @@ fn polymorphic_tag() { assert_evals_to!( indoc!( r#" - x : [Y U8]* + x : [Y U8] x = Y 3 x "# @@ -1695,7 +1695,7 @@ fn instantiate_annotated_as_recursive_alias_toplevel() { Value : [Nil, Array (List Value)] - foo : [Nil]* + foo : [Nil] foo = Nil it : Value @@ -1723,7 +1723,7 @@ fn instantiate_annotated_as_recursive_alias_polymorphic_expr() { main = Value : [Nil, Array (List Value)] - foo : [Nil]* + foo : [Nil] foo = Nil it : Value @@ -1750,7 +1750,7 @@ fn instantiate_annotated_as_recursive_alias_multiple_polymorphic_expr() { main = Value : [Nil, Array (List Value)] - foo : [Nil]* + foo : [Nil] foo = Nil v1 : Value diff --git a/crates/repl_test/src/tests.rs b/crates/repl_test/src/tests.rs index 303ba71589..e1d3383087 100644 --- a/crates/repl_test/src/tests.rs +++ b/crates/repl_test/src/tests.rs @@ -70,7 +70,7 @@ fn num_floor_division() { fn num_floor_checked_division_success() { expect_success( "Num.divTruncChecked 4 3", - "Ok 1 : Result (Int *) [DivByZero]*", + "Ok 1 : Result (Int *) [DivByZero]", ); } @@ -79,7 +79,7 @@ fn num_floor_checked_division_success() { fn num_floor_checked_division_divby_zero() { expect_success( "Num.divTruncChecked 4 0", - "Err DivByZero : Result (Int *) [DivByZero]*", + "Err DivByZero : Result (Int *) [DivByZero]", ); } @@ -94,7 +94,7 @@ fn num_ceil_division() { fn num_ceil_checked_division_success() { expect_success( "Num.divCeilChecked 4 3", - "Ok 2 : Result (Int *) [DivByZero]*", + "Ok 2 : Result (Int *) [DivByZero]", ) } @@ -380,30 +380,30 @@ fn num_mul_saturated() { #[cfg(not(feature = "wasm"))] #[test] fn num_add_checked() { - expect_success("Num.addChecked 1 1", "Ok 2 : Result (Num *) [Overflow]*"); + expect_success("Num.addChecked 1 1", "Ok 2 : Result (Num *) [Overflow]"); expect_success( "Num.addChecked Num.maxI64 1", - "Err Overflow : Result I64 [Overflow]*", + "Err Overflow : Result I64 [Overflow]", ); } #[cfg(not(feature = "wasm"))] #[test] fn num_sub_checked() { - expect_success("Num.subChecked 1 1", "Ok 0 : Result (Num *) [Overflow]*"); + expect_success("Num.subChecked 1 1", "Ok 0 : Result (Num *) [Overflow]"); expect_success( "Num.subChecked Num.minI64 1", - "Err Overflow : Result I64 [Overflow]*", + "Err Overflow : Result I64 [Overflow]", ); } #[cfg(not(feature = "wasm"))] #[test] fn num_mul_checked() { - expect_success("Num.mulChecked 20 2", "Ok 40 : Result (Num *) [Overflow]*"); + expect_success("Num.mulChecked 20 2", "Ok 40 : Result (Num *) [Overflow]"); expect_success( "Num.mulChecked Num.maxI64 2", - "Err Overflow : Result I64 [Overflow]*", + "Err Overflow : Result I64 [Overflow]", ); } @@ -437,11 +437,11 @@ fn list_sum() { fn list_first() { expect_success( "List.first [12, 9, 6, 3]", - "Ok 12 : Result (Num *) [ListWasEmpty]*", + "Ok 12 : Result (Num *) [ListWasEmpty]", ); expect_success( "List.first []", - "Err ListWasEmpty : Result a [ListWasEmpty]*", + "Err ListWasEmpty : Result a [ListWasEmpty]", ); } @@ -450,12 +450,12 @@ fn list_first() { fn list_last() { expect_success( "List.last [12, 9, 6, 3]", - "Ok 3 : Result (Num *) [ListWasEmpty]*", + "Ok 3 : Result (Num *) [ListWasEmpty]", ); expect_success( "List.last []", - "Err ListWasEmpty : Result a [ListWasEmpty]*", + "Err ListWasEmpty : Result a [ListWasEmpty]", ); } @@ -654,18 +654,18 @@ fn type_problem() { #[test] fn issue_2149() { - expect_success(r#"Str.toI8 "127""#, "Ok 127 : Result I8 [InvalidNumStr]*"); + expect_success(r#"Str.toI8 "127""#, "Ok 127 : Result I8 [InvalidNumStr]"); expect_success( r#"Str.toI8 "128""#, - "Err InvalidNumStr : Result I8 [InvalidNumStr]*", + "Err InvalidNumStr : Result I8 [InvalidNumStr]", ); expect_success( r#"Str.toI16 "32767""#, - "Ok 32767 : Result I16 [InvalidNumStr]*", + "Ok 32767 : Result I16 [InvalidNumStr]", ); expect_success( r#"Str.toI16 "32768""#, - "Err InvalidNumStr : Result I16 [InvalidNumStr]*", + "Err InvalidNumStr : Result I16 [InvalidNumStr]", ); } @@ -1155,7 +1155,7 @@ fn box_box_type_alias() { fn issue_2582_specialize_result_value() { expect_success( r#"\x, list -> if x > 0 then List.first list else Ok """#, - r" : Num *, List Str -> Result Str [ListWasEmpty]*", + r" : Num *, List Str -> Result Str [ListWasEmpty]", ) } @@ -1236,7 +1236,7 @@ fn dict_get_single() { Dict.single 0 {a: 1, c: 2} |> Dict.get 0 "# ), - r#"Ok { a: 1, c: 2 } : Result { a : Num *, c : Num * } [KeyNotFound]*"#, + r#"Ok { a: 1, c: 2 } : Result { a : Num *, c : Num * } [KeyNotFound]"#, ) } diff --git a/crates/reporting/src/error/canonicalize.rs b/crates/reporting/src/error/canonicalize.rs index 361d582261..429d3fd71d 100644 --- a/crates/reporting/src/error/canonicalize.rs +++ b/crates/reporting/src/error/canonicalize.rs @@ -1015,6 +1015,15 @@ pub fn can_problem<'b>( title = "OVERLOADED SPECIALIZATION".to_string(); severity = Severity::Warning; } + Problem::UnnecessaryOutputWildcard { region } => { + doc = alloc.stack([ + alloc.reflow("I see you annotated a wildcard in a place where it's not needed:"), + alloc.region(lines.convert_region(region)), + alloc.reflow("Tag unions that are constants, or the return values of functions, are always inferred to be open by default! You can remove this annotation safely."), + ]); + title = "UNNECESSARY WILDCARD".to_string(); + severity = Severity::Warning; + } }; Report { diff --git a/crates/reporting/tests/test_reporting.rs b/crates/reporting/tests/test_reporting.rs index d642635ddb..4443242371 100644 --- a/crates/reporting/tests/test_reporting.rs +++ b/crates/reporting/tests/test_reporting.rs @@ -11731,4 +11731,26 @@ All branches in an `if` must have the same type! denoted with .. - is that what you meant? "### ); + + test_report!( + unnecessary_extension_variable, + indoc!( + r#" + f : {} -> [A, B]* + f + "# + ), + @r###" + ── UNNECESSARY WILDCARD ────────────────────────────────── /code/proj/Main.roc ─ + + I see you annotated a wildcard in a place where it's not needed: + + 4│ f : {} -> [A, B]* + ^ + + Tag unions that are constants, or the return values of functions, are + always inferred to be open by default! You can remove this annotation + safely. + "### + ); } diff --git a/examples/cli/cli-platform/Arg.roc b/examples/cli/cli-platform/Arg.roc index bdb7ab00e7..1e805acca6 100644 --- a/examples/cli/cli-platform/Arg.roc +++ b/examples/cli/cli-platform/Arg.roc @@ -147,7 +147,7 @@ toHelpHelper = \@Parser parser, configs -> List.append configs (Positional config) |> Config -findOneArg : Str, Str, MarkedArgs -> Result { val : Str, newlyTaken : Taken } [NotFound]* +findOneArg : Str, Str, MarkedArgs -> Result { val : Str, newlyTaken : Taken } [NotFound] findOneArg = \long, short, { args, taken } -> argMatches = \{ index, found: _ }, arg -> if Set.contains taken index || Set.contains taken (index + 1) then diff --git a/examples/cli/cli-platform/Stdout.roc b/examples/cli/cli-platform/Stdout.roc index 6c0692b668..c8793c776c 100644 --- a/examples/cli/cli-platform/Stdout.roc +++ b/examples/cli/cli-platform/Stdout.roc @@ -2,13 +2,13 @@ interface Stdout exposes [line, write] imports [Effect, Task.{ Task }, InternalTask] -line : Str -> Task {} * [Write [Stdout]*]* +line : Str -> Task {} * [Write [Stdout]] line = \str -> Effect.stdoutLine str |> Effect.map (\_ -> Ok {}) |> InternalTask.fromEffect -write : Str -> Task {} * [Write [Stdout]*]* +write : Str -> Task {} * [Write [Stdout]] write = \str -> Effect.stdoutWrite str |> Effect.map (\_ -> Ok {}) diff --git a/examples/cli/false-interpreter/Context.roc b/examples/cli/false-interpreter/Context.roc index 6849d5fe04..86cf21ca5e 100644 --- a/examples/cli/false-interpreter/Context.roc +++ b/examples/cli/false-interpreter/Context.roc @@ -19,7 +19,7 @@ pushStack = \ctx, data -> # I think an open tag union should just work here. # Instead at a call sites, I need to match on the error and then return the same error. # Otherwise it hits unreachable code in ir.rs -popStack : Context -> Result [T Context Data] [EmptyStack]* +popStack : Context -> Result [T Context Data] [EmptyStack] popStack = \ctx -> when List.last ctx.stack is Ok val -> @@ -66,7 +66,7 @@ with = \path, callback -> callback { scopes: [{ data: Some handle, index: 0, buf: [], whileInfo: None }], state: Executing, stack: [], vars: List.repeat (Number 0) Variable.totalCount } # I am pretty sure there is a syntax to destructure and keep a reference to the whole, but Im not sure what it is. -getChar : Context -> Task [T U8 Context] [EndOfData, NoScope]* +getChar : Context -> Task [T U8 Context] [EndOfData, NoScope] getChar = \ctx -> when List.last ctx.scopes is Ok scope -> @@ -76,7 +76,7 @@ getChar = \ctx -> Err ListWasEmpty -> Task.fail NoScope -getCharScope : Scope -> Task [T U8 Scope] [EndOfData, NoScope]* +getCharScope : Scope -> Task [T U8 Scope] [EndOfData, NoScope] getCharScope = \scope -> when List.get scope.buf scope.index is Ok val ->