on master help sign in
Make error messages for ports a lot nicer
6b9a7edEvan Czaplicki
src/Canonicalize/Effects.hs+41−19
@@ -82,27 +82,49 @@ canonicalize env decls unions effects =
82canonicalizePort :: Env.Env -> Valid.Port -> Result i w (N.Name, Can.Port)
82canonicalizePort :: Env.Env -> Valid.Port -> Result i w (N.Name, Can.Port)
83canonicalizePort env (Valid.Port (A.At region portName) tipe) =
83canonicalizePort env (Valid.Port (A.At region portName) tipe) =
84 do (Can.Forall freeVars ctipe) <- Type.toAnnotation env tipe
84 do (Can.Forall freeVars ctipe) <- Type.toAnnotation env tipe
85− case Type.deepDealias ctipe of
85+ case reverse (Type.delambda (Type.deepDealias ctipe)) of
86− Can.TLambda outgoingType (Can.TType home name [Can.TVar _])
86+ Can.TType home name [msg] : revArgs
87− | home == ModuleName.cmd && name == "Cmd" ->
87+ | home == ModuleName.cmd && name == N.cmd ->
88− case checkPayload outgoingType of
88+ case revArgs of
89− Left (badType, err) ->
89+ [] ->
90− Result.throw (Error.PortPayloadInvalid region portName badType err)
90+ Result.throw (Error.PortTypeInvalid region portName Error.CmdNoArg)
91−
91+
92+ [outgoingType] ->
93+ case msg of
94+ Can.TVar _ ->
95+ case checkPayload outgoingType of
92− Right () ->
96+ Right () ->
93− Result.ok (portName, Can.Outgoing freeVars outgoingType ctipe)
97+ Result.ok (portName, Can.Outgoing freeVars outgoingType ctipe)
94−
98+
95− Can.TLambda (Can.TLambda incomingType (Can.TVar msg1)) (Can.TType home name [Can.TVar msg2])
99+ Left (badType, err) ->
100+ Result.throw (Error.PortPayloadInvalid region portName badType err)
101+
102+ _ ->
103+ Result.throw (Error.PortTypeInvalid region portName Error.CmdBadMsg)
104+
105+ _ ->
106+ Result.throw (Error.PortTypeInvalid region portName (Error.CmdExtraArgs (length revArgs)))
107+
96− | home == ModuleName.sub && name == "Sub" && msg1 == msg2 ->
108+ | home == ModuleName.sub && name == N.sub ->
97− case checkPayload incomingType of
109+ case revArgs of
98− Left (badType, err) ->
110+ [Can.TLambda incomingType (Can.TVar msg1)] ->
99− Result.throw (Error.PortPayloadInvalid region portName badType err)
111+ case msg of
100−
112+ Can.TVar msg2 | msg1 == msg2 ->
113+ case checkPayload incomingType of
101− Right () ->
114+ Right () ->
102− Result.ok (portName, Can.Incoming freeVars incomingType ctipe)
115+ Result.ok (portName, Can.Incoming freeVars incomingType ctipe)
116+
117+ Left (badType, err) ->
118+ Result.throw (Error.PortPayloadInvalid region portName badType err)
119+
120+ _ ->
121+ Result.throw (Error.PortTypeInvalid region portName Error.SubBad)
122+
123+ _ ->
124+ Result.throw (Error.PortTypeInvalid region portName Error.SubBad)
103
125
104 _ ->
126 _ ->
105− Result.throw (Error.PortTypeInvalid region portName ctipe)
127+ Result.throw (Error.PortTypeInvalid region portName Error.NotCmdOrSub)
106
128
107
129
108
130
src/Reporting/Error/Canonicalize.hs+132−57
@@ -4,6 +4,7 @@ module Reporting.Error.Canonicalize
4 ( Error(..)
4 ( Error(..)
5 , BadArityContext(..)
5 , BadArityContext(..)
6 , InvalidPayload(..)
6 , InvalidPayload(..)
7+ , PortProblem(..)
7 , DuplicatePatternContext(..)
8 , DuplicatePatternContext(..)
8 , PossibleNames(..)
9 , PossibleNames(..)
9 , VarKind(..)
10 , VarKind(..)
@@ -24,7 +25,7 @@ import qualified Elm.Name as N
24import qualified Reporting.Annotation as A
25import qualified Reporting.Annotation as A
25import qualified Reporting.Region as R
26import qualified Reporting.Region as R
26import qualified Reporting.Render.Code as Code
27import qualified Reporting.Render.Code as Code
27−import qualified Reporting.Render.Type as RenderType
28+import qualified Reporting.Render.Type as Type
28import qualified Reporting.Report as Report
29import qualified Reporting.Report as Report
29import qualified Reporting.Helpers as H
30import qualified Reporting.Helpers as H
30import Reporting.Helpers ( Doc, (<>) )
31import Reporting.Helpers ( Doc, (<>) )
@@ -68,7 +69,7 @@ data Error
68 | NotFoundCtorQual R.Region N.Name N.Name PossibleNames
69 | NotFoundCtorQual R.Region N.Name N.Name PossibleNames
69 | PatternHasRecordCtor R.Region N.Name
70 | PatternHasRecordCtor R.Region N.Name
70 | PortPayloadInvalid R.Region N.Name Can.Type InvalidPayload
71 | PortPayloadInvalid R.Region N.Name Can.Type InvalidPayload
71− | PortTypeInvalid R.Region N.Name Can.Type
72+ | PortTypeInvalid R.Region N.Name PortProblem
72 | RecursiveAlias R.Region N.Name [N.Name] Src.Type [N.Name]
73 | RecursiveAlias R.Region N.Name [N.Name] Src.Type [N.Name]
73 | RecursiveDecl [Can.Def]
74 | RecursiveDecl [Can.Def]
74 | RecursiveLet (A.Located N.Name) [N.Name]
75 | RecursiveLet (A.Located N.Name) [N.Name]
@@ -98,6 +99,14 @@ data InvalidPayload
98 | UnsupportedType N.Name
99 | UnsupportedType N.Name
99
100
100
101
102+data PortProblem
103+ = CmdNoArg
104+ | CmdExtraArgs Int
105+ | CmdBadMsg
106+ | SubBad
107+ | NotCmdOrSub
108+
109+
101data PossibleNames =
110data PossibleNames =
102 PossibleNames
111 PossibleNames
103 { _locals :: Set.Set N.Name
112 { _locals :: Set.Set N.Name
@@ -136,7 +145,7 @@ toKindInfo kind name =
136-- TO REPORT
145-- TO REPORT
137
146
138
147
139−toReport :: Code.Source -> RenderType.Localizer -> Error -> Report.Report
148+toReport :: Code.Source -> Type.Localizer -> Error -> Report.Report
140toReport source localizer err =
149toReport source localizer err =
141 case err of
150 case err of
142 AnnotationTooShort region name index leftovers ->
151 AnnotationTooShort region name index leftovers ->
@@ -414,78 +423,144 @@ toReport source localizer err =
414
423
415 PortPayloadInvalid region portName badType invalidPayload ->
424 PortPayloadInvalid region portName badType invalidPayload ->
416 let
425 let
417− (aBadKindOfThing, elaboration) =
426+ formatDetails (aBadKindOfThing, elaboration) =
418− case invalidPayload of
427+ Report.Report "PORT ERROR" region [] $
419− ExtendedRecord ->
428+ Report.toCodeSnippet source region Nothing
420 (
429 (
421− "an extended record"
422− ,
423 H.reflow $
430 H.reflow $
424− "The exact shape of the record must be known at compile time. No type variables!"
431+ "The `" <> N.toString portName <> "` port is trying to transmit " <> aBadKindOfThing <> ":"
425− )
426−
427− Function ->
428− (
429− "a function"
430 ,
432 ,
431− H.reflow $
433+ H.stack
432− "Functions cannot be sent in and out ports. If we allowed functions in from JS\
434+ [ H.indent 4 (H.dullyellow (Type.toDoc localizer badType))
433− \ they may perform some side-effects. If we let functions out, they could produce\
435+ , elaboration
434− \ incorrect results because Elm optimizations assume there are no side-effects."
436+ , H.link "Hint"
437+ "Ports are not a traditional FFI, so if you have tons of annoying ports, definitely read"
438+ "ports"
439+ "to learn how they are meant to work. They require a different mindset!"
440+ ]
435 )
441 )
442+ in
443+ formatDetails $
444+ case invalidPayload of
445+ ExtendedRecord ->
446+ (
447+ "an extended record"
448+ ,
449+ H.reflow $
450+ "But the exact shape of the record must be known at compile time. No type variables!"
451+ )
436
452
453+ Function ->
454+ (
455+ "a function"
456+ ,
457+ H.reflow $
458+ "But functions cannot be sent in and out ports. If we allowed functions in from JS\
459+ \ they may perform some side-effects. If we let functions out, they could produce\
460+ \ incorrect results because Elm optimizations assume there are no side-effects."
461+ )
437
462
438− TypeVariable name ->
439− (
440− "an unspecified type"
441− ,
442− H.reflow $
443− "Type variables like `" <> N.toString name <> "` cannot flow through ports because\
444− \ the marshalling code needs to know the exact data that it is going to process."
445− )
446
463
464+ TypeVariable name ->
465+ (
466+ "an unspecified type"
467+ ,
468+ H.reflow $
469+ "But type variables like `" <> N.toString name <> "` cannot flow through ports.\
470+ \ I need to know exactly what type of data I am getting, so I can guarantee that\
471+ \ unexpected data cannot sneak in and crash the Elm program."
472+ )
473+
447− UnsupportedType name ->
474+ UnsupportedType name ->
475+ (
476+ "a `" <> N.toString name <> "` value"
477+ ,
478+ H.stack
479+ [ H.reflow $ "I cannot handle that. The types that CAN flow in and out of Elm include:"
480+ , H.indent 4 $
481+ H.reflow $
482+ "Ints, Floats, Bools, Strings, Maybes, Lists, Arrays,\
483+ \ tuples, records, and JSON values."
484+ , H.reflow $
485+ "Since JSON values can flow through, you can use JSON encoders and decoders\
486+ \ to allow other types through as well. More advanced users often just do\
487+ \ everything with encoders and decoders for more control and better errors."
488+ ]
489+ )
490+
491+ PortTypeInvalid region name portProblem ->
492+ let
493+ formatDetails (before, after) =
494+ Report.Report "BAD PORT" region [] $
495+ Report.toCodeSnippet source region Nothing $
448 (
496 (
449− "a `" <> N.toString name <> "` value"
497+ H.reflow before
450 ,
498 ,
451 H.stack
499 H.stack
452− [ H.reflow $ "The types that can flow in and out of Elm include:"
500+ [ after
453− , H.indent 4 $
501+ , H.link "Hint" "Read" "ports"
454− H.reflow $
502+ "for more advice. For example, do not end up with one port per JS function!"
455− "Ints, Floats, Bools, Strings, Maybes, Lists, Arrays,\
456− \ tuples, records, and JSON values."
457− , H.reflow $
458− "Since JSON values can flow through, you can use JSON encoders and decoders\
459− \ to allow other types through as well. More advanced users often just do\
460− \ everything with encoders and decoders for more control and better errors."
461 ]
503 ]
462 )
504 )
463 in
505 in
464− Report.Report "PORT ERROR" region [] $
506+ formatDetails $
465− Report.toCodeSnippet source region Nothing
507+ case portProblem of
508+ CmdNoArg ->
509+ (
510+ "The `" <> N.toString name <> "` port cannot be just a command."
511+ ,
512+ H.reflow $
513+ "It can be (() -> Cmd msg) if you just need to trigger a JavaScript\
514+ \ function, but there is often a better way to set things up."
515+ )
516+
517+ CmdExtraArgs n ->
518+ (
519+ "The `" <> N.toString name <> "` port can only send ONE value out to JavaScript."
520+ ,
521+ let
522+ theseItemsInSomething
523+ | n == 2 = "both of these items into a tuple or record"
524+ | n == 3 = "these " ++ show n ++ " items into a tuple or record"
525+ | True = "these " ++ show n ++ " items into a record"
526+ in
527+ H.reflow $
528+ "You can put " ++ theseItemsInSomething ++ " to send them out though."
529+ )
530+
531+ CmdBadMsg ->
466 (
532 (
533+ "The `" <> N.toString name <> "` port cannot send any messages to the `update` function."
534+ ,
467 H.reflow $
535 H.reflow $
468− "The `" <> N.toString portName <> "` port is trying to transmit " <> aBadKindOfThing <> ":"
536+ "It must produce a (Cmd msg) type. Notice the lower case `msg` type\
537+ \ variable. The command will trigger some JS code, but it will not send\
538+ \ anything particular back to Elm."
539+ )
540+
541+ SubBad ->
542+ ( "There is something off about this `" <> N.toString name <> "` port declaration."
469 ,
543 ,
470 H.stack
544 H.stack
471− [ error "TODO" badType
545+ [ H.reflow $
472− , elaboration
546+ "To receive messages from JavaScript, you need to define a port like this:"
473− , H.link "Hint"
547+ , H.indent 4 $ H.dullyellow $ H.text $
474− "Ports are not a traditional FFI for calling JS functions directly. They need a different mindset! Read"
548+ "port " <> N.toString name <> " : (Int -> msg) -> Sub msg"
475− "ports"
549+ , H.reflow $
476− "to learn how to use ports effectively."
550+ "Now every time JS sends an `Int` to this port, it is converted to a `msg`.\
551+ \ And if you subscribe, those `msg` values will be piped into your `update`\
552+ \ function. The only thing you can customize here is the `Int` type."
477 ]
553 ]
478 )
554 )
479
555
480− PortTypeInvalid region name badType ->
556+ NotCmdOrSub ->
481− Report.Report "BAD PORT" region [] $
557+ (
482− Report.toCodeSnippet source region Nothing
558+ "I am confused about the `" <> N.toString name <> "` port declaration."
483− (
484− H.reflow $
485− "Port `" <> N.toString name <> "` has an invalid type."
486− ,
559+ ,
487− error "TODO PortTypeInvalid" badType
560+ H.reflow $
561+ "Ports need to produce a command (Cmd) or a subscription (Sub) but\
562+ \ this is neither. I do not know how to handle this."
488− )
563+ )
489
564
490 RecursiveAlias region name args tipe others ->
565 RecursiveAlias region name args tipe others ->
491 aliasRecursionReport source localizer region name args tipe others
566 aliasRecursionReport source localizer region name args tipe others
@@ -777,7 +852,7 @@ _argMismatchReport source region kind name expected actual =
777-- BAD ALIAS RECURSION
852-- BAD ALIAS RECURSION
778
853
779
854
780−aliasRecursionReport :: Code.Source -> RenderType.Localizer -> R.Region -> N.Name -> [N.Name] -> Src.Type -> [N.Name] -> Report.Report
855+aliasRecursionReport :: Code.Source -> Type.Localizer -> R.Region -> N.Name -> [N.Name] -> Src.Type -> [N.Name] -> Report.Report
781aliasRecursionReport source localizer region name args tipe others =
856aliasRecursionReport source localizer region name args tipe others =
782 case others of
857 case others of
783 [] ->
858 [] ->
@@ -791,7 +866,7 @@ aliasRecursionReport source localizer region name args tipe others =
791 "When I expand a recursive type alias, it just keeps getting bigger and bigger.\
866 "When I expand a recursive type alias, it just keeps getting bigger and bigger.\
792 \ So dealiasing results in an infinitely large type! Try this instead:"
867 \ So dealiasing results in an infinitely large type! Try this instead:"
793 , H.indent 4 $
868 , H.indent 4 $
794− RenderType.decl localizer name args [(name, [error "TODO" tipe])]
869+ Type.decl localizer name args [(name, [error "TODO" tipe])]
795 , H.link "Hint"
870 , H.link "Hint"
796 "This is kind of a subtle distinction. I suggested the naive fix, but I recommend reading"
871 "This is kind of a subtle distinction. I suggested the naive fix, but I recommend reading"
797 "recursive-alias"
872 "recursive-alias"