elm-in-elm/compiler/1.0.1

Elm.Compiler

Functions for working with Elm source code.

The compiler phases in general look like this:

Stages of the compiler

Parsing

Useful eg. for tools like elm-format that convert back to the Elm representation, trying to do as few changes as possible. Ie. it would be bad if elm-format changed

\a b c -> a + b + c

to

\a -> \b -> \c -> a + b + c

That transformation is one of the things the Desugar phase does. So tools like elm-format probably don't want to touch that phase, and will only want to parse!

parseExpr : FileContents -> Result Error LocatedExpr

Parse a single expression like

( 12, "Hello" )

into AST like

Located
    {start = ..., end = ...}
    (Tuple
        (Located ... (Int 12))
        (Located ... (String "Hello"))
    )

If you don't need the location information and want to only keep the expressions, use Elm.AST.Frontend.unwrap to get something like

Tuple
    (Int 12)
    (String "Hello")
{ filePath : FilePath
, sourceCode : FileContents
}
-> Result Error (Module LocatedExpr)

Parse a module (one *.elm file). Get a Module datastructure back, holding the information about its exposed values, imports, declarations and more.

A file like

module Main exposing (foo)


import Bar as B exposing (bar)


foo =
    123

will get parsed into

{ imports =
    Dict.fromList
        [ ( "Bar"
          , { moduleName = "Bar"
            , as_ = Just "B"
            , exposing_ = Just (ExposingSome [ ExposedValue "bar" ])
            }
          )
        ]
, name = "Foo"
, filePath = "src/Foo.elm" -- what you pass into the function
, declarations =
    Dict.fromList
        [ ( "foo"
          , { module_ = "Foo"
            , name = "foo"
            , body = Value (AST.Frontend.Int 123)
            }
          )
        ]
, type_ = PlainModule
, exposing_ = ExposingSome [ ExposedValue "foo" ]
}
List
{ filePath : FilePath
, sourceCode : FileContents
}
-> Result Error (Dict ModuleName (Module LocatedExpr))

Parse multiple modules (*.elm files) - see parseModule for details.

parseImport : FileContents -> Result Error Import

Parse a single import statement, like

import Foo as F exposing
    ( foo
    , Bar(..)
    , Baz
    )

into

{ moduleName = "Foo"
, as_ = Just "F"
, exposing_ =
    Just
        (ExposingSome
            [ ExposedValue "foo"
            , ExposedTypeAndAllConstructors "Bar"
            , ExposedType "Baz"
            ]
        )
}
{ moduleName : ModuleName
, declaration : FileContents
}
-> Result Error (Declaration LocatedExpr)

Parse a single declaration, like

foo =
    123

into

{ module_ = "Foo" -- what you pass into the function
, name = "foo"
, body = Value (AST.Frontend.Int 123)
}

Desugaring

After we parse the source code from a String to the AST, we desugar it - simplify the AST type as much as possible to make later phases simpler and easier.

