on master help sign in
Make error messages for ports a lot nicer

Much more specific now. And they stress that it’s necessary to change mindset, and not just grow angry that it is not a traditional FFI

commit6b9a7ed80db4ce8c6991cce155c772ed41c6b843 authorEvan Czaplicki <evancz@users.noreply.github.com> dateThu 1 Feb 2018, 18:57 (9 years ago) parent4c5f326
2 files changed+173−76
src/Canonicalize/Effects.hs+41−19 src/Reporting/Error/Canonicalize.hs+132−57
src/Canonicalize/Effects.hs+41−19
@@ -82,27 +82,49 @@ canonicalize env decls unions effects =
8282canonicalizePort :: Env.Env -> Valid.Port -> Result i w (N.Name, Can.Port)
8383canonicalizePort env (Valid.Port (A.At region portName) tipe) =
8484 do (Can.Forall freeVars ctipe) <- Type.toAnnotation env tipe
85− case Type.deepDealias ctipe of
86− Can.TLambda outgoingType (Can.TType home name [Can.TVar _])
87− | home == ModuleName.cmd && name == "Cmd" ->
88− case checkPayload outgoingType of
89− Left (badType, err) ->
90− Result.throw (Error.PortPayloadInvalid region portName badType err)
91−
92− Right () ->
93− Result.ok (portName, Can.Outgoing freeVars outgoingType ctipe)
94−
95− Can.TLambda (Can.TLambda incomingType (Can.TVar msg1)) (Can.TType home name [Can.TVar msg2])
96− | home == ModuleName.sub && name == "Sub" && msg1 == msg2 ->
97− case checkPayload incomingType of
98− Left (badType, err) ->
99− Result.throw (Error.PortPayloadInvalid region portName badType err)
100−
101− Right () ->
102− Result.ok (portName, Can.Incoming freeVars incomingType ctipe)
85+ case reverse (Type.delambda (Type.deepDealias ctipe)) of
86+ Can.TType home name [msg] : revArgs
87+ | home == ModuleName.cmd && name == N.cmd ->
88+ case revArgs of
89+ [] ->
90+ Result.throw (Error.PortTypeInvalid region portName Error.CmdNoArg)
91+
92+ [outgoingType] ->
93+ case msg of
94+ Can.TVar _ ->
95+ case checkPayload outgoingType of
96+ Right () ->
97+ Result.ok (portName, Can.Outgoing freeVars outgoingType ctipe)
98+
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+
108+ | home == ModuleName.sub && name == N.sub ->
109+ case revArgs of
110+ [Can.TLambda incomingType (Can.TVar msg1)] ->
111+ case msg of
112+ Can.TVar msg2 | msg1 == msg2 ->
113+ case checkPayload incomingType of
114+ Right () ->
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)
103125
104126 _ ->
105− Result.throw (Error.PortTypeInvalid region portName ctipe)
127+ Result.throw (Error.PortTypeInvalid region portName Error.NotCmdOrSub)
106128
107129
108130
src/Reporting/Error/Canonicalize.hs+132−57
@@ -4,6 +4,7 @@ module Reporting.Error.Canonicalize
44 ( Error(..)
55 , BadArityContext(..)
66 , InvalidPayload(..)
7+ , PortProblem(..)
78 , DuplicatePatternContext(..)
89 , PossibleNames(..)
910 , VarKind(..)
@@ -24,7 +25,7 @@ import qualified Elm.Name as N
2425import qualified Reporting.Annotation as A
2526import qualified Reporting.Region as R
2627import qualified Reporting.Render.Code as Code
27−import qualified Reporting.Render.Type as RenderType
28+import qualified Reporting.Render.Type as Type
2829import qualified Reporting.Report as Report
2930import qualified Reporting.Helpers as H
3031import Reporting.Helpers ( Doc, (<>) )
@@ -68,7 +69,7 @@ data Error
6869 | NotFoundCtorQual R.Region N.Name N.Name PossibleNames
6970 | PatternHasRecordCtor R.Region N.Name
7071 | PortPayloadInvalid R.Region N.Name Can.Type InvalidPayload
71− | PortTypeInvalid R.Region N.Name Can.Type
72+ | PortTypeInvalid R.Region N.Name PortProblem
7273 | RecursiveAlias R.Region N.Name [N.Name] Src.Type [N.Name]
7374 | RecursiveDecl [Can.Def]
7475 | RecursiveLet (A.Located N.Name) [N.Name]
@@ -98,6 +99,14 @@ data InvalidPayload
9899 | UnsupportedType N.Name
99100
100101
102+data PortProblem
103+ = CmdNoArg
104+ | CmdExtraArgs Int
105+ | CmdBadMsg
106+ | SubBad
107+ | NotCmdOrSub
108+
109+
101110data PossibleNames =
102111 PossibleNames
103112 { _locals :: Set.Set N.Name
@@ -136,7 +145,7 @@ toKindInfo kind name =
136145-- TO REPORT
137146
138147
139−toReport :: Code.Source -> RenderType.Localizer -> Error -> Report.Report
148+toReport :: Code.Source -> Type.Localizer -> Error -> Report.Report
140149toReport source localizer err =
141150 case err of
142151 AnnotationTooShort region name index leftovers ->
@@ -414,78 +423,144 @@ toReport source localizer err =
414423
415424 PortPayloadInvalid region portName badType invalidPayload ->
416425 let
417− (aBadKindOfThing, elaboration) =
418− case invalidPayload of
419− ExtendedRecord ->
426+ formatDetails (aBadKindOfThing, elaboration) =
427+ Report.Report "PORT ERROR" region [] $
428+ Report.toCodeSnippet source region Nothing
420429 (
421− "an extended record"
422− ,
423430 H.reflow $
424− "The exact shape of the record must be known at compile time. No type variables!"
425− )
426−
427− Function ->
428− (
429− "a function"
431+ "The `" <> N.toString portName <> "` port is trying to transmit " <> aBadKindOfThing <> ":"
430432 ,
431− H.reflow $
432− "Functions cannot be sent in and out ports. If we allowed functions in from JS\
433− \ they may perform some side-effects. If we let functions out, they could produce\
434− \ incorrect results because Elm optimizations assume there are no side-effects."
433+ H.stack
434+ [ H.indent 4 (H.dullyellow (Type.toDoc localizer badType))
435+ , elaboration
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+ ]
435441 )
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+ )
436452
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+ )
437462
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− )
446463
447− UnsupportedType name ->
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+
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 $
448496 (
449− "a `" <> N.toString name <> "` value"
497+ H.reflow before
450498 ,
451499 H.stack
452− [ H.reflow $ "The types that can flow in and out of Elm include:"
453− , H.indent 4 $
454− H.reflow $
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."
500+ [ after
501+ , H.link "Hint" "Read" "ports"
502+ "for more advice. For example, do not end up with one port per JS function!"
461503 ]
462504 )
463505 in
464− Report.Report "PORT ERROR" region [] $
465− Report.toCodeSnippet source region Nothing
506+ formatDetails $
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 ->
466532 (
533+ "The `" <> N.toString name <> "` port cannot send any messages to the `update` function."
534+ ,
467535 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."
469543 ,
470544 H.stack
471− [ error "TODO" badType
472− , elaboration
473− , H.link "Hint"
474− "Ports are not a traditional FFI for calling JS functions directly. They need a different mindset! Read"
475− "ports"
476− "to learn how to use ports effectively."
545+ [ H.reflow $
546+ "To receive messages from JavaScript, you need to define a port like this:"
547+ , H.indent 4 $ H.dullyellow $ H.text $
548+ "port " <> N.toString name <> " : (Int -> msg) -> Sub msg"
549+ , H.reflow $
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."
477553 ]
478554 )
479555
480− PortTypeInvalid region name badType ->
481− Report.Report "BAD PORT" region [] $
482− Report.toCodeSnippet source region Nothing
483− (
484− H.reflow $
485− "Port `" <> N.toString name <> "` has an invalid type."
486− ,
487− error "TODO PortTypeInvalid" badType
488− )
556+ NotCmdOrSub ->
557+ (
558+ "I am confused about the `" <> N.toString name <> "` port declaration."
559+ ,
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."
563+ )
489564
490565 RecursiveAlias region name args tipe others ->
491566 aliasRecursionReport source localizer region name args tipe others
@@ -777,7 +852,7 @@ _argMismatchReport source region kind name expected actual =
777852-- BAD ALIAS RECURSION
778853
779854
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
781856aliasRecursionReport source localizer region name args tipe others =
782857 case others of
783858 [] ->
@@ -791,7 +866,7 @@ aliasRecursionReport source localizer region name args tipe others =
791866 "When I expand a recursive type alias, it just keeps getting bigger and bigger.\
792867 \ So dealiasing results in an infinitely large type! Try this instead:"
793868 , H.indent 4 $
794− RenderType.decl localizer name args [(name, [error "TODO" tipe])]
869+ Type.decl localizer name args [(name, [error "TODO" tipe])]
795870 , H.link "Hint"
796871 "This is kind of a subtle distinction. I suggested the naive fix, but I recommend reading"
797872 "recursive-alias"