Each example below compiles the same (or, where noted, an equivalent) small Elm program with the official Elm 0.19.1 compiler on the left and with this fork's patched compiler on the right. Nothing runs until you open a link, and each one opens as a standalone page in a new tab — so a heavy benchmark (or one of the two Worker demos' deliberately blocking "old" sides) only ever freezes its own tab, never this page or any other example.
A chain of List.filter / List.map stages ending
in List.foldl or List.sum normally allocates
one full intermediate list per stage, then throws every one of them
away except the last. This fork recognizes that shape at compile time
and fuses the whole pipeline into a single loop that never allocates
the intermediate lists at all — a technique called deforestation, or
fusion.
pipeline4 : List Int -> Int
pipeline4 xs =
xs
|> List.filter isValid
|> List.map transform
|> List.filter isBig
|> List.foldl (+) 0
The same fusion idea, but for a chain with no final fold — the shape
you get from real UI code like
Html.ul [] (List.map viewItem (List.filter isVisible items)).
There's no single accumulator to fold into, so the compiler instead
fuses the chain into one loop that builds only the final list.
chain4 : List Int -> List String
chain4 xs =
xs
|> List.filter isValid
|> List.map transform
|> List.filter isBig
|> List.map final
Naive recursive list-building (n :: buildList (n + 1) end)
isn't tail-recursive in the usual sense — the :: happens
after the recursive call returns — so a plain compiler has no
choice but to grow the JavaScript call stack by one frame per element.
This fork detects this "tail recursion modulo cons" (TRMC) shape and
rewrites it into a loop that builds the list without growing the stack
at all.
buildList : Int -> Int -> List Int
buildList n end =
if n > end then
[]
else
n :: buildList (n + 1) end
Every freshly-built Elm list node has the same two-field shape
({ $, a, b }), but a non-empty node and the empty-list
value [] don't share that shape in the generated
JavaScript by default, which costs V8 an extra hidden-class check on
every access. This fork pads [] to match, in
--optimize builds only. It's a small, allocation-shape-level
win rather than an algorithmic one.
sumShortLists : Int -> Int
sumShortLists reps =
List.foldl
(\_ acc -> acc + List.sum (List.range 1 5))
0
(List.range 1 reps)
Every call through a generic higher-order function like
List.map or List.foldr normally goes through
Elm's generic A2/A3 arity-dispatch helpers,
even when the callback's arity is already known statically. This fork
bypasses that dispatch and calls the callback directly whenever the
whole-program analysis can prove it's safe.
mapOnce : List Int -> List Int
mapOnce xs =
List.map (\n -> n * 2 + 1) xs
This fork adds dotted-path record updates: { model |
account.address.city = "Berlin" } instead of manually
re-nesting three record updates by hand. The official compiler simply
can't parse that — it's new surface syntax, not just a runtime
difference — so the "old" panel below runs the verbose,
semantically-equivalent code Elm developers had to write before. On top
of the syntax, this fork's code generator now always compiles a record
update to a direct object literal in --optimize builds,
instead of sometimes calling a runtime helper.
-- New syntax (this fork only):
setCity model =
{ model | account.address.city = "Berlin" }
-- Old (official Elm), same result by hand:
setCity model =
let
account = model.account
address = account.address
in
{ model | account = { account | address = { address | city = "Berlin" } } }
Worker.run is a new fork-only feature (backed by the
separate andre-dietrich/worker package) that runs a plain
top-level function on a dedicated Web Worker and hands back a
Task. The official compiler doesn't trust that package as
a source of kernel code at all, so the "old" panels below run the exact
same computation directly, blocking the main thread — not a
compile-error showcase, just how it had to be written before.
A naive, exponential fib n takes a few real seconds around
n = 42 (both panels default to that). Both panels tick a
counter every 100ms while running — watch whether it keeps moving.
fib : Int -> Int
fib n =
let
go k = if k < 2 then k else go (k - 1) + go (k - 2)
in
go n
-- new side only:
Task.attempt GotResult (Worker.run fib n)
ackermann(3, n)'s doubly-nested recursion isn't a shape
either compiler can optimize away, so both eventually hit a real
JavaScript stack overflow — around n = 12 on the main thread,
a little earlier (around n = 10) inside the Web Worker, which
gets a smaller default stack than the main thread in this browser. Try
n = 9 first (succeeds on both), then n = 13. The
point isn't that the crash goes away — it's what happens around it.
ackermann : Int -> Int
ackermann n =
let
go m k =
if m == 0 then k + 1
else if k == 0 then go (m - 1) 1
else go (m - 1) (go m (k - 1))
in
go 3 n