The best example to illustrate this (it doesn't actually happen though!) is let vs where. Imagine if Elm allowed for where constructs in its syntax, like Haskell does:

foo = x + y
    where
        x = 123
        y = x + 2

Then we'd like to convert these to let constructs (or the other way round) as soon as possible, so that the other phases don't need to handle two almost identical scenarios all over the place.

Examples of real desugarings include:

  • making functions only take one argument,
  • fully qualify all variables
  • etc.
Dict ModuleName (Module LocatedExpr)
-> Module LocatedExpr
-> LocatedExpr
-> Result Error LocatedExpr

Desugar a single expression like

Lambda
    { arguments = [ "x", "y" ]
    , body = AST.Frontend.Int 42
    }

into AST like

Lambda
    { argument = "x"
    , body =
        Lambda
            { argument = "y"
            , body = AST.Frontend.Int 42
            }
    }
Dict ModuleName (Module LocatedExpr)
-> Module LocatedExpr
-> Result Error (Module LocatedExpr)

Desugar a module (one *.elm file).

Dict ModuleName (Module LocatedExpr)
-> Result Error (Dict ModuleName (Module LocatedExpr))

Desugar multiple modules (*.elm files) - see desugarModule for details.

desugarOnlyModule : Module LocatedExpr -> Result Error (Module LocatedExpr)

Desugar a module (one *.elm file), without the intention of desugaring another one.

Inferring types

These functions compute the types of the given expressions, as well as check them against the user-defined type annotations.

Note that the more of your code you'll give these functions at once, the better the type inference will be. So it's advisable to eg. run inferModules once instead of running inferModule on each of your modules.

inferExpr : LocatedExpr -> Result Error LocatedExpr

Infer the types of a single expression.

inferModule : Module LocatedExpr -> Result Error (Module LocatedExpr)

Infer the types of expressions in a module (a single *.elm file).

Dict ModuleName (Module LocatedExpr)
-> Result Error (Dict ModuleName (Module LocatedExpr))

Infer the types of expressions in multiple modules (*.elm files).

Optimizing

After typechecking the expressions are ready to be optimized. (The inferred types are available to you inside the optimizations! (Unfortunately, the location information is also available to you inside the optimization. Sorry.))

defaultOptimizations : List ( String, LocatedExpr -> Maybe LocatedExpr )

The default optimizations the elm-in-elm compiler uses.

Try evaluating it in the REPL: you should see that each optimization function
has a name String next to it.


    > import Elm.Compiler
    > Elm.Compiler.optimizations
    [ ("plus", ...)
    , ("cons", ...)
    , ("if-literal-bool", ...)
    ]


    >


You can use this to filter optimizations you don't want!


   wantedOptimizations = Set.fromList [ "plus", "if-literal-bool ]


   optimizations
       |> List.filter (\(name, _) -> Set.member name wantedOptimizations)

The optimizations are of the shape Expr -> Maybe Expr, and you shouldn't recurse inside them. That's done for you automatically. You should only consider one "layer", whatever that means for your optimization.

Eg. the plus optimization is (conceptually):

optimizePlus : Expr -> Maybe Expr
optimizePlus expr =
    case expr of
        Plus (Int a) (Int b) ->
            Just (Int (a + b))


        _ ->
            Nothing

Where it only is concerned with the addition of two integers. Any other expression will just get ignored.

Your optimizations are then run from the bottom up, repeated as many times as needed on the same expression until the optimization produces Nothing ("Nothing else I can do here!"). At that point, the engine moves "up" to the parent expression, and so on until it reaches and optimizes the topmost expression.

Note the optimizations in the list of optimizations are combined in such a way that they take turns on the expression.

optimizeExpr : LocatedExpr -> LocatedExpr

Optimize a given (typed) expression using the default set of optimizations.

For using your own optimizations instead of or in addition to the default ones, look at the optimizeExprWith function.

List ( String, LocatedExpr -> Maybe LocatedExpr )
-> LocatedExpr
-> LocatedExpr

Optimize a given (typed) expression using a custom set of optimizations.

optimizeModule : Module LocatedExpr -> Module LocatedExpr

Optimize all expressions in a given module using the default set of optimizations.

Note there is currently no inter-definition optimizations (inlining etc.) - only the optimizations on each separate expression.

For using your own optimizations instead of or in addition to the default ones, look at the optimizeModuleWith function.

List ( String, LocatedExpr -> Maybe LocatedExpr )
-> Module LocatedExpr
-> Module LocatedExpr

Optimize all expressions in a given module using a custom set of optimizations.

Note there is currently no inter-definition optimizations (inlining etc.) - only the optimizations on each separate expression.

Dict ModuleName (Module LocatedExpr)
-> Dict ModuleName (Module LocatedExpr)

Optimize all expressions in multiple modules using the default set of optimizations.

For using your own optimizations instead of or in addition to the default ones, look at the optimizeModulesWith function.

List ( String, LocatedExpr -> Maybe LocatedExpr )
-> Dict ModuleName (Module LocatedExpr)
-> Dict ModuleName (Module LocatedExpr)

Optimize all expressions in multiple modules using a custom set of optimizations.

Dropping types

If you want to typecheck the code but then don't do anything with the types afterwards, you can drop them from the expressions you have. This is essentially a move backwards in the compiler phases:

Stages of the compiler

dropTypesExpr : LocatedExpr -> LocatedExpr

Drop types from a single expression.

We're hitting limitations of the Elm Packages website, and the type shown isn't very descriptive. The real type of this function is:

Typed.LocatedExpr -> Canonical.LocatedExpr

Example usage:

Located
    { start = ..., end = ... }
    ( Tuple
        (Located ... ( Int 12, Type.Int ))
        (Located ... ( String "Hello", Type.String ))
    , Type.Tuple Type.Int Type.String
    )

becomes

Located
    { start = ..., end = ... }
    (Tuple
        (Located ... (Int 12)
        (Located ... (String "Hello")
    )

If location info is not useful to you either, look for the unwrap functions in the various Elm.AST.* modules.

dropTypesModule : Module LocatedExpr -> Module LocatedExpr

Drop types from all expressions in the module.

We're hitting limitations of the Elm Packages website, and the type shown isn't very descriptive. The real type of this function is:

Module Typed.LocatedExpr
-> Module Canonical.LocatedExpr
Dict ModuleName (Module LocatedExpr)
-> Dict ModuleName (Module LocatedExpr)

Drop types from all expressions in all the modules.

We're hitting limitations of the Elm Packages website, and the type shown isn't very descriptive. The real type of this function is:

Dict ModuleName (Module Typed.LocatedExpr)
-> Dict ModuleName (Module Canonical.LocatedExpr)