Make sure that Debug module is not used in --optimize mode

This is neat because you get one error message telling you all the
modules that have Debug remnants.

It is also neat because the Generate.Nitpick module seems like the
perfect place for things like “efficiently detect globally unused
definitions” and “detect packages that are no longer used”
This commit is contained in:
Evan Czaplicki 2018-04-03 18:54:02 +02:00
parent dac392a778
commit 6be8d82c67
5 changed files with 163 additions and 6 deletions

View File

@ -86,4 +86,5 @@ generateDocs summary@(Summary.Summary root project _ _ _) =
(dirty, ifaces) <- Plan.plan (Just docsPath) summary graph
answers <- Compile.compile project (Just docsPath) ifaces dirty
results <- Artifacts.ignore answers
Output.noDebugUsesInPackage summary graph
Artifacts.writeDocs results docsPath

View File

@ -0,0 +1,99 @@
module Generate.Nitpick
( findDebugUses
)
where
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified AST.Optimized as Opt
import qualified AST.Module.Name as ModuleName
import qualified Elm.Package as Pkg
import qualified Elm.Name as N
-- FIND DEBUG USES
findDebugUses :: Pkg.Name -> Opt.Graph -> [N.Name]
findDebugUses pkg (Opt.Graph _ graph _) =
Set.toList $ Map.foldrWithKey (addDebugUses pkg) Set.empty graph
addDebugUses :: Pkg.Name -> Opt.Global -> Opt.Node -> Set.Set N.Name -> Set.Set N.Name
addDebugUses here (Opt.Global (ModuleName.Canonical pkg home) _) node uses =
if pkg == here && nodeHasDebug node then
Set.insert home uses
else
uses
nodeHasDebug :: Opt.Node -> Bool
nodeHasDebug node =
case node of
Opt.Define expr _ -> hasDebug expr
Opt.DefineTailFunc _ expr _ -> hasDebug expr
Opt.Ctor _ _ _ -> False
Opt.Enum _ _ -> False
Opt.Box _ -> False
Opt.Link _ -> False
Opt.Cycle defs _ -> any (hasDebug . snd) defs
Opt.Manager _ -> False
Opt.Kernel _ _ -> False
Opt.PortIncoming expr _ -> hasDebug expr
Opt.PortOutgoing expr _ -> hasDebug expr
hasDebug :: Opt.Expr -> Bool
hasDebug expression =
case expression of
Opt.Bool _ -> False
Opt.Chr _ -> False
Opt.Str _ -> False
Opt.Int _ -> False
Opt.Float _ -> False
Opt.VarLocal _ -> False
Opt.VarGlobal _ -> False
Opt.VarEnum _ _ -> False
Opt.VarBox _ -> False
Opt.VarCycle _ _ -> False
Opt.VarDebug _ _ _ _ -> True
Opt.VarKernel _ _ -> False
Opt.List exprs -> any hasDebug exprs
Opt.Function _ expr -> hasDebug expr
Opt.Call e es -> hasDebug e || any hasDebug es
Opt.TailCall _ args -> any (hasDebug . snd) args
Opt.If conds finally -> any (\(c,e) -> hasDebug c || hasDebug e) conds || hasDebug finally
Opt.Let def body -> defHasDebug def || hasDebug body
Opt.Destruct _ expr -> hasDebug expr
Opt.Case _ _ d jumps -> deciderHasDebug d || any (hasDebug . snd) jumps
Opt.Accessor _ -> False
Opt.Access r _ -> hasDebug r
Opt.Update r fs -> hasDebug r || any hasDebug fs
Opt.Record fs -> any hasDebug fs
Opt.Unit -> False
Opt.Tuple a b c -> hasDebug a || hasDebug b || maybe False hasDebug c
Opt.Shader _ -> False
defHasDebug :: Opt.Def -> Bool
defHasDebug def =
case def of
Opt.Def _ expr -> hasDebug expr
Opt.TailDef _ _ expr -> hasDebug expr
deciderHasDebug :: Opt.Decider Opt.Choice -> Bool
deciderHasDebug decider =
case decider of
Opt.Leaf (Opt.Inline expr) -> hasDebug expr
Opt.Leaf (Opt.Jump _) -> False
Opt.Chain _ success failure -> deciderHasDebug success || deciderHasDebug failure
Opt.FanOut _ tests fallback -> any (deciderHasDebug . snd) tests || deciderHasDebug fallback
-- TODO: FIND GLOBALLY UNUSED DEFINITIONS?
-- TODO: FIND PACKAGE USAGE STATS? (e.g. elm-lang/core = 142, author/project = 2, etc.)

