Gleam 1.19: The Compiler Path from Source to BEAM
A Gleam program targeting the Erlang virtual machine still ends as BEAM bytecode. What changed in Gleam 1.19 is the handoff along the way. Gleam used to print Erlang source, then ask the Erlang compiler to parse that text and continue compiling it. Its rewritten code generator now produces Erlang abstract forms: the structured representation that Erlang’s parser would ordinarily create. Gleam serializes those forms and gives them to the Erlang compiler directly.
That is a fairly narrow change at the language boundary, but it has practical consequences. The compiler can avoid a text generation and parsing round trip. It can carry positions from the original Gleam file into the BEAM program, improving crash reports. And it keeps the mature Erlang compiler responsible for optimization and bytecode generation. Version 1.19 also contains useful work on the JavaScript backend, TypeScript declarations, mixed-language builds, the language server, and diagnostics. Here is how the pieces fit together.
The handoff moves past Erlang’s parser
The old path had two different source languages in flight. Gleam parsed and checked .gleam files, generated .erl text, and passed that text to Erlang’s tokenizer and parser. The parsed representation then entered the rest of Erlang’s compiler and emerged as a .beam module. The generated Erlang was an implementation detail, but its existence imposed formatting and parsing work on each fresh compilation.
Erlang’s abstract format represents modules, functions, clauses, expressions, and their annotations as Erlang terms. The standard library’s erl_parse documentation describes the individual forms. A compiler producing those forms can begin at the point where parsing Erlang source would have ended. Gleam’s new generator writes them in the Erlang external term format, which lets the Erlang compiler load the structure without scanning generated source.
This is a change to the Erlang target, not a removal of Gleam’s JavaScript target. Nor does it mean Gleam writes BEAM bytecode itself. The distinction matters: bypassing the front of a compiler is much less maintenance than replacing its back end. BEAM bytecode can change as the virtual machine evolves, and Erlang’s compiler already contains decades of optimization work. Gleam’s maintainers explicitly chose abstract forms as the sustainable point of integration for a community-funded language. Elixir takes a similar route through Erlang forms.
The new representation also changes the quality of source location data. Generated Erlang lines were only an approximation of the original Gleam lines, so a stack trace could identify a nearby function rather than the precise Gleam expression. The new generator attaches Gleam source locations to the forms; the release reports accurate Gleam line numbers in BEAM crash reports and stack traces. That may eventually help debuggers such as edb, but the team has not announced a finished debugger integration.
What the speed claim measures
Gleam’s release compares v1.17 with v1.19 on a language compilation benchmark derived from José Valim’s project. It builds 100 modules, each with 100 functions returning a small string. The first stage of the rewrite appeared in v1.18, so comparing 1.17 and 1.19 captures the broader change rather than one release’s isolated delta. The published chart shows a substantial improvement in a clean build without a cache.
That result is useful evidence for the new compiler path, but it is not a promise that every application builds by the same factor. Thousands of tiny, similar functions do not exercise the full mix of pattern matching, custom types, dependencies, and generated code in a real project. Existing Gleam builds also compile incrementally, so an everyday edit often avoids rebuilding most modules altogether. The sensible reading is that the Erlang backend’s fresh compilation cost fell in this test; teams should measure their own projects before planning around a number from a synthetic workload.
The release also compares languages and adds a Gleam-to-JavaScript run for context. Those bars are best treated as orientation, since different toolchains do different work and the benchmark deliberately uses a narrow program shape. The architectural explanation is stronger than a cross-language ranking: a serialized syntax tree removes work that Gleam previously asked the Erlang compiler to repeat.
JavaScript output gets less work at runtime
Although the headline concerns BEAM, the JavaScript backend received two separate optimizations. First, Gleam’s declarative case patterns can now become flatter JavaScript conditions. For a branch matching a constructor and two fields, the generator can combine checks such as isWibble(x) && x[0] === 1 && x[1] === 2 instead of nesting several if blocks and assigning temporary variables between them. This leaves fewer branches for the JavaScript engine to handle. The release says the change has little effect on minified and gzipped bundle size; the gain is in the generated control flow, not a headline byte-count reduction.
Second, short list literals no longer need to start as a JavaScript array that is converted into Gleam’s immutable linked-list representation. A list such as [1, 2, 3] can be constructed directly with a chain of prepend calls ending in empty. That avoids the intermediate array and conversion for the small lists common in some programs, particularly UI code using Lustre. The compiler still uses the array conversion for longer lists, where the team did not measure an improvement. The two paths are a reminder that equivalent source syntax need not imply equivalent runtime data structures: a Gleam list is not a mutable JavaScript array.
Type information survives the JavaScript boundary
Gleam can emit TypeScript declarations alongside JavaScript. For a generic custom type such as Box(value) with Full(value) and Empty variants, the generated variant-check function used to accept any and narrow to Full<unknown>. That told TypeScript which variant it had, but discarded the type of the value inside the box. Code that already knew it held a Box<number> lost that number during narrowing.
The new declaration includes a generic overload for a known Box<I>, refining it to Full<I>, plus a wider fallback for values of unknown origin. This is a small declaration-file change with a direct editor benefit: JavaScript and TypeScript callers can inspect a variant without giving up the payload’s known type.
Better entry points for Mix and rebar3
Running Gleam, Erlang, and Elixir code together on BEAM has long been straightforward once the code is compiled. Getting Gleam packages built inside another language’s project is harder because Mix and rebar3 do not automatically know Gleam’s build steps. Version 1.19 expands the compiler commands intended for those integrations.
Gleam now generates the .app resource file required for a compiled BEAM application instead of leaving that task to an external build-tool adapter. compile-package --no-dev limits compilation to production source and omits development dependencies. Package information and interface export commands can print to standard output, while JavaScript and TypeScript prelude exports can write to files. These are concrete plumbing changes that make adapters easier to write and run. They do not mean Mix or rebar3 has built-in Gleam support today; the release frames that as a goal.
Smaller changes that improve the daily loop
The built-in language server now supports go to definition, find references, and rename for field and argument labels. That completes a useful piece of navigation in codebases that use labeled calls and records heavily. In the compiler’s browser WebAssembly build, a new format_source function exposes the Gleam formatter. The team intends to bring it into the playground, but that user-facing feature is still future work.
Diagnostics also cover more mistakes with targeted messages. The compiler recognizes Git conflict markers left in source, a record-update expression with its spread value in the wrong position, unsupported operators such as +=, and an invalid | pattern that programmers might bring from another language. It gives more context for forbidden operators in constant expressions and for references to private declarations within the same package. An invalid type alias now produces less cascading noise across its uses. These details do not change the language’s central model, but they shorten the path from an error to the edit that fixes it.
Gleam 1.19’s largest design decision is therefore about where one compiler hands work to another. Abstract forms let Gleam preserve its source information and skip a redundant textual stage, while the Erlang compiler remains responsible for the changing BEAM target. The rest of the release follows the same pattern of careful boundary work: leaner JavaScript lowering, stronger TypeScript types, clearer external build APIs, and better feedback while editing.