View File

@ -3,6 +3,7 @@
module Generate.Output
( generate
, generateReplFile
, noDebugUsesInPackage
, Options(..)
, Output(..)
, output
@ -31,6 +32,8 @@ import qualified File.Crawl as Crawl
import qualified File.IO as IO
import qualified Generate.Functions as Functions
import qualified Generate.Html as Html
import qualified Generate.Nitpick as Nitpick
import qualified Reporting.Exit as Exit
import qualified Reporting.Task as Task
import qualified Stuff.Paths as Paths
import Terminal.Args (Parser(..), suggestFiles)
@ -55,19 +58,24 @@ generate options summary graph@(Crawl.Graph args _ _ _ _) =
return ()
Args.Roots name names ->
generateMonolith options summary graph (name:names)
do objectGraph <- organize summary graph
case _mode options of
Obj.Dev -> return ()
Obj.Prod -> noDebugUses summary objectGraph
generateMonolith options summary objectGraph (name:names)
-- GENERATE MONOLITH
generateMonolith :: Options -> Summary.Summary -> Crawl.Result -> [Module.Raw] -> Task.Task ()
generateMonolith (Options mode target maybeOutput) summary@(Summary.Summary _ project _ ifaces _) graph rootNames =
generateMonolith :: Options -> Summary.Summary -> Obj.Graph -> [Module.Raw] -> Task.Task ()
generateMonolith (Options mode target maybeOutput) (Summary.Summary _ project _ ifaces _) graph rootNames =
do let pkg = Project.getName project
let roots = map (Module.Canonical pkg) rootNames
objectGraph <- organize summary graph
case Obj.generate mode target ifaces objectGraph roots of
case Obj.generate mode target ifaces graph roots of
Obj.None ->
return ()
@ -145,6 +153,25 @@ loadPackageObj ( name, (version,_) ) =
-- NO DEBUG USES
noDebugUses :: Summary.Summary -> Obj.Graph -> Task.Task ()
noDebugUses (Summary.Summary _ project _ _ _) graph =
case Nitpick.findDebugUses (Project.getName project) graph of
[] ->
return ()
m:ms ->
Task.throw (Exit.CannotOptimizeDebug m ms)
noDebugUsesInPackage :: Summary.Summary -> Crawl.Result -> Task.Task ()
noDebugUsesInPackage summary graph =
noDebugUses summary =<< organize summary graph
-- OUTPUT

View File

@ -9,6 +9,7 @@ module Reporting.Exit
import qualified Data.ByteString.Builder as B
import Data.Monoid ((<>))
import System.IO (stderr)
import qualified Text.PrettyPrint.ANSI.Leijen as P
@ -44,9 +45,10 @@ data Exit
| Publish Publish.Exit
| BadHttp String Http.Exit
-- install
-- misc
| NoSolution [Pkg.Name]
| CannotMakeNothing
| CannotOptimizeDebug Module.Raw [Module.Raw]
@ -162,3 +164,30 @@ toReport exit =
, Help.reflow
"However many files you give, I will create one JS file out of them."
]
CannotOptimizeDebug m ms ->
Help.report "DEBUG REMNANTS" Nothing
"There are uses of the `Debug` module in the following modules:"
[ P.indent 4 $ P.red $ P.vcat $ map (P.text . Module.nameToString) (m:ms)
, Help.reflow "But the --optimize flag only works if all `Debug` functions are removed!"
, toSimpleNote $
"The issue is that --optimize strips out info needed by `Debug` functions.\
\ Here are two examples:"
, P.indent 4 $ Help.reflow $
"(1) It shortens record field names. This makes the generated JavaScript is\
\ smaller, but `Debug.toString` cannot know the real field names anymore."
, P.indent 4 $ Help.reflow $
"(2) Values like `type Height = Height Float` are unboxed. This reduces\
\ allocation, but it also means that `Debug.toString` cannot tell if it is\
\ looking at a `Height` or `Float` value."
, Help.reflow $
"There are a few other cases like that, and it will be much worse once we start\
\ inlining code. That optimization could move `Debug.log` and `Debug.todo` calls,\
\ resulting in unpredictable behavior. I hope that clarifies why this restriction\
\ exists!"
]
toSimpleNote :: String -> P.Doc
toSimpleNote message =
P.fillSep ((P.underline "Note" <> ":") : map P.text (words message))

View File

@ -108,6 +108,7 @@ Executable elm
File.Plan,
Generate.Functions,
Generate.Html,
Generate.Nitpick,
Generate.Output,
Reporting.Exit,
Reporting.Exit.Assets,