LFE MACHINE MANUAL
Adatped from multiple sources
by Duncan McGreggor and Robert Virding
Published by Cowboys 'N' Beans Books
https://github.com/cnbbooks ◈ http://cnbb.pub/ ◈ info@cnbb.pub
First electronic edition published: 2020
Portions © 1974, David Moon
Portions © 1978-1981, Daniel Weinreb and David Moon
Portions © 1979-1984, Douglas Adams
Portions © 1983, Kent Pitman
Portions © 1992-1993, Peter Norvig and Kent Pitman
Portions © 2003-2020, Ericsson AB
Portions © 2008-2012, Robert Virding
Portions © 2010-2020, Steve Klabnik and Carol Nichols
Portions © 2013-2023, Robert Virding and Duncan McGreggor
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License
About the Cover
The LFE "Chinenual" cover is based upon the Lisp Machine Manual covers of the early 80s. The Lisp Machine Manual editions we have seen from 1977 and 1979 had only hand-typed title pages, no covers, so we're not sure if the famous graphic/typographic design occurred any earlier than 1982. We've also been unable to discover who the original designer was, but would love to give them credit, should we find out.
The Original
The Software Preservation Group has this image on their site:
Bitsavers has a 3rd edition of the Chinenual with the full cover
The LFE Edition
Whole Cover
Back Cover
The Spine
Dedication
To all LFE Community members, Lispers, programmers as well as all our friends and families.
Preface
The original Lisp Machine Manual, the direct spiritiaul ancestor of the LFE Machine Manual, described both the language and the "operating system" of the Lisp Machine. The language was a dialect of Lisp called Zetalisp. Zetalisp was a direct descendant of MACLISP, created as a systems programming language for the MIT Lisp machines. This is of special note since Erlang was created as a systems programming language too. One of it's co-creators, Robert Virding, created Lisp Flavoured Erlang (LFE) based upon his expereinces with Franz Lisp (which based largely upon MACLISP), Portable Standard Lisp (itself an experiment in systems programming), and ultimately in an implementation he made of Lisp Machine Flavors on top of VAX/VMS where he extensively utilized the Lisp Machine Manual.
As such, LFE has a very strong inheritance of systems programming from both parents, as it were. First and foremost, it is a BEAM language written on top of the Erlang VM and from which it strays very little. Secondly, it is a Lisp dialect. It is, however, entirely a systems programming language.
Which brings us back to Zetalisp and the Lisp Machine Manual. It seemed only fitting to base the LFE manual upon the fantastic work and docuentation that was done on Lisp systems programming in the 70s and 80s, work that so many of us treasure and adore and to which we still defer. Thus the machine that is OTP in the context and syntax of the LFE Lisp dialect is extensively documented in the LFE MACHINE MANUAL.
Forward
The original Lisp programming language was implemented nearly 65 years ago1 at MIT (Massachusetts Institute of Technology), as part of the work done by John McCarthy and Marvin Minsky at the then-nascent AI (Artificial Intelligence) Lab. In the intervening half-century, the original Lisp evolved and experienced significant transformation in a technological diaspora originally fuelled by an explosion of research in the field of artificial intelligence. Through this, the industry witnessed 40 years of development where countless independent Lisp implementations were created. A small sampling of these number such favourites as Lisp 1.5, MacLisp, ZetaLisp, Scheme, Common Lisp, and ISLISP. However, the early 1990s saw the beginning of what would become the AI winter, and Lisp declined into obscurity – even notoriety – as graduating computer scientists were ushered into the “enterprise” world of Java, never to look back.
Except some did. By the early to mid-2000s, groundwork was being laid for what is now being recognized as a “Lisp renaissance.” The rediscovery of Lisp in the new millennium has led to the creation of a whole new collection of dialects, many implemented on top of other languages: Clojure on Java; LFE (Lisp Flavoured Erlang) and Joxa on Erlang; Hy on Python; Gherkin on Bash. New languages such as Go and Rust have several Lisp implementations, while JavaScript seems to gain a new Lisp every few years. While all of these ultimately owe their existence to the first Lisp, conceived in 19562 and defined in 19583, they represent a new breed and a new techno-ecological niche for Lisp: bringing the power of meta-programming and the clarity of near syntaxlessness4 to established language platforms. Whereas in the past Lisp has been an either-or choice, the new era of Lisps represents a symbiotic relationship between Lisp and the language platforms or VMs (virtual machines) upon which they are implemented; you now can have both, without leaving behind the accumulated experiences and comfort of your preferred platform.
Just as the Lisp implementations of the 1960s were greatly impacted by the advent of time-sharing computer systems, the new Lisps mentioned above like Clojure and LFE have been deeply influenced not only by their underlying virtual machines, but – more importantly – by the world view which the creators and maintainers of those VMs espoused. For the Erlang ecosystem of BEAM (Bogdan/Björn's Erlang Abstract Machine) languages, the dominant world view is the primacy of highly-concurrent, fault-tolerant, soft real-time, distributed systems. Erlang was created with these requirements in mind, and LFE inherits this in full. As such, LFE is more than a new Lisp; it is a language of power designed for creating robust services and systems. This point bears some discussion in order to properly prepare the intrepid programming language enthusiast who wishes to travel through the dimensions of LFE.
Unlike languages whose prototypical users were developers working in an interactive shell engaged in such tasks as solving math problems, Erlang's prototypical “user” was a telecommunications device in a world where downtime was simply unacceptable. As such, Erlang's requirements and constraints were very unusual when compared to most other programming languages of its generation.5 Erlang, and thus its constellation of dialects, was designed from the start to be a programming language for building distributed systems, one where applications created with it could survive network and systems catastrophes, millions of processes could be sustained on a single machine, where tens and hundreds of thousands of simultaneous network connections could be supported, where even a live, production deployment could have its code updated without downtime. Such is the backdrop against which the Erlang side of the LFE story unfolds – not only in the teaching and learning of it, but in its day-to-day use, and over time, in the minds of its developers.
When bringing new developers up to speed, this perspective is often overlooked or set aside for later. This is often done intentionally, since one doesn't want to overwhelm or discourage a newcomer by throwing them into the “deep end” of distributed systems theory and the critical minutia of reliability. However, if we ignore the strengths of LFE when teaching it, we do our members as well as ourselves a disservice that leads to much misunderstanding and frustration: “Why is LFE so different? You can do X so much more simply in language Y”. It should be stated quite clearly in all introductory materials that the BEAM languages are not like other programming languages; in many ways, the less you rely upon your previous experiences with C, Java, Python, Ruby, etc., the better off you will be.
When compared to mainstream programming languages, Erlang's development is akin to the divergent evolution of animals on a continent which has been completely isolated for hundreds of millions of years. For instance, programming languages have their “Hello, world”s and their “first project”s. These are like lap dogs for newcomers to the programming world, a distant and domesticated version of their far more powerful ancestors. Though each language has its own species of puppy to help new users, they are all loyal and faithful companions which share a high percentage of common genetic history: this is how you print a line, this is how you create a new project. Erlang – the Down Under of programming languages – has its “Hello, world”s and “first project”s, too. But in this case, the lapdog does not count the wolf in its ancestral line. It's not even a canid.6 It's a thylacine7 with terrifying jaws and and unfamiliar behaviours. Its “hello world” is sending messages to thousands of distributed peers and to nodes in supervised, monitored hierarchies. It has just enough familiarity to leave one feeling mystified by the differences and with the understanding that one is in the presence of something mostly alien.
That is the proper context for learning Erlang when coming from another programming language.
In LFE, we take that a step further by adding Lisp to the mix, supplementing a distributed programming language platform with the innovation laboratory that is Lisp. Far from making the learning process more difficult, this algebraic, time-honoured syntax provides an anchoring point, a home base for future exploration: it is pervasive and familiar, with very few syntactical rules to remember. We have even had reports of developers more easily learning Erlang via LFE.
In summary, by the end of this book we hope to have opened the reader's eyes to a new world of promise: distributed systems programming with a distinctly 1950s flavour – and a capacity to create applications that will thrive for another 50 years in our continually growing, decentralized technological world.
Duncan McGreggor2015, Lakefield, MN &
2023, Sleepy Eye, MN
Notes
-
The first draft of this forward which was written in 2015 said "almost 60 years ago" but was never published. Today, at the end of 2023, this content is finally seeing the light of day, with the origins of Lisp receding further into the past ... ↩
-
See McCarthy's 1979 paper History of Lisp, in particular the section entitled “LISP prehistory - Summer 1956 through Summer 1958”. ↩
-
Ibid., section “The implementation of LISP”. ↩
-
As you learn and then take advantage of Lisp's power, you will find yourself regularly creating new favourite features that LFE lacks. The author has gotten so used to this capability that he has applied this freedom to other areas of life. He hopes that you can forgive the occasional English language hack. ↩
-
In fact, in the 1980s when Erlang was born, these features were completely unheard of in mainstream languages. Even today, the combination of features Erlang/OTP (Open Telecom Platform) provides is rare; an argument can be made that Erlang (including its dialects) is still the only language which provides them all. ↩
-
The family of mammals that includes dogs, wolves, foxes, and jackals, among others. ↩
-
An extinct apex predator and marsupial also known as the Tasmanian tiger or Tasmanian wolf. ↩
Acknowledgments
Thank you Robert Virding for all that you have done to make Lisp Flavoured Erlang a reality.
Part I - Getting Started
Every programming journey begins with a single step, and in the Lisp tradition, that step is often taken at the REPL—the Read-Eval-Print Loop that transforms programming from a batch process into an interactive conversation with the computer. Part I of this manual guides you through your first steps in LFE (Lisp Flavored Erlang), from understanding what makes this language unique to building your first complete application.
LFE occupies a fascinating position in the programming language landscape. It brings the elegant syntax and powerful abstractions of Lisp to the robust, fault-tolerant world of the Erlang Virtual Machine (BEAM). This combination offers something remarkable: a language that supports both the reflective, code-as-data philosophy of Lisp and the "let it crash" resilience of actor-model concurrency. Whether you're drawn to LFE from the Lisp side or the Erlang side, you'll discover that this synthesis creates new possibilities for building distributed, fault-tolerant systems.
We begin with the traditional "Hello, World!" programs, but in three distinct flavors that showcase LFE's versatility. The REPL version demonstrates immediate interactivity—type an expression, see the result instantly. The main function approach shows how to create standalone programs, while the LFE/OTP version introduces you to the powerful supervision trees and process management that make Erlang applications famously reliable.
The heart of Part I is the guessing game walk-through, a complete project that evolves from simple procedural code to a proper (if very simple) OTP application. This progression mirrors the journey many LFE programs take: starting as interactive explorations at the REPL, growing into functions and modules, and ultimately becoming supervised processes that can handle failures gracefully and scale across multiple machines.
Finally, we dive deep into the REPL itself—not just as a place to test code fragments, but as a powerful development environment. The REPL is where LFE's dual nature as both Lisp and Erlang becomes most apparent. You can manipulate code as data using traditional Lisp techniques, while simultaneously spawning processes, sending messages, and exploring the vast ecosystem of Erlang and OTP libraries.
By the end of Part I, you'll have not just written your first LFE programs, but developed an intuition for the LFE way of thinking—interactive, incremental, and designed for systems that never stop running. You'll understand how to leverage both the immediate feedback of Lisp development and the industrial-strength reliability of the BEAM platform, setting the foundation for the deeper explorations that follow in the subsequent parts of this manual.
Introduction
Far out in the uncharted backwaters of the unfashionable end of computer science known as "distributed systems programming" lies a small red e. Orbitting this at a distance roughly proportional to the inverse of the likelihood of it being noticed is an utterly insignificant little green mug filled with the morning beverage stimulant equivalent of That Old Janx Spirit. Upon that liquid floats a little yellow 𝛌 whose adherents are so amazingly primitive that they still think cons, car, and cdr are pretty neat ideas.
This is their book.
Their language, Lisp Flavoured Erlang (henceforth "LFE"), lets you use the archaic and much-beloved S-expressions to write some of the most advanced software on the planet. LFE is a general-purpose, concurrent, functional Lisp whose underlying virtual machine (Erlang) was designed to create distributed, fault-tolerant, soft-realtime, highly-availale, always-up, hot-swappable appliances, applications, and services. In addition to fashionable digital watches, LFE sports immutable data, pattern-matching, eager evaluation, and dynamic typing.
This manual will not only teach you what all of that means and why you want it in your breakfast cereal, but also: how to create LFE programs; what exactly are the basic elements of the language; the ins-and-outs of extremely efficient and beautiful clients and servers; and much, much more.
Note, however, that the first chapter is a little different than most other books, and is in fact different from the rest of the chapters in this manual. We wrote this chapter with two guiding thoughts: firstly and foremost, we wanted to provide some practical examples of code-in-action as a context in which a programmer new to LFE could continuously refer -- from the very beginning through to the successful end -- while learning the general principles of that language; secondly, most programming language manuals are dry references to individual pieces of a language or system, not representatives of the whole, and we wanted to provide a starting place for those who learn well via examples, who would benefit from a view toward that whole. For those who have already seen plenty of LFE examples and would rather get to down to the nitty-gritty, rest assured we desire your experience to be guilt-free and thus encourage you to jump into next chapter immediately!
This book is divided into 6 parts with the following foci:
- Introductory material
- Core data types and capabilities
- The basics of LFE code and projects
- Advanced language features
- The machine that is OTP
- Concluding thoughts and summaries
There is a lot of material in this book, so just take it section by section. If at any time you feel overwhelmed, simply set down the book, take a deep breath, fix yourself a cuppa, and don't panic.
Welcome to the LFE MACHINE MANUAL, the definitive LFE reference.
About LFE
LFE is a modern programming language of two lineages, as indicated by the expansion of its acronym: Lisp Flavoured Erlang. In this book we aim to provide the reader with a comprehensive reference for LFE and will therefore explore both parental lines. Though the two language branches which ultimately merged in LFE are separated by nearly 30 years, and while LFE was created another 20 years after that, our story of their origins reveals that age simply doesn't matter. More significantly, LFE has unified Lisp and Erlang with a grace that is both simple and natural. This chapter is a historical overview on how Lisp, Erlang, and ultimately LFE came to be, thus providing a broad context for a complete learning experience.
What Is LFE?
LFE is a Lisp dialect which is heavily flavoured by the programming language virtual machine upon which it rests, the Erlang VM.1 Lisps are members of a programming language family whose typical distinguishing visual characteristic is the heavy use of parentheses and what is called prefix notation.2 To give you a visual sense of the LFE language, here is some example code:
(defun remsp
(('())
'())
(((cons #\ tail))
(remsp tail))
(((cons head tail))
(cons head (remsp tail))))
This function removes spaces from a string that it passed to it. We will postpone explanation and analysis of this code, but in a few chapters you will have the knowledge necessary to understand this bit of LFE.
Besides the parentheses and prefix notation, other substantial features which LFE shares with Lisp languages include the interchangeability of data with code and the ability to write code which generates new code using the same syntax as the rest of the language. Examples of other Lisps include Common Lisp, Scheme, and Clojure.
Erlang, on the other hand, is a language inspired most significantly by Prolog and whose virtual machine supports not only Erlang and LFE, but also newer BEAM languages including Joxa, Elixir, and Erlog. BEAM languages tend to focus on the key characteristics of their shared VM: fault-tolerance, massive scalability, and the ability to easily build soft real-time systems.
One way of describing LFE is as a programming language which unites these two. LFE is a happy mix of the serious, fault-tolerant philosophy of Erlang combined with the flexibility and extensibility offered by Lisp dialects. It is a homoiconic distributed systems programming language with Lisp macros which you will soon come to know in much greater detail.
A Brief History
To more fully understand the nature of LFE, we need to know more about Lisp and Erlang – how they came to be and even more importantly, how they are used today. To do this we will cast our view back in time: first we'll look at Lisp, then we'll review the circumstances of Erlang's genesis, and finally conclude the section with LFE's beginnings.
The Origins of Lisp
Lisp, originally spelled LISP, is an acronym for "LISt Processor". The language's creator, John McCarthy, was inspired by the work of his colleagues who in 1956 created IPL (Information Processing Language), an assembly programming language based upon the idea of manipulating lists. Though initially intrigued with their creation, McCarthy's interests in artificial intelligence required a higher-level language than IPL with a more general application of its list-manipulating features. So after much experimentation with FORTRAN, IPL, and heavy inspiration from Alonzo Church's lambda calculus,3 McCarthy created the first version of Lisp in 1958.
In the 1950s and 1960s programming languages were actually created on paper, due to limited computational resources. Volunteers, grad students, and even the children of language creators were used to simulate registers and operations of the language. This is how the first version of Lisp was “run”.4 Furthermore, there were two kinds of Lisp: students in the AI lab wrote a form called S-expressions, which was eventually input into an actual computer. These instructions had the form of nested lists, the syntax that eventually became synonymous with Lisp. The other form, called M-expressions, was used when McCarthy gave lectures or presented papers.5 These had a syntax which more closely resembles what programmers expect. This separation was natural at the time: for over a decade programmers had entered instructions using binary or machine language while describing these efforts in papers using natural language or pseudocode. McCarthy's students programmed entirely in S-expressions and as their use grew in popularity, the fate of M-expressions was sealed: they were never implemented.6
The Lisp 1.5 programmer's manual, first published in 1962, used M-expressions extensively to introduce and explain the language. Here is an example function7 for removing spaces from a string input defined using M-expressions:
remsp[string] = [null[string]→F;
eq[car[string];" "]→member[cdr[string]];
T→cons[car[string];remsp[cdr[string]]]]
The corresponding S-expression is what the Lisp programmer would actually enter into the IBM 704 machine that was used by the AI lab at MIT:8
DEFINE ((
(REMSP (LAMBDA (STRING)
(COND ((NULL STRING)
F)
((EQ (CAR STRING) " ")
(REMSP (CDR STRING)))
(T
(CONS (CAR STRING)
(REMSP (CDR STRING)))))))))
The period from 1958 to 1962, when Lisp 1.5 was released, marked the beginning of a new era in computer science. Since then Lisp dialects have made an extraordinary impact on the design and theory of other programming languages, changing the face of computing history perhaps more than any other language group. Language features that Lisp pioneered include such significant examples as: homoiconicity, conditional expressions, recursion, meta-programming, meta-circular evaluation, automatic garbage collection, and first class functions. A classic synopsis of these accomplishments was made by the computer scientist of great renown, Edsger Dijkstra in his 1972 Turing Award lecture, where he said the following about Lisp:
“With a few very basic principles at its foundation, [Lisp] has shown a remarkable stability. Besides that, Lisp has been the carrier for a considerable number of, in a sense, our most sophisticated computer applications. Lisp has jokingly been described as ‘the most intelligent way to misuse a computer’. I think that description a great compliment because it transmits the full flavour of liberation: it has assisted a number of our most gifted fellow humans in thinking previously impossible thoughts.”
Lisp usage is generally described as peaking in the 80s and early 90s, experiencing an adoption setback with the widespread view that problems in artificial intelligence were far more difficult to solve than originally anticipated.9 Another problem which faced Lisp was related hardware requirements: specialized architectures were developed in order to provide sufficient computational power to its users. These were expensive with few vendors, and a slow product cycle.
In the midst of this Lisp cold-spell, two seminal Lisp books were published: On Lisp, and ANSI Common Lisp, both by famed entrepreneur Paul Graham.10 Despite a decade of decline, these events helped catalyse a new appreciation for the language by a younger generation of programmers and within a few short years, the number of Lisp books and Lisp-based languages began growing, giving the world the likes of Practical Common Lisp and Let Over Lambda in the case of the former, and Clojure and LFE, in the case of the latter.
Constructing Erlang
Erlang was born in the heart of Ericsson's Computer Science Laboratory,11 just outside of Stockholm, Sweden.12 The lab had the general aim “to make Ericsson software activities as efficient as possible through the purposeful exploitation of modern technology.” The Erlang programming language was the lab's crowning achievement, but the effort leading up to this was extensive with a great many people engaged in the creation of many prototypes and the use of numerous of programming languages.
One example of this is the work that Nabiel Elshiewy and Robert Virding did in 1986 with Parlog, a concurrent logic programming language based on Prolog. Though this work was eventually abandoned, you can read the paper The Phoning Philosopher's Problem or Logic Programming for Telecommunications Applications and see the impact its features had on the future development of Erlang. The paper provides some examples of Parlog usage; using that for inspiration we can envision what our space-removing program would looking like:13
remsp([]) :-
[].
remsp([$ |Tail]) :-
remsp(Tail).
remsp([Head|Tail]) :-
[Head|remsp(Tail)].
Another language that was part of this department-wide experimentation was Smalltalk. Joe Armstrong started experimenting with it in 1985 to model a telephone exchanges and used this to develop a telephony algebra with it. A year later, when his colleague Roger Skagervall showed him the equivalence between this and logic programming, Prolog began its rise to prominence, and the first steps were made towards the syntax of Erlang as the world now knows it. In modern Erlang, our program has the following form:
remsp([]) ->
[];
remsp([$ |Tail]) ->
remsp(Tail);
remsp([Head|Tail]) ->
[Head|remsp(Tail)].
The members of the Ericsson lab who were tasked with building the next generation telephone exchange system, and thus involved with the various language experiments made over the course of a few years, came to the following conclusions:
- Small languages seemed better at succinctly addressing the problem space.
- The functional programming paradigm was appreciated, if sometimes viewed as awkward.
- Logic programming provided the most elegant solutions in the given problem space.
- Support for concurrency was viewed as essential.
If these were the initial guideposts for Erlang development, its guiding principles were the following:
- To handle high-concurrency
- To handle soft real-time constraints
- To support non-local, distributed computing
- To enable hardware interaction
- To support very large scale software systems
- To support complex interactions
- To provide non-stop operation
- To allow for system updates without downtime
- To allow engineers to create systems with only seconds of down-time per year
- To easily adapt to faults in both hardware and software
These were accomplished using such features as immutable data structures, light weight processes, no shared memory, message passing, supervision trees, and heartbeats. Furthermore, having adopted message-passing as the means of providing high-concurrency, Erlang slowly evolved into the exemplar of a programming paradigm that it essentially invented and even today, dominates: concurrent functional programming.
After four years of development from the late 80s into the early 90s, Erlang matured to the point where it was adopted for large projects inside Ericsson. In 1998 it was released as open source software, and has since seen growing adoption in the wider world of network- and systems-oriented programming.
The Birth of LFE
One of the co-inventors of Erlang, and part of the Lab's early efforts in language experimentation was Robert Virding. Virding first encountered Lisp in 1980 when he started his PhD in theoretical physics at Stockholm University. His exposure to the language came as a result of the physics department's use in performing symbolic algebraic computations. Despite this, he spent more time working on micro-processor programming and didn't dive into it until a few years later when he was working at Ericsson's Computer Science Laboratory. One of the languages evaluated for use in building telephony software was Lisp, but to do so properly required getting to know it in-depth – both a the language level as well as the operating system level.14 It was in this work that Virding's passion for Lisp blossomed and he came to appreciate deeply its functional nature, macros, and homoiconicity – all excellent and time-saving tools for building complicated systems.
Though the work on Lisps did not become the focus of Erlang development, the seeds of LFE were planted even before Erlang itself had come to be. After 20 years of contributions to the Erlang programming language, these began to bear fruit. In 2007 Virding decided to do something fun in his down time: to see what a Lisp would look like if written on top of the Prolog-inspired Erlang VM. After several months of hacking, he announced a first version of LFE to the Erlang mail list in early 2008.
A few years latter, when asked about the origins of LFE and the motivating elements behind his decision to start the project, Virding shared the following on the LFE mail list:
- It had always been a goal of Robert's to make a Lisp which could fully interact with Erlang/OTP, to see what it would look like and how it would run.
- He was looking for some interesting programming projects that were not too large to do in his spare time.
- He thought it would be a fun, open-ended problem to solve with many interesting parts.
- He likes implementing languages.
We showed an example of LFE at the beginning of this chapter; in keeping with our theme for each language subsection, we present it here again, though in a slightly altered form:
(defun remsp
(('())
'())
((`(32 . ,tail))
(remsp tail))
((`(,head . ,tail))
(cons head (remsp tail))))
What is LFE Good For?
Very few languages have the powerful capabilities which Erlang offers – both in its standard library as well as the set of Erlang libraries, frameworks, and patterns that are provided in OTP. This covers everything from fault-tolerance, scalability, soft real time capacity, and high-availability to proper design, component assembly, and deployment in distributed environments.
Similarly, despite the impact that Lisp has had on so many programming languages, its full suite of features is still essentially limited to Lisp dialects. This includes the features we have already mentioned: the ability to treat code as data, easily generate new code from data, as well as the interrelated power of writing macros – the last allows developers to modify the language to suit their needs. These rare features from two different language branches are unified in LFE and there is no well-established language that provides the union of these.
As such, LFE gives developers everything they need to envision, prototype, and then build distributed applications – ones with unique requirements that no platform provides and which can be delivered thanks to LFE's language-building capabilities.
To paraphrase and augment the opening of Chapter 1 in Designing for Scalability with Erlang/OTP:
“You need to implement a fault tolerant, scalable soft real time system with requirements for high availability. It has to be event driven and react to external stimulus, load and failure. It must always be responsive. You also need language-level features that don't exist yet. You would like to encode your domain's best practices and design patterns seamlessly into your chosen platform.”
LFE has everything you need to realize this dream ... and so much more.
In Summary
What LFE Is
Here's what you can expect of LFE:
- A proper Lisp-2, based on the features and limitations of the Erlang VM
- Compatibility with vanilla Erlang and OTP
- It runs on the standard Erlang VM
Furthermore, as a result of Erlang's influence (and LFE's compatibility with it), the following hold:
- there is no global data
- data is not mutable
- only the standard Erlang data types are used
- you get pattern matching and guards
- you have access to Erlang functions and modules
- LFE has a compiler/interpreter
- functions with declared arity and fixed number of arguments
- Lisp macros
What LFE Isn't
Just to clear the air and set some expectations, we'll go a step further. Here's what you're not going to find in LFE:
- An implementation of Scheme
- An implementation of Common Lisp
- An implementation of Clojure
As such, you will not find the following:
- A Scheme-like single namespace
- CL packages or munged names faking packages
- Access to Java libraries
Notes
-
Robert Virding, the creator of LFE and one of the co-creators of the Erlang programming language, has previously stated that, were he to start again, he would name his Lisp dialect EFL, since it truly is a Lisp with an Erlang flavour, rather than the other way round. ↩
-
We will be covering prefix notation when we cover symbolic expressions later in the book. ↩
-
Alonzo Church was one of McCarthy's professors at Princeton. McCarthy did not use all of the lambda calculus when creating Lisp, as there were many esoteric aspects for which he had no practical need. ↩
-
In the case of Lisp, university students were the primary computer hardware ... and sometimes even high school students (see REPL footnote below). ↩
-
This approach was not uncommon at the time: the ALGOL 58 specification defined a syntax for the language reference, one for publications, and a third for implementation. ↩
-
The single greatest contributor to the ascendance of the S-expression is probably the invention of the REPL by L Peter Deutsch, which allowed for interactive Lisp programming. This was almost trivial in S-expressions, whereas a great deal of effort would have been required to support a similar functionality for M-expressions. ↩
-
The function we use in this chapter to demonstrate various syntaxes and dialects was copied from the cover of Byte Magazine's August 1979 issue which focused on Lisp and had part of a Lisp 1.5 program on its cover. ↩
-
The formatting applied to the S-expression version of the function is a modern convention, added here for improved readability. There was originally no formatting, since there was no display – a keypunch was used to enter text on punchcards, 80 characters at a time. As such, a more historically accurate representation would perhaps be:
DEFINE (((REMSP (LAMBDA (STRING) (COND ((NULL STRING) F) ((EQ (CAR STRING) " ") (REMSP (CDR STRING))) (T (CONS (CAR STRING) (REMSP (CDR STRING)))))))))↩ -
This time period is commonly referred to as the “AI winter”. ↩
-
Paul Graham sold his Lisp-based e-commerce startup to Yahoo! In 1998. ↩
-
The majority of this section's content was adapted from Joe Armstrong's paper “A History of Erlang” by, written for the HOPL III conference in 2007. ↩
-
The Computer Science Laboratory operated from 1982 to 2002 in Älvsjö, Stockholm. ↩
-
We've taken the liberty of envisioning the Parlog of 1986 as one that supported pattern matching on characters. ↩
-
One of Virding's project aims was to gain a deeper understanding of Lisp internals. As part of this, he ported the Lisp Machine Lisp object framework Flavors to Portable Standard Lisp running on UNIX. His work on this project contributed to his decision to use Flavour as part of the name for LFE (spelling divergence intentional). ↩
Prerequisites
Anyone coming to LFE should have experience programming in another language, ideally a systems programming language, especially if that language was found lacking. If the corageous reader is attmping to use LFE as a means of entering the study of computer science, we might offer several other paths of study which may bear fruit more quickly and with less pain.
No prior Lisp experience is required, but that would certinaly be helpful. The same goes for Erlang/OTP (or any of the suite of BEAM languages). The reader with experience writing concurrent applications, wrestling with fault-tolerance, or maintaining highly-available applications and services does receive bonus points for preparedness. Such well-prepared readers landing here may have, in fact, done so due to a quest for a distributed Lisp. For those whom this does apply, your quest has found its happy end.
This book assumes the reader has the following installed upon their system:
- a package manager for easily installing software (in particular, development tools and supporting libraries)
git,make, and other core open source software development tools- a modern version of Erlang (as of the writing of this book, that would include versions 19 through 23); the rebar3 documentation has great suggestions on what to use here, depending upon your need
- the
rebar3build tool for Erlang (and other BEAM languages); see its docs for installation instructions
Conventions
Typography
Key Entry
We use the angle bracket convention to indicate typing actual key on the keyboard. For instance, when the reader sees <ENTER> they should interpret this as an actual key they should type. Note that all keys are given in upper-case. If the reader is expected to use an upper-case "C" instead of a lower-case "c", they will be presented with the key combination <SHIFT><C>.
Code
Color syntax highlighting is used in this text to display blocks of code. The formatting of this display is done in such a way as to invoke in the mind of the reader the feeling of a terminal, thus making an obvious visual distinction in the text. For instance:
(defun fib
((0) 0)
((1) 1)
((n)
(+ (fib (- n 1))
(fib (- n 2)))))
Examples such as this one are readily copied and may be pasted without edit into a file or even the LFE REPL itself.
For interactive code, we display the default LFE prompt the reader will see when in the REPL:
lfe> (integer_to_list 42 2)
;; "101010"
We also distinguish the output from the entered LFE code using code comments displayed afer the command.
For shell commands, the commands to enter at the prompt are prefixed by a $ for the prompt. Input and any relevant output are provided as comment strings:
$ echo "I am excited to learn LFE"
# I am excited to learn LFE
LiffyBot
This is a severly hoopy frood. With an attitude. He pops up from time to time, generally with good advice. Or simply as a marker for something the authors hope you will pay special note.
Messages of Note
From time to time you will see call-out boxes, aimed at drawing your attention to something of note. There are four differnt types of these:
- ones that share useful info (blue)
- ones that highlight something of a momentus nature (green)
- ones that offer warnings to tred carefully (orange)
- ones that beg you not to follow a particular path (red)
These messages will take the following forms:
Information
Here you will see a message of general interest that could have a useful or even positive impact on your experience in programming LFE.
The icon associated with this type of message is the "i" in a circle.
Amazing!
Here you will see a message of general celebration for sitations that warrant it, above and beyond the general celebration you will feel writing programs in a distributed Lisp.
The icon assocated with this type of message is that of LiffyBot.
Warning!
Here you will see a message indicating a known isssue or practice you should avoid if possible.
The icon assocated with this type of message is the "!" in a caution triangle.
Danger!
Here you will see a message indicating something that could endanger the proper function of an LFE system or threaten the very existence of the universe itself.
The icon assocated with this type of message is "do not enter".
Development Setup
rebar3 Configuration
Having followed the notes and linked instructions in the Prerequisites section, you are ready to add global support for the LFE rebar3 plugin.
First, unless you have configured other rebar3 plugins on your system, you will need to create the configuration directory and the configuration file:
$ mkdir ~/.config/rebar3
$ touch ~/.config/rebar3/rebar.config
Next, open up that file in your favourite editor, and give it these contents:
{plugins, [
{rebar3_lfe, "0.4.8"}
]}.
If you already have a rebar.config file with a plugins entry, then simply add a comma after the last plugin listed and paste the {rebar3_lfe, ...} line from above (with no trailing comma!). When a new version of rebar3_lfe is released, you can follow the instructions in the rebar3_lfe repo to upgrade.
For Windows users
Some notes on compatibility:
While LFE, Erlang, and rebar3 work on *NIX, BSD, and Windows systems, much of the development the community does occurs predominently on the first two and sometimes Windows support is spotty and less smooth than on the more used platforms (this is more true for rebar3 and LFE, and _most_ true for LFE).
In particular, starting a REPL in Windows can take a little more effort (an extra step or two) than it does on, for example, Linux and Mac OS X machines.
A Quick Test with the REPL
With the LFE rebar3 plugin successfully configured, you should be able to start up the LFE REPL anywhere on your system with the following:
$ rebar3 lfe repl
Erlang/OTP 23 [erts-11.0] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [hipe]
..-~.~_~---..
( \\ ) | A Lisp-2+ on the Erlang VM
|`-.._/_\\_.-': | Type (help) for usage info.
| g |_ \ |
| n | | | Docs: http://docs.lfe.io/
| a / / | Source: http://github.com/rvirding/lfe
\ l |_/ |
\ r / | LFE v1.3-dev (abort with ^G)
`-E___.-'
lfe>
Exit out of the REPL for now by typing <CTRL><G> and then <Q>.
For Windows users
On Windows, this currently puts you into the Erlang shell, not the LFE REPL. To continue to the LFE REPL, you will need to enter lfe_shell:server(). and then press <ENTER>.
'Hello, World!'
Hello-World style introductory programs are intended to give the prospective programmer for that language a sense of what it is like to write a minimalist piece of software with the language in question. In particular, it should show off the minimum capabilitiues of the language. Practically, this type of program should sigify to the curious coder what they could be in for, should they decide upon this particular path.
In the case of LFE/OTP, a standard Hello-World program (essentially a "print" statement) is extremely misleading; more on that in the OTP version of the Hello-World program. Regardless, we concede to conventional practice and produce a minimal Hello-World that does what many other languages' Hello-World programs do. We do, however, go further afterwards ...
From the REPL
As previously demonstrated, it is possible to start up the LFE 'read-eval-print loop' (REPL) using rebar3:
$ rebar3 lfe repl
Once you are at the LFE prompt, you may write a simple LFE "program" like the following:
lfe> (io:format "~p~n" (list "Hello, World!"))
Or, for the terminally lazy:
lfe> (io:format "~p~n" '("Hello, World!"))
While technically a program, it is not a very interesting one: we didn't create a function of our own, nor did we run it from outside the LFE interactive programming environment.
Let's address one of those points right now. Try this:
lfe> (defun hello-world ()
lfe> (io:format "~p~n" '("Hello, World!")))
This is a simple function definition in LFE.
We can run it by calling it:
lfe> (hello-world)
;; "Hello, World!"
;; ok
When we execute our hello-world function, it prints our message to standard-output and then lets us know everything really quite fine with a friendly ok.
Note
LFE displays `ok` as output for functions that do not return a value.
Now let's address the other point: running a Hello-World programming from outside LFE.
Hit <CTRL-G><Q> to exit the REPL and get back to your terminal.
From the Command Line
From your system shell prompt, run the following to create a new project that will let us run a Hello-World program from the command line:
$ rebar3 new lfe-main hello-world
$ cd ./hello-world
Once in the project directory, you can actually just do this:
$ rebar3 lfe run
You will see code getting downloaded and compiled, and then your script will run, generating the following output:
Running script '/usr/local/bin/rebar3' with args [] ...
'hello-world
When you created a new LFE project of type 'main', a Hello-World function was automatically generated for you, one that's even simpler than what we created in the previous section:
(defun my-fun ()
'hello-world)
The other code that was created when we executed rebar3 new lfe-main hello-world was a script meant to be used by LFE with LFE acting as a shell interpreter:
#!/usr/bin/env lfescript
(defun main (args)
(let ((script-name (escript:script_name)))
(io:format "Running script '~s' with args ~p ...~n" `(,script-name ,args))
(io:format "~p~n" `(,(hello-world:my-fun)))))
You may be wondering about the args argument to the main function, and the fact that the printed output for the args when we ran this was []. Let's try something:
$ rebar3 lfe run -- Fenchurch 42
Running script '/usr/local/bin/rebar3' with args [<<"Fenchurch">>,<<"42">>] ...
'hello-world'
We have to provide the two dashes to let rebar3 know that we're done with it, that the subsequent argsuments are not for it, but rather for the program we want it to start for us. Using it causes everything after the -- to be passed as arguments to our script.
As for the code itself, it's tiny. But there is a lot going on just with these two files. Have no fear, though: the remainder of this book will explore all of that and more. For now, know that the main function in the executable is calling the hello-world module's my-fun function, which takes no arguments. To put another way, what we really have here is a tiny, trivial library project with the addition of a script that calls a function from that library.
For now just know that an executable file which starts with #!/usr/bin/env lfescript and contains a main function accepting one argument is an LFE script capable of being executed from the command line -- as we have shown!
LFE/OTP 'Hello, World!'
What have been demonstrated so far are fairly vanilla Hello-World examples; there's nothing particularly interesting about them, which puts them solidly iin the company of the millions of other Hello-World programs. As mentioned before, this approach is particularly vexing in the case of LFE/OTP, since it lures the prospective developer into the preconception that BEAM languages are just like other programming languages. They most decidedly are not.
What makes them, and in this particular case LFE, special is OTP. There's nothing quite like it, certainly not another language with OTP's feature set baked into its heart and soul. Most useful applications you will write in LFE/OTP will be composed of some sort of long-running service or server, something that manages that server and restarts it in the event of errors, and lastly, a context that contains both -- usually referred to as the "application" itself.
As such, a real Hello-World in LFE would be honest and let the prospective developer know what they are in for (and what power will be placed at their fingertips). That is what we will show now, an LFE OTP Hello-World example.
If you are still in the directory of the previous Hello-World project, let's get out of that:
cd ../
Now we're going to create a new project, one utilising the some very basic OTP patterns:
rebar3 new lfe-app hello-otp-world
cd ./hello-otp-world
We won't look at the code for this right now, since there are chapters dedicated to that in the second half of the book. But let's brush the surface with a quick run in the REPL:
rebar3 lfe repl
To start your new hello-world application, use the OTP application module:
lfe> (application:ensure_all_started 'hello-otp-world)
;; #(ok (hello-otp-world))
That message lets you know that not only was the hello-otp-word application and server started without issue, any applications upon which it depends were also started. Furthermore, there is a supervisor for our server, and it has started as well. Should our Hello-World server crash for any reason, the supervisor will restart it.
To finish the demonstration, and display the clichéd if classic message:
(hello-otp-world:echo "Hello, OTP World!")
;; "Hello, OTP World!"
And that, dear reader, is a true LFE/OTP Hello-World program, complete with message-passing and pattern-matching!
Feel free to poke around in the code that was generated for you, but know that eventually all its mysteries will be revealed, and by the end of this book, that program's magic will just seem like ordinary code to you, ordinary, dependable, fault-tolerant, highly-availble, massively-concurrent code.
Walk-through: An LFE Guessing Game
Now that you've seen some LFE in action, let's do something completely insane: write a whole game before we even know the language!
We will follow the same patterns established in the Hello-World examples, so if you are still in one of the Hello-World projects, change directory and then create a new LFE project:
$ cd ../
$ rebar3 new lfe-app guessing-game
$ cd ./guessing-game
We will create this game by exploring functions in the REPL and then saving the results in a file. Open up your generated project in your favourite code-editing application, and then open up a terminal from your new project directory, and start the REPL:
$ rebar3 lfe repl
Planning the Game
We've created our new project, but before we write even a single atom of code, let's take a moment to think about the problem and come up with a nice solution. By doing this, we increase our chances of making something both useful and elegant. As long as what we write remains legible and meets our needs, the less we write the better. This sort of practice elegance will make the code easier to maintain and reduce the chance for bugs (by the simple merrit of there being less code in which a bug may arise; the more code, the greater opportunities for bugs).
Our first step will be making sure we understand the problem and devising some minimal abstractions. Next, we'll think about what actually need to happen in the game. With that in hand, we will know what state we need to track. Then, we're off to the races: all the code will fall right into place and we'll get to play our game.
Key Abstractions
In a guessing game, there are two players: one who has the answer, and one who seeks the answer. Our code and data should clearly model these two players.
Actions
The player with the answer needs to peform the following actions:
- At the beginning of the game, state the problem and tell the other player to start guessing
- Receive the other player's guess
- Check the guess against the answer
- Report back to the other player on the provided guess
- End the game if the guess was correct
The guessing player needs to take only one action:
- guess!
State
We need to track the state of the game. Based upon the actions we've examined, the overall state is very simple. Through the course of the game, will only need to preserve the answer that will be guessed.
Code Explore
Now that we've thought through our problem space clearly and cleanly, let's do some code exploration and start defining some functions we think we'll need.
We've already generated an OTP application using the LFE rebar3 plugin, and once we've got our collection of functions that address the needed game features, we can plug those into the application.
We'll make those changes in the code editor you've opened, and we'll explore a small set of possible functions to use for this using the project REPL session you've just started.
Getting User Input
How do we get user input in LFE? Like this!
lfe> (io:fread "Guess number: " "~d")
This will print the prompt Guess number: and then await your input and the press of the <ENTER> key. The input you provide needs to match the format type given in the second argument. In this case, the ~d tells us that this needs to be a decimal (base 10) integer.
fe> (io:fread "Guess number: " "~d")
;; Guess number: 42
;; #(ok "*")
If we try typing something that is not a base 10 integer, we get an error:
lfe> (io:fread "Guess number: " "~d")
;; Guess number: forty-two
;; #(error #(fread integer))
With correct usage, how do we capture the value in a variable? The standard way to do this in LFE is destructuring via pattern matching. The following snippet extracts the value and then prints the extracted value in the REPL:
lfe> (let ((`#(ok (,value)) (io:fread "Guess number: " "~d")))
lfe> (io:format "Got: ~p~n" `(,value)))
;; Guess number: 42
;; Got: 42
;; ok
We'll talk a lot more about pattern matching in the future, as well as the meaning of the backtick and commas. For now,let's keep pottering in the REPL with these explorations, and make a function for this:
lfe> (defun guess ()
lfe> (let ((`#(ok (,value)) (io:fread "Guess number: " "~d")))
lfe> (io:format "You guessed: ~p~n" `(,value))))
And call it:
lfe> (guess)
;; Guess number: 42
;; You guessed: 42
;; ok
Checking the Input
In LFE there are several ways in which you can perform checks on values:
- the
ifform - the
condform - the
caseform - pattern-matching and/or guards in function heads
The last one is commonly used in LFE when passing messages / data between functions. Our initial, generated project code is already doing this, and given the game state data we will be working with, this feels like a good fit what we need to implement.
Normally records are used for application data, but since we just care about the value of two integers (the number selected for the answer and the number guessed by the player), we'll keep things simple in this game:
(set answer 42)
Let's create a function with a guard:
lfe> (defun check
lfe> ((guess) (when (< guess answer))
lfe> (io:format "Guess is too low~n")))
The extra parenthesis around the function's arguments is due to the use of the pattern-matching form of function definition we're using here. We need this form, since we're going to use a guard. The when after the function args is called a "guard" in LFE. As you might imagine, we could use any number of these.
lfe> (check 10)
;; Guess is too low
;; ok
Let's add some more guards for the other checks we want to perform:
lfe> (defun check
lfe> ((guess) (when (< guess answer))
lfe> (io:format "Guess is too low~n"))
lfe> ((guess) (when (> guess answer))
lfe> (io:format "Guess is too high~n"))
lfe> ((guess) (when (== guess answer))
lfe> (io:format "Correct!~n")))
lfe> (check 10)
;; Guess is too low
;; ok
lfe> (check 100)
;; Guess is too high
;; ok
lfe> (check 42)
;; Correct!
;; ok
This should give a very general sense of what is possible.
Integrating into an Application
We're only going to touch one of the files that was generated when you created the guessing-game project: ./src/guessing-game.lfe. You can ignore all the others. Once we've made all the changes summarized below, we will walk through this file at a high level, discussing the changes and how those contribute to the completion of the game.
First though, we need to reflect on the planning we just did, remembering the actions and states that we want to support. There's also another thing to consider, since we're writing this as is an always-up OTP app. With some adjustments for state magagement, it could easily be turned into something that literally millions of users could be accessing simultaneouslyi. So: how does a game that is usually implemented as a quick CLI toy get transformed in LFE/OTP such that it can be run as a server?
In short, we'll use OTP's gen_server capability ("behaviour") and the usual message-passing practices. As such, the server will need to be able to process the following messages:
#(start-game true)(create a record to track game state)#(stop-game true)(clear the game state)#(guess n)- check for guess equal to the answer
- greater than the answer, and
- less than the answer
We could have just used atoms for the first two, and done away with the complexity of using tuples for those, but symmetry is nice :-)
To create the game, we're going to need to perform the following integration tasks:
- Update the
handle_castfunction to process the commands and guards we listed above - Create API functions that cast the appropriate messages
- Update the
exportform in the module definition - Set the random seed so that the answers are different every time you start the application
handle_cast
The biggest chunk of code that needs to be changed is the handle_cast function. Since our game doesn't return values, we'll be using handle_cast. (If we needed to have data or results returned to us in the REPL, we would have used handle_call instead. Note that both are standard OTP gen_server callback functions.)
The generated project barely populates this function and the function isn't of the form that supports patten-matching (which we need here) so we will essentially be replacing what was generated. In the file ./src/guessing-game.lfe, change this:
(defun handle_cast (_msg state)
`#(noreply ,state))
to this:
(defun handle_cast
((`#(start-game true) _state)
(io:format "Guess the number I have chosen, between 1 and 10.~n")
`#(noreply ,(random:uniform 10)))
((`#(stop-game true) _state)
(io:format "Game over~n")
'#(noreply undefined))
((`#(guess ,n) answer) (when (== n answer))
(io:format "Well-guessed!!~n")
(stop-game)
'#(noreply undefined))
((`#(guess ,n) answer) (when (> n answer))
(io:format "Your guess is too high.~n")
`#(noreply ,answer))
((`#(guess ,n) answer) (when (< n answer))
(io:format "Your guess is too low.~n")
`#(noreply ,answer))
((_msg state)
`#(noreply ,state)))
That is a single function in LFE, since for every match the arity of the function remains the same. It is, however, a function with six different and separate arguement-body forms: one for each pattern and/or guard.
These patterns are matched:
- start
- stop
- guess (three times)
- any
For the three guess patterns (well, one pattern, really) since there are three different guards we want placed on them:
- guess is equal
- guess is greater
- guess is less
Note that the pattern for the function argument in these last three didn't change, only the guard is different between them.
Finally, there's the original "pass-through" or "match-any" pattern (this is used to prevent an error in the event of an unexpected message type).
Game API
In order to send a message to a running OTP server, you use special OTP functions for the type of server you are running. Our game is running a gen_server so we'll be using that OTP module to send messages, in particular we'll be calling gen_server:cast. However, creating messages and sending them via the appropriate gen_server function can get tedious quickly, so it is common practice to create API functions that do these things for you.
In our case, we want to go to the section with the heading ;;; our server API and add the following:
(defun start-game ()
(gen_server:cast (SERVER) '#(start-game true)))
(defun stop-game ()
(gen_server:cast (SERVER) '#(stop-game true)))
(defun guess (n)
(gen_server:cast (SERVER) `#(guess ,n)))
Functions in LFE are private by default, so simply adding these functions doesn't make them publicly accessible. As things now stand these will not be usable outside their module; if we want to use them, e.g., from the REPL, we need to export them.
Go to the top of the guessing-game module and update the "server API" sectopm of the exports, chaning this:
;; server API
(pid 0)
(echo 1)))
to this:
;; server API
(pid 0)
(echo 1)
(start-game 0)
(stop-game 0)
(guess 1)))
The final form of your module definition should look like this:
(defmodule guessing-game
(behaviour gen_server)
(export
;; gen_server implementation
(start_link 0)
(stop 0)
;; callback implementation
(init 1)
(handle_call 3)
(handle_cast 2)
(handle_info 2)
(terminate 2)
(code_change 3)
;; server API
(pid 0)
(echo 1)
(start-game 0)
(stop-game 0)
(guess 1)))
Now our game functions are public, and we'll be able to use them from the REPL.
Finishing Touches
There is one last thing we can do to make our game more interesting. Right now, the game will work. But every time we start up the REPL and kick off a new game, the same "random" number will be selected for the answer. In order to make things interesting, we need to generate a random seed when we initialize our server.
We want to only do this once, though -- not every time the game starts, and certainly not every time a user guesses! When the LFE server supervisor starts our game server, one functions is called and called only once: init/1. That's where we want to make the change to support a better-than-default random seed.
Let's change that function:
(defun init (state)
`#(ok ,state))
to this:
(defun init (state)
(random:seed (erlang:phash2 `(,(node)))
(erlang:monotonic_time)
(erlang:unique_integer))
`#(ok ,state))
Now we're ready to play!
Playing the Game
If you are still in the REPL, quit out of it so that rebar3 can rebuild our changed module. Then start it up again:
$ rebar3 lfe repl
Once at the LFE propmpt, start up the application:
lfe> (application:ensure_all_started 'guessing-game)
With the application and all of its dependencies started, we're ready to start the game and play it through:
lfe> (guessing-game:start-game)
;; ok
;; Guess the number I have chosen, between 1 and 10.
lfe> (guessing-game:guess 10)
;; ok
;; Your guess is too high.
lfe> (guessing-game:guess 1)
;; ok
;; Your guess is too low.
lfe> (guessing-game:guess 5)
;; ok
;; Your guess is too low.
lfe> (guessing-game:guess 7)
;; ok
;; Your guess is too low.
lfe> (guessing-game:guess 8)
;; ok
;; Well-guessed!!
;; Game over
Success! You've just done something pretty amazing, if still mysterious: you've not only created your first OTP application running a generic server, you've successully run it through to completion!
Until we can dive into all the details of what you've seen in this walkthrough, much of what you've just written will seem strange and maybe even overkill. For now, though, we'll mark a placeholder for those concepts: the next section will briefly review what you've done and indicate which parts of this book will provide the remaining missing pieces.
Review
We've got the whole rest of the book ahead of us to cover much of what you've seen in the sample project we've just created with our guessing game. In the coming pages, you will revisit every aspect of what you've seen so far in lots of detail with correspondingly useful instructions on these matters.
That being said, it would be unfair to not at least read through the code together and mention the high-level concepts involved. Since we only touched the code in one file, that will be the one that gets the most of our attention for this short review, but let's touch on the others here, too.
Project Files
rebar.config
This is the file you need in every LFE project you will write in order to take advantage of the features (and time-savings!) that rebar3 provides. For this project, the two important parts are:
- the entry for dependencies (only LFE in this case), and
- the plugins entry for the LFE rebar3 plugin.
Project setup will be covered in Chapter XXX, section XXX.
Source Files
The source files for our sample program in this walkthrough are for an OTP application. OTP-based projects will be covered in Chapter XXX, section XXX.
.app.src
This file is mostly used for application metadata. Most of what our app uses in this file is pretty self-explanatory. Every LFE application will have one of these in the project source code. Every LFE library and application needs this file.
guessing-game-app.lfe
This is the top-level file for our game, an OTP application. It only exports two functions: one to start the app and the other to stop it. The application is responsible for starting up whatever supervisors all your services/servers need. For this sample application, only one supervisor is needed (with a very simple supervision tree).
guessing-game-sup.lfe
This module is a little more invloved and has all the configuration and code necessary to properly set up a supervisor for our server. When something goes wrong with our server, the restart strategy defined by our supervisor will kick in and get things back up and running again. This is one of the key secrets to OTP's wizardry, and we will be covering this in great detail later.
src/guessing-game.lfe
This is the last file we'll look at, and is the one we'll cover in the most detail right now. Here's the entire content of what we created for our game:
(defmodule guessing-game
(behaviour gen_server)
(export
;; gen_server implementation
(start_link 0)
(stop 0)
;; callback implementation
(init 1)
(handle_call 3)
(handle_cast 2)
(handle_info 2)
(terminate 2)
(code_change 3)
;; server API
(pid 0)
(echo 1)
(start-game 0)
(stop-game 0)
(guess 1)))
;;; ----------------
;;; config functions
;;; ----------------
(defun SERVER () (MODULE))
(defun initial-state () '#())
(defun genserver-opts () '())
(defun unknown-command () #(error "Unknown command."))
;;; -------------------------
;;; gen_server implementation
;;; -------------------------
(defun start_link ()
(gen_server:start_link `#(local ,(SERVER))
(MODULE)
(initial-state)
(genserver-opts)))
(defun stop ()
(gen_server:call (SERVER) 'stop))
;;; -----------------------
;;; callback implementation
;;; -----------------------
(defun init (state)
(random:seed (erlang:phash2 `(,(node)))
(erlang:monotonic_time)
(erlang:unique_integer))
`#(ok ,state))
(defun handle_cast
((`#(start-game true) _state)
(io:format "Guess the number I have chosen, between 1 and 10.~n")
`#(noreply ,(random:uniform 10)))
((`#(stop-game true) _state)
(io:format "Game over~n")
'#(noreply undefined))
((`#(guess ,n) answer) (when (== n answer))
(io:format "Well-guessed!!~n")
(stop-game)
'#(noreply undefined))
((`#(guess ,n) answer) (when (> n answer))
(io:format "Your guess is too high.~n")
`#(noreply ,answer))
((`#(guess ,n) answer) (when (< n answer))
(io:format "Your guess is too low.~n")
`#(noreply ,answer))
((_msg state)
`#(noreply ,state)))
(defun handle_call
(('stop _from state)
`#(stop shutdown ok state))
((`#(echo ,msg) _from state)
`#(reply ,msg state))
((message _from state)
`#(reply ,(unknown-command) ,state)))
(defun handle_info
((`#(EXIT ,_from normal) state)
`#(noreply ,state))
((`#(EXIT ,pid ,reason) state)
(io:format "Process ~p exited! (Reason: ~p)~n" `(,pid ,reason))
`#(noreply ,state))
((_msg state)
`#(noreply ,state)))
(defun terminate (_reason _state)
'ok)
(defun code_change (_old-version state _extra)
`#(ok ,state))
;;; --------------
;;; our server API
;;; --------------
(defun pid ()
(erlang:whereis (SERVER)))
(defun echo (msg)
(gen_server:call (SERVER) `#(echo ,msg)))
(defun start-game ()
(gen_server:cast (SERVER) '#(start-game true)))
(defun stop-game ()
(gen_server:cast (SERVER) '#(stop-game true)))
(defun guess (n)
(gen_server:cast (SERVER) `#(guess ,n)))
The beginning of the file opens with a declaration of the module: not only its name, but the public functions we want to expose as part of our API. This will be covered in Chapter XXX, section XXX.
Next, we have a few constant functions. Functions are necessary here due to the fact that LFE does not have global variables. This will be covered in Chapter XXX, section XXX.
Then we define the functions that will be used as this module's implementation of a generic OTP server. There is some boilerplate here that will be discussed when we dive into LFE/OTP. This will be covered in Chapter XXX, section XXX.
After that, we define the functions that are used by the OTP machinery that will run our server. Here you see several examples of pattern matching function heads in LFE, a very powerful feature that lends itself nicely to consise and expressive code. This will be covered in Chapter XXX, section XXX.
Lastly, we define our own API. Most of these functions simply send messages to our running server. More on this in Chapter XXX, section XXX.
The LFE REPL
We briefly introduced the REPL in the first version of the Hello-World example we wrote, stating that it was an acronym for 'read-eval-print loop' and how to start it with rebar3. As an LFE developer, this is one of the primnary tools -- arguably the most powerful -- at your disposal, so we're going to do a more thorough job of introducing its capabilities in this section.
Historical Note
The first Lisp interpreter was created sometime in late 1958 by then-grad student Steve Russell after reading John McCarthy's definition of eval. He had the idea that the theoretical description provided there could actually be implemented in machine code.
In 1963 L Peter Deutsch, a high school student at the time, combined the read, eval, and print core functions to create the first REPL (or, as he termed it then, the 'READ-EVAL-PRINT cycle'). This was done as part of his successful effort to port Lisp 1.5 from the IBM 7090 to the DEC PDP-1 and is referenced briefly in a written report filed with the Digital Equipment Computer Users Society in 1964.
A basic REPL can be implemented with just four functions; such an implementation could be started with the following:
(LOOP (PRINT (EVAL (READ))))
LFE has implemented most these functions for us already (and quite robustly), but we could create our own very limited REPL (single lines with no execution context or environment) within the LFE REPL using the following convenience wrappers:
(defun read ()
(case (io:get_line "myrepl> ")
("quit\n" "quit")
(str (let ((`#(ok ,expr) (lfe_io:read_string str)))
expr))))
(defun print (result)
(lfe_io:format "~p~n" `(,result))
result)
(defun loop
(("quit")
'good-bye)
((code)
(loop (print (eval (read))))))
Now we can start our custom REPL inside the LFE REPL:
lfe> (loop (print (eval (read))))
This gives us a new prompt:
myrepl>
At this prompt we can evaluate basic LFE expressions:
myrepl> (+ 1 2 3)
;; 6
myrepl> (* 2 (lists:foldl #'+/2 0 '(1 2 3 4 5 6)))
;; 42
myrepl> quit
;; good-bye
lfe>
Note that writing an evaluator is the hard part, and we've simply re-used the LFE evaluator for this demonstration.
Now that we've explored some of the background of REPLs and Lisp interpreters, let's look more deeply into the LFE REPL and how to best take advantage of its power when using the machine that is LFE and OTP.
Core Features
In keeping with the overall herritage of LFE, its REPL is both a Lisp REPL as well as an Erlang shell. In fact, when support was added for the LFE REPL to the rebar3_lfe plugin, it utilised all of the plumbing for the Erlang shell support in rebar3.
For the developer, though, this means that the LFE REPL holds a dual set of features, multiplying the set of features available to just the Erlang shell. These features include the following support:
- Evaluation of Lisp S-expressions
- Definition of functions, completely with LFE tail-recursion support
- Definition of records and use of record-specific support functions/macros
- Creation of LFE macros via standard Lisp-2 syntax
- Macro examination and debugging with various expansion macros
- The ability to start the LFE REPL in distribution mode, complete with Erlang cookie, and thus to not only access remote LFE and Erlang nodes, but to be accessed as a remote node itself (for nodes that have been granted access
- Access to the Erlang JCL and the ability to start separate, LFE shells running concurrently
Unsupported
The following capabilities are not supported in the LFE REPL:
- Module definitions; these are a file-based feature in LFE, just as with Erlang.
Starting LFE
The lfe executable
While this book focuses upon the use of rebar3 and its LFE plugin -- due entirely to the amount of time it saves through various features it supports -- LFE may be used quite easily without it.
To use LFE and its REPL without rebar3, you'll need to clone the repo, e.g.:
cd ~/lab
git clone https://github.com/lfe/lfe.git
cd lfe
Since you have read the earlier section on dependencies, you already have Erlang, make, and your system build tools installed. As such, all you have to do is run the following to build LFE:
make
This will generate an executable in ./bin and you can start the LFE REPL by calling it:
./bin/lfe
Erlang/OTP 28 [erts-16.0] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
..-~.~_~---..
( \\ ) | A Lisp-2+ on the Erlang VM
|`-.._/_\\_.-': | Type (help) for usage info.
| g |_ \ |
| n | | | Docs: http://docs.lfe.io/
| a / / | Source: http://github.com/lfe/lfe
\ l |_/ |
\ r / | LFE v2.2.0 (abort with ^G)
`-E___.-'
lfe>
If you opt to install LFE system-wide with make install, then you can start the REPL from anywhere by simply executing lfe.
Via rebar3 lfe repl
As demonstrated earlier on several occasions, you can start the LFE REPL with the rebar3 LFE plugin (and this is what we'll do in the rest of this manual):
rebar3 lfe repl
Since you have updated your global rebar3 settings (in the "Prerequisites" section, after following the instructions on the rebar3 site), you may also start the LFE REPL from anywhere on your machine using the rebar3 command.
readline Support
The LFE REPL, being built atop the Erlang shell, can benefit from GNU Readline support to provide enhanced line editing capabilities including command history, keyboard shortcuts, and tab completion. This section covers how to enable and configure these features for optimal development experience.
Background
The GNU Readline library provides a common interface for line editing and history management across many interactive programs. Originally developed for the Bash shell, it has become the de facto standard for command-line editing in Unix-like systems.
Built-in History Support (Erlang/OTP 20+)
Starting with Erlang/OTP 20, persistent shell history is available out of the box, though it's disabled by default. This feature provides basic command history persistence across sessions.
Enabling Shell History
To enable persistent history for all Erlang-based shells (including LFE), add the following to your shell's configuration file (~/.bashrc, ~/.zshrc, etc.):
# Enable Erlang shell history
export ERL_AFLAGS="-kernel shell_history enabled"
Advanced History Configuration
You can customize the history behavior using additional kernel parameters:
# Complete history configuration
export ERL_AFLAGS="+pc unicode"
ERL_AFLAGS="$ERL_AFLAGS -kernel shell_history enabled"
ERL_AFLAGS="$ERL_AFLAGS -kernel shell_history_path '\"$HOME/.erl_history\"'"
ERL_AFLAGS="$ERL_AFLAGS -kernel shell_history_file_bytes 1048576"
ERL_AFLAGS="$ERL_AFLAGS -kernel shell_history_drop '[\"q().\",\"init:stop().\",\"halt().\"]'"
export ERL_AFLAGS
Where:
shell_history_path- Custom path for history file (default:~/.erlang-history.*))shell_history_file_bytes- Maximum history file size in bytes (default: 512KB, minimum: 50KB)shell_history_drop- Commands to exclude from history (e.g., exit commands)
Note on String Escaping
Erlang application parameters must be passed as proper Erlang terms. Strings require double quotes, which must be escaped in shell environment variables as shown above.
Built-in Tab Completion
The Erlang shell (and by extension, LFE) includes sophisticated built-in tab completion functionality through the edlin and edlin_expand modules. LFE extends this with its own lfe_edlin_expand module to provide Lisp-aware completion capabilities. Starting with Erlang/OTP 26, the auto-completion feature has been vastly improved, supporting auto-completion of variables, record names, record field names, map keys, function parameter types, and file names.
LFE-Specific Tab Completion
LFE provides enhanced tab completion that understands Lisp syntax and LFE-specific constructs. When you type in the LFE REPL, tab completion works seamlessly with:
- Module names: Type
(codeand press TAB to seecodeandcode_server - Function names with arities: Type
(code:and press TAB to see all available functions with their arities displayed in a paged format - LFE syntax: Completion works properly with LFE's S-expression syntax
- Standard Erlang modules: Full access to Erlang's standard library with completion
What Tab Completion Covers
The standard expansion function is able to expand strings to valid Erlang terms, including module names, and automatically add commas or closing parentheses when no other valid expansion is possible. Specifically, tab completion works for:
- Module names: Type
liand press TAB to complete tolists: - Function names: Type
lists:and press TAB to see all available functions - Variables: Complete previously defined shell variables by typing the first letter(s) and pressing TAB
- Record names: Type
#and TAB to see available records - Record fields: Complete field names within record syntax
- Map keys: Complete keys when working with maps
- Built-in functions (BIFs): Complete standard Erlang functions
- File names: When working with file operations
Using Tab Completion
- Single TAB: Autocomplete the current word, or show 5 lines of possible completions
- Double TAB: Output all possible tab completions
- Navigation: Use move_expand_up and move_expand_down to navigate through completion lists in the expand area
LFE Tab Completion Examples
Here are some examples of LFE's tab completion in action:
;; Module completion
lfe> (code<TAB>
code code_server
;; Function completion with paged display
lfe> (code:<TAB>
add_path/1 add_path/2 add_patha/1
add_patha/2 add_paths/1 add_paths/2
add_pathsa/1 add_pathsa/2 add_pathsz/1
add_pathsz/2 add_pathz/1 add_pathz/2
...
rows 1 to 7 of 8
;; LFE syntax-aware completion
lfe> (defun my-func<TAB>
;; Will complete user-defined functions and LFE constructs
The completion system understands LFE's parenthesized syntax and provides contextually appropriate suggestions.
Customizing Tab Completion
You can customize tab completion by setting an expand function using io:setopts/1,2 with the {expand_fun, expand_fun()} option:
;; In LFE, you would set this via Erlang interop
(: io setopts `(#(expand_fun ,(lambda (input)
;; Custom completion logic here
'#(yes "completion" ("option1" "option2"))))))
Full Readline Integration
For a more complete readline experience with features like reverse search (Ctrl-R), customizable key bindings, and improved line editing beyond the built-in capabilities, you have several options.
Option 1: Using rlwrap
rlwrap (readline wrapper) is a utility that adds GNU Readline capabilities to any command-line program. It's the most straightforward way to add full readline support to LFE.
Installing rlwrap
On most Unix-like systems:
# Ubuntu/Debian
sudo apt-get install rlwrap
# macOS (Homebrew)
brew install rlwrap
# CentOS/RHEL/Fedora
sudo yum install rlwrap # or dnf install rlwrap
Basic Usage
# Start LFE with readline support
rlwrap rebar3 lfe repl
# Or if using the lfe executable directly
rlwrap lfe
Advanced rlwrap Configuration
For optimal LFE/Lisp experience, you can customize rlwrap with specific options:
# Enhanced configuration for Lisp-like languages
rlwrap -r -m -q '"' -b "(){}[],^%#@\\;:'" rebar3 lfe repl
Where:
-rputs all words seen on input and output on the completion list-menables multi-line editing-q '"'handles double quotes specially-b "(){}[],^%#@\\;:'"defines characters that break words for completion
Creating a Wrapper Script
For convenience, create a wrapper script in your $PATH (e.g., ~/bin/lfe-repl):
#!/bin/bash
# LFE REPL with readline support
# Basic version
rlwrap rebar3 lfe repl
# Or enhanced version with better Lisp support
rlwrap -r -m -q '"' -b "(){}[],^%#@\\;:'" rebar3 lfe repl "$@"
Make it executable:
chmod +x ~/bin/lfe-repl
Tab Completion vs rlwrap
Important Note: Using rlwrap with the -a option (always use readline) will disable the Erlang shell's built-in tab completion functionality. This is a trade-off between rlwrap's enhanced features (like persistent history and colored prompts) and the shell's native completion capabilities.
If you prioritize tab completion:
- Use the built-in shell history (
ERL_AFLAGS="-kernel shell_history enabled") - Use rlwrap without the
-aflag, though this may provide a less integrated experience
If you prioritize rlwrap's features:
- Accept that built-in tab completion will be disabled
- Consider creating custom completion files for rlwrap (advanced)
Option 2: Terminal-Specific Configuration
Many modern terminal emulators provide their own line editing features that can complement or replace readline functionality.
Using .inputrc Configuration
The ~/.inputrc file configures GNU Readline behavior system-wide. Here's a useful configuration for Lisp development:
# ~/.inputrc - GNU Readline configuration
# Use vi or emacs editing mode
set editing-mode emacs
# Enable case-insensitive completion
set completion-ignore-case on
# Show completion matches immediately
set show-all-if-ambiguous on
set show-all-if-unmodified on
# Enable colored completion
set colored-stats on
set colored-completion-prefix on
# Better history search
"\e[A": history-search-backward
"\e[B": history-search-forward
# Alternative: use Ctrl-P/Ctrl-N for history search
# "\C-p": history-search-backward
# "\C-n": history-search-forward
# Disable terminal bell
set bell-style none
# Enable bracketed paste mode (safe pasting)
set enable-bracketed-paste on
# Show mode indicator for vi mode (if using vi editing-mode)
set show-mode-in-prompt on
After editing ~/.inputrc, you can reload it with:
bind -f ~/.inputrc
Environment Variable Summary
For a complete readline-enabled LFE environment, add these to your shell configuration:
# ~/.bashrc, ~/.zshrc, etc.
# Enable Erlang shell history
export ERL_AFLAGS="-kernel shell_history enabled"
# Optional: customize history location and behavior
# export ERL_AFLAGS="$ERL_AFLAGS -kernel shell_history_path '\"$HOME/.lfe_history\"'"
# export ERL_AFLAGS="$ERL_AFLAGS -kernel shell_history_file_bytes 1048576"
# Create an alias for LFE with readline
alias lfe-repl='rlwrap rebar3 lfe repl'
# Or for the lfe executable
alias lfe='rlwrap lfe'
Troubleshooting
History Not Persisting
If command history isn't being saved between sessions:
- Ensure
shell_historyis enabled inERL_AFLAGS - Check that the history directory is writable
- Wait a moment before exiting the shell (history is written asynchronously)
- Try setting a custom history path with proper permissions
rlwrap Issues
If rlwrap isn't working correctly:
- Verify rlwrap is installed:
which rlwrap - Check that your terminal supports readline properly
- Try with minimal options first:
rlwrap rebar3 lfe repl - On some systems, you may need to omit the
-qoption
Tab Completion Problems
If tab completion behaves unexpectedly:
- Try adjusting the word-breaking characters in rlwrap's
-boption - Check your
~/.inputrcconfiguration - Some terminal emulators may require specific settings for completion
Performance Issues
If the REPL feels sluggish with readline enabled:
- Reduce history file size with
shell_history_file_bytes - Use fewer rlwrap options for minimal overhead
- Consider using built-in history only without rlwrap for simple use cases
(help)
As you gain familiarity with the LFE REPL, one of the most useful and convenient references will be the summary of functions, commands, and variablese that come with the LFE REPL.
To see these, simple call the help or h function:
lfe> (help)
That will result in the following being displyed to your terminal:
LFE shell built-in functions
(c file) -- compile and load code in <file>
(cd dir) -- change working directory to <dir>
(clear) -- clear the REPL output
(doc mod) -- documentation of a module
(doc mod:mac) -- documentation of a macro
(doc m:f/a) -- documentation of a function
(ec file) -- compile and load code in erlang <file>
(ep expr) -- print a term in erlang form
(epp expr) -- pretty print a term in erlang form
(exit) -- quit - an alias for (q)
(flush) -- flush any messages sent to the shell
(h) -- an alias for (help)
(help) -- help info
(i) -- information about the system
(i pids) -- information about a list of pids
(l module) -- load or reload <module>
(ls) -- list files in the current directory
(ls dir) -- list files in directory <dir>
(m) -- which modules are loaded
(m mod) -- information about module <mod>
(p expr) -- print a term
(pp expr) -- pretty print a term
(pid x y z) -- convert <x>, <y> and <z> to a pid
(pwd) -- print working directory
(q) -- quit - shorthand for init:stop/0
(regs) -- information about registered processes
LFE shell built-in commands
(reset-environment) -- reset the environment to its initial state
(run file) -- execute all the shell commands in a <file>
(set pattern expr)
(set pattern (when guard) expr) -- evaluate <expr> and match the result with
pattern binding
(slurp file) -- slurp in a LFE source <file> and makes
everything available in the shell
(unslurp) -- revert back to the state before the last
slurp
LFE shell built-in variables
+/++/+++ -- the tree previous expressions
*/**/*** -- the values of the previous expressions
- -- the current expression output
$ENV -- the current LFE environment
ok
Most of those are documented in stdlib reference for their Erlang counterparts, so be sure to reference that information for details on many of the above.
Those not covered in that Erlang reference manual, or those that are different in their LFE versionsm, include:
- Built-in Functions
- Compilation functions
- LFE code documentation
- Printing and pretty-printing
- Built-in commands
- Built-in variables
REPL Functions
Most of the LFE REPL functions are documented in stdlib reference for their Erlang counterparts. This section documents where the LFE REPL help diverges from the Erlang Shell help.
Compilation
If you view the Erlang reference manual documentation for compiling files in the shell, you will see differences from what is show in the LFE help text. In particular, (c) is for compiling LFE modules and (ec) needs to be used for compiling Erlang source files.
In both cases, the resulting .beam files are compiled to the current working directory and not to an ebin directory. These .beam files will be found by LFE, since the current working directory is included in the path, but you'll likely want to perform some cleanup afterward.
Documentation
You may access the documentation for LFE modules, macros, and functions in the REPL via the doc function. For instance, the Common Lisp compatibility module's documentation:
lfe> (doc cl)
;; ____________________________________________________________
;; cl
;;
;; LFE Common Lisp interface library.
;;
;; ok
That module's cond macro documentation:
lfe> (doc cl:cond)
;; ____________________________________________________________
;; cond
;; args
;; CL compatible cond macro.
;;
;; ok
That module's pairlis/2 function documentation:
lfe> (doc cl:pairlis/2)
;; ____________________________________________________________
;; pairlis/2
;; keys values
;; Make an alist from pairs of keys values.
;;
;; ok
Documentation for Erlang modules and fucntions is available via the Command Interface
Printing Data
LFE Formatting
LFE provides some nice convenience functions for displaying data structions in the REPL. Let's say we had a data structure defined thusly:
lfe> (set data `(#(foo bar baz) #(quux quuz) #(corge grault garply)
lfe> #(plugh xyzzy) #(flurb nirf) #(zobod zirfid)))
We can print our data with the following:
lfe> (p data)
;; (#(foo bar baz) #(quux quuz) #(corge grault garply) #(plugh xyzzy) #(flurb nirf) #(zobod zirfid))
;; ok
Or we can pretty-print it:
lfe> (pp data)
;;(#(foo bar baz)
;; #(quux quuz)
;; #(corge grault garply)
;; #(plugh xyzzy)
;; #(flurb nirf)
;; #(zobod zirfid))
;; ok
Erlang Formatting
The same may be done for displaying data in the Erlang format:
lfe> (ep data)
;; [{foo,bar,baz},{quux,quuz},{corge,grault,garply},{plugh,xyzzy},{flurb,nirf},{zobod,zirfid}]
;; ok
lfe> (epp data)
;; [{foo,bar,baz},
;; {quux,quuz},
;; {corge,grault,garply},
;; {plugh,xyzzy},
;; {flurb,nirf},
;; {zobod,zirfid}]
;; ok
REPL Commands
The LFE REPL provides several useful commands users:
(reset-environment) -- reset the environment to its initial state
(run file) -- execute all the shell commands in a <file>
(set pattern expr)
(set pattern (when guard) expr) -- evaluate <expr> and match the result with
pattern binding
(slurp file) -- slurp in a LFE source <file> and makes
everything available in the shell
(unslurp) -- revert back to the state before the last
slurp
These are fairly self-explanatory, with the possible exception of clarifying how run and slurp differ:
- Calling
(run "some/file.lfe")will cause the LFE REPL to read the contents of that file and then execute every line in that file as if they had been typed at the terminal. This is a convenient way of duplicating REPL state between sessions. (If you haven't kept track of your entries, you can always open up your BEAM history file and create an.lfefile with all the required commands!) - Calling
(slurp "some/other/file.lfe")will place all functions, records, and macros defined in that file into the LFE environment, allowing you to call them without amodule:prefix. Note that the code is not executed, but is instead placed into the current environment, ready for use.
Special Variables
LFE shell built-in variables
+/++/+++ -- the tree previous expressions
*/**/*** -- the values of the previous expressions
- -- the current expression output
$ENV -- the current LFE environment
Most of these variables are taken directly from Common Lisp and have the same exact meaning. From the Common Lisp HyperSpec for +,++,+++:
The variables +, ++, and +++ are maintained by the Lisp read-eval-print loop to save forms that were recently evaluated.
The value of + is the last form that was evaluated, the value of ++ is the previous value of +, and the value of +++ is the previous value of ++.
And for *,**,***:
The variables *, **, and *** are maintained by the Lisp read-eval-print loop to save the values of results that are printed each time through the loop.
The value of * is the most recent primary value that was printed, the value of ** is the previous value of *, and the value of *** is the previous value of **.
Lastly, for -:
The value of - is the form that is currently being evaluated by the Lisp read-eval-print loop.
The $ENV variable in the LFE REPL is a critical tool for debugging particularly tricky issues in the REPL (especially useful when creating complex macros).
Command Interface
While many of the functions listed in the LFE (help) have their documentation in the Erlang Command Interface module (CIM), not everything in the CIM has been provided in the LFE REPL, some of which can be useful at times.
Here are some of the more useful functions you may with to be aware of from that Erlang module:
(c:bt pid)- Stack backtrace for a process. Equivalent to(erlang:process_display pid 'backtrace).(c:h mod)- Print the documentation formod(c:h mod fun)- Print the documentation for allmod:fun(c:h mod fun arity)- Print the documentation formod:fun/arity(c:lm)- Reloads all currently loaded modules that have changed on disk (see(c:mm)). Returns the list of results from calling(l mod)for each such loaded module.(c:memory)- Memory allocation information. Equivalent to(erlang:memory).(c:mm)-(c:ni)- Display system information, listing information about all nodes on the network(c:nl mod)- Loads Module on all nodes(c:nregs)- Displays information about all registered processes for all nodes in the network.(c:uptime)- Prints the node uptime (as specified by(erlang:statistics 'wall_clock)) in human-readable form.(c:xm mod)- Finds undefined functions, unused functions, and calls to deprecated functions in a module by calling xref:m/1.
Job Control
Thanks to Erlang, when working in the LFE REPL you have access to a powerful job control system that allows you to manage multiple shell sessions, interrupt running processes, and switch between different contexts. This system is accessed through Job Control Language (JCL) mode, which becomes available when you press <CTRL-G>.
Entering JCL Mode
When in the LFE REPL, typing <CTRL-G> will detach the current job and activate JCL mode:
lfe> (set ltuae (* 2 (+ 1 2 3 4 5 6)))
42
lfe>
User switch command (enter 'h' for help)
-->
At the JCL --> prompt, you may get help text by typing ? or h:
--> h
c [nn] - connect to job
i [nn] - interrupt job
k [nn] - kill job
j - list all jobs
s [shell] - start local shell
r [node [shell]] - start remote shell
q - quit erlang
? | h - this message
-->
Understanding Jobs
In the context of the Erlang/LFE shell, a job refers to a single evaluator process along with any local processes it spawns. When you start the LFE REPL, you begin with one job that acts as your interactive shell session.
Each job maintains its own:
- Variable bindings
- Process dictionary
- Record definitions
- Evaluation context
Only the currently connected job can perform operations with standard I/O, while detached jobs are blocked from using standard I/O (though they can continue running background computations).
JCL Commands
Each JCL command serves a specific purpose in managing your shell sessions:
-
c [nn]- Connect to job: Connects to job numbernnor the current job if no number is specified. This resumes the standard shell and allows you to interact with that job's evaluation context. -
i [nn]- Interrupt job: Stops the evaluator process for job numbernnor the current job, but preserves the shell process. Variable bindings and the process dictionary are maintained, so you can reconnect to the job later. This is particularly useful for interrupting infinite loops or long-running computations. -
k [nn]- Kill job: Completely terminates job numbernnor the current job. All spawned processes in the job are killed (provided they haven't changed their group leader and are on the local machine). This permanently destroys the job's context. -
j- List all jobs: Displays all current jobs with their numbers and descriptions. The currently connected job is marked with an asterisk (*). -
s [shell]- Start local shell: Creates a new job with a fresh shell environment. If no shell module is specified, it starts the default shell (LFE shell in our case). -
r [node [shell]]- Start remote shell: Starts a remote job on the specified node. This is used in distributed Erlang to control applications running on other nodes in the network. -
q- Quit Erlang: Completely exits the Erlang runtime system. Note that this command may be disabled if Erlang was started with the+Bisystem flag.
Running Multiple Shells: A Practical Example
Here's a practical demonstration of using multiple jobs to maintain separate evaluation contexts:
$ lfe
lfe> (set ltuae (* 2 (+ 1 2 3 4 5 6)))
42
lfe> ^G
User switch command (enter 'h' for help)
--> j
1* {lfe_shell,start,[]}
--> s lfe_shell
--> j
1 {lfe_shell,start,[]}
2* {lfe_shell,start,[]}
--> c 2
lfe> ltuae
** exception error: symbol ltuae is unbound
lfe> (set arch (erlang:system_info 'system_architecture))
"aarch64-apple-darwin24.4.0"
lfe> ^G
User switch command (enter 'h' for help)
--> j
1 {lfe_shell,start,[]}
2* {lfe_shell,start,[]}
--> c 1
lfe> ltuae
42
lfe> arch
** exception error: symbol arch is unbound
lfe> ^G
User switch command (enter 'h' for help)
--> c 2
lfe> arch
"aarch64-apple-darwin24.4.0"
This example demonstrates how each job maintains its own variable bindings:
- Job 1 has
ltuaedefined but notarch - Job 2 has
archdefined but notltuae - Each job represents a completely separate evaluation environment
Common Use Cases
1. Interrupting Infinite Loops
If your code gets stuck in an infinite loop:
lfe> (defun infinite-loop () (infinite-loop))
infinite-loop
lfe> (infinite-loop)
;; Process gets stuck here - press Ctrl-G
User switch command
--> i
--> c
lfe> ;; Back to a responsive shell
2. Experimental Development
Use separate jobs for different experiments:
;; Job 1: Working on feature A
lfe> (set feature-a-data '(1 2 3 4))
;; Switch to Job 2 for feature B
User switch command
--> s lfe_shell
--> c 2
lfe> (set feature-b-data '(a b c d))
;; Switch back to continue feature A work
User switch command
--> c 1
lfe> feature-a-data
(1 2 3 4)
3. Remote Development
Connect to remote nodes in distributed systems:
User switch command
--> r other_node@other_host
;; Now connected to a shell on the remote node
Important Notes
- Variable Isolation: Each job maintains completely separate variable bindings and evaluation contexts
- Process Preservation: Using
i(interrupt) preserves the job's state, whilek(kill) destroys it permanently - I/O Blocking: Only the connected job can use standard I/O; detached jobs will block if they attempt I/O operations
- Record Definitions: Each job has its own set of record definitions, though some default records may be loaded automatically
Customizing JCL Behavior
The behavior of the shell escape key (Ctrl-G) can be modified using the STDLIB application variable shell_esc:
jcl(default): Activates JCL modeabort: Terminates the current shell and starts a new one instead of entering JCL mode
This is set with: erl -stdlib shell_esc abort
Job Control in LFE provides a powerful way to manage multiple development contexts, handle problematic code execution, and work with distributed systems, making it an essential tool for effective LFE development.
Files
So far, everything we've looked at in the REPL involves typing (or pasting) code. When wanting to use the REPL to experiment with more complicated code, there's a better, time-honoured way: files. There are several ways you can use file-based code in the REPL:
- evaluation
- compilation
- including
- loading
Each of these is covered in more detail in the following sub-sections.
Evaluation: : slurp and unslurp
LFE provides a unique and powerful way to evaluate file contents directly in the REPL through the slurp and unslurp commands. This feature allows you to temporarily import function and macro definitions from files into your current REPL session.
Using slurp
The slurp function reads an LFE source file and makes all functions and macros defined in that file available directly in the shell, without requiring module prefixes:
lfe> (slurp "examples/my-functions.lfe")
#(ok -no-mod-)
lfe> $ENV
;; Shows all the new function and macro definitions from the file
Key characteristics of slurp:
- Only one file can be slurped at a time
- Slurping a new file automatically removes all data from the previously slurped file
- Functions and macros become available without module prefixes
- The code is evaluated in the current REPL environment
- Variable bindings from the file are added to your current session
Using unslurp
The unslurp command reverts the REPL back to the state before the last slurp, removing all function and macro definitions that were imported:
lfe> (unslurp)
ok
lfe> $ENV
;; Back to the original environment state
This is particularly useful when experimenting with different versions of functions or when you want to clean up your REPL environment.
Practical Example
Let's say you have a file called math-utils.lfe:
(defun double (x)
(* x 2))
(defun square (x)
(* x x))
(defmacro when-positive (x body)
`(if (> ,x 0) ,body 'not-positive))
After slurping this file:
lfe> (slurp "math-utils.lfe")
#(ok -no-mod-)
lfe> (double 21)
42
lfe> (square 6)
36
lfe> (when-positive 5 'yes)
yes
lfe> (when-positive -1 'yes)
not-positive
Compilation
The LFE REPL provides several functions for compiling source files and loading the resulting modules.
Compiling LFE Files
Use the c function to compile and load LFE modules:
lfe> (c "my-module.lfe")
#(module my-module)
This function:
- Compiles the LFE source file
- Loads the resulting
.beamfile into the current session - Makes the module's exported functions available for use
- Places the compiled
.beamfile in the current working directory
You can also provide compilation options:
lfe> (c "my-module.lfe" '(debug_info export_all))
#(module my-module)
Common compilation options include:
debug_info- Include debugging informationexport_all- Export all functions (useful for development)warn_unused_vars- Warn about unused variables
Compiling Erlang Files
To compile Erlang source files from the LFE REPL, use the ec function:
lfe> (ec "utility.erl")
#(module utility)
This allows you to work with existing Erlang code from within your LFE development session.
Working with Compiled Modules
Once a module is compiled and loaded, you can call its functions using the standard module:function syntax:
lfe> (my-module:some-function arg1 arg2)
result
You can also check which modules are currently loaded:
lfe> (m)
;; Lists all loaded modules
lfe> (m 'my-module)
;; Shows information about a specific module
Cleanup
Take care!
When using the (c) command in the REPL, compiled .beam files are placed in the current working directory, not in a proper ebin directory. This can lead to serious development issues if not properly managed.
If you compile a module in the REPL and forget to clean up the resulting .beam file, you may encounter mysterious issues later, naming continuing to see old behaviour from the module compiled in your current working directory instead of the .beam file most recently updated.
Inclusion
LFE supports including header files and library files, which is essential for larger projects and when working with Erlang/OTP libraries.
include-lib
The include-lib directive allows you to include files from installed OTP applications or other libraries:
(include-lib "kernel/include/file.hrl")
(include-lib "stdlib/include/qlc.hrl")
This searches for the include file in the standard library locations and makes the definitions available in your code.
include-file
The include-file directive includes files using relative or absolute paths:
(include-file "local-definitions.lfe")
(include-file "../shared/common.lfe")
(include-file "/absolute/path/to/file.lfe")
Include File Content
Include files typically contain:
- Record definitions
- Macro definitions
- Constant definitions
- Type specifications
Example include file (records.lfe):
(defrecord person
name
age
email)
(defmacro debug (msg)
`(io:format "DEBUG: ~p~n" (list ,msg)))
After including this file, you can use the record and macro definitions in your code:
lfe> (include-file "records.lfe")
debug
;; In LFE, when including a file in the REPL, the last
;; function defined in the file is printed to stdout.
lfe> (make-person name "Robert" age 54 email "robert@lfe.io")
#(person "Robert" 54 "robert@lfe.io")
lfe>
lfe> (debug "oops")
DEBUG: "oops"
ok
Loading
LFE provides several ways to load pre-compiled modules into your REPL session.
Using l (Load Module)
The l function loads or reloads a specific module:
lfe> (l 'my-module)
#(module my-module)
This is useful when:
- You've recompiled a module and want to reload it
- You want to load a module that exists but isn't currently loaded
- You're working with hot code reloading during development
Using code:ensure_loaded
For more control over the loading process, you can use Erlang's code server directly:
lfe> (code:ensure_loaded 'my-module)
#(module my-module)
This function:
- Loads the module if it's not already loaded
- Does nothing if the module is already loaded
- Returns error information if the module can't be found
Checking Module Status
You can check what modules are currently loaded and get information about them:
;; List all loaded modules
lfe> (m)
;; Get information about a specific module
lfe> (m 'lists)
;; Check if a module is loaded
lfe> (code:is_loaded 'my-module)
#(file "/path/to/my-module.beam")
Hot Code Reloading
During development, you can reload modules without restarting the REPL:
;; Edit and recompile your module file
lfe> (c "my-module.lfe")
#(module my-module)
;; The module is automatically reloaded with new code
lfe> (my-module:updated-function)
Part II - Code as Data
In the Lisp tradition, all program code is written as s-expressions, or parenthesized lists Lisp, making the primary representation of programs also a data structure in a primitive type of the language itself. This fundamental principle—code as data—means that before we can understand how LFE programs execute, we must first master the data structures from which they are constructed.
Part II explores the foundational data types and structures that form the vocabulary of LFE programming. Every LFE program, from the simplest function call to the most complex macro, is ultimately composed of the primitive types and data structures covered in these chapters. We begin with the atomic elements—integers, floats, atoms, and characters—and progress through increasingly sophisticated composite structures like lists, tuples, maps, and records.
The interchangeability of code and data gives Lisp its instantly recognizable syntax, and LFE inherits this property. When you write (+ 1 2 3) in LFE, you are simultaneously creating a list data structure and expressing a computation. The list contains the atom + followed by three integers, but it also represents the addition of those numbers. This duality is not accidental—it is the essence of homoiconicity that makes LFE programs both readable as data and executable as code.
Understanding these data structures deeply is crucial because data representing code can be passed between the meta and base layer of the program. Pattern matching, one of LFE's most powerful features, allows you to decompose and analyze these structures with precision. By the end of Part II, you will have the tools to construct, manipulate, and understand any LFE data structure—knowledge that directly translates to understanding LFE code itself.
Variables
In LFE, variables are implemented with atoms. Atoms are used in the language for such things as naming functions and macros, use as keywords and in data structures. As you'll find out when reading about atoms, they are evaluated as simply themselves. However, when they are used as variable names, they evaulate to the value of which they were assigned.
There are two contexts for setting variables:
- in the LFE REPL
- in LFE modules
This distinction is important, not only because the contexts use different forms, but because like Erlang, LFE does not support global variables.
This chpater will also introduce the reader to pattern-matching in LFE, as it applies to variable assignment; a fuller discussion is presented in a dedicated chapter as well in the chapters that cover forms which support pattern-matching (i.e., compound data types, functions, etc.).
Lastly we'll talk more about some of the LFE nuances around global variables.
Bindings
In the REPL
To set a variable in the LFE REPL, use the set macro:
lfe> (set answer 42)
42
In the language itself, LFE doesn't support global variables -- a valuable
feature inherited from Erlang. However, in order for a REPL experience to be
useful, an environment must be maintained in which the user may write
expressions and then refer to them later. This environment is essentially a
mechanism for global state in the context of a single user running a single
Erlang VM. If we set a variable called answer, that variable will be available
to us as long as the REPL process continues or until we reset the environment.
Setting another value with the same variable name is allowed: it merely replaces the assignment in the current REPL environment:
lfe> (set answer "forty-two")
"forty-two"
With a variable assigned with set it may be used at any time in the REPL
environment where it was defined:
lfe> (++ "The answer is " answer)
"The answer is forty-two"
Attempting to use a variable that has not been defined results in an error:
lfe> (++ "The question was " question)
** exception error: symbol question is unbound
in lfe_eval:eval_error/1 (src/lfe_eval.erl, line 1292)
in lists:map/2 (lists.erl, line 1243)
in lists:map/2 (lists.erl, line 1243)
If you don't have any need to update the environment with data that you only
need for a specific calculation, you may use the let form:
lfe> (let ((short-lived-value (* 2 (+ 1 2 3 4 5 6))))
lfe> (io:format "The answer is ~p~n" `(,short-lived-value)))
The answer is 42
ok
Let's make sure that variable wasn't saved to our environment:
lfe> short-lived-value
** exception error: symbol short-lived-value is unbound
The lexical scope for the short-lived-value is within the let only and is
not available outside of that scope.
In Functions and Macros
Within functions, variables are lexically scoped and bound with let and
let*. One may also define lexically scoped functions inside other fucntions,
and this is done with flet and fletrec (the latter required for defining
recursive functions inside another function). These will be covered in detail
later in the book.
We've seen let used above in the REPL; the same applies inside functions:
(defun display-answer ()
(let ((answer (* 2 (+ 1 2 3 4 5 6))))
(io:format "The answer is ~p~n" `(,answer))))
This is a straight-forward case of assignment; but what if we needed to assign
a varaible that depended upon another variable. Using let, you'd have to do
this:
(defun display-answer ()
(let ((data '(1 2 3 4 5 6)))
(let ((answer (* 2 (lists:sum data))))
(io:format "The answer is ~p~n" `(,answer)))))
However, as with other Lisps, LFE provides a convenience macro for this: let*.
Here's how it's used:
(defun display-answer ()
(let* ((data '(1 2 3 4 5 6))
(answer (* 2 (lists:sum data))))
(io:format "The answer is ~p~n" `(,answer))))
Lexical scoping helps one isolate unrelated data or calculations, even in the
same function: multiple let or let* blocks may be declared in a function
and none of the bound variables in one block will be available to another block.
Attempting to do so will result in an error.
In Modules
In LFE, one cannot bind variables at the module-level, only functions and
macros. This is part of the "no global variables" philosophy (and practice!) of
Erlang and LFE. Module-level bindings are done with defun for functions and
defmacro for macros. The creation of modules,
functions, and macros will all be
covered in detail later in the book.
Shadowing
One shadows a variable in one scope when, at a higher scope, that variable was also defined. Here's an annotated example:
(defun shadow-demo ()
(let ((a 5))
(io:format "~p~n" `(,a)) ; prints 5
(let ((a 'foo)) ; 'a' here shadows 'a' in the previous scope
(io:format "~p~n" `(,a))) ; prints foo
(io:format "~p~n" `(,a))) ; prints 5; the shadow binding is out of scope
(let ((a 42))
(io:format "~p~n" `(,a)))) ; prints 42 - new scope, no shadowing
Shadowing also may occur at the module-level with the definition of functions, and the shadowing could be of functions at one of several levels. Here's a run-down on function shadowing in modules, from the highest or "outermost" to the lowest or "innermost":
- Predefined Erlang built-in functions (BIFs) may be shadowed by any of the following
- Predefined LFE BIFs may be shadowed by any of the following
- Module imports may shadow any of the above via aliasing
- Functions defined in a module may shadow any of the above
- Functions defined inside a function (e.g., via
fletorfletrec) may shadow any of the above
Note that to shadow functions in LFE, functions must match both in name as well as arity (number of arguments).
The hd Erlang BIF returns the "head" of a list (the first item). Here's an
example of shadowing it in the REPL. Here's the BIF at work:
lfe> (hd '(a b c d e))
a
Next, paste this into the REPL:
(defun hd (_)
;; part of the pun here is that the same function in Lisp is called 'car'
"My other car is The Heart of Gold.")
The hd function takes one argument (a list), so our function also needs to
take one. However, since we don't do anything with that, we use the "don't care"
variable _.
Now let's call hd again:
lfe> (hd '(a b c d e))
"My other car is The Heart of Gold."
Shadowed!
Note that, like many other Lisps, LFE has the car function, but since this is
a core form, it can't be shadowed (see the next section).
The Unshadowable
Information
Core LFE forms can never be shadowed.
Shadowing does not apply to the supported LFE core forms. It may appear that your code is shadowing those forms, but the compiler will always use the core meaning and never an alternative. It does this silently, without warning -- so take care and do not be surprised!
Pattern-matching Preview
Pattern matching is deeply integral to Erlang, and thus LFE. Great swaths of the language touch upon it, so therefore it is a farily large subject to cover in LFE. We will be revisiting pattern matching throughout the rest of the book, but for now we will provide a brief preview in the context of variables and binding values to them.
In the previous section, we saw in-REPL binding like this:
lfe> (set answer 42)
42
But what if it wasn't a simple integer we wanted to assign? What if we wanted to bind something in a data structure, like the tuple #(answer 42)? This is one of the ways in which pattern matching is used in Erlang:
lfe> (set (tuple 'answer answer) `#(answer 42))
#(answer 42)
lfe> answer
42
If we want to capture the key name, too?
lfe> (set (tuple key answer) `#(answer 42))
#(answer 42)
lfe> answer
42
lfe> key
answer
And if we don't care about the key at all?
lfe> (set (tuple _ answer) `#(answer 42))
#(answer 42)
lfe> answer
42
We can do the same thing in many other LFE forms, but here's a quick example of variable assignment with pattern matching in a let form, again borrowing from the previous section:
(defun display-answer ()
(let (((tuple _ answer) `#(answer 42)))
(io:format "The answer is ~p~n" `(,answer))))
Pattern matching can be used in may places in LFE, but things really start getting interesting when you define function heads to extract specific values! We will cover examples of that later in the book, in addition to many others.
Global Variables Revisited
In traditional programming languages, global variables are widely considered dangerous and bug-prone. They introduce several critical problems:
- Race conditions: Multiple threads can modify global variables simultaneously, leading to unpredictable state and difficult-to-reproduce bugs
- Hidden side effects: Functions that modify globals create non-obvious dependencies, making code harder to understand and maintain
- Debugging difficulties: When any function can modify a global variable, tracing the source of bugs becomes extremely challenging
- Testing challenges: Global state creates hidden dependencies that make isolated unit testing nearly impossible
Erlang fundamentally solves these problems through immutable data. When you create a data structure in Erlang, its contents cannot be modified in-place. Operations create entirely new data structures, preserving the original. For example, updating a tuple element creates a new tuple rather than modifying the existing one. This eliminates race conditions, makes data flow explicit and predictable, and enables safer concurrent programming without the pitfalls of shared mutable state.
However, real-world applications still need to maintain state across function calls or share data between processes. Erlang provides several safe alternatives, each with its own trade-offs and appropriate use cases.
The Process Dictionary
The process dictionary is a key-value storage mechanism unique to each Erlang process. It provides simple functions like put/2, get/1, and erase/1 for storing and retrieving values within a process's lifetime.
Benefits: The process dictionary offers quick access to process-local data without passing it explicitly through function parameters. Each process has its own isolated dictionary, preventing interference between processes. It's particularly useful for cross-cutting concerns like request IDs in logging or maintaining parse state in complex recursive operations.
Weaknesses: Using the process dictionary breaks functional programming principles by introducing hidden mutable state. This makes code less transparent, harder to test, and more difficult to debug, as data flow becomes implicit rather than explicit. Most Erlang style guides recommend avoiding it except in specific cases where its convenience outweighs these concerns, such as instrumenting third-party libraries or maintaining debug context.
ETS Tables
ETS (Erlang Term Storage) provides in-memory database tables that can be shared across multiple processes. ETS supports several table types: set (unique keys), ordered_set (keys in sorted order), bag (multiple objects per key), and duplicate_bag (allowing duplicate objects).
Benefits: ETS tables provide extremely fast access—constant time O(1) for set tables and logarithmic time O(log N) for ordered_set tables. They support concurrent access with atomic and isolated operations, making them ideal for caching, session storage, counters, and high-performance in-memory data structures. Tables can store large amounts of data efficiently within the Erlang runtime, and access patterns don't require message passing between processes.
Weaknesses: ETS tables have no automatic garbage collection—they persist until explicitly deleted or their owner process terminates. Select and match operations can be expensive as they typically scan the entire table unless properly indexed. Memory management requires careful attention, as tables exist outside normal process memory and don't benefit from Erlang's generational garbage collector. Overly complex match specifications can also become difficult to maintain.
State and OTP Servers
While huge chunks of this book are dedicated to OTP servers and state management, here's the essential concept: gen_server is Erlang's standard behavior for implementing stateful server processes.
A gen_server initializes state through an init/1 callback and maintains that state by passing it through callback functions like handle_call/3 and handle_cast/2. State updates work by creating new copies of data structures with the desired changes, then returning that new immutable data as the current state. This approach combines the benefits of persistent, mutable-seeming state with the safety guarantees of immutable data.
Benefits: OTP servers provide structured, predictable state management with built-in support for supervision, debugging, and code upgrades. State changes are centralized in a single process, making the system easier to reason about. The gen_server behavior handles message queuing, timeouts, and system messages automatically.
Weaknesses: All requests must pass through a single process, which can become a bottleneck under high load. Each state update creates new data structures, which can impact performance for very large or frequently-updated state. Care must be taken to avoid blocking operations that could make the server unresponsive.
External Databases
Erlang applications often connect to external databases for persistent storage. Mnesia, Erlang's built-in distributed database, provides a native solution with strong integration into the Erlang ecosystem. It offers ACID transactions, table replication across nodes, and the ability to store tables in RAM, on disk, or both.
For other databases, Erlang has drivers and libraries for PostgreSQL, MySQL, MongoDB, Redis, and more. These typically use connection pools and message-passing patterns to maintain safety, with processes dedicated to managing database connections and handling queries asynchronously.
Benefits: External databases provide durable persistence across system restarts, ACID transaction guarantees, and the ability to handle datasets larger than available RAM. They enable data sharing across multiple applications and programming languages, and often include sophisticated query capabilities, indexes, and analytical tools. Databases like PostgreSQL offer mature backup, replication, and disaster recovery solutions.
Weaknesses: Database operations introduce network latency and I/O overhead, becoming a potential bottleneck in high-throughput systems. They create external dependencies that can affect system reliability and require careful connection management to avoid resource leaks. The impedance mismatch between Erlang's term-based data model and SQL's relational model can complicate data mapping. Database connections are limited resources that must be pooled and managed carefully.
Choosing the Right Approach
Each alternative serves different needs:
- Process Dictionary: Use sparingly for cross-cutting concerns or when integrating with third-party code
- ETS Tables: Ideal for shared, high-performance in-memory data that multiple processes need to access
- OTP Servers: The default choice for managing application state with clear ownership and structure
- External Databases: Essential for persistent data, large datasets, or sharing data across systems
The key insight is that Erlang doesn't prevent you from maintaining state—it just ensures you do so safely and explicitly, avoiding the pitfalls of traditional global variables.
Primitive Types
This chapter covers the basic types of data available to LFE, upon which primatives rest all LFE libraries and applications.
- Integers
- Floats
- Atoms
- Booleans
- Characters
Each of these types has an LFE test function of the form TYPENAMEp (which
wrap the respective Erlang is_TYPENAME function). These are used to perform
type checks (especially common in guard expressions). These predicate functions
will be covered in their respective type sections.
Integers
Integers in LFE may be either positive or negative and are by default base 10. Like Erlang, LFE does not have a maximum size of integer (in contrast to languages like C). This is accomplished via automatic conversion to larger (or smaller) internal representations (including the use of bignums).
lfe> 42
42
lfe> -42
-42
lfe> 1764
1764
lfe> 150130937545296561928688012959677941476701514734130607701636390322176
150130937545296561928688012959677941476701514734130607701636390322176
Bases
Several bases are supported via special syntax:
lfe> #b101010 ; binary
42
lfe> #o52 ; octal
42
lfe> #d42 ; decimal (explicit base 10)
42)
lfe> #x2a ; hexadecimal
42
Generic bases are supported, too:
lfe> #36r16
42
The number after the hash # is the base and may be any positive integer from 2 through 36. The number after the radix r is the actual value and must only bve comprised of integers allowed for the given base.
Converting between bases in LFE is most easily done via the integer_to_list Erlang function. For example:
lfe> (integer_to_list 1000 2)
"1111101000"
lfe> (integer_to_list 1000 8)
"1750"
lfe> (integer_to_list 1000 10)
"1000"
lfe> (integer_to_list 1000 16)
"3E8"
lfe> (integer_to_list 1000 36)
"RS"
If, for whatever reason, you want your base 10 integrer as a list (Erlang/LFE string), you can do that with this:
lfe> (integer_to_list 1000)
"1000"
Conversion to the binary type is also supported:
lfe> (integer_to_binary 1000)
#"1000"
lfe> (integer_to_binary 1000 2)
#"1111101000"
lfe> (integer_to_binary 1000 8)
#"1750"
The results above show LFE's literal representations of binary data; this will be covered in the chapter on "Bytes and Binaries".
Arithmetic Operators
Integers may be operated upon with the following:
lfe> (+ 1)
1
lfe> (+ 1 2)
3
lfe> (+ 1 2 3)
6
lfe> (- 1 2 3)
-4
lfe> (* 1 2 3)
6
lfe> (/ 1 2 3)
0.16666666666666666
Note that the division operator returns a float; floats will be covered in the next section.
Integer division is supported with a 2-arity function:
lfe> (div 1 2)
0
lfe> (div 10 2)
5
LFE also supports the remainder operation:
lfe> (rem 10 3)
1
As with any functional programming language, these operations may be composed (to any depth):
lfe> (div (* 12 (+ 1 2 3 4 5 6)) 6)
42
Mathematical Functions
The auto-loaded erlang module has several mathematical functions and is
accessible in LFE without having to type the erlang: module prefix in the
function calls. These include the following:
lfe> (abs -42)
42
lfe> (min 1 2)
1
lfe> (max 1 2)
2
Additional maths functions are provided via the math module. Since this module
is not auto-loaded, in order to auto-complete it in the REPL you will need to
load it:
lfe> (code:ensure_loaded 'math)
#(module math)
Now you can hit <TAB> after typing the following:
lfe> (math:
Which gives:
acos/1 acosh/1 asin/1 asinh/1 atan/1
atan2/2 atanh/1 ceil/1 cos/1 cosh/1
erf/1 erfc/1 exp/1 floor/1 fmod/2
log/1 log10/1 log2/1 module_info/0 module_info/1
pi/0 pow/2 sin/1 sinh/1 sqrt/1
tan/1 tanh/1
lfe> (round (math:pow 42 42))
150130937545296561928688012959677941476701514734130607701636390322176
The documentation for these functions is limited (available here) due in part to the fact that these are C library wrappers. Those that aren't documented should be self-explanatory for anyone who has used simular mathematical functions in other programming language libraries.
Predicates
To test if a value is an integer, we will first include some code:
lfe> (include-lib "lfe/include/cl.lfe")
That adds Common Lisp inspired functions and macros to our REPL session.
lfe> (integerp 42)
true
lfe> (integerp 42.24)
false
lfe> (integerp "forty-two")
false
If you prefer the Clojure-style of predicates:
lfe> (include-lib "lfe/include/clj.lfe")
lfe> (integer? 42)
true
lfe> (integer? "forty-two")
false
Of course there is always the Erlang predicate, usable without having to do any includes:
lfe> (is_integer 42)
true
lfe> (is_integer "forty-two")
false
Floats
Real numbers in LFE are represented using the stadnard floating point numbers.
lfe> 42.42
42.42
lfe> (/ 10 3)
3.3333333333333335
lfe> (math:pow 42 42)
1.5013093754529656e68
Note that the ~1.5e68 above is the floating point equivalent of scientific
notation, namely 1.5 x 1068. LFE follows the 64-bit
standard for float representation given by IEEE 754-1985.
Converting
An integer may be converted to a float explicitly:
lfe> (float 42)
42.0
Or, as with integers, to binaries:
lfe> (float_to_binary 42.42)
#"4.24200000000000017053e+01"
lfe> (float_to_binary 42.42 '(#(scientific 10)))
#"4.2420000000e+01"
lfe> (float_to_binary 42.42 '(#(scientific 20)))
#"4.24200000000000017053e+01"
lfe> (float_to_binary 42.42 '(#(decimals 10)))
#"42.4200000000"
lfe> (float_to_binary 42.42 '(#(decimals 10) compact))
#"42.42"
Or to lists (LFE and Erlang strings):
lfe> (float_to_list 42.42 '(#(scientific 10)))
"4.2420000000e+01"
lfe> (float_to_list 42.42 '(#(scientific 20)))
"4.24200000000000017053e+01"
lfe> (float_to_list 42.42 '(#(decimals 10)))
"42.4200000000"
lfe> (float_to_list 42.42 '(#(decimals 10) compact))
"42.42"
Formatting
If you need to round floating point numbers to a specific precision, you'll want to use the format function from either the io, io_lib, or lfe_io modules. If just want to print a value using Erlang syntax and formatting, the io module is what you want. If you prefer LFE syntax and formatting for your output, you'll want to use the lfe_io module. If you want to use the data or store it in a variable, you'll need the io_lib library.
For default precision:
lfe> (io_lib:format "~f" `(,(math:pow 42 42)))
"150130937545296561929000000000000000000000000000000000000000000000000.000000"
Two decimal places:
lfe> (io_lib:format "~.2f" `(,(math:pow 42 42)))
"150130937545296561929000000000000000000000000000000000000000000000000.00"
20 decimal places:
lfe> (io_lib:format "~.20f" `(,(math:pow 42 42)))
"150130937545296561929000000000000000000000000000000000000000000000000.00000000000000000000"
Arithmetic Operators & Mathematical Functions
Floats use most of the same operators and functions as integers, so be sure to review these subsections in the "Integers" section.
Others include:
lfe> (abs -42)
42
lfe> (ceil 42.1)
43
lfe> (floor 42.1)
42
lfe> (round 42.4)
42
lfe> (round 42.5)
43
lfe> (min 1 2)
1
lfe> (max 1 2)
2
Predicates
To test if a value is an integer, we will first include some code:
lfe> (include-lib "lfe/include/cl.lfe")
That adds Common Lisp inspired functions and macros to our REPL session.
lfe> (floatp 42.42)
true
lfe> (floatp 42)
false
lfe> (floatp "forty-two.forty-two")
false
If you prefer the Clojure-style of predicates:
lfe> (include-lib "lfe/include/clj.lfe")
lfe> (float? 42.42)
true
lfe> (float? "forty-two.forty-two")
false
Of course there is always the Erlang predicate, usable without having to do any includes:
lfe> (is_float 42.42)
true
lfe> (is_float "forty-two.forty-two")
false
Atoms
The cloest analog in LFE to what most Lisp dialects call symbols is the Erlang atom. Just as with Lisps do with symbols, LFE uses atoms for its variable and function names. Atoms are literals and constants, which means that their value is the same as their name and once created, cannot be changed.
Some basic examples of atoms:
lfe> 'zark
zark
lfe> 'zarking-amazing
zarking-amazing
Slightly less straight-forward examples which start with non-alphanumeric characters:
lfe> ':answer
:answer
lfe> '42answer
42answer
lfe> '42.0e42answer
42.0e42answer
lfe> '42°C
42°C
Standard LFE atom names may be comprised of all the latin-1 character set except the following:
- control character
- whitespace
- the various brackets
- double quotes
- semicolon
Of these, only |, \, ', ,, and # may not be the first character of the
symbol's name (but they are allowed as subsequent letters).
Non-standard atom names may be created using atom quotes:
lfe> '|symbol name with spaces|
|symbol name with spaces|
lfe> '|'with staring quote!|
|'with staring quote!|
lfe> '|| ; <-- empty atoms are supported too
||
lfe> '|really weird atom: '#[]{}()<>";\||
|really weird atom: '#[]{}()<>";\||
In this case the name can contain any character of in the range from 0 to 255, and even no character at all.
In the case of atoms, it is important to understand a little something about their internals. In particular, Erlang and LFE atoms are global for each instance of a running Erlang virtual machine. Atoms are maintained by the VM in a table and are not garbage collected. By default, the Erlang atom table allows for a maximum of 1,048,576 entries.
Danger!
Uncontrolled autoamtic creation of atoms can crash the VM!
See the "Caveats" section below for more details.
As Symbols
The following code shows LFE's use of atoms in variable names. First, let's use
a function for a slighly different purpose than designed: calling
list_to_existing_atom on a string will only return a result if an atom
of the same name already exists in the atom table. Otherwise, it will return
an error:
lfe> (list_to_existing_atom "zaphod")
** exception error: bad argument
in (erlang : list_to_existing_atom "zaphod")
This confirms that there is no zaphod atom in the atom table.
Now let's create a variable, assigning a value to it, and then use our indirect means of checkihg the atom table:
lfe> (set zaphod "frood")
"frood"
lfe> (list_to_existing_atom "zaphod")
zaphod
And here's an example showing LFE's use of atoms in function names using the same approach as above:
lfe> (list_to_existing_atom "beez")
** exception error: bad argument
in (erlang : list_to_existing_atom "beez")
lfe> (defun beez (x) x)
beez
lfe> (list_to_existing_atom "beez")
beez
Converting
Atoms may be converted to strings and bitstrings, and vice versa.
(atom_to_binary 'trisha)
#"trisha"
(atom_to_binary 'mcmillan 'latin1)
#"mcmillan"
lfe> (atom_to_list 'trillian)
"trillian"
Note that the first one above is only available in Erlang 23.0 and above.
Some more examples for encoding:
lfe> (atom_to_binary '42°C 'latin1)
#B(52 50 176 67)
lfe> (atom_to_binary '42°C 'utf8)
#"42°C"
Functions that convert atoms only if they already exist in the atom table:
lfe> (binary_to_existing_atom #"trisha")
trisha
lfe> (binary_to_existing_atom #"trisha" 'latin1)
trisha
lfe> (list_to_existing_atom "trisha")
trisha
Operators
The only operators you may use on atoms are the comparison operators, e.g.:
lfe> (> 'a 'b)
false
lfe> (< 'a 'b)
true
lfe> (=:= 'a 'a)
true
Predicates
To test if a value is an atom, we will first include some code:
lfe> (include-lib "lfe/include/cl.lfe")
That adds Common Lisp inspired functions and macros to our REPL session.
lfe> (atomp 'arthur)
true
lfe> (atomp 42)
false
lfe> (atomp "forty-two.forty-two")
false
If the atom in question has been used in a function name definition:
If you prefer the Clojure-style of predicates:
lfe> (include-lib "lfe/include/clj.lfe")
lfe> (atom? 'dent)
true
lfe> (atom? "Ford")
false
Of course there is always the Erlang predicate, usable without having to do any includes:
lfe> (is_atom 'arthur)
true
lfe> (is_atom "forty-two.forty-two")
false
Caveats
As mentioned above (and documented), one needs to take care when creating atoms. By default, the maximum number of atoms that the Erlang VM will allow is 1,048,576; any more than that, and the VM will crash.
The first rule of thumb is not to write any code that generates large numbers of atoms. More explicitly useful, there are some handy functions for keeping track of the atom table, should you have the need.
lfe> (erlang:memory 'atom_used)
244562
lfe> (erlang:system_info 'atom_count)
9570
lfe> (erlang:system_info 'atom_limit)
1048576
Note that support for easily extracting the current atom data
from system_info -- as demonstrated by the last two function calls above --
were added in Erlang 20; should you be running an older
version of Erlang, you will need to parse the (system_info 'info)
bitstring.
The default atom table size may be overridden during startup by passing a value
with the +t options:
$ lfe +t 200000001
lfe> (erlang:system_info 'atom_limit)
200000001
Booleans
Strictly speaking, LFE has no Boolean type, just like Erlang. Instead, the atoms
true and false are treated as Booleans
lfe> (== 42 "the question")
false
lfe> (== 42 42)
true
lfe> (> 1 2)
false
lfe> (< 1 2)
true
Operators
The standard logical operators are available to LFE Booleans:
lfe> (not 'true)
false
lfe> (not 'false)
true
lfe> (and 'true 'false)
false
lfe> (or 'true 'false)
true
lfe> (and 'true 'true 'true)
true
lfe> (and 'true 'true 'false)
false
lfe> (or 'false 'false 'false)
false
lfe> (or 'false 'false 'true)
true
lfe> (xor 'true 'true)
false
lfe> (xor 'true 'false)
true
lfe> (xor 'false 'false)
false
lfe> (xor 'false 'true)
true
With the and and or Boolean oprators, every argument is evaluated. To
accomodate situations where complex and possible expensive logical expressions
comprise the arguments to Boolean operators, short-circuit versions of these
functions are also provided:
- with
andalso, returns as soon as the first'falseis encountered; - with
orelse, returns as soon as the first'trueis encountered.
To demonstrate this, we'll define a boolean function that prints to
standard out when it is evaluated:
(defun hey (x)
(io:format "Made it here!~n") x)
Short-circuit demonstration of andalso:
lfe> (andalso 'true 'true 'false (hey 'true))
false
lfe> (andalso 'false 'true 'true (hey 'true))
false
lfe> (andalso 'true 'true 'true (hey 'true))
Made it here!
true
Short-circuit demonstration of orelse:
lfe> (orelse 'false 'false 'true (hey 'true))
true
lfe> (orelse 'true 'false 'false (hey 'true))
true
lfe> (orelse 'false 'false 'false (hey 'true))
Made it here!
true
Predicates
To test if a value is a Boolean, we will first include some code:
lfe> (include-lib "lfe/include/cl.lfe")
That adds Common Lisp inspired functions and macros to our REPL session.
lfe> (booleanp 'true)
true
lfe> (booleanp 'false)
true
lfe> (booleanp 'arthur)
false
lfe> (booleanp 42)
false
If the atom in question has been used in a function name definition:
If you prefer the Clojure-style of predicates:
lfe> (include-lib "lfe/include/clj.lfe")
lfe> (boolean? 'true)
true
lfe> (boolean? 'false)
true
lfe> (boolean? 'arthur)
false
Of course there is always the Erlang predicate, usable without having to do any includes:
lfe> (is_boolean 'true)
true
lfe> (is_boolean 'false)
true
lfe> (is_boolean 'arthur)
false
Note that, since LFE Booleans are also atoms, these are valid as well:
lfe> (atomp 'true)
true
lfe> (atom? 'false)
true
lfe> (is_atom 'true)
true
Characters
Characters in LFE Are represented internally by integers, however a literal syntax is offered for convenience:
lfe> #\a
97
lfe> #\A
65
lfe> #\ü
252
lfe> #\Æ
198
Converting
Since a character literal and integer are the same thing as far as LFE is concerned, there is no such thing as converting between a "char" and "ord" like there is in some other languages.
However, one can format an integer as a string by telling the class of format
functions that the input is "character" type:
lfe> (io_lib:format "~c" `(198))
"Æ"
For merely printing to standard out instead of returning a value, one may
use:
lfe> (lfe_io:format "~c~n" `(198))
Æ
ok
lfe> (io:format "~c~n" `(198))
Æ
ok
Operators
All operations that are valid for integers are valid for characters.
Predicates
All predicates that are valid for integers are valid for characters.
Cons Cells
Cons cells are the fundamental building blocks of lists in both Lisp and Erlang, and by extension, LFE. Understanding cons cells is essential to mastering list manipulation and pattern matching in LFE. While the concept originates from Lisp, Erlang (and thus LFE) adapts it in ways that make it particularly powerful for functional programming and recursive operations.
At first encounter, cons cells might seem like an unnecessarily complicated way to think about lists—after all, why not just have an array-like structure that holds multiple values? The answer lies in the elegance and efficiency they provide for functional programming. Think of a cons cell as a simple container with exactly two compartments: one holds a value (the "head" or "first element"), and the other holds a pointer to the rest of the list (the "tail"). This structure creates a chain—like a linked list in other languages—but with an important difference: in functional programming, these chains are immutable. When you "add" an element to the front of a list, you're not modifying the original list; you're creating a new cons cell that points to the existing list. This makes operations like prepending elements incredibly fast (O(1) constant time) and allows multiple "versions" of a list to safely share structure in memory.
The real power of cons cells emerges when combined with pattern matching. Instead of writing imperative code that indexes into lists or checks for empty conditions, you can write function definitions that naturally express "here's what to do with an empty list, and here's what to do with a list that has at least one element." The cons cell structure makes this pattern matching both intuitive and efficient: destructuring a list into its head and tail is a natural operation that matches how the data is actually stored in memory. As you work through the following sections, you'll see how this simple two-slot structure enables elegant recursive algorithms, efficient list manipulation, and a programming style that feels natural once the initial conceptual hurdle is overcome.
Lisp Cons Cells
In Lisp dialects, a cons cell is a fundamental data structure that holds two values or pointers to values. These two slots are traditionally called the CAR (Contents of the Address Register) and the CDR (Contents of the Decrement Register), names that derive from the original Lisp implementation on IBM 704 hardware.
The cons function constructs these memory objects, and the expression "to cons x onto y" means to construct a new cons cell with x in the car slot and y in the cdr slot.
Structure and Notation
A simple cons cell holding two values can be represented in dotted pair notation:
(cons 'a 'b) ; Creates a cons cell
=> (a . b) ; Dotted pair notation
Lists in Lisp are built by having the car slot contain an element and the cdr slot point to another cons cell or to nil (the empty list). This creates a singly-linked list structure:
(cons 1 (cons 2 (cons 3 nil)))
=> (1 2 3)
Visual Representation
Here's a diagram showing how the list (1 2 3) is constructed from cons cells:
graph LR
A["cons cell 1"] -->|car| V1["1"]
A -->|cdr| B["cons cell 2"]
B -->|car| V2["2"]
B -->|cdr| C["cons cell 3"]
C -->|car| V3["3"]
C -->|cdr| N["nil"]
style A fill:#e1f5ff
style B fill:#e1f5ff
style C fill:#e1f5ff
style V1 fill:#fff4e1
style V2 fill:#fff4e1
style V3 fill:#fff4e1
style N fill:#ffe1e1
Each cons cell contains two pointers: the car points to the element value, and the cdr points to the next cons cell (or nil for the last cell).
Erlang Cons Cells
Erlang lists are built as sequences of cons cells, with each cell composed of a value and a pointer to another cons cell or empty list. While structurally similar to Lisp cons cells, Erlang's implementation and usage patterns differ in important ways.
The Pipe Operator
In Erlang, the cons operator is represented by the pipe symbol (|), which separates the head of a list from its tail. The syntax [Head | Tail] constructs or pattern-matches a cons cell.
% Building a list with cons
[1 | [2, 3]] % => [1, 2, 3]
[1 | [2 | [3]]] % => [1, 2, 3]
[1 | [2 | [3 | []]]] % => [1, 2, 3]
Pattern Matching and Head/Tail
Every function operating on lists in Erlang is defined in terms of two primitives: head and tail, which return the first element and the rest of the list respectively. Pattern matching with cons cells provides an elegant idiom for recursive list operations, where you can extract the head and tail in function definitions.
% Pattern matching to extract head and tail
[Head | Tail] = [1, 2, 3].
% Head => 1
% Tail => [2, 3]
This makes recursive list processing natural and efficient:
length([]) -> 0;
length([_Head | Tail]) -> 1 + length(Tail).
Proper vs Improper Lists
A proper list in Erlang ends with an empty list as its last cell. When the tail of the last cons cell contains something other than another cons cell or the empty list, you have an improper list:
[1 | 2] % Improper list - tail is not a list
[1 | [2]] % Proper list
While improper lists are valid Erlang terms, most standard list functions expect proper lists.
LFE Cons Cells
LFE provides Lisp-style syntax for working with Erlang's cons cell implementation, giving you the best of both worlds: familiar Lisp notation with Erlang's powerful pattern matching.
The cons Function
In LFE, the cons function creates a new list by prepending an element to an existing list:
lfe> (cons 1 '())
(1)
lfe> (cons 1 '(2 3))
(1 2 3)
lfe> (cons 1 (cons 2 (cons 3 '())))
(1 2 3)
Pattern Matching with Cons
LFE allows pattern matching on cons cells in function definitions, making recursive list processing elegant and readable:
lfe> (defun my-length
(('()) 0)
(((cons _ tail)) (+ 1 (my-length tail))))
my-length
lfe> (my-length '(a b c d))
4
In this example:
- The first clause matches the empty list
()and returns 0 - The second clause uses
(cons _ tail)to extract the tail, ignoring the head with_ - The function recursively processes the tail
Common Patterns
Here are some common patterns for working with cons cells in LFE:
Accessing the head and tail:
lfe> (set (cons head tail) '(1 2 3 4))
(1 2 3 4)
lfe> head
1
lfe> tail
(2 3 4)
Building lists incrementally:
lfe> (defun build-list (n)
(build-list n '()))
lfe> (defun build-list
((0 acc) acc)
((n acc) (build-list (- n 1) (cons n acc))))
lfe> (build-list 5)
(1 2 3 4 5)
Pattern matching in let bindings:
lfe> (let (((cons first (cons second rest)) '(a b c d e)))
(list first second rest))
(a b (c d e))
Recursive list transformation:
lfe> (defun double-all
(('()) '())
(((cons h t)) (cons (* 2 h) (double-all t))))
lfe> (double-all '(1 2 3 4 5))
(2 4 6 8 10)
Using Backtick Syntax
LFE also supports backtick (quasiquote) syntax for pattern matching, which can be more concise:
lfe> (set `(,first ,second . ,rest) '(1 2 3 4 5))
(1 2 3 4 5)
lfe> first
1
lfe> second
2
lfe> rest
(3 4 5)
The dot (.) in the pattern (,first ,second . ,rest) represents the cons operator, separating the explicitly matched elements from the remaining tail.
List Construction vs Traversal
One crucial performance consideration: prepending to a list with cons is O(1), but appending to the end requires traversing the entire list and is O(n):
; Fast - O(1)
lfe> (cons 0 '(1 2 3))
(0 1 2 3)
; Slow for large lists - O(n)
lfe> (++ '(1 2 3) '(4))
(1 2 3 4)
This is why many recursive functions in LFE build lists in reverse order using an accumulator, then reverse the final result:
(defun map-helper (f lst acc)
(case lst
('() (lists:reverse acc))
((cons h t) (map-helper f t (cons (funcall f h) acc)))))
(defun my-map (f lst)
(map-helper f lst '()))
Predicates
To check if a value is a list (a chain of cons cells ending in []), you can use the standard predicates:
lfe> (is_list '(1 2 3))
true
lfe> (is_list '())
true
lfe> (is_list 42)
false
With Common Lisp-style predicates:
lfe> (include-lib "lfe/include/cl.lfe")
lfe> (listp '(1 2 3))
true
lfe> (listp '())
true
Or Clojure-style:
lfe> (include-lib "lfe/include/clj.lfe")
lfe> (list? '(1 2 3))
true
Summary
Cons cells are the foundation of list processing in LFE. Understanding how they work—as pairs of values forming linked structures—is essential for effective functional programming. The ability to pattern match on cons cells makes LFE code both elegant and efficient, allowing you to express complex list operations with clarity and precision.
The key insights to remember:
- Lists are chains of cons cells terminating in the empty list
() - Pattern matching with
(cons head tail)is the idiomatic way to destructure lists - Prepending with
consis fast; building lists in reverse and then reversing is a common pattern - Proper lists always end in
[], while improper lists end in other values
Lists and Strings
In most programming languages, lists and strings are distinct creatures—separate types with their own operations, behaviors, and internal representations. In LFE, however, these two concepts share a rather intimate relationship: a string is a list. More precisely, a string is simply a list of integers, where each integer represents the character code of a printable character. This might seem peculiar at first, perhaps even a bit unsettling if you're accustomed to languages where strings are special, privileged types with their own dedicated machinery.
But consider the elegance of this approach: if strings are just lists, then all the wonderful things you can do with lists—pattern matching, recursive processing, higher-order functions—work equally well on strings. Want to reverse a string? Use the same function you'd use to reverse any list. Want to filter characters? Use the same filter function you'd use on a list of numbers or atoms. This unification means fewer concepts to learn, more code reuse, and a consistency that permeates the entire language.
The practical implication is straightforward: when you see "hello" in LFE, you're looking at syntactic sugar for (104 101 108 108 111)—a list of five integers corresponding to the ASCII values of those letters. The REPL helpfully displays lists of printable character codes as strings, but underneath, it's lists all the way down. This chapter explores both lists and strings, first separately to understand their individual characteristics, then together to appreciate how this design choice shapes the way you write LFE code.
Lists
In the beginning, there was the cons cell. And from the cons cell came forth lists, which were chains of cons cells stretching out across memory like improbability particles across the fabric of spacetime—each pointing to the next with what can only be described as dogged determination. And lo, these lists were good, for they were immutable, they were elegant, and most importantly, they made recursion look less like wizardry and more like afternoon tea.
Lists are the fundamental sequential data structure in LFE, inherited from both Erlang's practical heritage and Lisp's theoretical elegance. They are singly-linked lists built from cons cells (as explored in the previous chapter), making them ideally suited for functional programming patterns where you process data from front to back, building new structures as you go rather than mutating old ones.
Creating Lists
The most straightforward way to create a list is with the quote:
lfe> '(1 2 3 4 5)
(1 2 3 4 5)
lfe> '(arthur ford trillian zaphod)
(arthur ford trillian zaphod)
lfe> '()
()
You can also use the list function:
lfe> (list 1 2 3 4 5)
(1 2 3 4 5)
lfe> (list 'arthur 'ford 'trillian 'zaphod)
(arthur ford trillian zaphod)
lfe> (list)
()
Lists can contain elements of mixed types:
lfe> '(42 "meaning" of life 42.0)
(42 "meaning" of life 42.0)
lfe> (list 'atom 42 3.14 "string" #b101010 '(nested list))
(atom 42 3.14 "string" 42 (nested list))
Building lists with cons:
lfe> (cons 1 '())
(1)
lfe> (cons 1 (cons 2 (cons 3 '())))
(1 2 3)
lfe> (cons 'first '(second third))
(first second third)
List Operations
Accessing elements:
lfe> (car '(1 2 3 4 5))
1
lfe> (cdr '(1 2 3 4 5))
(2 3 4 5)
lfe> (cadr '(1 2 3 4 5)) ; equivalent to (car (cdr ...))
2
lfe> (cddr '(1 2 3 4 5)) ; equivalent to (cdr (cdr ...))
(3 4 5)
LFE provides Common Lisp-style accessor combinations up to four levels deep:
lfe> (caddr '(1 2 3 4)) ; third element
3
lfe> (cadddr '(1 2 3 4)) ; fourth element
4
Finding length:
lfe> (length '(1 2 3 4 5))
5
lfe> (length '())
0
lfe> (length '(a (nested (list structure)) here))
3
Appending lists:
lfe> (++ '(1 2 3) '(4 5 6))
(1 2 3 4 5 6)
lfe> (++ '(a) '(b) '(c) '(d))
(a b c d)
lfe> (append '(1 2) '(3 4) '(5 6))
(1 2 3 4 5 6)
Note that ++ is the operator form while append is the function form. Both are O(n) operations where n is the total length of all lists except the last one.
Reversing lists:
lfe> (lists:reverse '(1 2 3 4 5))
(5 4 3 2 1)
lfe> (lists:reverse '())
()
Taking and dropping elements:
lfe> (lists:sublist '(1 2 3 4 5) 3)
(1 2 3)
lfe> (lists:sublist '(1 2 3 4 5) 2 3)
(2 3 4)
lfe> (lists:nthtail 2 '(1 2 3 4 5))
(3 4 5)
Membership testing:
lfe> (lists:member 3 '(1 2 3 4 5))
(3 4 5)
lfe> (lists:member 6 '(1 2 3 4 5))
false
Note that lists:member returns the tail of the list starting with the found element, or false if not found.
Finding elements:
lfe> (lists:nth 1 '(a b c d))
a
lfe> (lists:nth 3 '(a b c d))
c
Be aware that lists:nth uses 1-based indexing (Erlang convention), unlike 0-based indexing common in many languages.
Sorting:
lfe> (lists:sort '(5 2 8 1 9 3))
(1 2 3 5 8 9)
lfe> (lists:sort '(zaphod arthur ford trillian))
(arthur ford trillian zaphod)
lfe> (lists:sort (lambda (a b) (> a b)) '(5 2 8 1 9 3))
(9 8 5 3 2 1)
Higher-Order List Functions
Mapping:
lfe> (lists:map (lambda (x) (* x 2)) '(1 2 3 4 5))
(2 4 6 8 10)
lfe> (lists:map #'atom_to_list/1 '(arthur ford trillian))
("arthur" "ford" "trillian")
Filtering:
lfe> (lists:filter (lambda (x) (> x 3)) '(1 2 3 4 5 6))
(4 5 6)
lfe> (lists:filter #'is_atom/1 '(1 arthur 2 ford 3))
(arthur ford)
Folding (reducing):
lfe> (lists:foldl #'+/2 0 '(1 2 3 4 5))
15
lfe> (lists:foldl (lambda (x acc) (cons x acc)) '() '(1 2 3))
(3 2 1)
lfe> (lists:foldr (lambda (x acc) (cons x acc)) '() '(1 2 3))
(1 2 3)
The difference between foldl (fold left) and foldr (fold right) is the direction of traversal. foldl is tail-recursive and generally more efficient, while foldr processes from right to left.
Flatmapping:
lfe> (lists:flatmap (lambda (x) (list x (* x 2))) '(1 2 3))
(1 2 2 4 3 6)
Zipping:
lfe> (lists:zip '(1 2 3) '(a b c))
((1 a) (2 b) (3 c))
lfe> (lists:zip3 '(1 2 3) '(a b c) '(x y z))
((1 a x) (2 b y) (3 c z))
List Comprehensions
LFE supports powerful list comprehensions that combine filtering, mapping, and Cartesian products:
lfe> (lc ((<- x '(1 2 3 4 5))) (* x 2))
(2 4 6 8 10)
lfe> (lc ((<- x '(1 2 3 4 5 6)) (> x 3)) (* x x))
(16 25 36)
lfe> (lc ((<- x '(1 2 3)) (<- y '(a b))) (tuple x y))
(#(1 a) #(1 b) #(2 a) #(2 b) #(3 a) #(3 b))
The <- operator draws elements from a list, and optional guard conditions filter elements before the body expression is evaluated.
Pattern Matching with Lists
List pattern matching is one of the most elegant features in LFE:
lfe> (set (list first second) '(1 2))
(1 2)
lfe> first
1
lfe> second
2
lfe> (set (cons head tail) '(1 2 3 4))
(1 2 3 4)
lfe> head
1
lfe> tail
(2 3 4)
lfe> (set `(,a ,b . ,rest) '(1 2 3 4 5))
(1 2 3 4 5)
lfe> a
1
lfe> b
2
lfe> rest
(3 4 5)
Predicates
To test if a value is a list, first include the Common Lisp compatibility library:
lfe> (include-lib "lfe/include/cl.lfe")
lfe> (listp '(1 2 3))
true
lfe> (listp '())
true
lfe> (listp 42)
false
Clojure-style predicates:
lfe> (include-lib "lfe/include/clj.lfe")
lfe> (list? '(1 2 3))
true
lfe> (list? "string")
false
Standard Erlang predicate:
lfe> (is_list '(1 2 3))
true
lfe> (is_list '())
true
lfe> (is_list 42)
false
Performance Considerations
Understanding the performance characteristics of list operations is crucial for writing efficient LFE code:
- Prepending (
cons) is O(1) — constant time, very fast - Appending (
++,append) is O(n) — requires traversing the entire first list - Length is O(n) — must traverse the entire list
- Accessing by index is O(n) — must traverse to the position
- Reversing is O(n) — must traverse the entire list
For these reasons, idiomatic LFE code often:
- Builds lists in reverse order then reverses once at the end
- Uses cons instead of append when possible
- Avoids repeated length calculations
- Uses pattern matching instead of index access
Example of the reverse-and-accumulate pattern:
(defun process-list (lst)
(process-list-helper lst '()))
(defun process-list-helper
(('() acc) (lists:reverse acc))
(((cons h t) acc)
(let ((processed (* h 2)))
(process-list-helper t (cons processed acc)))))
Common Patterns
Collecting results:
(defun collect-evens
(('()) '())
(((cons h t)) (when (== (rem h 2) 0))
(cons h (collect-evens t)))
(((cons _ t))
(collect-evens t)))
Processing pairs:
(defun process-pairs
(('()) '())
(((list x)) (list x))
(((list x y . rest))
(cons (+ x y) (process-pairs rest))))
Searching:
(defun find-first
((_ '()) 'not-found)
((pred (cons h t))
(case (funcall pred h)
('true h)
('false (find-first pred t)))))
Strings
In LFE, as in Erlang, strings are simply lists of integers representing character codes. This might seem unusual if you're coming from languages with dedicated string types, but it's a perfectly sensible representation that fits naturally with list processing. A string is just a list where each element happens to be an integer in the valid character range.
Strings vs. Binaries
While strings-as-lists are the traditional representation, modern Erlang and LFE code increasingly uses binaries for text due to their superior memory efficiency and performance with large text. Binaries will be covered in detail in a later chapter. For now, we focus on list-based strings as they're simpler to understand and still widely used for smaller text operations.
Creating Strings
String literals are enclosed in double quotes:
lfe> "Hello, World!"
"Hello, World!"
lfe> "The answer is 42"
"The answer is 42"
lfe> ""
""
Under the hood, these are lists of integers:
lfe> (== "ABC" '(65 66 67))
true
lfe> (== "ABC" (list 65 66 67))
true
You can see this more clearly by forcing LFE to show the underlying representation:
lfe> (io:format "~w~n" '("ABC"))
[65,66,67]
ok
Character literals make this relationship explicit:
lfe> (list #\A #\B #\C)
"ABC"
lfe> #\A
65
String Operations
Since strings are lists, all list operations work on strings:
lfe> (++ "Hello, " "World!")
"Hello, World!"
lfe> (lists:reverse "stressed")
"desserts"
lfe> (length "Hello")
5
Accessing characters:
lfe> (car "Hello")
72
lfe> (cdr "Hello")
"ello"
lfe> (lists:nth 2 "Hello")
101
Case conversion:
lfe> (string:uppercase "hello world")
"HELLO WORLD"
lfe> (string:lowercase "HELLO WORLD")
"hello world"
lfe> (string:titlecase "hello world")
"Hello World"
Trimming whitespace:
lfe> (string:trim " hello ")
"hello"
lfe> (string:trim " hello " 'leading)
"hello "
lfe> (string:trim " hello " 'trailing)
" hello"
Splitting and joining:
lfe> (string:split "one,two,three" "," 'all)
("one" "two" "three")
lfe> (string:split "a:b:c:d" ":")
("a" "b:c:d")
lfe> (string:split "a:b:c:d" ":" 'all)
("a" "b" "c" "d")
lfe> (string:join '("one" "two" "three") ", ")
"one, two, three"
Searching:
lfe> (string:find "hello world" "world")
"world"
lfe> (string:find "hello world" "universe")
'nomatch
lfe> (string:str "hello world" "world")
7
lfe> (string:str "hello world" "universe")
0
Note that string:str returns 1-based position or 0 if not found.
Replacing:
lfe> (string:replace "hello world" "world" "universe")
"hello universe"
lfe> (string:replace "a,b,c,d" "," ":" 'all)
"a:b:c:d"
Checking prefixes and suffixes:
lfe> (string:prefix "hello world" "hello")
"world"
lfe> (string:prefix "hello world" "goodbye")
'nomatch
lfe> (lists:prefix "hello" "hello world")
true
lfe> (lists:prefix "goodbye" "hello world")
false
lfe> (lists:suffix "world" "hello world")
true
String Predicates
Testing for empty strings:
lfe> (string:is_empty "")
true
lfe> (string:is_empty "hello")
false
Testing if all characters match a predicate:
lfe> (lists:all (lambda (c) (and (>= c #\a) (=< c #\z))) "hello")
true
lfe> (lists:all (lambda (c) (and (>= c #\a) (=< c #\z))) "Hello")
false
Formatting Strings
The format family of functions provides powerful string interpolation:
lfe> (io_lib:format "The answer is ~p" '(42))
"The answer is 42"
lfe> (io_lib:format "~s ~s ~p" '("Hello" "world" 123))
"Hello world 123"
lfe> (io_lib:format "~.2f" '(3.14159))
"3.14"
lfe> (io_lib:format "~10.2.0f" '(3.14159))
" 3.14"
Common format specifiers:
~p— pretty-print any term~s— string~w— write in Erlang term format~c— character~f— float~e— exponential notation~.Nf— float with N decimal places~n— newline
For console output, use lfe_io:format or io:format:
lfe> (lfe_io:format "Hello, ~s!~n" '("World"))
Hello, World!
ok
lfe> (io:format "The answer is ~p~n" '(42))
The answer is 42
ok
Converting Between Types
Atoms to strings:
lfe> (atom_to_list 'hello)
"hello"
lfe> (atom_to_list 'hello-world)
"hello-world"
Strings to atoms:
lfe> (list_to_atom "hello")
hello
lfe> (list_to_existing_atom "hello")
hello
Caution: Atom Creation
Remember that atoms are not garbage collected. Use list_to_atom cautiously with user input, as creating too many atoms can exhaust the atom table and crash the VM. Prefer list_to_existing_atom when you expect the atom to already exist.
Numbers to strings:
lfe> (integer_to_list 42)
"42"
lfe> (integer_to_list 42 16)
"2A"
lfe> (float_to_list 3.14159)
"3.14159000000000009237e+00"
lfe> (float_to_list 3.14159 '(#(decimals 2)))
"3.14"
Strings to numbers:
lfe> (list_to_integer "42")
42
lfe> (list_to_integer "2A" 16)
42
lfe> (list_to_float "3.14159")
3.14159
Binaries to strings:
lfe> (binary_to_list #"Hello")
"Hello"
lfe> (list_to_binary "Hello")
#"Hello"
Unicode Support
LFE strings can contain Unicode characters, though the integer representation may be surprising:
lfe> "Hello, 世界"
"Hello, 世界"
lfe> (length "Hello, 世界")
9
lfe> (lists:nth 8 "Hello, 世界")
19990
For proper Unicode string handling, especially with grapheme clusters, use the Unicode-aware functions in the string module:
lfe> (string:length "Hello, 世界")
9
lfe> (string:slice "Hello, 世界" 7 2)
"世界"
The modern string module (Erlang 20+) handles Unicode correctly and should be preferred over older list-based string functions when working with international text.
Common Patterns
Building strings incrementally:
(defun build-message (name age)
(++ "Name: " name ", Age: " (integer_to_list age)))
Processing each character:
(defun count-vowels
(("") 0)
(((cons c rest))
(let ((is-vowel (lists:member c "aeiouAEIOU")))
(if is-vowel
(+ 1 (count-vowels rest))
(count-vowels rest)))))
Filtering characters:
(defun remove-spaces
(("") "")
(((cons #\space rest)) (remove-spaces rest))
(((cons c rest)) (cons c (remove-spaces rest))))
Using list comprehensions on strings:
lfe> (lc ((<- c "hello")) (- c 32))
"HELLO"
lfe> (lc ((<- c "hello world") (=/= c #\space)) c)
"helloworld"
Performance Notes
String operations have the same performance characteristics as list operations because they are list operations:
- Concatenation with
++is O(n) in the length of the first string - Repeated concatenation in a loop can be O(n²)
- For building large strings, accumulate in a list and call
lists:flattenonce - Consider using binaries for large text or when performance is critical
Example of efficient string building:
(defun build-csv-row (items)
(lists:flatten
(lists:join ","
(lists:map #'format-item/1 items))))
Summary
Strings in LFE are lists of character codes—simple, elegant, and slightly peculiar if you're new to this representation. This design choice means:
- All list operations work on strings
- Strings naturally integrate with pattern matching
- String manipulation is intuitive if you think in terms of list processing
- Performance characteristics match those of lists
- Unicode requires some care but is fully supported
For most string operations in modern code, prefer the string module's Unicode-aware functions. For performance-critical text processing with large strings, consider using binaries (covered in a later chapter). But for everyday string manipulation, list-based strings remain a perfectly reasonable and often elegant choice.
Remember: in LFE, as in life, it's not about having a separate type for everything—it's about recognizing that sometimes, a list of integers masquerading as text is exactly what you need.
Bits, Bytes, and Binaries in LFE
Or: How I Learned to Stop Worrying and Love the Bit Stream
Introduction
In most programming languages, manipulating binary data at the bit level is rather like performing surgery with a pickaxe. It's technically possible, and if you squint hard enough while wielding your bit shifts and masking operations, you might even get something resembling the desired result. But it won't be pretty, and there's a non-zero chance you'll need therapy afterward.
LFE, inheriting Erlang's particularly sophisticated relationship with binary data, takes a different approach. Instead of making you grovel through arcane incantations of bitwise operations (though those are available if you're feeling nostalgic), it provides what can only be described as pattern matching on steroids. Or perhaps pattern matching that's been to finishing school and learned proper table manners. The result is that tasks which would normally require careful bit-shuffling and a comprehensive insurance policy can be accomplished with code that looks suspiciously like the specification of the data format you're working with.
This is not an accident. It's the kind of elegant design that makes you wonder what other programming languages have been doing with their time.
Why Binaries Matter
Let us consider, for a moment, the humble byte. Eight bits, representing values from 0 to 255 (or -128 to 127 if you're feeling signed about it). Unremarkable in isolation, perhaps, but stack enough of them together and you have:
- Network protocol packets pretending to be well-behaved structured data
- Image files storing millions of pixels that somehow form pictures of cats
- Audio streams carrying the complete works of humanity's musical genius (and also whatever's popular on the charts)
- Video codecs performing computational miracles that would have made our ancestors believe in magic
- Encrypted data that looks like someone spilled alphabet soup into a blender
In the world of telecommunications, distributed systems, and general data wrangling—which is to say, the world Erlang and LFE were designed for—the ability to efficiently pack, unpack, and pattern-match binary data isn't merely convenient. It's essential. It's the difference between "this will work" and "this will work while handling millions of messages per second without breaking a sweat."
The LFE Advantage
What makes LFE's approach to binaries particularly noteworthy is that it treats binary data as a first-class citizen with its own syntax and pattern-matching capabilities. You don't convert things to binaries as an afterthought; you work with them directly, naturally, and—dare we say it—elegantly.
Consider this: in many languages, if you wanted to parse an MPEG audio frame header (which consists of 11 bits of sync word, 2 bits of version ID, 2 bits of layer description, and so on), you'd be looking at code that resembles a particularly obtuse mathematical proof. In LFE, you write something that looks like:
(binary (11 (sync bits)) (2 (version bits)) (2 (layer bits)) ...)
And that's it. You've just pattern-matched against the bit-level structure of the data. The code reads like the specification. This is the kind of thing that makes programmers weep with joy, or at least nod approvingly while adjusting their glasses.
What You'll Learn
In the chapters that follow, we'll explore:
- The fundamentals: What binaries are, how they're represented in memory, and why the BEAM VM is particularly good at handling them
- Basic construction and manipulation: Creating binaries with the
#B(...)syntax and using built-in functions to dissect them - The bit syntax: The full glory of size specifications, type qualifiers, and pattern matching at arbitrary bit boundaries
- Pattern matching: Because if you can pattern-match lists and tuples, why not pattern-match the individual bits in a TCP packet?
- Binary comprehensions: List comprehensions for binaries, because sometimes you need to transform a stream of bytes and regular loops are just so... imperative
- Real-world applications: From parsing network protocols to implementing codec algorithms, because theory is nice but practice pays the bills
By the end of this exploration, you'll be able to look at a binary data format specification and translate it almost directly into working LFE code. You'll understand why binary pattern matching is one of the secret weapons that makes Erlang and LFE particularly well-suited for building telecommunications systems, distributed databases, and anything else that involves pushing large quantities of structured binary data through pipes at high velocity.
More importantly, you'll have in your toolkit an approach to binary data manipulation that doesn't feel like medieval torture. And in the grand scheme of programming, that's worth quite a lot.
A Note on Bitstrings
Before we dive in, a clarification: when the number of bits in your data is evenly divisible by 8, we call it a binary. When it's not—when you're dealing with, say, 13 bits or 47 bits—we call it a bitstring. Most operations work identically on both, but we'll use the more specific term when the distinction matters. Think of it as the difference between a byte-aligned data structure and one that wandered off the alignment grid to explore more interesting territory.
The convention is simple: binaries are the well-behaved, byte-aligned data structures your file I/O operations expect. Bitstrings are their more adventurous cousins that hang out at odd bit boundaries, usually because some protocol designer thought it would be clever to pack flags into 3 bits instead of wasting a whole byte.
Both are equally valid. Both are equally supported. And both, as we shall see, are equally elegant to work with in LFE.
Right, then. Shall we begin?
What Binaries Are
The Nature of the Beast
A binary, in the LFE (and Erlang) universe, is fundamentally a reference to a chunk of raw, untyped memory. If you're coming from a background in higher-level languages where everything must be wrapped in three layers of abstraction and a bow tie before it's considered respectable, this might feel a bit like meeting someone who still knows how to start a fire without matches. Primitive, perhaps, but undeniably useful when you need to cook dinner.
Binaries were originally conceived for a rather specific purpose: loading code over networks in the Erlang runtime system. Imagine trying to send compiled programs between machines in the 1980s, when network bandwidth was measured in units that would make modern developers weep. You needed something efficient, something compact, something that wouldn't spend more time being transferred than actually running. Thus, the binary was born—a data structure optimized for moving large quantities of bits around without the overhead of, say, converting everything to strings and hoping for the best.
What the designers discovered, rather like scientists who set out to invent a better glue and accidentally created Post-It notes, was that binaries were extraordinarily useful for many things. Socket-based communication. Protocol parsing. Codec implementation. Any situation where you need to deal with data in its raw, unadorned, byte-level glory.
Memory Representation
In memory, a binary is essentially a sequence of bytes (which are, if we're being pedantic, sequences of 8 bits each). The system represents them efficiently—more efficiently, in fact, than lists. This might come as a surprise if you've been taught that lists are the fundamental data structure and everything else is just lists in disguise. But consider: a list in Erlang (and thus LFE) is a linked list, where each element requires not only its own storage but also a pointer to the next element. A binary is more like a C array: a contiguous block of memory with minimal overhead.
For small binaries (less than 64 bytes), the data is typically stored directly in the process heap. For larger binaries, they're stored in a shared memory area and processes keep references to them. This means that when you send a large binary in a message between processes, you're not copying the entire thing—you're just passing a reference. It's rather like instead of photocopying a lengthy document and mailing it, you simply tell someone where the filing cabinet is.
This reference-based approach has pleasant implications for garbage collection as well. Large binaries can be shared among many processes without being duplicated, and they're only freed when the last reference disappears. It's communism for data structures, but the kind that actually works.
Visual Representation
When the REPL prints a binary, it uses the notation #B(...) (or sometimes <<...>> if you're looking at Erlang documentation). The contents are displayed as integers, each representing a byte:
lfe> #B(1 2 3)
#B(1 2 3)
If the binary happens to contain bytes that form printable ASCII characters, the shell might helpfully display it as a string:
lfe> #B(72 101 108 108 111)
#"Hello"
Note the #"..." notation for binary strings. This is the shell being clever, recognizing that the bytes 72 101 108 108 111 correspond to the ASCII codes for "Hello" and presenting them in a more readable format. The bytes haven't changed—only their cosmetic presentation has been upgraded for human consumption.
This is similar to how the number 42 and the string "42" are different beasts, even though they might occasionally be mistaken for each other at parties. One is a numeric value; the other is a sequence of character codes that, when interpreted by a human visual system, evoke the concept of forty-two. The distinction matters more than you might think.
The Binary/Bitstring Distinction
As mentioned in our introduction, binaries are the special case of bitstrings where the number of bits is evenly divisible by 8. A bitstring with 16 bits? That's a binary. A bitstring with 15 bits? That's still a bitstring, but it's not quite respectable enough to join the binary club. It's the data structure equivalent of showing up to a formal dinner in jeans and a t-shirt—perfectly valid, but people will notice.
The important point is that most operations work identically on both. Pattern matching doesn't care if you're byte-aligned or wandering off into the wilderness of arbitrary bit boundaries. The system handles both with equal grace, which is more than can be said for most programming languages' relationship with non-byte-aligned data.
You can verify what you're dealing with using the predicates (binary? x) and (bitstring? x):
lfe> (binary? #B(1 2 3))
true
lfe> (bitstring? #B(1 2 3))
true
lfe> (let ((b (binary (1 (size 9))))) ; 9 bits
(tuple (binary? b) (bitstring? b)))
#(false true)
Note that all binaries are bitstrings, but not all bitstrings are binaries. It's a classic is-a relationship, the kind that object-oriented programmers spend their entire careers thinking about but which here just... works, without requiring seventeen layers of inheritance and a PhD in type theory.
Why Not Just Use Lists?
A reasonable question. After all, you could represent binary data as a list of integers from 0 to 255. Many languages force you to do exactly that. But consider:
-
Space efficiency: A list needs space for both the data and the links between elements. A binary just needs space for the data. For large data structures, this difference can be substantial enough to matter, even in the age of abundant RAM.
-
Access patterns: Lists are optimized for sequential access from the front. Binaries can be indexed directly. If you need to extract the 47th byte, you don't have to walk through the first 46 elements holding hands with yourself.
-
Message passing: When you send a binary in a message, you're sending a reference. When you send a list, you're sending... well, the entire list, links and all. For large data structures, one of these is significantly less expensive than the other.
-
Pattern matching performance: Pattern matching on binary structures is highly optimized in the BEAM VM. It's the kind of optimization that the Erlang team spent years perfecting because they needed it for telecommunications. You get the benefits of their decades of effort for free.
Lists are wonderful data structures. They have their place, and that place is wherever you need flexible, dynamic, recursively-defined collections of things. But for raw bytes and bits? Binaries are simply better suited to the task, in the same way that a screwdriver is better suited to turning screws than is a particularly determined hammer.
An Important Property: Immutability
Like all data structures in LFE, binaries are immutable. Once created, they cannot be modified. If you need to "change" a binary, you create a new one. This might seem inefficient until you realize that the BEAM VM is rather clever about this—it can often share unchanged portions of the data structure and only allocate new space for the parts that actually differ.
This immutability has profound implications for concurrent programming, which is rather the point of Erlang and LFE. You never need to worry about another process modifying a binary out from under you, because it simply cannot happen. The binary you have is the binary you'll always have, unchanging and eternal, like a particularly stubborn philosophy professor or the value of e.
In practice, this means you can pass binaries between processes with abandon, secure in the knowledge that they won't suddenly transform into something else when you're not looking. In a world of concurrent processes and message passing, this guarantee is worth its weight in gold. Or at least in silicon.
Basic Binary Syntax
In the beginning, there was #B(...). Or possibly <<...>> if you're consulting Erlang documentation and wondering why everyone keeps using those cheerful angle brackets. Both notations refer to the same underlying concept, though LFE, having a certain aesthetic sensibility about parentheses, prefers the former. It's the difference between wearing a properly tailored suit and showing up in whatever was clean that morning—functionally equivalent, but one makes a better impression at job interviews.
The simplest possible binary is the empty one:
lfe> #B()
#B()
This binary contains exactly nothing, which might seem like an odd thing to create deliberately, but philosophers have been creating nothing deliberately for millennia and they're considered rather clever. In practice, the empty binary occasionally proves useful as a starting point for accumulation patterns, or when you need to pass a binary but have no actual data. It's the binary equivalent of a polite nod of acknowledgment.
Constructing Simple Binaries
To create a binary containing actual data, you simply list the values between the parentheses:
> #B(1 2 3)
#B(1 2 3)
Each integer you provide becomes a byte in the resulting binary. The values must be in the range 0 to 255, because that's what fits in 8 bits, and attempting to stuff larger numbers into bytes is a bit like trying to fit an elephant into a phone booth—technically possible if you're willing to ignore conventional constraints like physics, but generally frowned upon by those who have to clean up afterward.
> #B(42)
#B(42)
> #B(255)
#B(255)
> #B(256)
; This will result in an error, because 256 requires 9 bits
The system will politely inform you when you've exceeded the byte boundary, typically by throwing an exception. It's being helpful, really. Better to know at compile time than to discover later that your carefully constructed binary has been truncated in mysterious and exciting ways.
Binary Strings
A particularly convenient special case involves text. Since strings in many contexts are just sequences of character codes, you can create a binary directly from string literals:
lfe> #"Hello"
#"Hello"
This is exactly equivalent to:
lfe> #B(72 101 108 108 111)
#"Hello"
The ASCII codes for H, e, l, l, and o are 72, 101, 108, 108, and 111, respectively. You can verify this with a character code lookup, or by asking any passing computer scientist, though the latter approach may result in a lengthy discussion about character encodings and why Unicode was necessary and why UTF-8 is simultaneously brilliant and a source of ongoing existential dread.
Binary strings are particularly efficient for storing text that you don't plan to manipulate character-by-character. If you're reading a file and want to keep its contents in memory without converting everything to a list (which, remember, has pointer overhead for every element), binary strings are your friend. A friend who doesn't talk much and just does their job efficiently, like a particularly competent butler.
The syntax #"..." is exactly like the list string syntax "...", except the result is a binary instead of a list. Everything about escape sequences works identically:
lfe> #"Line 1\nLine 2"
#"Line 1\nLine 2"
lfe> #"Tab\there"
#"Tab\there"
You can even use the hexadecimal escape sequences:
lfe> #"\x48\x65\x6c\x6c\x6f"
#"Hello"
Though one might reasonably ask why you'd want to write "Hello" in hexadecimal, unless you're being deliberately obscure for job security purposes. Security through obscurity might not work for cryptography, but it works reasonably well for ensuring you're the only person who can maintain your code.
Mixing Integers and Strings
The #B(...) syntax allows you to mix integers and strings freely:
lfe> #B(1 2 "Hello" 3 4)
#B(1 2 72 101 108 108 111 3 4)
Note how the string "Hello" was expanded into its constituent byte values. The binary doesn't remember that those bytes once belonged to a string—it just stores the raw values. The distinction between "a byte containing 72" and "the letter H" exists only in our meat-based interpretation systems, not in the silicon.
This mixing capability is occasionally useful when you need to construct a binary containing both protocol-specific numeric values and actual text data. Which is to say, it's useful frequently, because protocols are designed by people who can never quite decide whether they want to use magic numbers or human-readable identifiers.
Nested Binaries
You can nest binaries within binary construction:
lfe> (let ((b1 #B(1 2))
(b2 #B(3 4)))
#B(b1 b2))
#B(1 2 3 4)
This concatenates the binaries, flattening them into a single contiguous sequence. It's the binary equivalent of appending lists, except more efficient because you're not walking linked structures and rebuilding chains of cons cells. You're just stacking memory blocks end-to-end like a particularly orderly game of digital Tetris.
The nested binary must, of course, be bound to a variable. Attempting to write #B(#B(1 2)) will confuse the parser, much like attempting to say "this sentence is false" will confuse certain types of philosophers and most logical paradox detection systems.
Variables in Binary Construction
You can use variables in binary construction, as we just demonstrated:
lfe> (let ((x 42))
#B(x))
#B(42)
The variable is evaluated and its value is inserted into the binary. This works as long as the value is something sensible—an integer in the correct range, another binary, or a string. Attempting to insert tuples, lists, or atoms directly will result in the system politely declining your request, typically by throwing a bad argument exception.
If you want to insert complex terms into binaries, you'll need to serialize them first, which we'll discuss in a later chapter. For now, just remember: binaries like numbers and other binaries. They're not particularly sociable with the more structured data types, preferring to maintain their raw, untyped aesthetic.
Efficiency
When you construct a binary, the system allocates the appropriate amount of memory and copies the data into it. This is a relatively fast operation, but it's still worth being aware of. If you're building up a large binary piece by piece, you might want to accumulate the pieces in a list and construct the final binary all at once:
(let ((pieces (list #B(1 2) #B(3 4) #B(5 6))))
(list-to-binary pieces))
This is more efficient than repeatedly appending to a binary, because immutable data structures mean that each append operation creates a new binary. Better to collect everything and do one allocation than to do many allocations as you build up the structure incrementally.
Of course, premature optimization is the root of all evil (or at least the root of spending three weeks optimizing code that runs once at startup and accounts for 0.0001% of your execution time). But when dealing with large binaries in performance-critical code, this distinction can matter. The BEAM VM is fast, but physics still applies, and copying gigabytes of data byte-by-byte is still gigabytes of data that need to be copied.
The List-to-Binary Bridge
Speaking of list-to-binary, this BIF (Built-In Function) is your primary tool for converting list-based data into binary form:
lfe> (list-to-binary '(72 101 108 108 111))
#"Hello"
It accepts what Erlang calls an "iolist"—a structure that can be a flat list of integers (0-255) and binaries, or a nested structure of the same. The nesting can be arbitrarily deep, because sometimes data structures grow organically and you don't want to flatten everything just to convert it to a binary:
lfe> (list-to-binary (list #B(1 2) (list 3 4) #B(5 6)))
#B(1 2 3 4 5 6)
Note how the nested list (list 3 4) was automatically flattened. The system understands that you want all the bytes, regardless of how they were structured in your list. It's remarkably forgiving, which is pleasant when you're dealing with data that was accumulated through various means and hasn't been tidied up into a perfectly flat structure.
Summary
Basic binary construction in LFE is refreshingly straightforward: put integers and strings between #B( and ), and out comes a binary. Variables work. Nesting works. It's the kind of intuitive design that makes you wonder why other languages make it so complicated. The answer, as is often the case, involves historical accidents, committee decisions, and the fact that once something is in a language specification, removing it requires an act of international treaty negotiation and possibly divine intervention.
For now, we have what we need: a simple, expressive syntax for creating binaries. In the next sections, we'll explore what to do with them once they exist, which is where things get interesting enough that people wrote their doctoral theses on it. Not because it's complicated—it isn't—but because it's powerful in ways that aren't immediately obvious until you try to accomplish the same things in languages that don't have bit syntax and realize you've been sent to extract data from a binary format armed only with a toothpick and determination.
Binary BIFs
The Erlang runtime system, in its infinite wisdom (or at least its well-considered pragmatism developed over three decades of telecommunications infrastructure), provides a collection of Built-In Functions—BIFs, to those in the know—for manipulating binaries. These are not functions written in Erlang or LFE themselves, but rather direct calls into the runtime's implementation in C, which means they're fast in the way that things are fast when they don't have to worry about abstractions or politeness and can just get on with the business of moving bytes around memory.
Think of BIFs as the difference between asking someone nicely to complete a task and simply reaching into their brain and adjusting the relevant neurons directly. More efficient, certainly, though probably less socially acceptable in most cultures.
list-to-binary: The Great Flattener
We've already met list-to-binary, but it deserves a more formal introduction:
(list-to-binary list-of-bytes-and-binaries)
This function takes what Erlang calls an "iolist"—a deeply recursive definition that essentially means "a list containing integers (0-255), binaries, or other iolists." The function then flattens this entire structure into a single contiguous binary, removing all the list overhead and leaving you with just the raw bytes.
lfe> (list-to-binary '(1 2 3))
#B(1 2 3)
lfe> (list-to-binary (list #B(1 2) 3 (list 4 5) #B(6)))
#B(1 2 3 4 5 6)
Note how nesting is handled transparently. The function doesn't care how your data is structured—it cares only about the bytes, and those it will find no matter how deeply you've hidden them in nested lists. It's like having a particularly determined detective who will find what they're looking for even if you've buried it under seventeen layers of bureaucratic paper trails and misdirection.
binary-to-list: The Decomposer
The inverse operation is equally straightforward:
(binary-to-list binary)
This extracts each byte from the binary and produces a list of integers:
lfe> (binary-to-list #B(1 2 3))
(1 2 3)
lfe> (binary-to-list #"Hello")
(72 101 108 108 111)
Why would you want to convert a space-efficient binary into a less-efficient list? Perhaps you need to manipulate individual elements and the list structure makes that more convenient. Perhaps you're interfacing with code that expects lists. Perhaps you're being deliberately perverse for artistic purposes. The function doesn't judge; it merely transforms.
There's also a variant that converts a slice of the binary:
(binary-to-list binary start end)
Where start and end are byte positions (1-indexed, because someone, somewhere, decided that counting should start at 1 despite computers preferring 0, and we're all just living with the consequences). This allows you to extract a portion of the binary without converting the entire thing to a list first.
split-binary: The Precision Slicer
When you need to divide a binary into two parts at a specific position:
(split-binary binary position)
This returns a tuple containing two binaries—the part before the position and the part from the position onward:
lfe> (split-binary #B(1 2 3 4 5 6 7 8 9 10) 3)
#(#B(1 2 3) #B(4 5 6 7 8 9 10))
Note that the position is 0-indexed (unlike the list version), because different functions in the standard library have different opinions about whether counting should start at 0 or 1, and consistency is apparently for lesser languages. This is the kind of thing that makes perfect sense when you understand the historical context and which committee member was arguing for what during the standardization process, and makes no sense whatsoever if you expect software design to be guided by principles like "intuitive interface design" or "not making users remember special cases."
The first binary contains position bytes, and the second contains everything else. If you try to split at a position larger than the binary's size, you'll get an error of the "what did you expect to happen?" variety. If you split at position 0, you get an empty binary and the original binary. If you split at the size of the binary, you get the original binary and an empty binary. Edge cases are handled with all the enthusiasm of someone filing tax returns, which is to say, correctly but joylessly.
term-to-binary: The Universal Serializer
This is where things get interesting enough that people write papers about it:
(term-to-binary any-erlang-term)
This function takes any Erlang/LFE term—atoms, integers, lists, tuples, maps, binaries, other terms nested arbitrarily deep—and converts it into a binary representation. This binary uses the "Erlang external term format," which is precisely specified and designed to be efficient both in space and encoding/decoding time.
lfe> (term-to-binary '#(test 12 true (1 2 3)))
#B(131 104 4 100 0 4 116 101 115 116 97 12 100 0 4 116 114
117 101 107 0 3 1 2 3)
Those numbers might look like line noise, but they're actually a carefully structured representation of the tuple #(test 12 true (1 2 3)). The format includes type tags, length specifications, and the actual data, all packed efficiently into as few bytes as possible.
This is extraordinarily useful for:
- Storing complex data structures in files
- Sending data structures over network sockets
- Implementing persistence layers
- Debugging (though the binary output is not particularly human-readable without tools)
- Implementing distributed Erlang (which uses this format for inter-node communication)
The format is version-stable, meaning binaries created with older versions of Erlang can generally be read by newer versions. This is the kind of forward compatibility that database designers dream about and which distributed systems rely upon to avoid version upgrade nightmares where everything must be upgraded simultaneously or face the consequences (those consequences typically involving angry phone calls at 3 AM and rapidly updating one's résumé).
binary-to-term: The Reanimator
byte-size: The Measurer
bit-size: The Precise Measurer
Summary
Bit Syntax Fundamentals
The General Form
Segments: The Atomic Unit
Size Specifications
Default Sizes
Type Annotations
Combining Size and Type
Variables in Segments
Philosophical Implications
Type Specifiers
The Type Itself
Sign
Endianness
The Unit Specifier
Combining Specifiers
Special Cases and Edge Conditions
Summary
Size and Unit Specifications
Default Sizes
Explicit Size Specifications
The Unit Specification
The Constraint of Divisibility by Eight
Practical Example: Fixed-Width Records
The Pattern Matching Constraint
A Note on UTF Strings
Summary
Endianness
The Parable of Big-Endian and Little-Endian
The Endianness Specifiers in LFE
When Endianness Matters
The Native Option: Runtime Flexibility
Pattern Matching with Endianness
Cross-Platform Binary Protocols
A Historical Note
Practical Examples
Summary
Pattern Matching Binaries
The Fundamental Symmetry
Basic Pattern Matching Examples
The Rest of the Binary
Don't Care Variables
Pattern Matching in Function Clauses
Bit-Level Pattern Matching
The Dual Nature of Size Variables
Complex Real-World Example: Decoding a TLV Structure
Pattern Matching with Guards
Common Pitfalls and How to Avoid Them
Summary
Binary Comprehensions
The Basic Syntax
Binary to List: The Simplest Case
List to Binary: The Reverse Journey
Transforming Values
Binary to Binary: The Power Move
Bit-Level Manipulation
Practical Example: RGB Color Manipulation
Multiple Generators: The Cartesian Product
Bit String Manipulation
Complex Example: Run-Length Encoding
Advanced Pattern: Sliding Window
Performance Considerations
Summary
Bitstrings
What Bitstrings Are
Creating Bitstrings
Size Queries
Pattern Matching Bitstrings
Appending Bitstrings
Practical Example: Morse Code
Variable-Length Encoding Example
The Alignment Problem
Extracting Bits from Bytes
Why Bitstrings Matter
Performance Characteristics
Summary
Bitwise Operators
Bitwise AND
Bitwise OR
Bitwise XOR
Bitwise NOT
Bit Shift Left
Bit Shift Right
Practical Use: Extraction and Division
Combining Operators: Real-World Example
Performance Characteristics
When to Use Bitwise Operators
A Note on Negative Numbers
Summary
Serialization
The Basics
Why Serialize
A More Sophisticated Example: The Deep Space Signal
The Secret Sauce: What Gets Preserved
Practical Considerations: Size and Compression
Storage Patterns: Files and Mnesia
Versioning and Evolution: The Heat Death Problem
The Distributed Angle: Cross-Node Communication
Security Considerations: Trust No Binary
Summary
Real-World Applications
Example 1: Dissecting IPv4 Datagrams
Example 2: Building a Custom Binary Protocol
Example 3: Log File Format with Binary Efficiency
Performance Considerations: The Need for Speed
Debugging Binary Code: When Patterns Don't Match
Summary
Conclusion: The Binary Achievement
"For a moment, nothing happened. Then, after a second or so, nothing continued to happen."
— Douglas Adams
With binary data in LFE, however, quite a lot happens, and it happens with a precision and elegance that would make a Swiss watchmaker weep with professional jealousy.
What We've Learned
We began this journey in a state of innocence—or possibly ignorance—regarding the true nature of binary data. We end it having witnessed the full majesty of LFE's approach to bits and bytes, which can only be described as "what would happen if pattern matching and binary data had a child that was subsequently raised by telecommunications engineers with exacting standards and a distaste for unnecessary complexity."
Let us review what we've discovered, shall we?
The Fundamental Insights
- Binaries Are First-Class Citizens
- The Bit Syntax Is Pattern Matching for Bits
- Construction and Deconstruction Are Symmetric
- Bitstrings Liberate Us From Byte Boundaries
- Binary Comprehensions Extend List Comprehensions
- Bitwise Operators Complement the Bit Syntax
- Serialization Provides Persistence
The Conceptual Shift
Perhaps the most important lesson is the conceptual shift that LFE's binary handling represents. In most languages, binary data manipulation is:
- Imperative: You write code that explicitly manipulates bytes
- Error-prone: Off-by-one errors, endianness mistakes, sign confusion
- Obscure: The code doesn't resemble the data format specification
- Performance-focused: Often written in low-level languages for speed
In LFE, binary data manipulation is:
- Declarative: You describe the structure, the system handles the details
- Correct: Pattern matching fails cleanly, no buffer overruns
- Readable: Code documents the data format
- Still Fast: The BEAM VM optimizes these operations extensively
This is the difference between:
// C version - manual bit manipulation
unsigned int value = (buffer[0] << 24) | (buffer[1] << 16) |
(buffer[2] << 8) | buffer[3];
And:
;; LFE version - declarative structure description
(binary ((value (size 32) big)) buffer)
Both extract a 32-bit big-endian integer, but only one makes you double-check the bit shift amounts and worry about whether you got the byte order right.
The Performance Reality
A reasonable concern: doesn't all this abstraction come with performance costs?
The answer is nuanced. The BEAM VM has spent decades optimizing binary operations because Erlang was designed for telecommunications systems handling millions of messages per second. The bit syntax compiles to specialized VM instructions that are highly optimized.
discipline to not micro-optimize code that runs perfectly adequately.
The Philosophical Takeaway
Binary data in LFE represents something deeper than just "a convenient way to handle bytes." It represents a philosophy of language design where:
- Domain problems drive language features: Erlang needed to parse protocols, so it got bit syntax
- Abstractions should clarify, not obscure: The bit syntax makes structure explicit
- Similar tasks should have similar syntax: Binary comprehensions mirror list comprehensions
- Safety and expressiveness aren't opposed: Pattern matching provides both
This is design by people who had real problems to solve and weren't satisfied with solutions that merely worked—they wanted solutions that worked well and could be understood six months later at 3 AM when production was down and the logs were unhelpful.
Where to Go From Here
Having absorbed these concepts, you're equipped to:
- Parse binary protocols: Network protocols, file formats, data streams
- Implement codecs: Compression, encryption, encoding schemes
- Interface with hardware: Bit-level register manipulation, device protocols
- Build distributed systems: Using serialization for persistence and communication
- Optimize data structures: Bit packing, custom binary formats
A Final Note
In the grand taxonomy of programming language features, binary handling is rarely exciting. It's not as philosophically interesting as type systems, not as practically dramatic as garbage collection, and not as immediately gratifying as first-class functions. It's the plumbing of the programming world—essential, but not glamorous.
Yet LFE's approach to binary data is worth studying precisely because it takes something mundane and makes it elegant. It takes a domain where most languages say "here's malloc(), good luck" and provides sophisticated pattern matching, comprehensions, and a syntax that reads like the specifications you're implementing.
This is what good language design looks like: taking hard problems and making them approachable without sacrificing power or performance. It's the difference between a language that lets you solve problems and a language that helps you solve problems. The first provides tools. The second provides tools and shows you how to use them effectively.
Tuples
Property Lists
Maps
Arrays
Dicts
Graphs
Queues
Records
Pattern Matching
Generic Sequence Functions
Part III - Data as Code
Where Part II explored how LFE represents information through data structures, Part III reveals how those same structures spring to life as executable programs. In languages such as Lisp, data structures can be directly entered and evaluated as code, and LFE exemplifies this principle through its evaluation model and execution semantics.
The transition from data to code occurs through evaluation—the process by which static data structures become dynamic computations. Part III begins with expressions and functions, showing how lists like (factorial 5) transform from mere data into function calls that produce results. We explore how LFE's evaluator breathes life into these structures, turning symbols into function references and nested lists into complex program flows.
The concept of treating code as data and the manipulation and evaluation thereof becomes particularly powerful when we examine processes and message passing. In LFE, not only can you construct programs as data, but you can also send code between processes, enabling sophisticated distributed computing patterns. A process might receive a data structure that represents a function, evaluate it locally, and send the results elsewhere in the system.
The later chapters of Part III demonstrate how data-as-code principles scale to larger programming constructs. Modules and packages organize collections of functions and data, while still maintaining the fundamental property that program structure can be analyzed and manipulated as data. This makes metaprogramming easier than in a language without this property, as you can write LFE programs that generate, modify, or analyze other LFE programs using the same data manipulation techniques you use for ordinary programming tasks.
By Part III's conclusion, you will understand not just how to write LFE programs, but how LFE programs can write themselves—the ultimate expression of the data-as-code philosophy that makes Lisp family languages uniquely powerful for both practical programming and theoretical computer science.RetryClaude can make mistakes. Please double-check cited sources.
Expressions
Functions
Closures
Evaluation
Flow of Control
Processes
Messages and Their Passing
Objects and Flavors
I/O
Accessing Files
Modules
Packages
Scripting with LFE
Creating LFE Projects
Using rebar3
Project Layout Conventions
Part IV - Advanced Topics
Having firmly established in our minds the fundamental data structures, evaluation semantics, and basic programming constructs of LFE, Part IV ventures into the advanced territories where LFE truly demonstrates its power as both a Lisp and a dialect of Erlang. These chapters explore the sophisticated techniques and system-level capabilities that distinguish expert LFE programmers from casual users, covering everything from metaprogramming to distributed computing.
The journey begins with the practical realities of software development: errors and debugging. In LFE, error handling is not an afterthought but a fundamental design principle inherited from Erlang's "let it crash" philosophy. Lisp pioneered many ideas in computer science, including automatic storage management and error handling, and LFE extends these concepts with Erlang's supervision trees and process isolation. Understanding how to debug LFE programs means understanding both the introspective capabilities of Lisp and the fault-tolerance mechanisms of OTP.
Testing in LFE spans multiple paradigms, from traditional unit testing to property-based testing with Propr. The latter represents a particularly powerful approach where you specify the properties your functions should satisfy, and the testing framework generates thousands of test cases automatically. This mathematical approach to testing aligns naturally with LFE's functional programming roots while leveraging Erlang's QuickCheck heritage.
The chapter on macros reveals LFE's true metaprogramming capabilities. This property allows code to be treated as data and vice versa, enabling powerful metaprogramming techniques, and LFE macros exemplify this principle. Unlike preprocessor macros in other languages, LFE macros operate on the parsed abstract syntax tree, allowing you to extend the language itself. You can create new control structures, embed domain-specific languages, and transform code in ways that would be impossible in non-homoiconic languages.
Distributed LFE programming showcases one of the platform's greatest strengths. The actor model that underlies Erlang extends seamlessly across network boundaries, allowing LFE processes to communicate transparently whether they're running on the same core or on opposite sides of the globe. This capability, combined with OTP's battle-tested distribution protocols, enables building systems that scale horizontally while maintaining the same programming model.
Ports and port drivers represent LFE's gateway to the broader computing ecosystem. While the BEAM virtual machine provides excellent isolation and fault tolerance, real systems often need to interface with external programs, databases, or hardware. Ports allow you to communicate safely with external processes, while port drivers provide high-performance integration for systems programming tasks.
The final chapters on servers and clients bring together all these advanced concepts in the context of building production systems. LFE servers can handle thousands of concurrent connections while maintaining state consistency and fault tolerance. Client architectures demonstrate how to build resilient distributed applications that gracefully handle network partitions and service failures.
Part IV represents the transition from learning LFE to mastering it. These advanced topics require not just technical knowledge but a deep understanding of the design philosophies that make LFE effective: the principle that complexity should be managed through composition rather than monolithic design, that failure should be expected and planned for rather than avoided, and that the most powerful abstractions are often the simplest ones consistently applied. By the end of Part IV, you'll have the tools and knowledge to build LFE systems that are not just functional, but elegant, robust, and scalable.
Errors and Debugging
Writing Unit Tests
The Common Test Framework
The Propr Test Framework
The Compiler
Macros
Distributed LFE
Ports and Port Drivers
Servers
Clients
Part V - OTP
Open Telecom Platform (OTP) represents one of the most sophisticated and battle-tested frameworks for building distributed, fault-tolerant systems ever created. Originally developed by Ericsson for telecommunications equipment that must run continuously for years without failure, OTP has evolved into a general-purpose platform for building any system that demands high availability, scalability, and reliability. Part V explores how LFE leverages this remarkable foundation to create applications that embody the famous "nine nines" reliability (99.9999999% uptime) that has made Erlang legendary in industries where downtime is measured in millions of dollars per minute.
OTP is built around a fundamental insight: most distributed systems follow predictable patterns, and these patterns can be abstracted into reusable components called behaviours. Rather than reimplementing the complex logic of process supervision, state management, or event handling from scratch, OTP provides tested implementations that handle the difficult edge cases while allowing you to focus on your application's unique business logic. The behaviours chapter introduces these powerful abstractions - gen_server for stateful services, gen_statem for complex state machines, supervisor for fault tolerance, and others that form the building blocks of robust systems.
Applications in OTP are more than just programs - they are complete, self-contained units that can be started, stopped, and supervised as atomic entities. An OTP application encapsulates not just code, but also configuration, dependencies, and metadata about how the application should behave in various circumstances. This packaging model enables sophisticated deployment strategies and makes it possible to upgrade running systems without stopping them - a capability that has allowed some Erlang systems to achieve continuous operation measured in decades.
The progression from applications to releases represents the scaling of OTP concepts from individual components to complete systems. A release bundles multiple applications together with the Erlang runtime itself, creating a self-contained deployment artifact that can run on any compatible platform. OTP's release handling tools support hot code swapping, allowing you to upgrade production systems while they continue serving requests - a capability that seems almost magical until you understand the careful engineering that makes it possible.
Tables and databases in the OTP context extend far beyond simple data storage. ETS (Erlang Term Storage) and DETS (Disk ETS) provide high-performance, concurrent access to in-memory and persistent data structures, while Mnesia offers a distributed database designed specifically for telecom applications. These storage systems are deeply integrated with OTP's supervision and distribution mechanisms, ensuring that data remains consistent and available even in the face of node failures or network partitions.
The example OTP project serves as the capstone of Part V, demonstrating how all these concepts integrate into a complete, production-ready system. You'll see how behaviours coordinate to handle complex workflows, how applications compose to create larger systems, and how OTP's supervision trees ensure that failures in one component don't cascade throughout the system. Lisp pioneered many ideas in computer science, including automatic storage management and recursion, and OTP extends these concepts with process supervision and automatic restart capabilities that make systems truly self-healing.
What makes OTP particularly powerful in the context of LFE is how it preserves the interactive, exploratory nature of Lisp development while providing the industrial-strength reliability guarantees needed for production systems. You can experiment with OTP behaviours at the REPL, hot-swap code in running applications, and inspect system state using the same tools you use for everyday development. This combination of power and accessibility has made OTP the secret weapon behind some of the world's most reliable distributed systems.
By the end of Part V, you'll understand not just how to use OTP, but why it represents such a significant advancement in systems programming. You'll have the knowledge to build applications that gracefully handle the complexities of distributed computing - network failures, partial updates, cascading errors, and load balancing - using patterns that have been refined through decades of real-world deployment in some of the most demanding environments on Earth.
OTP Behaviours
Applications
Releases
Tables and Databases
Example OTP Project
Part VI - Tooling
The most elegant programming language in the world is only as powerful as the tools that support it. Part VI explores the ecosystem that transforms LFE from an interesting academic exercise into a practical platform for building real software. At the heart of this ecosystem lies rebar3, the de facto standard build tool for Erlang and LFE projects, extended with a comprehensive plugin that brings first-class LFE support to every aspect of the development lifecycle.
Rebar3 represents more than just a build system - it embodies the collective wisdom of the Erlang community about how software projects should be structured, built, tested, and deployed. Originally created to manage the complex dependencies and compilation requirements of Erlang applications, rebar3 has evolved into a sophisticated project management platform that handles everything from generating initial project scaffolding to creating production releases. The LFE plugin seamlessly integrates into this ecosystem, ensuring that LFE developers can leverage the same battle-tested workflows that have made Erlang development so productive.
The Quick Start section demonstrates how modern tooling can dramatically reduce the friction of getting started with a new language. What once required understanding complex compilation pipelines and dependency management can now be accomplished with a few simple commands. You'll go from an empty directory to a running LFE application in minutes, with a project structure that follows community conventions and includes everything needed for testing, documentation, and deployment.
The plugin reference reveals the depth of integration between LFE and the broader Erlang ecosystem. The rebar3 LFE plugin doesn't just compile LFE code - it understands LFE's unique characteristics and provides specialized support for features like macros, packages, and interactive development. Whether you're running unit tests with eunit, exploring code at the REPL, or building complex multi-application releases, the plugin ensures that LFE feels like a first-class citizen in the Erlang world.
Project creation commands showcase the flexibility of modern LFE development. Different project types - libraries, standalone applications, escripts, OTP applications, and full releases - each come with appropriate scaffolding and configuration. This isn't just convenience; it's the embodiment of best practices accumulated over years of real-world development. When you generate a new LFE project, you're not starting from scratch - you're beginning with a foundation that incorporates decades of lessons learned about how to structure maintainable, scalable software.
The testing integration deserves particular attention, as it demonstrates how tooling can elevate testing from an afterthought to a central part of the development process. The plugin supports multiple testing frameworks - eunit for traditional unit testing, ltest for LFE-specific testing patterns, and Common Test for integration and system testing. This multi-layered approach reflects the reality that different types of testing require different tools, and good tooling should make it easy to use the right tool for each job. Running and deployment commands bridge the gap between development and production. The ability to run code directly from the build system, execute escripts, or start full releases provides a seamless transition from experimentation to deployment. Hot code loading and release management - capabilities that seem almost magical in other languages - become routine operations supported by simple commands.
Perhaps most importantly, Part VI demonstrates how good tooling preserves the interactive, exploratory nature of Lisp development while providing the structure needed for serious software engineering. The REPL integration ensures that you can always drop into an interactive session to explore ideas, test hypotheses, or debug problems. This combination of structure and flexibility exemplifies the LFE philosophy: powerful abstractions that don't constrain creativity.
By the end of Part VI, you'll have mastered not just the mechanics of LFE development, but the workflows that make LFE development productive and enjoyable. You'll understand how to leverage community conventions while maintaining the flexibility to adapt tools to your specific needs. Most importantly, you'll have the confidence that comes from working with mature, well-designed tools that scale from quick experiments to large, complex systems.
rebar3
Quick Start
Introduction
This is a version of the LFE quick start that has been re-imagined with the LFE rebar3 plugin in mind.
We will cover the following:
- How to get started quickly with LFE using just
rebar3and your local installation of Erlang - Creating a new LFE project
- Looking at LFE code in the REPL and in modules
- Provide a little insight on how this works
- Leave you with resources for jumping into LFE in more detail
About rebar3
Rebar3 is an Erlang tool that makes it easy to create, develop, and release Erlang libraries, applications, and systems in a repeatable manner.
Rebar3 will:
- respect and enforce standard Erlang/OTP conventions for project structure so they are easily reusable by the community;
- manage source dependencies and Erlang packages while ensuring repeatable builds;
- handle build artefacts, paths, and libraries such that standard development tools can be used without a headache;
- adapt to projects of all sizes on almost any platform;
- treat documentation as a feature, and errors or lack of documentation as a bug.
Rebar3 is also a self-contained Erlang script. It is easy to distribute or embed directly in a project. Tasks or behaviours can be modified or expanded with a plugin system flexible enough that even other languages on the Erlang VM will use it as a build tool.
The rebar3 site provides some nice instructions on installing rebar3.
Going Plaid

Remember: this is a quick-start; it's going to be a blur of colour! You're not going to get very many details here, but you will get to jump into the LFE REPL and see a little code in action.1
The rest of this quick-start assumes that you've followed the links in the previous section and have installed both Erlang as well as rebar3, but to take things further, you'll need to do one more thing: set up the LFE plugin.
Each project you create with the LFE rebar3 plugin will generate a rebar.config file that automatically includes the plugin dependency, but that's only inside an LFE project. You need to bootstrap the LFE plugin by setting it up in your global rebar.config.
The rebar3 docs tell you this file is located at ~/.config/rebar3/rebar.config. To set this up, you can safely execute the following in a terminal, even if the file already exists:
mkdir -p ~/.config/rebar3/
touch ~/.config/rebar3/rebar.config
Then, in your preferred editor, open that file and add the entry for LFE rebar3 plugin. If that file is empty when you open it, then you can paste this whole thing in there:
{plugins, [
{rebar3_lfe,
{git, "https://github.com/lfe-rebar3/rebar3_lfe.git", {branch, "main"}}}
]}.
If you want to pin your project to a specific release of the plugin, you can view the list of released versions here:
- https://github.com/lfe-rebar3/rebar3_lfe/tags
And then use tag (with the version) instead of branch:
{plugins, [
{rebar3_lfe,
{git, "https://github.com/lfe-rebar3/rebar3_lfe.git", {tag, "x.y.z"}}}
]}.
If your global rebar3 config file already has one or more plugins in it, then simply add a comma after the last one and paste the {rebar3_lfe ...} line from above (with no trailing comma!).
Next Stop
Ready for some LFE? Next you'll learn how to create a new LFE project with just one command ...
- For those that would enjoy a more in-depth introduction and would appreciate having the time to see the stars (and not just stunning plaid), you may be interested in checking out The LFE Tutorial.
Creating a New Project
A project? Already?! It sounds
daunting, but it's easier than you might think. Open up a terminal window
and do this in a directory of your choosing:
rebar3 new lfe-lib my-test-lib
It might take a minute or two to finish; here's what's happening:
rebar3downloads the LFE plugin- Finds its dependencies, downloads those too
- Compiles all of them (plugins and dependencies)
rebar3then executes thenewcommand, searches for (and finds!) the lfe-lib template- Creates the project files for the given template
As that last step executes, you will see the following output:
===> Writing my-test-lib/README.md
===> Writing my-test-lib/LICENSE
===> Writing my-test-lib/rebar.config
===> Writing my-test-lib/.gitignore
===> Writing my-test-lib/src/my-test-lib.lfe
===> Writing my-test-lib/src/my-test-lib.app.src
It's as simple as that! Your new project is ready to go :-)
Next Stop
You can taste it, can't you? That LFE flavour coming your way? Yup, you're right. You're going to be looking at LFE code next ...
Hitting the Code
It may not seem like it,
but we're off to a pretty fast start. If you had to do everything we've
done, manually, you'd have given up by now. Seriously.
(Okay, maybe not.)
Time to put the brakes on, though, 'cause you're gonna want to see this next part in slow motion.
REPL Me Up!
Make sure you've cded into your new LFE project directory, and then do this:
$ rebar3 lfe repl
On windows first enter Erlang's repl then run lfe_shell:start().
D:\Lfe\my-test-lib
λ rebar3 lfe repl
===> Verifying dependencies...
===> Compiling my-test-lib
Eshell V10.3 (abort with ^G)
1> lfe_shell:start().
This should give you something that looks like the following:
Erlang/OTP 23 [erts-11.0.2] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [hipe] [dtrace]
..-~.~_~---..
( \\ ) | A Lisp-2+ on the Erlang VM
|`-.._/_\\_.-': | Type (help) for usage info.
| g |_ \ |
| n | | | Docs: http://docs.lfe.io/
| a / / | Source: http://github.com/rvirding/lfe
\ l |_/ |
\ r / | LFE v1.3-dev (abort with ^G)
`-E___.-'
lfe>
Try setting a variable:
> (set my-list (lists:seq 1 6))
(1 2 3 4 5 6)
Here are some operations using more functions from the built-in Erlang lists module:
> (* 2 (lists:sum my-list))
42
> (* 2 (lists:foldl (lambda (n acc) (+ n acc)) 0 my-list))
42
Let's turn that into a function:
> (defun my-sum (start stop)
(let ((my-list (lists:seq start stop)))
(* 2 (lists:foldl
(lambda (n acc)
(+ n acc))
0 my-list))))
my-sum
And try it out!
> (my-sum 1 6)
42
Enough with the fancy REPL-play ... What about some real code? What does a project look like?
Sample Code
Well, you've already seen some! But here is the full, if minimal, module generated by the LFE rebar3 plugin:
(defmodule my-test-lib
(export (my-fun 0)))
;;; -----------
;;; library API
;;; -----------
(defun my-fun ()
'hello-world)
You'll note that the function we define has been exported via the export
form in the module definition. The number after the function is the arity
of that function (Erlang views functions of the same name but different
arity as different functions, and LFE does the same).
In the REPL you will have access to this module and its one function. Try it out:
lfe> (my-test-lib:my-fun)
hello-world
Let's add to this module our new my-sum function from the REPL jam session
in the previous section. In another terminal window (or text editor pane) open up the
src/my-test-lib.lfe file and paste the my-sum function at the bottom. Afterwards,
add (my-sum 2) to the export section of defmodule at the top.
When you're done, the entire file should look like this:
(defmodule my-test-lib
(export (my-fun 0)
(my-sum 2)))
;;; -----------
;;; library API
;;; -----------
(defun my-fun ()
'hello-world)
(defun my-sum (start stop)
(let ((my-list (lists:seq start stop)))
(* 2 (lists:foldl
(lambda (n acc)
(+ n acc))
0 my-list))))
Then come back to the REPL sessions and compile the module with its new addition:
> (c "src/my-test-lib.lfe")
#(module my-test-lib)
And call the module functions:
> (my-test-lib:my-sum 1 6)
42
> (my-test-lib:my-sum 1 60)
3660
>
Here's something a little more involved you may enjoy, from the examples in the LFE source code:
(defun print-result ()
(receive
((tuple pid msg)
(io:format "Received message: '~s'~n" (list msg))
(io:format "Sending message to process ~p ...~n" (list pid))
(! pid (tuple msg))
(print-result))))
(defun send-message (calling-pid msg)
(let ((spawned-pid (spawn 'my-test-lib 'print-result ())))
(! spawned-pid (tuple calling-pid msg))))
That bit of code demonstrates one of Erlang's core features in lovely Lisp syntax: message passing. When loaded into the REPL, that code can demonstrate bidirectional message passing between the LFE shell and a spawned process.
Want to give it a try? Add those two new functions to your module, and don't forget to update
the export section, too! (note that one function has an arity
of 0 and the other and arity of 2).
When you're done, your project module should look like this:
(defmodule my-test-lib
(export (my-fun 0)
(my-sum 2)
(print-result 0)
(send-message 2)))
;;; -----------
;;; library API
;;; -----------
(defun my-fun ()
'hello-world)
(defun my-sum (start stop)
(let ((my-list (lists:seq start stop)))
(* 2 (lists:foldl
(lambda (n acc)
(+ n acc))
0 my-list))))
(defun print-result ()
(receive
((tuple pid msg)
(io:format "Received message: '~s'~n" (list msg))
(io:format "Sending message to process ~p ...~n" (list pid))
(! pid (tuple msg))
(print-result))))
(defun send-message (calling-pid msg)
(let ((spawned-pid (spawn 'my-test-lib 'print-result ())))
(! spawned-pid (tuple calling-pid msg))))
Message-Passing
We glossed over this in the previous section, but in LFE (and Erlang) you can compile on-the-fly in a REPL session. This is super-convenient when prototyping functionality for a new project where you want to use the REPL, but you also want the benefits of using a file, so you don't lose your work.
We made some changes to the sample code in the last section; let's compile it and take it for a spin:
> (c "src/my-test-lib.lfe")
#(module my-test-lib)
Next, let's send two messages to another Erlang process (in this case, we'll
send it to our REPL process, (self):
> (my-test-lib:send-message (self) "And what does it say now?")
#(<0.26.0> "And what does it say now?")
Received message: 'And what does it say now?'
Sending message to process <0.26.0> ...
> (my-test-lib:send-message (self) "Mostly harmless.")
#(<0.26.0> "Mostly harmless.")
Received message: 'Mostly harmless.'
Sending message to process <0.26.0> ...
In the above calls, for each message sent we got a reply acknowledging the message (because the example was coded like that). But what about the receiver itself? What did it, our REPL process, see? We can flush the message queue in the REPL to find out.
What, what? Does each ...
Yup, every process in LFE (and Erlang, of course) has an inbox. You can see how many messages a given process has by looking at the process' info:
lfe> (erlang:process_info (self) 'message_queue_len)
#(message_queue_len 2)
And there you can see that our REPL process has two messages queued up in its inbox. Let's take a look!
> (c:flush)
Shell got {"And what does it say now?"}
Shell got {"Mostly harmless."}
ok
If you found this last bit interesting and want to step through a tutorial on Erlang's light-weight threads in more detail, you may enjoy this tutorial.
Next Stop
We did promise a bit more information, so we're going to do that next and then wrap up the quick start and point you in some directions for your next LFE adventures ...
Behind the Scenes

As we warned in the beginning, there's a lot going on behind the scenes: in rebar3 as well as LFE and Erlang (we haven't even touched OTP here ...!). This guide just gives you a quick taste of the LFE flavour :-) Before parting ways, though, there are some more bits we should share.
Some of those things are hinted at when just checking the current versions you are
running using the LFE plugin's versions command (from a terminal where you have
cded into your project directory):
rebar3 lfe versions
(#(apps (#(my-test-lib git)))
#(languages
(#(lfe "1.3-dev")
#(erlang "23")
#(emulator "11.0.2")
#(driver_version "3.3")))
#(tooling (#(rebar "3.10.0") #(rebar3_lfe "0.2.0"))))
To give a sense of what you'll encounter in the future: very often Erlang,
LFE, and other BEAM language apps include more than just themselves when they
are shipped. For instance, if you're in the REPL and you type (regs) you will
see a list of applications that have been registered by name, currently running
in support of the REPL. Usually, each app will have its own version. There is an
LFE blog series
on such things, showing you how to create and mess around with different types
of LFE apps.
The LFE rebar3 plugin will also help you create OTP apps in LFE and perform other key tasks you may wish to integrate into your development workflow. You can learn more about those in the plugin's Command Reference
Next Stop
Where to go from here ...
Where Next?
We've mentioned the following resources so far:
But there are also these:
- The old-skool LFE User Guide
- Example code in the LFE repository
- The lfex org - Community-contributed libraries
- The lfe org - Core code, tools, and on-line resources (docs, blog, etc.)
True mastery of LFE is not matter of syntax, though: it requires a deep knowledge of Erlang/OTP and how to best apply that knowledge. The Erlang site has links to great Erlang books you can read.
Plugin Reference
Introduction
The rebar3_lfe project is a rebar3 plugin for the LFE language.
It provides many of the conveniences one has come to expect of a programming
language's build tool:
- Project Creation
- A REPL
- Compilation
- Maintenane Tasks (e.g., file cleanup)
- Metadata
Features
- Create new LFE projects:
rebar3 new lfe-librebar3 new lfe-mainrebar3 new lfe-escriptrebar3 new lfe-apprebar3 new lfe-release
- Start up an LFE REPL:
rebar3 lfe repl
- Compile LFE source code:
rebar3 lfe compile
- Run eunit tests
rebar3 eunit
- Run an LFE project's
main/1function as an lfescript (runrebar3 new lfe-mainto see an example):rebar3 lfe runrebar3 lfe run -- 1 2 5rebar3 lfe run -main some/path/main.lfe
- Escriptize an LFE escript project:
rebar3 ecsriptize
- Run an escriptized LFE escript project:
rebar3 lfe run-ecsript
- Generate an LFE/OTP release
rebar3 release
- Run an LFE/OTP release project's release script (
COMMANDcan bestart,stop,status,ping, etc.):rebar3 lfe run-release COMMAND
- Cleanup
rebar3 lfe cleanrebar3 lfe clean-buildrebar3 lfe clean-cacherebar3 lfe clean-all
- Metadata
rebar3 lfe versions
Background
This plugin originally started life as a shell script (lfetool -- there's
even a T-shirt for it!), then it toyed with integrating with rebar (the
original). Around that time, though, rebar3 was under initial development,
and LFE took a chance on it as an early adopter. This lead to a whole series of
LFE plugins, but after a few years momentum was lost.
Those early rebar3 efforts have been combined into a single plugin in this
project, with many updates and using all the latest approaches developed in
rebar3's now mature ecosystem.
Setup
Dependencies
In order to use the LFE rebar3 plugin, you need to have the following installed on your system:
- Erlang (only tested with versions 19 and above)
rebar3(tested with 3.10 and 3.12)
You don't need to download LFE; the plugin will do that for you.
Using the Plugin
After installing rebar3, the only thing you need to do in order to take full
advantage of the LFE rebar3 plugin is add it to the plugins in your global
rebar.config file.
Stable
To use the latest stable release, update your `rebar.config` to:
{plugins, [
{rebar3_lfe,
{git, "https://github.com/lfe-rebar3/rebar3_lfe.git", {tag, "0.2.0"}}}
]}.
Unstable
If you want to use the current development branch (unstable):
{plugins, [
{rebar3_lfe,
{git, "https://github.com/lfe-rebar3/rebar3_lfe.git", {branch, "release/0.3.x"}}}
]}.
Command Reference
The following sections detail actual usage of the suite of rebar3 lfe
commands.
Compiling
The single most imporant convenience provided by the LFE rebar3 plugin is arguably the compiler. This allows any LFE project to be downloaded, compile, and used by any BEAM language that is also using rebar3 to manage its dependencies, etc.
To compile an LFE project:
rebar3 lfe compile
If you are publishing your LFE code, or using it in another project, you'll
want to update your rebar.config file so that it is compile when a user
(or script) executes the regular rebar3 compile command.
To ensure your LFE code will compile in other projects, add the following to
your project's rebar.config:
{provider_hooks, [
{pre, [{compile, {lfe, compile}}]}
]}.
Package Support
The LFE rebar3 plugin provides support for pseudo-packages. There is no such thing as a pckage in Erlang, but using this plugin, you can emulate some of the behaviours of packages.
This is accomplished by traversing top-level source directories for any subdirectories: if the plugin finds any .lfe or .erl files in subdirectories under configured source directories, it will create a dotted module name composed of the relative path to that file, and write that name to the ebin directory after successful compilation.
Here are some examples of how combinations of subdirectories and files will be transformed in their final form as .beam files:
./src/my.package1.lfe -> ebin/my.package1.beam
./src/my/package2.lfe -> ebin/my.package2.beam
./src/my/other/package.lfe -> ebin/my.other.package.beam
./src/my/really/deeply/nested/package1.lfe -> ebin/my.really.deeply.nested.package1.beam
./src/my/really/deeply/nested.package2.lfe -> ebin/my.really.deeply.nested.package2.beam
./src/my/really.deeply.nested.package3.lfe -> ebin/my.really.deeply.nested.package3.beam
./src/my.really.deeply.nested.package4.lfe -> ebin/my.really.deeply.nested.package4.beam
Running the REPL
An LFE project and all of its dependencies may be interacted with via a REPL that is started with the LFE rebar3 plguin, as rebar3 sets all of the correct library locations for use by shells and REPLs.
To start up a REPL:
rebar3 lfe repl
At which point you will be greeted with the familiar:
Erlang/OTP 25 [erts-13.2.2.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit]
..-~.~_~---..
( \\ ) | A Lisp-2+ on the Erlang VM
|`-.._/_\\_.-': | Type (help) for usage info.
| g |_ \ |
| n | | | Docs: http://docs.lfe.io/
| a / / | Source: http://github.com/lfe/lfe
\ l |_/ |
\ r / | LFE v2.1.2 (abort with ^C; enter JCL with ^G)
`-E___.-'
lfe>
Known Issue: Erlang 26+!
Erlang 26.0 completely refactored its support for shells. While a fix was released for LFE-proper's REPL, that same fix does not work for the rebar3 LFE REPL. To follow the progress, you can subscribe to this ticket: https://github.com/lfe/rebar3/issues/79.
The LFE banner is highly configurable in in rebar3_lfe and accepts the following configuration in your project's rebar.config file:
{lfe, [
{repl, [
{nobanner, false},
{version, "9999.2"},
{quit_message, "You can never LEEEEEAVE!"},
{banner_template, "Weeee!~nLFE v~s ~s~n"}
]}
]}.
A project configuration with those values would have a banner like the following:
Erlang/OTP 25 [erts-13.2.2.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit]
Weeee!
LFE v9999.2 You can never LEEEEEAVE!
lfe>
To disable the banner, use a configuration like this:
{lfe, [
{repl, [
{nobanner, true}
]}
]}.
Which would give:
Erlang/OTP 25 [erts-13.2.2.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit]
lfe>
Testing
[In progress]
eunit
If you have written eunit tests in Erlang or LFE, they will be compiled by
either the rebar3 compile or rebar3 lfe compile command and thus be
available and ready to run using rebar3 as-is.
Run compiled eunit tests:
rebar3 eunit
ltest
[In progress]
Common Test
[In progress]
Creating Projects
The rebar3_lfe plugin is capable of creating several common project layouts.
This and following sections provide details on those that are currently
supported.
Creating LFE Libraries
Library projects are those with no running applications or scripts; they simply provide some core bit of functionality intended for use by applications, scripts, or other libraries.
To create a library project with the default name:
rebar3 new lfe-lib
This will generate the following output:
===> Writing my-lfe-lib/README.md
===> Writing my-lfe-lib/LICENSE
===> Writing my-lfe-lib/rebar.config
===> Writing my-lfe-lib/.gitignore
===> Writing my-lfe-lib/src/my-lfe-lib.lfe
===> Writing my-lfe-lib/src/my-lfe-lib.app.src
You can also explicitely name your project:
rebar3 new lfe-lib forty-two
Which will produce the following:
===> Writing forty-two/README.md
===> Writing forty-two/LICENSE
===> Writing forty-two/rebar.config
===> Writing forty-two/.gitignore
===> Writing forty-two/src/forty-two.lfe
===> Writing forty-two/src/forty-two.app.src
As mentioned abouve, the REPL offers a nice way to quickly interact your new project.
Start the REPL:
cd forty-two
rebar3 lfe repl
Call the generated/sample LFE function:
lfe> (mything:my-fun)
hello-world
Creating main Scripts
LFE supports the creation of scripts, and these can be integrated with libraries, allowing for the best of both worlds: a simple collection of useful functions (library) and a means of running them from the command line.
Create a library that also has a script with a main function:
rebar3 new lfe-main special-proj
Which generates:
===> Writing special-proj/README.md
===> Writing special-proj/LICENSE
===> Writing special-proj/rebar.config
===> Writing special-proj/.gitignore
===> Writing special-proj/src/special-proj.lfe
===> Writing special-proj/scripts/main.lfe
===> Writing special-proj/src/special-proj.app.src
The generated project's main script + function may then be run with:
cd mymain
rebar3 lfe run -- 42
Which will produce the following output:
Running script '/usr/local/bin/rebar3' with args [<<"42">>] ...
'hello-worl
Creating escripts
The LFE rebar3 plugin also supports generating escript-based projects
in LFE. This is similar in nature to the main-based project, but is more
standard in the BEAM family of languages.
To create an escript-based project:
rebar3 new lfe-escript myapp
===> Writing myapp/README.md
===> Writing myapp/LICENSE
===> Writing myapp/rebar.config
===> Writing myapp/.gitignore
===> Writing myapp/src/myapp.lfe
===> Writing myapp/src/myapp.app.src
Compile the LFE and then bundle all the code up by "escriptizing" it:
cd myapp
rebar3 lfe compile
rebar3 escriptize
Run the newly-created escript:
rebar3 lfe run-escript 1 2 5 no '3!'
Which will display the following:
Got args: ("1" "2" "5" "no" "3!")
Answer: 42
Creating OTP Applications
In the wider Erlang community, it is very common to see applications that
run one or more gen_servers (or other server behaviours) managed by a
supervision tree (with the appropriate restart strategy defined). The LFE
rebar3 plugin provides the ability to generate OTP gen_server applications
with the server managed in a supervision tree.
To create an LFE/OTP application:
rebar3 new lfe-app otp-lfe
===> Writing otp-lfe/README.md
===> Writing otp-lfe/LICENSE
===> Writing otp-lfe/rebar.config
===> Writing otp-lfe/.gitignore
===> Writing otp-lfe/src/otp-lfe.lfe
===> Writing otp-lfe/src/otp-lfe-app.lfe
===> Writing otp-lfe/src/otp-lfe-sup.lfe
===> Writing otp-lfe/src/otp-lfe.app.src
We can use the plugin's REPL command to demonstrate usage.
Start the REPL:
cd otp-lfe
rebar3 lfe repl
Start the app:
lfe> (application:ensure_all_started 'otp-lfe)
#(ok (otp-lfe))
Make sure the supervisor is running:
lfe> (erlang:whereis 'otp-lfe-sup)
#Pid<0.205.0>
Make an API call to the gen_server:
lfe> (otp-lfe:echo "testing the supervised gen_server ...")
"testing the supervised gen_server ..."
Creating OTP Releases
If you're going to be running an LFE application in production, you will very likely want to do so using the "release" functionality provided by OTP.
Create a release-based project with:
rebar3 new lfe-release prod-lfe
===> Writing prod-lfe/README.md
===> Writing prod-lfe/LICENSE
===> Writing prod-lfe/rebar.config
===> Writing prod-lfe/.gitignore
===> Writing prod-lfe/apps/prod-lfe/src/prod-lfe.lfe
===> Writing prod-lfe/apps/prod-lfe/src/prod-lfe-app.lfe
===> Writing prod-lfe/apps/prod-lfe/src/prod-lfe-sup.lfe
===> Writing prod-lfe/apps/prod-lfe/src/prod-lfe.app.src
===> Writing prod-lfe/config/sys.config
===> Writing prod-lfe/config/vm.args
Change directoy into your new app:
cd prod-lfe
Build the release:
rebar3 release
Start up the application:
rebar3 lfe run-release start
Check the status of the application:
rebar3 lfe run-release ping
pong
Known Issue!
If your ping doesn't get a pong after starting the
release, this is a known issue that is being investigated in the following ticket:
https://github.com/lfe-rebar3/rebar3_lfe/issues/33
Workaround
The current workaround for a relese that doesn't start is simply to run the following again:
rebar3 release
rebar3 lfe run-release start
rebar3 lfe run-release ping
In addition to using the LFE rebar3 commands to start the application, you can start up a release console and then switch to the LFE REPL.
Start the console:
./_build/default/rel/prod-lfe/bin/prod-lfe console
Eshell V11.0 (abort with ^G)
(prod-app@spacemac)1> lfe_shell:start().
(prod-app@spacemac)lfe> (erlang:whereis 'prod-lfe-sup)
#Pid<0.351.0>
(prod-app@spacemac)lfe> (prod-lfe:echo "testing from production!")
"testing from production!"
Running Code
The LFE rebar3 plugin provides developers the ability to run LFE code directly, taking advantage of rebar3's setting of library paths for dependencies, etc.
run
To run an LFE project that was generated with rebar3 new lfe-main:
rebar3 lfe run
run-escript
To run an LFE project that was generated with rebar3 new lfe-escript:
rebar3 lfe run-escript
run-release
To run an LFE project that was generated with rebar3 new lfe-release
and has been compiled with rebar3 release:
rebar3 lfe run-release COMMAND
Where COMMAND is one of the non-interactive commands supported by the
release script:
startstoprestartrebootpidping
Others non-interactive commands not listed may also work, but they have not been tested.
Known Issue!
If your release doesn't start
(e.g., running rebar3 lfe run-release ping doesn't return
pong),
this is a known issue that is being investigated in the following ticket:
https://github.com/lfe-rebar3/rebar3_lfe/issues/33
Workaround
The current workaround for a relese that doesn't start is simply to re-run
rebar3 release and rebar3 lfe run-release start.
Cleanup Commands
There are a handful of tasks the author of the plugin has found useful when
debugging LFE applications, rebar3 plugins, etc. These include various
"cleanup" tasks that are not currently supported by rebar3 (or whose support
is problematic).
clean
Remove the apps' ebin/* files:
rebar3 lfe clean
clean-build
Remove the _build directory:
rebar3 lfe clean-build
clean-cache
Remove the the cached directories for the app and the rebar3_lfe plugin, both global and local:
rebar3 lfe clean-cache
clean-all
Perform all clean tasks as well as remove the files erl_crash.dump,
rebar3.crashdump, and rebar.lock:
rebar3 lfe clean-all
rebar3 Internals - A Developer's Reference
Introduction
The rebar3 compilation system is a sophisticated, multi-stage build system designed to compile Erlang/OTP applications and their dependencies efficiently. The system follows a provider-based plugin architecture, uses directed acyclic graphs (DAGs) for dependency tracking, and supports incremental and parallel compilation.
This document provides a bird's-eye view of the entire compilation process, outlining the major stages and their relationships. For detailed technical information about each stage, see the individual stage documents referenced throughout.
High-Level Architecture
The rebar3 compilation chain is built on several architectural principles:
- Provider-Based Execution: All functionality is implemented as providers that declare dependencies and execution order
- State Management: A central state object flows through all stages, accumulating configuration and results
- Graph-Based Dependencies: File and application dependencies are tracked using directed acyclic graphs
- Incremental Compilation: Only changed files and their dependents are recompiled
- Parallel Compilation: Independent files can be compiled concurrently
- Extensibility: Custom compilers and hooks allow modification of the build process
Major Compilation Stages
The compilation process can be divided into the following major stages:
- Initialization and Configuration Loading
- Dependency Resolution and Locking
- Dependency Acquisition
- Application Discovery
- Compilation Order Determination
- Dependency Compilation
- Project Application Compilation
- Single Application Compilation
- Individual File Compilation
- Build Verification and Completion
Key Dependencies
- Dependency resolution must precede acquisition
- Applications must be discovered before determining compilation order
- Dependencies must compile before project applications
- Compilation order is critical for correctness
- Verification ensures build integrity
Diagrams
Flow Diagram
graph TD
A[Start: rebar3 compile] --> B[Initialization and Configuration Loading]
B --> C[Dependency Resolution and Locking]
C --> D[Dependency Acquisition]
D --> E[Application Discovery]
E --> F[Compilation Order Determination]
F --> G{deps_only flag?}
G -->|Yes| H[Dependency Compilation Only]
G -->|No| I[Dependency Compilation]
H --> H1[For Each Dependency]
H1 --> H2[Single Application Compilation]
H2 --> H3{More deps?}
H3 -->|Yes| H1
H3 -->|No| Z[End]
I --> I1[For Each Dependency]
I1 --> I2[Single Application Compilation]
I2 --> I3{More deps?}
I3 -->|Yes| I1
I3 -->|No| J[Project Application Compilation]
J --> J1[For Each Project App]
J1 --> J2[Single Application Compilation]
J2 --> J3{More apps?}
J3 -->|Yes| J1
J3 -->|No| K[Root Extra Dirs Compilation]
K --> L[Build Verification and Completion]
L --> Z[End]
style A fill:#e1f5ff
style Z fill:#e1f5ff
style H2 fill:#fff4e1
style I2 fill:#fff4e1
style J2 fill:#fff4e1
Single Application Compilation Flow
graph TD
SA[Single Application Compilation] --> SA1[Application Preparation]
SA1 --> SA2[Pre-Compilation Hooks]
SA2 --> SA3[Compiler Preparation Hooks]
SA3 --> SA4[Source Compilation Loop]
SA4 --> SC1[Leex Compiler: .xrl → .erl]
SC1 --> SC2[Yecc Compiler: .yrl → .erl]
SC2 --> SC3[MIB Compiler: .mib → .bin + .hrl]
SC3 --> SC4[Erlang Compiler: .erl → .beam]
SC4 --> SC5[Custom Compilers]
SC5 --> SA5[Post-Compiler Hooks]
SA5 --> SA6[App File Preparation Hooks]
SA6 --> SA7[Application File Generation]
SA7 --> SA8[App File Finalization Hooks]
SA8 --> SA9[Compilation Finalization]
SA9 --> SAE[Application Compiled]
style SA fill:#fff4e1
style SAE fill:#e1ffe1
Individual Compiler Execution Flow
graph TD
C[Compiler Execution] --> C1[Load or Initialize DAG]
C1 --> C2[Get Compiler Context]
C2 --> C3[Scan Source Directories]
C3 --> C4[Prune Deleted Files from DAG]
C4 --> C5[Analyze Source Dependencies]
C5 --> C6[For Each Source File]
C6 --> C7[Parse Dependencies]
C7 --> C8[Add to DAG]
C8 --> C9{More sources?}
C9 -->|Yes| C6
C9 -->|No| C10[Propagate Timestamps]
C10 --> C11[Determine Needed Files]
C11 --> C12{Files to compile?}
C12 -->|No| C20[Save DAG and Exit]
C12 -->|Yes| C13[Separate First Files]
C13 --> C14[Compile Parse Transforms]
C14 --> C15[Split Sequential/Parallel]
C15 --> C16[Compile Sequential Files]
C16 --> C17[Compile Parallel Files]
C17 --> C18[Store Artifact Metadata]
C18 --> C19[Update DAG Timestamps]
C19 --> C20
C20 --> CE[Compiler Done]
style C fill:#e1f5ff
style CE fill:#e1ffe1
Stage Dependencies
The following diagram shows dependencies between major stages:
graph LR
Init[Initialization] --> DepResolve[Dependency Resolution]
DepResolve --> DepAcq[Dependency Acquisition]
DepAcq --> AppDisc[Application Discovery]
AppDisc --> CompOrder[Compilation Order]
CompOrder --> DepComp[Dependency Compilation]
CompOrder --> ProjComp[Project Compilation]
DepComp --> ProjComp
ProjComp --> Verify[Build Verification]
style Init fill:#e1f5ff
style Verify fill:#e1ffe1
Key Concepts
Provider
A provider is a unit of functionality in rebar3. Each provider:
- Has a unique name and namespace
- Declares dependencies on other providers
- Implements initialization and execution functions
- Can be extended or overridden by plugins
The compile provider depends on the lock provider, ensuring dependencies are resolved before compilation begins.
State
The rebar state is a record that flows through all stages, containing:
- Configuration options from
rebar.config - Lists of applications (project apps, dependencies, plugins)
- Code paths for different contexts
- Active profiles
- Registered providers and compilers
- Current execution context
Application Info
Each application (project or dependency) has an associated app info record containing:
- Application name and version
- Source and output directories
- Path to
.app.srcand.appfiles - Application-specific configuration options
- Dependency list
- Profile information
Directed Acyclic Graph (DAG)
Each compiler maintains a DAG to track file dependencies:
- Vertices: Source files, header files, and artifacts with timestamps
- Edges: Dependency relationships
- Purpose: Incremental compilation, determining compilation order
- Persistence: Saved to disk between builds
- Invalidation: Recalculated if compiler version or critical options change
Compiler
A compiler is a module implementing the rebar_compiler behavior:
- Defines what source files it handles (extensions, directories)
- Analyzes dependencies between files
- Determines compilation order
- Executes compilation for individual files
- Tracks generated artifacts
Hooks
Hooks allow execution of custom code at specific points:
- Provider hooks: Run other rebar3 providers
- Shell hooks: Execute shell commands
- Pre/post hooks: Run before or after specific stages
- Platform-specific: Conditional execution based on OS
Profiles
Profiles allow configuration variations:
- Default profile:
default - Common profiles:
test,prod - Custom profiles: User-defined
- Profile stacking: Configuration merges from default + active profiles
- Separate build outputs:
_build/PROFILE/
Incremental Compilation
Optimization that avoids unnecessary recompilation:
- Tracks file modification timestamps
- Propagates timestamps through dependency chains
- Recompiles only changed files and their dependents
- Detects compiler option changes
- Stores metadata with artifacts
Parallel Compilation
Performance optimization for independent files:
- Files with no interdependencies compiled concurrently
- Worker pool manages parallel tasks
- Parse transforms and dependent files compiled sequentially
- Configurable parallelism level
Initialization & Configuration
Purpose
The initialization stage prepares the rebar3 runtime environment by loading configuration files, setting up logging, initializing the state structure, loading plugins, and registering providers. This stage establishes the foundation for all subsequent compilation and build operations.
When It Executes
This is the first stage in the rebar3 execution chain. It runs immediately when:
rebar3 compile(or any command) is invoked from the command linerebar3:run/2is called from the Erlang API
Prerequisites
- Erlang/OTP runtime is available
- Current working directory is accessible
- Required Erlang applications (crypto, ssl, inets) can be started
Outputs
- Initialized
rebar_state:t()record containing:- Loaded configuration from
rebar.config - Merged global configuration (if
~/.config/rebar3/rebar.configexists) - Applied profiles
- Registered providers
- Installed plugins
- Code paths for the build
- Lock file data
- Loaded configuration from
Execution Flow
graph TD
A[Start: rebar3 main/1] --> B[start_and_load_apps]
B --> B1[Load rebar application]
B1 --> B2[Start crypto, asn1, public_key]
B2 --> B3{Offline mode?}
B3 -->|No| B4[Start ssl, inets, httpc]
B3 -->|Yes| C
B4 --> C[init_config/0]
C --> C1[Set HTTPC options]
C1 --> C2[Initialize logging system]
C2 --> C3[Consult rebar.config]
C3 --> C4[Consult rebar.lock]
C4 --> C5[Merge locks into config]
C5 --> C6[Create initial state]
C6 --> C7{Global config exists?}
C7 -->|Yes| C8[Load global config]
C7 -->|No| C9
C8 --> C9[Set escript path]
C9 --> C10[Initialize vsn cache]
C10 --> D[run_aux]
D --> D1[Set Unicode encoding]
D1 --> D2{REBAR_PROFILE env set?}
D2 -->|Yes| D3[Apply profile from env]
D2 -->|No| D4
D3 --> D4[Check minimum OTP version]
D4 --> D5{HEX_CDN env set?}
D5 -->|Yes| D6[Set CDN URL]
D5 -->|No| D7
D6 --> D7[Set compilers from app env]
D7 --> D8[Create resources]
D8 --> D9[Bootstrap test profile]
D9 --> D10{REBAR_BASE_DIR env set?}
D10 -->|Yes| D11[Override base_dir]
D10 -->|No| D12
D11 --> D12{REBAR_CACHE_DIR env set?}
D12 -->|Yes| D13[Set global rebar dir]
D12 -->|No| D14
D13 --> D14[Create logic providers]
D14 --> D15{REBAR_SKIP_PROJECT_PLUGINS?}
D15 -->|No| D16[Install project plugins]
D15 -->|Yes| D17
D16 --> D17[Install top-level plugins]
D17 --> D18[Set default opts]
D18 --> D19[Parse command args]
D19 --> D20{--offline flag?}
D20 -->|Yes| D21[Set offline mode]
D20 -->|No| D22
D21 --> D22[Set code paths]
D22 --> D23[init_command]
D23 --> E[State Ready]
style A fill:#e1f5ff
style E fill:#e1ffe1
Detailed Steps
-
Application Loading (
start_and_load_apps/1)- Load the
rebarapplication - Start required applications:
crypto,asn1,public_key - If not offline: start
ssl,inets, and create an httpc profile
- Load the
-
Base Configuration (
init_config/0)- Set HTTPC options for package downloads
- Initialize logging with appropriate verbosity level
- Read
rebar.configfrom project root - Read
rebar.lockif it exists - Merge lock data into configuration
- Create initial state with merged configuration
-
Global Configuration (in
init_config/0)- Check for
~/.config/rebar3/rebar.config - If exists, load and merge with project configuration
- Install global plugins (from global config)
- Check for
-
Environment Setup (
run_aux/2)- Set shell encoding to Unicode
- Apply
REBAR_PROFILEenvironment variable if set - Validate OTP version requirements
- Configure Hex CDN URL if specified
-
Compilers and Resources
- Load compilers from application environment
- Create and register resource modules (for deps)
- Bootstrap test profile with TEST macro and test directories
-
Directory Configuration
- Set
base_dir(default:_build, override withREBAR_BASE_DIR) - Set
global_rebar_dir(default:~/.cache/rebar3, override withREBAR_CACHE_DIR)
- Set
-
Provider and Plugin System
- Register built-in providers
- Install project plugins (unless
REBAR_SKIP_PROJECT_PLUGINSis set) - Install top-level plugins
- Merge all provider lists
-
Command Preparation
- Parse command-line arguments
- Set offline mode if requested
- Initialize code paths
- Call
rebar_core:init_command/2to dispatch to the actual command
Functions & API Calls
rebar3:main/1
Purpose: Entry point from escript
Signature:
-spec main([string()]) -> no_return().
Arguments:
Args([string()]): Command-line arguments
Returns: Does not return; calls erlang:halt/1
Flow:
- Calls
rebar3:run/1 - Handles success/error
- Exits with appropriate code
rebar3:run/1
Purpose: Main execution entry point from command line
Signature:
-spec run([string()]) -> {ok, rebar_state:t()} | {error, term()}.
Arguments:
RawArgs([string()]): Command-line arguments
Returns:
{ok, State}: Successful execution with final state{error, Reason}: Error occurred
Flow:
- Start and load applications
- Call
init_config/0to create base state - Set
callertocommand_line - Call
set_options/2to process global flags - Call
run_aux/2for actual execution
rebar3:init_config/0
Purpose: Set up base configuration and initial state
Signature:
-spec init_config() -> rebar_state:t().
Returns: Initialized state record
Flow:
- Set HTTPC options via
rebar_utils:set_httpc_options/0 - Initialize logging via
rebar_log:init/2 - Read
rebar.configviarebar_config:consult_root/0 - Read
rebar.lockviarebar_config:consult_lock_file/1 - Merge locks into config via
rebar_config:merge_locks/2 - Create state via
rebar_state:new/1 - Load global config if exists
- Set escript path
- Initialize vsn cache
Example Usage:
BaseState = rebar3:init_config()
rebar_config:consult_root/0
Purpose: Read the main rebar.config file
Signature:
-spec consult_root() -> [term()].
Returns: List of configuration terms
Flow:
- Looks for
rebar.configin current directory - Parses as Erlang terms
- Returns empty list if file doesn't exist
Called From: Initialization & Configuration
rebar_config:consult_lock_file/1
Purpose: Read and parse the lock file
Signature:
-spec consult_lock_file(file:filename()) -> [term()].
Arguments:
File(file:filename()): Path torebar.lock
Returns: List of lock entries in internal format
Flow:
- Read lock file
- Detect version (beta, "1.2.0", etc.)
- Parse locks based on version
- Extract package hashes
- Expand locks with hash information
- Return internal lock format
Lock File Versions:
- Beta format:
[Locks]. - Versioned format:
{"1.2.0", Locks}. [Attrs].
rebar_state:new/0,1,2,3
Purpose: Create a new rebar state record
Signature:
-spec new() -> t().
-spec new(list()) -> t().
-spec new(t() | atom(), list()) -> t().
-spec new(t(), list(), file:filename_all()) -> t().
Arguments (for new/1):
Config(list()): Configuration terms fromrebar.config
Returns: New state record
Flow (for new/1):
- Convert config list to dict via
base_opts/1 - Create base state with opts
- Set current working directory
- Set default opts
State Record:
-record(state_t, {
dir :: file:name(),
opts = dict:new() :: rebar_dict(),
code_paths = dict:new() :: rebar_dict(),
default = dict:new() :: rebar_dict(),
escript_path :: undefined | file:filename_all(),
lock = [],
current_profiles = [default] :: [atom()],
namespace = default :: atom(),
command_args = [],
command_parsed_args = {[], []},
current_app :: undefined | rebar_app_info:t(),
project_apps = [] :: [rebar_app_info:t()],
deps_to_build = [] :: [rebar_app_info:t()],
all_plugin_deps = [] :: [rebar_app_info:t()],
all_deps = [] :: [rebar_app_info:t()],
compilers = [] :: [module()],
project_builders = [] :: [{project_type(), module()}],
resources = [],
providers = [],
allow_provider_overrides = false :: boolean()
}).
rebar_state:apply_profiles/2
Purpose: Apply configuration profiles to state
Signature:
-spec apply_profiles(t(), [atom()]) -> t().
Arguments:
State(t()): Current stateProfiles([atom()]): List of profiles to apply (e.g.,[test],[prod])
Returns: State with merged profile configuration
Flow:
- Get profiles config from state
- For each profile (except
default):- Get profile-specific options
- Merge with current opts using
rebar_opts:merge_opts/3
- Update
current_profilesfield - Return modified state
Profile Merging:
- Base configuration is from
defaultprofile - Profile-specific configs override base
- Multiple profiles stack in order given
rebar_plugins:project_plugins_install/1
Purpose: Install plugins specified in rebar.config
Signature:
-spec project_plugins_install(rebar_state:t()) -> rebar_state:t().
Arguments:
State(rebar_state:t()): Current state
Returns: State with plugins installed and providers registered
Flow:
- Get
project_pluginsfrom configuration - For each plugin:
- Fetch plugin (from Hex or Git)
- Compile plugin
- Load plugin application
- Discover and register providers from plugin
- Update state with new providers
rebar_state:create_logic_providers/2
Purpose: Register built-in providers with the state
Signature:
-spec create_logic_providers([module()], t()) -> t().
Arguments:
Providers([module()]): List of provider modulesState(t()): Current state
Returns: State with providers registered
Flow:
- For each provider module:
- Call
Module:init(State)to get provider record - Add to providers list
- Call
- Return updated state
Built-in Providers (from application env):
rebar_prv_app_discoveryrebar_prv_compilerebar_prv_cleanrebar_prv_install_depsrebar_prv_lock- And many more...
rebar_core:init_command/2
Purpose: Initialize and dispatch to the requested command
Signature:
-spec init_command(rebar_state:t(), atom()) -> {ok, rebar_state:t()} | {error, term()}.
Arguments:
State(rebar_state:t()): Current stateCommand(atom()): Command to execute (e.g.,compile,test)
Returns:
{ok, NewState}: Command executed successfully{error, Reason}: Command failed
Flow:
- Handle special commands (
do,as) - Call
process_namespace/2to resolve namespace - Call
process_command/2to execute provider chain
State Modification
Fields Set During Initialization
| Field | When Set | Value | Purpose |
|---|---|---|---|
dir | new/0,1 | rebar_dir:get_cwd() | Project root directory |
opts | new/1 | Parsed rebar.config | Configuration options |
default | new/1 | Copy of opts | Original configuration before profile application |
escript_path | init_config/0 | Path to rebar3 escript | For extracting embedded resources |
lock | new/3 | Parsed rebar.lock | Locked dependency versions |
current_profiles | apply_profiles/2 | [default] or specified | Active configuration profiles |
compilers | run_aux/2 | [rebar_compiler_xrl, ...] | Registered compiler modules |
resources | run_aux/2 | [rebar_git_resource, ...] | Resource modules for fetching deps |
providers | create_logic_providers/2 | List of provider records | Available commands |
command_args | run_aux/2 | Remaining CLI args | Arguments to pass to command |
Configuration Keys in opts
Common configuration keys stored in the opts dictionary:
deps: List of dependenciesplugins: List of pluginsproject_plugins: List of project-scoped pluginsprofiles: Profile-specific configurationerl_opts: Erlang compiler optionssrc_dirs: Source directoriesextra_src_dirs: Additional source directoriesminimum_otp_vsn: Minimum OTP version requiredbase_dir: Build output directoryartifacts: List of expected artifacts
Configuration
Configuration Files
rebar.config
Location: Project root directory
Format: Erlang terms
Common Options:
{erl_opts, [debug_info, warnings_as_errors]}.
{deps, [
{jsx, "3.1.0"},
{cowboy, {git, "https://github.com/ninenines/cowboy.git", {tag, "2.9.0"}}}
]}.
{plugins, [rebar3_hex]}.
{profiles, [
{test, [
{deps, [meck, proper]},
{erl_opts, [{d, 'TEST'}]}
]}
]}.
rebar.lock
Location: Project root directory
Format: Versioned Erlang terms
Purpose: Lock dependency versions for reproducible builds
Example:
{"1.2.0",
[{<<"jsx">>, {pkg,<<"jsx">>,<<"3.1.0">>}, 0}]}.
[
{pkg_hash,[
{<<"jsx">>, <<"...">>}]}
].
~/.config/rebar3/rebar.config
Location: User's global configuration directory
Purpose: Global settings and plugins
Common Uses:
- Global plugins (hex, dialyzer, etc.)
- Hex repository credentials
- Global compiler options
Environment Variables
| Variable | Effect | Default |
|---|---|---|
REBAR_PROFILE | Override active profile | none |
REBAR_BASE_DIR | Build output directory | _build |
REBAR_CACHE_DIR | Global cache location | ~/.cache/rebar3 |
REBAR_OFFLINE | Disable network access | not set |
REBAR_SKIP_PROJECT_PLUGINS | Skip project plugin installation | not set |
HEX_CDN | Hex package CDN URL | default Hex CDN |
QUIET | Minimal output | not set |
DEBUG | Verbose debugging output | not set |
DIAGNOSTIC | Very verbose output | not set |
Command-Line Options
Global options (available for all commands):
-h,--help: Show help-v,--version: Show version information--offline: Run in offline mode (no network access)
Configuration Precedence
Configuration is merged in the following order (later overrides earlier):
- Built-in defaults
~/.config/rebar3/rebar.config(global config)- Project
rebar.config - Profile-specific configuration
- Environment variables
- Command-line options
File System Operations
Files Read
| File | Purpose | Required | When Read |
|---|---|---|---|
rebar.config | Project configuration | No (uses defaults) | init_config/0 |
rebar.lock | Locked dependencies | No | init_config/0 |
~/.config/rebar3/rebar.config | Global configuration | No | init_config/0 |
| Escript archive | Plugin/template extraction | No | When needed |
Files Written
None in this stage. Files are written in later stages.
Directories Created
None in this stage. Directories are created during compilation.
Directories Accessed
- Current working directory (project root)
~/.config/rebar3/(for global config)~/.cache/rebar3/(may be accessed for cached data)
Error Conditions
Missing Required Applications
Condition: Required Erlang application fails to start (e.g., crypto, ssl)
Detection: application:start/1 returns {error, Reason}
Error Message:
Rebar dependency <app> could not be loaded for reason <reason>
Recovery: None; rebar3 exits with code 1
Unsupported OTP Version
Condition: OTP version is below minimum_otp_vsn
Detection: rebar_utils:check_min_otp_version/2
Error Message:
ERROR: OTP release <version> does not match required regex <regex>
Recovery: None; must upgrade OTP
Blacklisted OTP Version
Condition: OTP version matches blacklisted_otp_vsns
Detection: rebar_utils:check_blacklisted_otp_versions/1
Error Message:
ERROR: OTP release <version> matches blacklisted version <regex>
Recovery: None; must change OTP version
Invalid Configuration Format
Condition: rebar.config contains invalid Erlang terms
Detection: Parse error in rebar_config:consult_file/1
Error Message:
ERROR: Error reading config file <file>: <parse error>
Recovery: None; must fix rebar.config
Invalid Profile Configuration
Condition: Profile configuration is not a list
Detection: rebar_state:apply_profiles/2
Error:
{profile_not_list, Profile, Other}
Error Message:
Profile config must be a list but for profile '<profile>' config given as: <other>
Recovery: None; must fix profile configuration
Newer Lock File Version
Condition: rebar.lock has a newer version than supported
Detection: Version check in rebar_config:consult_lock_file/1
Warning Message:
Rebar3 detected a lock file from a newer version. It will be loaded in compatibility mode,
but important information may be missing or lost. It is recommended to upgrade Rebar3.
Recovery: Loads in compatibility mode; may lose some information
Command Not Found
Condition: Requested command doesn't match any provider
Detection: rebar_core:process_namespace/2
Error Message:
Command <command> not found
Recovery: None; check command spelling or available commands
Edge Cases
No rebar.config File
Behavior: rebar3 uses built-in defaults
Impact:
- No dependencies
- Default compiler options
- Standard directory layout expected
Empty rebar.config
Behavior: Treated same as missing file
Impact: Uses all defaults
rebar.config.script
Behavior: If rebar.config.script exists, it's evaluated as Erlang code
Purpose: Dynamic configuration generation
Example:
CONFIG1 = case os:getenv("ENV") of
"prod" -> [{erl_opts, [no_debug_info]}];
_ -> []
end,
CONFIG1 ++ CONFIG.
Global Config Without Local Config
Behavior: Global config provides base configuration
Use Case: Setting up a new project with global plugins available
Multiple Profiles Applied
Behavior: Profiles merge in order, later values override earlier
Example:
rebar3 as test,custom compile
Applies profiles: default → test → custom
REBAR_PROFILE Environment Variable with as Command
Behavior: Environment variable overrides are replaced by as profiles
Recommendation: Use as command instead of environment variable
Cross References
Stages That Depend On This Stage
- All stages: Every stage requires initialized state
- Dependency Resolution & Locking: Uses lock data from initialization
- Dependency Acquisition: Uses configuration for app discovery
- Compilation Order Determination: Uses compiler configuration
Related Documentation
- State Management: Detailed state structure
- Configuration Reference: All configuration options
Example Scenarios
Scenario 1: Simple Project Initialization
Setup:
my_app/
├── rebar.config
└── src/
└── my_app.erl
rebar.config:
{erl_opts, [debug_info]}.
Execution: rebar3 compile
Flow:
- Read
rebar.config→[{erl_opts, [debug_info]}] - No
rebar.lock→ empty lock - Create state with opts
- Apply default profile
- No plugins to install
- Register built-in providers
- Dispatch to compile command
Result: State initialized with debug_info enabled
Scenario 2: Project with Test Profile
rebar.config:
{erl_opts, [debug_info]}.
{profiles, [
{test, [
{deps, [meck]},
{erl_opts, [nowarn_export_all]}
]}
]}.
Execution: rebar3 as test compile
Flow:
- Load base config
- Apply
defaultprofile - Apply
testprofile:- Add
meckto deps - Add
nowarn_export_alltoerl_opts
- Add
- Result:
{erl_opts, [nowarn_export_all, debug_info]}
Scenario 3: Global Plugin Installation
~/.config/rebar3/rebar.config:
{plugins, [rebar3_hex]}.
Execution: rebar3 compile (in any project)
Flow:
- Load project config
- Detect global config exists
- Load global config
- Install
rebar3_hexplugin globally - Register hex providers (publish, etc.)
- Proceed with project compilation
Result: Hex commands available in all projects
Scenario 4: Environment Variable Override
rebar.config:
{erl_opts, [debug_info]}.
{profiles, [
{prod, [{erl_opts, [no_debug_info]}]}
]}.
Execution: REBAR_PROFILE=prod rebar3 compile
Flow:
- Load base config
- Detect
REBAR_PROFILE=prod - Apply
prodprofile - Result:
{erl_opts, [no_debug_info]}
Scenario 5: Offline Mode
Execution: rebar3 compile --offline
Flow:
- Parse
--offlineflag - Set
REBAR_OFFLINE=1environment variable - Skip starting
sslandinets - Set
offlineflag in state - Later stages skip network operations
Impact: Dependencies must already be cached
Dependency Resolution & Locking
Purpose
The dependency resolution and locking stage determines which dependencies are required for the project, resolves version constraints for package dependencies, builds a complete dependency tree, detects circular dependencies, and ensures reproducible builds through lock files.
When It Executes
This stage executes after Initialization & Configuration and is typically triggered by:
- The
install_depsprovider (dependency ofcompile) - The
lockprovider (explicitly locking dependencies) - Any command that requires dependencies to be resolved
The compile provider depends on the lock provider, which depends on install_deps.
Prerequisites
- State initialized with configuration loaded
rebar.configparsed with dependencies listrebar.lockread (if exists) with locked versions- Application discovery completed (for project apps)
Outputs
- Complete list of all dependencies (direct and transitive)
- Dependency tree with levels (depth from project root)
- Resolved versions for all package dependencies
- Updated
rebar.lockfile with locked versions - Topologically sorted dependency list (compilation order)
- Updated state with:
all_deps: All resolved dependenciesdeps_to_build: Dependencies that need compilationlock: Current lock data
Execution Flow
graph TD
A[Start: install_deps provider] --> B[Get current profiles]
B --> C[Get project apps]
C --> D[Check upgrade flag]
D --> E[deps_per_profile]
E --> E1[For each profile]
E1 --> E2[Get parsed deps for profile]
E2 --> E3{More profiles?}
E3 -->|Yes| E1
E3 -->|No| F[Get locks from state]
F --> G[handle_profile_level]
G --> G1{Deps remaining?}
G1 -->|No| H[find_cycles]
G1 -->|Yes| G2[For each dep at current level]
G2 --> G3{Dep is top-level app?}
G3 -->|Yes| G4[Skip this dep]
G3 -->|No| G5{Dep already seen?}
G4 --> G6{More deps at level?}
G5 -->|Yes| G7[Check version conflict]
G5 -->|No| G8[maybe_lock]
G7 --> G9{Same source?}
G9 -->|Yes| G6
G9 -->|No| G10[Warn about skip]
G10 --> G6
G8 --> G11{In default profile?}
G11 -->|Yes| G12[Add to lock list]
G11 -->|No| G13[Skip locking]
G12 --> G14[maybe_fetch]
G13 --> G14
G14 --> G15[handle_dep]
G15 --> G16[Parse dep's deps]
G16 --> G17[Add to next level]
G17 --> G6
G6 -->|Yes| G2
G6 -->|No| G18[Process next level]
G18 --> G1
H --> H1[Build digraph of all apps]
H1 --> H2[Add vertices for each app]
H2 --> H3[Add edges for dependencies]
H3 --> H4[digraph_utils:topsort]
H4 --> H5{Acyclic?}
H5 -->|Yes| H6[Return sorted list]
H5 -->|No| H7[Find strongly connected components]
H7 --> H8[Return cycles error]
H6 --> I[cull_compile]
I --> I1[Remove project apps from sorted list]
I1 --> I2[Drop deps that don't need compile]
I2 --> J[Update state: deps_to_build]
J --> K[lock provider]
K --> K1{Running in default profile?}
K1 -->|No| L[Skip locking]
K1 -->|Yes| K2[build_locks]
K2 --> K3[Get all locked deps from state]
K3 --> K4[For each dep not checkout]
K4 --> K5[Create lock tuple]
K5 --> K6[Sort locks by name]
K6 --> K7[maybe_write_lock_file]
K7 --> K8{Locks changed?}
K8 -->|Yes| K9[Write rebar.lock]
K8 -->|No| K10[Check format changed]
K10 --> K11{Format different?}
K11 -->|Yes| K9
K11 -->|No| L
K9 --> L[End: Dependencies resolved and locked]
style A fill:#e1f5ff
style L fill:#e1ffe1
Detailed Steps
-
Profile-Based Dependency Collection (
deps_per_profile/3)- Get active profiles (e.g.,
[default],[default, test]) - For each profile, get parsed dependencies
- Load lock file data for consistency
- Get active profiles (e.g.,
-
Level-Order Dependency Traversal (
handle_profile_level/7)- Process dependencies level by level (breadth-first)
- Level 0: Direct dependencies from
rebar.config - Level 1: Dependencies of level 0 dependencies
- Continue until no more dependencies found
-
Dependency Processing (
update_dep/9)- For each dependency:
- Check if already seen (to avoid duplicates)
- Check if it's a top-level app (skip if so)
- Add to lock list if in default profile
- Fetch the dependency (or verify it exists)
- Parse the dependency's own dependencies
- Add transitive deps to next level
- For each dependency:
-
Lock Management (
maybe_lock/5)- Only lock dependencies in default profile
- Skip checkout dependencies
- Track dependency level (depth in tree)
- Replace existing locks if new one is shallower
-
Cycle Detection (
find_cycles/1)- Build directed graph of all applications
- Vertices: application names
- Edges: application → dependency relationships
- Use
digraph_utils:topsort/1for topological sort - If sort fails, find strongly connected components (cycles)
-
Compilation Order (
cull_compile/2)- Start with topologically sorted dependency list
- Remove project applications (they compile separately)
- Drop dependencies that don't need compilation (already built)
- Return final list for
deps_to_build
-
Lock File Writing (
lockprovider)- Only runs in default profile
- Build lock entries from state
- Each lock:
{Name, Source, Level} - Sort locks alphabetically
- Write to
rebar.lockif changed - Preserve lock file format version
Functions & API Calls
rebar_prv_install_deps:do/1
Purpose: Main entry point for dependency resolution
Signature:
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
Arguments:
State(rebar_state:t()): Current state with configuration
Returns:
{ok, State}: Dependencies resolved and state updated{error, Reason}: Resolution failed
Flow:
- Get current profiles
- Get project apps
- Check if upgrade mode is enabled
- Resolve deps per profile via
deps_per_profile/3 - Update state with all deps
- Build and update code paths
- Detect cycles via
find_cycles/1 - Determine compile order via
cull_compile/2 - Update state with
deps_to_build
Called From: Provider system (as dependency of lock and compile)
deps_per_profile/3
Purpose: Collect and resolve dependencies for all active profiles
Signature:
-spec deps_per_profile([atom()], boolean(), rebar_state:t()) ->
{[rebar_app_info:t()], rebar_state:t()}.
Arguments:
Profiles([atom()]): Active profiles (e.g.,[default, test])Upgrade(boolean()): Whether to upgrade dependenciesState(rebar_state:t()): Current state
Returns:
{Apps, State}: Resolved dependencies and updated state
Flow:
- Get locks from state
- For each profile, get parsed dependencies at level 0
- Create
RootSeenset with project app names - Call
handle_profile_level/7for traversal
handle_profile_level/7
Purpose: Level-order traversal of dependency tree across all profiles
Signature:
-spec handle_profile_level(
[{Profile, Deps, Level}],
Apps,
RootSeen,
Seen,
Upgrade,
Locks,
State
) -> {Apps, State} when
Profile :: atom(),
Deps :: [rebar_app_info:t()],
Level :: integer(),
Apps :: [rebar_app_info:t()],
RootSeen :: sets:set(),
Seen :: sets:set(),
Upgrade :: boolean(),
Locks :: [term()],
State :: rebar_state:t().
Arguments:
- Profile/Deps/Level tuples: Dependencies per profile at each level
Apps: Accumulated resolved dependenciesRootSeen: Set of top-level app names (never process as deps)Seen: Set of already-processed dependency namesUpgrade: Whether upgradingLocks: Lock file dataState: Current state
Returns: {Apps, State} with all resolved dependencies
Algorithm:
For each {Profile, Deps, Level}:
For each Dep in Deps:
If Dep is in RootSeen:
Skip (it's a top-level app)
Else if Dep is in Seen:
Check for version conflicts, warn if needed
Else:
Lock the dependency
Fetch the dependency
Parse the dep's own dependencies
Add new deps to next level
If new deps were found:
Append {Profile, NewDeps, Level+1} to queue
Process next level
update_dep/9
Purpose: Process a single dependency
Signature:
-spec update_dep(
AppInfo,
Profile,
Level,
Deps,
Apps,
State,
Upgrade,
Seen,
Locks
) -> {NewDeps, NewApps, NewState, NewSeen} when
AppInfo :: rebar_app_info:t(),
Profile :: atom(),
Level :: integer(),
Deps :: [rebar_app_info:t()],
Apps :: [rebar_app_info:t()],
State :: rebar_state:t(),
Upgrade :: boolean(),
Seen :: sets:set(),
Locks :: [term()].
Arguments:
AppInfo: Dependency to processProfile: Current profileLevel: Current level in dependency treeDeps: Accumulated dependencies for next levelApps: All resolved apps so farState: Current stateUpgrade: Upgrade flagSeen: Set of seen dependency namesLocks: Lock data
Returns: Updated accumulator tuple
Flow:
- Get dependency name
- Check if already seen
- If seen: check for conflicts, possibly warn
- If not seen:
- Lock the dependency
- Fetch/verify the dependency
- Handle the dependency (parse its deps)
- Add to accumulated apps
- Add transitive deps to next level
maybe_lock/5
Purpose: Add dependency to lock list if appropriate
Signature:
-spec maybe_lock(Profile, AppInfo, Seen, State, Level) -> {NewSeen, NewState} when
Profile :: atom(),
AppInfo :: rebar_app_info:t(),
Seen :: sets:set(),
State :: rebar_state:t(),
Level :: integer().
Arguments:
Profile: Current profileAppInfo: Dependency to potentially lockSeen: Set of seen dependenciesState: Current stateLevel: Depth in dependency tree
Returns: {NewSeen, NewState} with updated lock
Logic:
- Skip if checkout dependency
- Skip if not in default profile
- If already in lock at deeper level, replace with shallower
- Otherwise add to lock list
- Always add to seen set
find_cycles/1
Purpose: Detect circular dependencies
Signature:
-spec find_cycles([rebar_app_info:t()]) ->
{no_cycle, Sorted} | {cycles, Cycles} | {error, Error} when
Sorted :: [rebar_app_info:t()],
Cycles :: [[binary()]],
Error :: term().
Arguments:
Apps([rebar_app_info:t()]): All applications (project + deps)
Returns:
{no_cycle, Sorted}: No cycles; sorted topologically{cycles, Cycles}: Circular dependencies detected{error, Error}: Other error
Flow:
- Call
rebar_digraph:compile_order/1 - Which creates digraph and calls
digraph_utils:topsort/1 - If sort succeeds: return sorted list
- If sort fails: find strongly connected components
- Filter components with length > 1 (these are cycles)
- Return cycles
rebar_digraph:compile_order/1
Purpose: Build dependency graph and return topological sort
Signature:
-spec compile_order([rebar_app_info:t()]) ->
{ok, [rebar_app_info:t()]} | {error, no_sort | {cycles, [[binary()]]}}.
Arguments:
Apps([rebar_app_info:t()]): Applications to sort
Returns:
{ok, Sorted}: Topologically sorted applications{error, {cycles, Cycles}}: Circular dependencies found{error, no_sort}: Topological sort failed for other reason
Flow:
- Create new digraph
- For each app:
- Add vertex with app name
- Get all dependencies (from
applicationslist anddepsconfig) - Add edges from app to each dependency
- Call
digraph_utils:topsort/1 - If successful: reverse the list (dependencies first)
- If failed: determine if cyclic, extract cycles
- Delete digraph
- Return result
Example Graph:
my_app → [cowboy, jsx]
cowboy → [cowlib, ranch]
cowlib → []
ranch → []
jsx → []
Sorted: [cowlib, ranch, jsx, cowboy, my_app]
all_apps_deps/1
Purpose: Get all dependencies for an application
Signature:
-spec all_apps_deps(rebar_app_info:t()) -> [binary()].
Arguments:
App(rebar_app_info:t()): Application to analyze
Returns: List of dependency names (binaries)
Flow:
- Get
applicationslist from.appfile (runtime deps) - Get
depslist fromrebar.config(build deps) - Convert all to binaries
- Sort and merge (union)
Why Both Sources:
applications: Runtime dependencies declared in.appdeps: Build-time dependencies fromrebar.config- Union ensures all dependencies are considered for build order
cull_compile/2
Purpose: Filter dependency list to those needing compilation
Signature:
-spec cull_compile([rebar_app_info:t()], [rebar_app_info:t()]) -> [rebar_app_info:t()].
Arguments:
TopSortedDeps([rebar_app_info:t()]): All apps in compile orderProjectApps([rebar_app_info:t()]): Project's own applications
Returns: Dependencies that need compilation
Flow:
- Remove project apps from sorted list (they're compiled separately)
- Drop dependencies from start of list until finding one that needs compile
- Return remaining list
Logic for "needs compile":
- Checkout dependencies always need compile
- Package dependencies from Hex usually don't (pre-compiled)
- Source dependencies (Git, etc.) need compile
rebar_prv_lock:do/1
Purpose: Write dependency locks to rebar.lock
Signature:
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
Arguments:
State(rebar_state:t()): Current state with resolved deps
Returns: {ok, State} with locks saved
Flow:
- Check if running in default profile (only lock in default)
- Get old locks from state
- Build new locks via
build_locks/1 - Sort locks alphabetically
- Write to
rebar.lockif changed - Update state with new locks
- Report useless locks (removed dependencies)
- Report checkout dependencies (can't be locked)
build_locks/1
Purpose: Convert state's lock data to lock file format
Signature:
-spec build_locks(rebar_state:t()) -> [lock_entry()].
Arguments:
State(rebar_state:t()): Current state
Returns: List of lock entries
Lock Entry Format:
{Name :: binary(),
Source :: lock_source(),
Level :: integer()}
Example:
{<<"cowboy">>,
{git, "https://github.com/ninenines/cowboy.git",
{ref, "abc123def456..."}},
0}
Flow:
- Get all locked deps from state
- Filter out checkout dependencies
- For each dep:
- Get name
- Get lock source via
rebar_fetch:lock_source/2 - Get dependency level
- Create tuple
State Modification
Fields Modified During This Stage
| Field | Operation | Value | Purpose |
|---|---|---|---|
all_deps | Set | All resolved dependencies | Complete dep list |
deps_to_build | Set | Filtered dependency list | Deps needing compilation |
lock | Updated | Lock entries | For writing to rebar.lock |
{locks, default} | Set | Saved lock data | Lock file contents |
code_paths (all_deps key) | Set | Dep ebin directories | For compilation phase |
Dependency Information Stored
Each dependency (rebar_app_info:t()) contains:
name: Dependency namesource: Where it comes from (Hex, Git, etc.)dep_level: Depth in dependency tree (0 = direct)dir: Source directoryout_dir: Build output directoryis_checkout: Whether it's a checkout dependencyis_lock: Whether from lock filedeps: Transitive dependencies
Configuration
Dependency Specification Formats
Package Dependency (Hex)
Simple:
{deps, [
jsx % Latest version
]}.
With Version:
{deps, [
{jsx, "3.1.0"}, % Exact version
{jsx, "~> 3.1"}, % Semantic versioning
{jsx, ">= 2.0.0"} % Version constraint
]}.
Source Dependency (Git)
{deps, [
{cowboy, {git, "https://github.com/ninenines/cowboy.git", {tag, "2.9.0"}}},
{ranch, {git, "git://github.com/ninenines/ranch.git", {branch, "master"}}},
{gun, {git, "https://github.com/ninenines/gun.git", {ref, "abc123"}}}
]}.
Source Dependency (Mercurial)
{deps, [
{my_dep, {hg, "https://bitbucket.org/user/my_dep", {tag, "1.0.0"}}}
]}.
Checkout Dependency
Not in rebar.config. Created by placing dependency in _checkouts/ directory.
Behavior:
- Always compiled
- Never locked
- Overrides any version specified in config
Profile-Specific Dependencies
{profiles, [
{test, [
{deps, [
meck,
proper
]}
]},
{prod, [
{deps, [
recon
]}
]}
]}.
Merging:
- Profile deps added to default profile deps
- No removal; only addition
- Active profiles merge in order
Lock File Configuration
File: rebar.lock
Format Version: "1.2.0" (current)
Structure:
{"1.2.0",
[{<<"cowboy">>, {git, "https://github.com/ninenines/cowboy.git",
{ref, "abc123"}}, 0},
{<<"jsx">>, {pkg, <<"jsx">>, <<"3.1.0">>}, 0},
{<<"ranch">>, {git, "https://github.com/ninenines/ranch.git",
{ref, "def456"}}, 1}]}.
[
{pkg_hash, [
{<<"jsx">>, <<"ABCD1234...">>},
{<<"cowlib">>, <<"EFGH5678...">>}
]}
].
Components:
- Version string
- List of lock entries (name, source, level)
- List of package hashes (for integrity verification)
File System Operations
Files Read
| File | Purpose | When | Required |
|---|---|---|---|
rebar.lock | Get locked versions | Start of stage | No |
_checkouts/*/rebar.config | Checkout dep configs | During traversal | If checkout exists |
_build/*/lib/*/rebar.config | Cached dep configs | During traversal | For already-fetched deps |
Files Written
| File | Content | When | Conditions |
|---|---|---|---|
rebar.lock | Locked dependency versions | End of lock provider | Locks changed AND default profile |
Directories Accessed
_checkouts/: Checkout dependencies_build/PROFILE/lib/: Dependency locations- Various remote locations for fetching (if needed)
Error Conditions
Circular Dependency Detected
Condition: Dependency graph contains cycles
Detection: digraph_utils:topsort/1 returns false and digraph_utils:is_acyclic/1 returns false
Error Format:
{cycles, [[<<"app1">>, <<"app2">>, <<"app3">>], ...]}
Error Message:
Dependency cycle(s) detected:
applications: app1 app2 app3 depend on each other
Example:
app1 depends on app2
app2 depends on app3
app3 depends on app1
→ Cycle: [app1, app2, app3]
Recovery: None; must break the cycle in dependencies
Package Not Found in Registry
Condition: Hex package doesn't exist
Error Format:
{missing_package, Package, Version}
{missing_package, Package}
Error Message:
Package not found in registry: jsx-3.1.0
Package not found in registry: nonexistent_package
Recovery: Fix package name or check Hex registry
Bad Version Constraint
Condition: Invalid version specification
Error Format:
{bad_constraint, Name, Constraint}
Error Message:
Unable to parse version for package jsx: ~~>3.1.0
Recovery: Fix version constraint syntax
Package Not Buildable
Condition: Hex package exists but isn't rebar3-compatible
Error Format:
{not_rebar_package, Package, Version}
Error Message:
Package not buildable with rebar3: old_package-1.0.0
Recovery: Use different version or different package
Failed to Load Registry
Condition: Cannot download Hex registry
Error Format:
{load_registry_fail, Dep}
Error Message:
Error loading registry to resolve version of jsx. Try fixing by running 'rebar3 update'
Recovery: Run rebar3 update or check network connection
Dependency Application Not Found
Condition: Dependency fetched but application not in expected location
Error Format:
{dep_app_not_found, AppDir, AppName}
Error Message:
Dependency failure: Application my_dep not found at the top level of directory /path/to/dep
Possible Causes:
- Dependency doesn't have
.app.srcfile - Application name doesn't match directory name
- Dependency is not an OTP application
Recovery: Fix dependency structure or use different version
Edge Cases
Conflicting Dependency Versions
Scenario: Two dependencies require different versions of the same package
Behavior:
- First encountered version wins
- Later conflicts are warned about but skipped
- Lock file preserves the chosen version
Warning:
Skipping jsx (git source) as an app of the same name has already been fetched
Solution:
- Check if all dependencies can work with one version
- May need to upgrade/downgrade parent dependencies
Dependency at Multiple Levels
Scenario: Same dependency appears at different depths
Example:
my_app → dep_a → dep_common (level 2)
my_app → dep_b → dep_common (level 2)
my_app → dep_common (level 1)
Behavior:
- First occurrence wins (usually shallowest level)
- Lock file records the shallowest level
- Deeper occurrences skipped
Upgrade Mode
Triggered by: rebar3 upgrade
Behavior:
- Ignore lock file constraints
- Fetch latest versions matching constraints
- Update lock file with new versions
Effect on Warnings:
- Suppresses "already seen" warnings
- Shows upgrade activity
Checkout Dependencies
Location: _checkouts/dep_name/
Behavior:
- Always used, regardless of lock file
- Always compiled (never treated as binary)
- Never added to lock file
- Overrides any version specification
Use Cases:
- Local development on a dependency
- Testing unreleased dependency changes
- Temporary patches
Warning:
App my_dep is a checkout dependency and cannot be locked.
Empty Lock File
Behavior: All dependencies resolved from scratch
When This Happens:
- First time running rebar3
- After deleting
rebar.lock - In a new clone of the repository (if lock file not committed)
Profile Dependencies Don't Lock
Behavior: Only default profile dependencies are locked
Reason: Different profiles used in different contexts
Example:
{profiles, [
{test, [{deps, [meck]}]}
]}.
Running: rebar3 as test compile
Result: meck resolved but NOT added to rebar.lock
Transitive Dependency Version Pinning
Scenario: Lock file pins version of transitive dependency
Example:
% rebar.config
{deps, [{cowboy, "2.9.0"}]}.
% rebar.lock includes:
{<<"ranch">>, {git, "...", {ref, "..."}}, 1}
Behavior:
- Even though ranch is not direct dependency, it's locked
- Ensures exact same version tree on every build
- Provides true reproducibility
Cross References
Dependencies
- Initialization & Configuration: Configuration and lock file loading
- Application Discovery: Discovering project apps (needed for
RootSeen)
Dependents
- Dependency Acquisition: Fetching resolved dependencies
- Compilation Order Determination: Uses resolved deps
- Source File Compilation: Compiles resolved dependencies
Related
- State Management: State structure details
- Configuration Reference: Dependency configuration options
Example Scenarios
Scenario 1: Simple Project with Hex Dependencies
rebar.config:
{deps, [
jsx,
{cowboy, "2.9.0"}
]}.
Execution: rebar3 compile (first time, no lock file)
Flow:
-
Parse deps:
jsx(latest),cowboy"2.9.0" -
Query Hex registry for
jsxlatest → "3.1.0" -
Resolve
cowboy→ "2.9.0" -
Get cowboy's dependencies →
[cowlib, ranch] -
Resolve cowlib and ranch versions from cowboy's requirements
-
Build dependency graph:
my_app → [jsx, cowboy] cowboy → [cowlib, ranch] jsx → [] cowlib → [] ranch → [] -
Topological sort →
[cowlib, ranch, jsx, cowboy, my_app] -
Write lock file with all versions
-
Return deps to build:
[jsx, cowlib, ranch, cowboy]
rebar.lock created:
{"1.2.0",
[{<<"cowboy">>, {pkg, <<"cowboy">>, <<"2.9.0">>}, 0},
{<<"cowlib">>, {pkg, <<"cowlib">>, <<"2.11.0">>}, 1},
{<<"jsx">>, {pkg, <<"jsx">>, <<"3.1.0">>}, 0},
{<<"ranch">>, {pkg, <<"ranch">>, <<"1.8.0">>}, 1}]}.
[{pkg_hash, [...]}].
Scenario 2: Circular Dependency Detection
Setup:
app_a/rebar.config: {deps, [app_b]}.
app_b/rebar.config: {deps, [app_c]}.
app_c/rebar.config: {deps, [app_a]}.
Execution: rebar3 compile
Flow:
- Resolve app_a → depends on app_b
- Resolve app_b → depends on app_c
- Resolve app_c → depends on app_a
- Build graph: app_a → app_b → app_c → app_a
- Attempt topological sort → fails
- Find strongly connected components →
[[app_a, app_b, app_c]] - Return error
Error:
Dependency cycle(s) detected:
applications: app_a app_b app_c depend on each other
Scenario 3: Profile-Specific Dependencies
rebar.config:
{deps, [jsx]}.
{profiles, [
{test, [{deps, [meck, proper]}]}
]}.
Execution: rebar3 as test compile
Flow:
- Active profiles:
[default, test] - Level 0, default profile:
[jsx] - Level 0, test profile:
[meck, proper] - Resolve all at level 0
- Continue with transitive deps
- Build graph with all apps
- Topological sort
- Write lock file with ONLY
jsx(default profile only)
rebar.lock:
{"1.2.0",
[{<<"jsx">>, {pkg, <<"jsx">>, <<"3.1.0">>}, 0}]}.
[...].
Note: meck and proper NOT in lock file
Scenario 4: Using Checkout Dependencies
Setup:
my_app/
├── _checkouts/
│ └── my_dep/
│ ├── src/
│ └── rebar.config
├── rebar.config: {deps, [{my_dep, "1.0.0"}]}
└── src/
Execution: rebar3 compile
Flow:
- Parse deps:
my_depversion "1.0.0" - Discover checkout in
_checkouts/my_dep/ - Mark as checkout dependency
- Skip fetching (use local version)
- Resolve transitive deps from checkout's
rebar.config - In locking stage: skip adding to lock file
Output:
App my_dep is a checkout dependency and cannot be locked.
rebar.lock: Does NOT include my_dep
Scenario 5: Dependency Upgrade
Initial rebar.lock:
{"1.2.0",
[{<<"jsx">>, {pkg, <<"jsx">>, <<"3.0.0">>}, 0}]}.
Execution: rebar3 upgrade jsx
Flow:
- Set upgrade flag to
true - Ignore locked version for
jsx - Query Hex for latest version → "3.1.0"
- Fetch new version
- Resolve dependencies normally
- Update lock file with new version
New rebar.lock:
{"1.2.0",
[{<<"jsx">>, {pkg, <<"jsx">>, <<"3.1.0">>}, 0}]}.
Scenario 6: Git Dependency with Transitive Deps
rebar.config:
{deps, [
{my_git_dep, {git, "https://github.com/user/my_git_dep.git", {tag, "v1.0.0"}}}
]}.
my_git_dep/rebar.config:
{deps, [jsx]}.
Flow:
-
Resolve
my_git_depfrom Git -
Fetch and clone the repository
-
Parse
my_git_dep/rebar.config -
Find transitive dependency:
jsx -
Resolve
jsxfrom Hex -
Build graph:
my_app → [my_git_dep] my_git_dep → [jsx] jsx → [] -
Topological sort →
[jsx, my_git_dep, my_app]
rebar.lock:
{"1.2.0",
[{<<"jsx">>, {pkg, <<"jsx">>, <<"3.1.0">>}, 1},
{<<"my_git_dep">>, {git, "https://github.com/user/my_git_dep.git",
{ref, "abc123..."}}, 0}]}.
Note: jsx is level 1 (transitive), my_git_dep is level 0 (direct)
Dependency Acquisition
Purpose
The dependency acquisition stage fetches dependencies from their respective sources (Hex package registry, Git repositories, Mercurial repositories, local checkouts), verifies their integrity, extracts them to the appropriate locations, and prepares them for compilation. This stage ensures all required dependencies are available locally before compilation begins.
When It Executes
This stage executes as part of the dependency resolution process in Dependency Resolution & Locking, specifically within the update_unseen_dep/9 function via the maybe_fetch/5 call. It runs during:
install_depsprovider execution- First compilation when dependencies aren't cached
- When dependencies need updating
- After modifying
rebar.configdependencies
Prerequisites
- Dependencies resolved with specific versions/refs
- Source information available (Hex, Git URL, etc.)
- Network access (unless offline mode or dependencies cached)
- Git/Mercurial tools installed (for VCS dependencies)
Outputs
- Dependencies downloaded to
_build/PROFILE/lib/DEPNAME/ - Dependency configurations parsed and validated
- Application info updated with actual dependency structure
- Dependencies ready for compilation
Execution Flow
graph TD
A[Start: maybe_fetch] --> B{Offline mode?}
B -->|Yes| C{Dep already exists?}
B -->|No| D[download_source]
C -->|Yes| E[Use cached version]
C -->|No| F[Error: offline, no cache]
D --> D1[Resource type detection]
D1 --> D2{Source type?}
D2 -->|pkg| G[Hex Package Download]
D2 -->|git| H[Git Clone/Fetch]
D2 -->|hg| I[Mercurial Clone]
G --> G1[Check local cache]
G1 --> G2{Cached with valid ETag?}
G2 -->|Yes| G3{ETag matches server?}
G2 -->|No| G4[Download from Hex]
G3 -->|Yes| G5[Use cached tarball]
G3 -->|No| G4
G4 --> G6[r3_hex_repo:get_tarball]
G6 --> G7[Verify checksum]
G7 --> G8{Checksum valid?}
G8 -->|Yes| G9[Store tarball in cache]
G8 -->|No| G10[Error: bad checksum]
G9 --> G11[Extract tarball to TmpDir]
G5 --> G11
H --> H1{Git version check}
H1 --> H2{Branch, Tag, or Ref?}
H2 -->|Branch| H3[git clone -b branch --single-branch]
H2 -->|Tag| H4[git clone -b tag --single-branch]
H2 -->|Ref| H5[git clone + checkout ref]
H3 --> H6[Clone complete]
H4 --> H6
H5 --> H6
I --> I1[hg clone]
I1 --> I2{Tag or revision?}
I2 -->|Tag| I3[hg update tag]
I2 -->|Revision| I4[hg update -r rev]
I3 --> I5[Clone complete]
I4 --> I5
G11 --> J[Move TmpDir to FetchDir]
H6 --> J
I5 --> J
E --> J
J --> K[Read dep's rebar.config]
K --> L[Update app info with config]
L --> M[find_app: Discover application]
M --> M1{Application found?}
M1 -->|Yes| N[Mark as available]
M1 -->|No| O[Error: dep_app_not_found]
N --> P[Return updated AppInfo]
style A fill:#e1f5ff
style P fill:#e1ffe1
style F fill:#ffe1e1
style G10 fill:#ffe1e1
style O fill:#ffe1e1
Detailed Steps
-
Fetch Decision (
maybe_fetch/5)- Check if dependency already exists locally
- Check if update is needed
- Skip if checkout dependency (already local)
- Decide whether to fetch or use cached version
-
Source Type Detection
- Determine resource type from source tuple:
{pkg, Name, Version, _, _}→ Hex package{git, URL, RefSpec}→ Git repository{hg, URL, RevSpec}→ Mercurial repository
- Determine resource type from source tuple:
-
Offline Mode Handling
- If offline mode and dependency not cached: error
- If offline mode and dependency cached: use cache
- Otherwise proceed with download
-
Package Download (Hex) (
rebar_pkg_resource:download/4)- Check local cache directory for existing package
- Read ETag file to get cached ETag
- Request package from Hex with If-None-Match header
- If 304 response: use cached tarball
- If 200 response: download new tarball
- Verify checksum against registry
- Store tarball and ETag in cache
- Extract tarball to temporary directory
-
Git Clone (
rebar_git_resource:download/4)- Detect Git version for optimal clone command
- For branches:
git clone -b branch --single-branch - For tags:
git clone -b tag --single-branch - For refs:
git clone+git checkout ref - Clone to temporary directory
- Handle authentication if required
-
Mercurial Clone (
rebar_hg_resource:download/4)- Use
hg cloneto clone repository - Update to specific tag or revision
- Clone to temporary directory
- Use
-
Extraction and Placement
- Create temporary directory with
ec_file:insecure_mkdtemp/0 - Download/extract to temporary directory
- Remove old cached version if exists
- Move temporary directory to final location
- Final location:
_build/PROFILE/lib/DEPNAME/
- Create temporary directory with
-
Post-Download Verification
- Read dependency's
rebar.config - Update app info with dependency's configuration
- Discover application structure with
rebar_app_discover:find_app/4 - Verify application is valid
- Mark application as available
- Read dependency's
Functions & API Calls
maybe_fetch/5
Purpose: Determine if dependency needs fetching and fetch if necessary
Signature:
-spec maybe_fetch(AppInfo, Profile, Upgrade, Seen, State) -> {Cached | false, AppInfo} when
AppInfo :: rebar_app_info:t(),
Profile :: atom(),
Upgrade :: boolean(),
Seen :: sets:set(),
State :: rebar_state:t(),
Cached :: boolean().
Arguments:
AppInfo: Dependency application infoProfile: Current profileUpgrade: Whether in upgrade modeSeen: Set of already-seen dependenciesState: Current rebar state
Returns: {Cached, UpdatedAppInfo}
Flow:
- Check if checkout dependency → skip fetch
- Check if already exists locally
- If exists and not upgrade mode → check if needs update
- If needs fetch/update → call
rebar_fetch:download_source/2 - Return updated app info
rebar_fetch:download_source/2
Purpose: Main entry point for downloading any dependency
Signature:
-spec download_source(rebar_app_info:t(), rebar_state:t()) ->
rebar_app_info:t() | {error, any()}.
Arguments:
AppInfo(rebar_app_info:t()): Dependency to downloadState(rebar_state:t()): Current state
Returns:
- Updated
AppInfowith dependency downloaded {error, Reason}if download failed
Flow:
- Check offline mode → error if offline and not cached
- Call
download_source_online/2 - After download: read dependency's
rebar.config - Update app info opts with dependency's config
- Discover application with
rebar_app_discover:find_app/4 - Mark as available
- Return updated app info
Example Usage:
AppInfo1 = rebar_fetch:download_source(AppInfo, State)
download_source_online/2
Purpose: Perform actual download operation
Signature:
-spec download_source_online(rebar_app_info:t(), rebar_state:t()) -> ok | {error, term()}.
Arguments:
AppInfo: Dependency to downloadState: Current state
Returns: ok or {error, Reason}
Flow:
- Get app directory from app info
- Create temporary directory with
ec_file:insecure_mkdtemp/0 - Call
rebar_resource_v2:download/3with TmpDir - Resource module downloads to TmpDir
- Ensure app directory exists
- Remove old version from code path
- Remove old fetch directory
- Move TmpDir to final location (FetchDir)
rebar_resource_v2:download/3
Purpose: Dispatch to appropriate resource module for download
Signature:
-spec download(TmpDir, AppInfo, State) -> ok | {error, term()} when
TmpDir :: file:filename(),
AppInfo :: rebar_app_info:t(),
State :: rebar_state:t().
Arguments:
TmpDir: Temporary directory for downloadAppInfo: Dependency info with sourceState: Current state
Returns: ok or {error, Reason}
Flow:
- Determine resource type from source
- Call appropriate resource module's
download/4:rebar_pkg_resource:download/4for Hex packagesrebar_git_resource:download/4for Git reposrebar_hg_resource:download/4for Mercurial repos
rebar_pkg_resource:download/4
Purpose: Download Hex package
Signature:
-spec download(TmpDir, AppInfo, State, ResourceState) -> ok | {error, term()} when
TmpDir :: file:name(),
AppInfo :: rebar_app_info:t(),
State :: rebar_state:t(),
ResourceState :: rebar_resource_v2:resource_state().
Arguments:
TmpDir: Temporary directoryAppInfo: Package infoState: Current stateResourceState: Resource-specific state (repos config)
Returns: ok or {error, Reason}
Flow:
- Get package cache directory
- Build cache path:
~/.cache/rebar3/hex/default/packages/name-version.tar - Build ETag file path:
~/.cache/rebar3/hex/default/packages/name-version.etag - Call
cached_download/7with cache info - Extract tarball contents to TmpDir
cached_download/7
Purpose: Download package with caching and ETag support
Signature:
-spec cached_download(TmpDir, CachePath, Pkg, State, ETag, ETagPath, UpdateETag) ->
ok | {error, term()} when
TmpDir :: file:name(),
CachePath :: file:name(),
Pkg :: package(),
State :: rebar_state:t(),
ETag :: binary() | undefined,
ETagPath :: file:name(),
UpdateETag :: boolean().
Flow:
- Check if cached tarball exists
- If exists: read ETag from ETag file
- Request package from Hex with ETag (If-None-Match header)
- If 304 Not Modified: use cached tarball
- If 200 OK: download new tarball
- Verify checksum:
calc_checksum(Tarball)vs registry checksum - If valid: store tarball and ETag in cache
- Extract tarball to TmpDir
r3_hex_repo:get_tarball/3
Purpose: HTTP request to Hex for package tarball
Signature:
-spec get_tarball(Config, Name, Version) ->
{ok, {StatusCode, Headers, Body}} | {error, Reason} when
Config :: map(),
Name :: binary(),
Version :: binary(),
StatusCode :: integer(),
Headers :: map(),
Body :: binary(),
Reason :: term().
Arguments:
Config: Hex repo configuration (includes ETag)Name: Package nameVersion: Package version
Returns:
{ok, {200, Headers, Tarball}}: New tarball downloaded{ok, {304, Headers, _}}: Cached version still valid{ok, {Code, _, _}}: Other HTTP status{error, Reason}: Network/HTTP error
Headers:
<<"etag">>: ETag value for caching
rebar_git_resource:download/4
Purpose: Clone Git repository
Signature:
-spec download(TmpDir, AppInfo, State, ResourceState) -> ok | {error, term()} when
TmpDir :: file:name(),
AppInfo :: rebar_app_info:t(),
State :: rebar_state:t(),
ResourceState :: rebar_resource_v2:resource_state().
Arguments:
TmpDir: Temporary directory for cloneAppInfo: Git source infoState: Current stateResourceState: Resource state
Returns: ok or {error, Reason}
Flow:
- Ensure TmpDir exists
- Detect Git version with
git_vsn/0 - Parse source ref spec (branch/tag/ref)
- Call appropriate
git_clone/5variant - Clone to
TmpDir/basename
git_clone/5
Purpose: Execute git clone with appropriate options
Signature:
-spec git_clone(Type, GitVersion, Url, Dir, RefSpec) -> ok | {error, term()} when
Type :: branch | tag | ref | rev,
GitVersion :: {Major, Minor, Patch} | undefined,
Url :: string(),
Dir :: file:name(),
RefSpec :: string().
Arguments:
Type: Type of ref (branch, tag, ref, rev)GitVersion: Detected Git versionUrl: Git repository URLDir: Target directoryRefSpec: Specific branch/tag/ref value
Commands Based on Git Version:
Branch (Git >= 2.3.0):
git clone [options] URL DIR -b BRANCH --single-branch
Branch (Git < 2.3.0):
git clone [options] URL DIR
cd DIR && git checkout -b BRANCH origin/BRANCH
Tag (Git >= 2.3.0):
git clone [options] URL DIR -b TAG --single-branch
Tag (Git < 2.3.0):
git clone [options] URL DIR
cd DIR && git checkout TAG
Ref:
git clone [options] URL DIR
cd DIR && git checkout REF
Environment:
GIT_TERMINAL_PROMPT=0: Disable interactive prompts
rebar_git_resource:lock/2
Purpose: Get current commit ref for locking
Signature:
-spec lock(AppInfo, ResourceState) -> {git, Url, {ref, Ref}} when
AppInfo :: rebar_app_info:t(),
ResourceState :: rebar_resource_v2:resource_state(),
Url :: string(),
Ref :: string().
Arguments:
AppInfo: Dependency app infoResourceState: Resource state
Returns: Lock source tuple with commit ref
Command:
git -C DIR rev-parse --verify HEAD
Result: Full commit SHA (40 characters)
rebar_git_resource:needs_update/2
Purpose: Check if Git dependency needs updating
Signature:
-spec needs_update(AppInfo, ResourceState) -> boolean() when
AppInfo :: rebar_app_info:t(),
ResourceState :: rebar_resource_v2:resource_state().
Arguments:
AppInfo: Dependency infoResourceState: Resource state
Returns: true if update needed, false otherwise
Logic by Type:
Tag:
git describe --tags --exact-match
Compare with specified tag and URL
Branch:
git fetch origin BRANCH
git log HEAD..origin/BRANCH --oneline
Update needed if new commits exist
Ref:
git rev-parse --short=7 -q HEAD
Compare with specified ref (truncated to same length)
rebar_hg_resource:download/4
Purpose: Clone Mercurial repository
Signature:
-spec download(TmpDir, AppInfo, State, ResourceState) -> ok | {error, term()} when
TmpDir :: file:name(),
AppInfo :: rebar_app_info:t(),
State :: rebar_state:t(),
ResourceState :: rebar_resource_v2:resource_state().
Commands:
hg clone URL DIR
cd DIR && hg update -r REVISION
Or for tags:
hg clone URL DIR
cd DIR && hg update TAG
State Modification
No Direct State Modifications
This stage doesn't modify the global state directly. Instead, it:
- Downloads files to filesystem
- Updates
rebar_app_info:t()records - These updated records are accumulated in the resolution stage
AppInfo Modifications
For each fetched dependency, the rebar_app_info:t() record is updated:
| Field | Update | Value |
|---|---|---|
dir | Set | Directory where dep was extracted |
fetch_dir | Set | Same as dir |
opts | Merged | With dependency's rebar.config |
is_available | Set | true after successful fetch |
valid | Set | true after app discovery |
Configuration
Resource Configuration
Hex Repository Configuration
In rebar.config:
{hex, [
{repos, [
#{name => <<"hexpm">>,
url => <<"https://repo.hex.pm">>,
auth_key => {<<"SOME_KEY">>}
}
]}
]}.
Default Hex CDN: https://repo.hex.pm
Environment Variables:
HEX_CDN: Override Hex CDN URLHEX_MIRROR: Alternative to HEX_CDN (for Mix compatibility)
Git Configuration
Authentication:
- SSH: Uses SSH keys from
~/.ssh/ - HTTPS: May prompt for credentials (disabled with
GIT_TERMINAL_PROMPT=0) - Token: Can embed in URL:
https://token@github.com/user/repo.git
Git Version Requirements:
- Minimum: 1.8.5 (for locking with
-Cflag) - Recommended: 2.3.0+ (for
--single-branchoptimization)
Cache Directories
Package Cache: ~/.cache/rebar3/hex/REPONAME/packages/
Contents:
PACKAGE-VERSION.tar: Package tarballPACKAGE-VERSION.etag: ETag for cache validation
Environment Variable: REBAR_CACHE_DIR overrides cache location
File System Operations
Files Read
| File | Purpose | When | Required |
|---|---|---|---|
CACHE/name-version.tar | Cached package | Package download | No |
CACHE/name-version.etag | Cache validation | Package download | No |
DEP/rebar.config | Dep configuration | After download | Yes |
.git/config | Git remote URL | needs_update check | For Git deps |
Files Written
| File | Content | When | Location |
|---|---|---|---|
| Package tarball | Hex package | First download | ~/.cache/rebar3/hex/.../name-version.tar |
| ETag file | HTTP ETag | After download | ~/.cache/rebar3/hex/.../name-version.etag |
| Dependency files | Source code | After download | _build/PROFILE/lib/DEPNAME/ |
Directories Created
| Directory | Purpose | When |
|---|---|---|
_build/PROFILE/lib/ | Dependency storage | Before first dep download |
_build/PROFILE/lib/DEPNAME/ | Individual dependency | Per dependency |
| Temporary directory | Staging area | During download |
~/.cache/rebar3/hex/REPO/packages/ | Package cache | First Hex download |
Directories Removed
| Directory | When | Why |
|---|---|---|
| Old dep version | Before moving new version | Clean old version |
| Temporary directory | After successful move | Cleanup |
| Failed download temp | On error | Cleanup |
Error Conditions
Offline Mode Without Cache
Condition: Dependency not cached and offline mode enabled
Error Format:
{?MODULE, offline}
Error Message:
Cannot fetch dependency in offline mode
Recovery:
- Disable offline mode
- Or pre-cache dependencies
Network Failure
Condition: Cannot connect to remote server
Causes:
- No internet connection
- Server down
- Firewall blocking
Error Format:
{fetch_fail, Source}
Error Message:
Failed to fetch and copy dep: {git, "https://github.com/user/repo.git", {tag, "1.0.0"}}
Recovery: Check network and retry
Bad Registry Checksum
Condition: Downloaded package checksum doesn't match registry
Error Format:
{bad_registry_checksum, Name, Version, Expected, Found}
Error Message:
The checksum for package at jsx-3.1.0 (ABC123) does not match the checksum
expected from the registry (DEF456). Run `rebar3 do unlock jsx, update` and then try again.
Causes:
- Corrupted download
- Tampered package
- Cache corruption
Recovery:
rebar3 do unlock jsx, update
Git Clone Failure
Condition: Git clone command fails
Common Causes:
- Invalid URL
- Authentication failure
- Repository doesn't exist
- Network timeout
Error Message:
fatal: repository 'https://github.com/user/repo.git' not found
Recovery:
- Verify URL
- Check authentication
- Verify repository exists
Git Version Too Old
Condition: Git < 1.8.5 when locking
Error Message:
Can't lock git dependency: git version must be 1.8.5 or higher.
Recovery: Upgrade Git
Dependency Application Not Found
Condition: Downloaded dependency doesn't contain valid OTP application
Error Format:
{dep_app_not_found, Name}
Error Message:
Dependency failure: source for my_dep does not contain a recognizable project and can not be built
Causes:
- No
.app.srcfile - Invalid application structure
- Wrong directory layout
Recovery:
- Check dependency is valid OTP app
- Verify repository URL is correct
Hex Package Not Found
Condition: Hex returns 404 for package
Error: Handled in resolution stage, not acquisition
Edge Cases
Cached Package with Modified ETag
Scenario: Cached package exists but ETag file doesn't match
Behavior:
- Re-download package
- Verify checksum
- Update cache and ETag
Interrupted Download
Scenario: Download interrupted mid-process
Behavior:
- Temporary directory removed on error
- Next run will re-attempt download
- Cached packages may be incomplete
Protection: Always use temporary directory first, then atomic move
Git Shallow Clone Limitations
Behavior: rebar3 uses --single-branch for optimization
Limitation: Cannot switch to other branches without re-cloning
Impact: Minimal; dependencies shouldn't change branches
SSH vs HTTPS Git URLs
SSH: git@github.com:user/repo.git
HTTPS: https://github.com/user/repo.git
Differences:
- SSH requires SSH keys configured
- HTTPS may prompt for credentials
- Both work equivalently for fetching
Lock File: Both normalize to same format
Git Tag vs Branch vs Ref
Tag: Immutable reference to specific commit Branch: Mutable, points to latest commit on branch Ref: Direct commit SHA
Recommendation: Use tags for releases, refs for locking
Example:
{deps, [
{cowboy, {git, "https://github.com/ninenines/cowboy.git", {tag, "2.9.0"}}}
]}.
Lock File Result:
{<<"cowboy">>, {git, "https://github.com/ninenines/cowboy.git",
{ref, "abc123..."}}, 0}
Note: Tag converted to ref in lock file
Local Git URLs
Format: file:///path/to/repo.git or /path/to/repo
Warning:
Local git resources (file:///...) are unsupported and may have odd behaviour.
Use remote git resources, or a plugin for local dependencies.
Recommendation: Use _checkouts/ for local development
Package Download with Slow Connection
Behavior: May timeout after default httpc timeout
Solution: Configure longer timeout in ~/.config/rebar3/rebar.config:
{hex, [
{http_timeout, 120000} % 120 seconds
]}.
Hex CDN Changes
Multiple CDNs Supported:
- Official:
https://repo.hex.pm - Mirrors: Various regions
Configuration: Use HEX_CDN environment variable
Example:
HEX_CDN=https://hexpm.example.com rebar3 compile
Transitive Dependencies of Git Deps
Scenario: Git dependency has its own dependencies
Behavior:
- After cloning Git dep, read its
rebar.config - Resolve its dependencies recursively
- Add to dependency tree
Example:
my_app → git_dep (Git)
git_dep → hex_dep (Hex)
Both fetched, Git first, then Hex
Cross References
Dependencies
- Initialization & Configuration: Offline mode, cache directories
- Dependency Resolution & Locking: Calls
maybe_fetch
Dependents
- Application Discovery: Discovers fetched applications
- Source File Compilation: Compiles fetched dependencies
Related
- State Management: AppInfo structure
- Error Handling: Error handling details
Example Scenarios
Scenario 1: First Hex Package Download
Dependency: {jsx, "3.1.0"}
Execution: rebar3 compile (first time)
Flow:
- Resolution determines need for jsx 3.1.0
- Call
maybe_fetch→ needs download - Call
rebar_pkg_resource:download/4 - Check cache:
~/.cache/rebar3/hex/hexpm/packages/jsx-3.1.0.tardoesn't exist - Request from Hex:
GET /tarballs/jsx-3.1.0 - Download tarball (HTTP 200)
- Verify checksum:
sha256(tarball)vs registry checksum - Store in cache with ETag
- Extract to
_build/default/lib/jsx/ - Read
jsx/rebar.config - Discover jsx application
- Mark as available
Files Created:
~/.cache/rebar3/hex/hexpm/packages/jsx-3.1.0.tar~/.cache/rebar3/hex/hexpm/packages/jsx-3.1.0.etag_build/default/lib/jsx/(full source tree)
Scenario 2: Cached Hex Package (ETag Valid)
Dependency: {jsx, "3.1.0"}
Execution: rebar3 compile (after clean, cache still valid)
Flow:
- Check cache: tarball exists
- Read ETag file:
"abc123" - Request from Hex with
If-None-Match: "abc123" - Hex returns HTTP 304 Not Modified
- Use cached tarball
- Extract to
_build/default/lib/jsx/ - Discover application
Network: Minimal (only HTTP HEAD request equivalent)
Scenario 3: Git Dependency with Tag
Dependency:
{cowboy, {git, "https://github.com/ninenines/cowboy.git", {tag, "2.9.0"}}}
Execution: rebar3 compile (first time)
Flow:
-
Call
rebar_git_resource:download/4 -
Detect Git version: 2.30.0
-
Execute:
git clone --depth 1 --no-single-branch \ https://github.com/ninenines/cowboy.git \ /tmp/rebar-abc123/cowboy \ -b 2.9.0 --single-branch -
Clone completes to temporary directory
-
Move to
_build/default/lib/cowboy/ -
Read
cowboy/rebar.config:{deps, [cowlib, ranch]}. -
Discover cowboy application
-
Add cowlib and ranch to dependency queue
Subsequent Runs:
- Check if update needed:
git describe --tags --exact-match - Compare with "2.9.0"
- If match: skip re-download
Scenario 4: Git Dependency with Branch
Dependency:
{my_dep, {git, "https://github.com/user/my_dep.git", {branch, "main"}}}
First Run:
- Clone with
-b main --single-branch - Extract to
_build/default/lib/my_dep/
Subsequent Runs:
-
Check for updates:
cd _build/default/lib/my_dep git fetch origin main git log HEAD..origin/main --oneline -
If output not empty: new commits available
-
Pull updates
-
Re-compile
Warning: Branches are mutable; can cause non-reproducible builds
Scenario 5: Offline Mode with Cache
Setup:
- jsx 3.1.0 previously downloaded and cached
- Offline mode enabled
Execution: REBAR_OFFLINE=1 rebar3 compile
Flow:
- Attempt fetch for jsx
- Detect offline mode
- Check cache: tarball exists
- Use cached tarball without network request
- Extract and proceed
Success: Works without network
Scenario 6: Offline Mode Without Cache
Setup:
- Fresh dependency not in cache
- Offline mode enabled
Execution: REBAR_OFFLINE=1 rebar3 compile
Flow:
- Attempt fetch for new_dep
- Detect offline mode
- Check cache: not found
- Error: cannot fetch in offline mode
Error:
Cannot fetch dependency in offline mode
Solution: Disable offline mode or pre-cache
Scenario 7: Corrupted Package Cache
Setup: Cached package has wrong checksum (corruption or tampering)
Execution: rebar3 compile
Flow:
- Check cache: jsx-3.1.0.tar exists
- Use cached tarball
- Calculate checksum
- Compare with registry
- Mismatch detected
- Clear ETag file
- Error reported
Error:
The checksum for package at jsx-3.1.0 (WRONG_HASH) does not match
the checksum expected from the registry (CORRECT_HASH).
Run `rebar3 do unlock jsx, update` and then try again.
Recovery:
rm ~/.cache/rebar3/hex/hexpm/packages/jsx-3.1.0.*
rebar3 compile
Scenario 8: Authentication Required for Private Git Repo
Dependency:
{private_dep, {git, "git@github.com:company/private_dep.git", {tag, "1.0.0"}}}
Execution: rebar3 compile
Prerequisites: SSH key added to GitHub account
Flow:
- Git uses SSH authentication
- Reads
~/.ssh/id_rsaor configured key - Authenticates with GitHub
- Clone proceeds normally
If Authentication Fails:
Permission denied (publickey).
fatal: Could not read from remote repository.
Solution:
- Add SSH key to GitHub
- Or use HTTPS with token:
https://TOKEN@github.com/company/private_dep.git
Application Discovery
Purpose
The application discovery stage scans project directories to locate all OTP applications, parses their application resource files (.app, .app.src, .app.src.script, or mix.exs), extracts application metadata, validates application structure, and prepares application information records for compilation. This stage determines which applications are part of the project and which are dependencies.
When It Executes
This stage executes early in the build process via the app_discovery provider, which is a dependency of install_deps. It runs:
- After Initialization & Configuration
- Before Dependency Resolution & Locking
- On every
rebar3 compileinvocation - Whenever project structure changes
Prerequisites
- State initialized with configuration
- Project root directory determined
- Configuration specifying
lib_dirsandsrc_dirs - File system accessible
Outputs
- List of project applications (
rebar_state:project_apps/1) - Parsed dependencies per profile (
{parsed_deps, Profile}in state) - Application info records for each discovered app
- Determination of top-level app vs umbrella project structure
Execution Flow
graph TD
A[Start: app_discovery provider] --> B[Get lib_dirs from state]
B --> C[Get src_dirs from opts]
C --> D[find_apps: Scan directories]
D --> D1[For each lib_dir]
D1 --> D2[Build file patterns]
D2 --> D3[Search for app resources]
D3 --> D4{Resource files found?}
D4 -->|app| D5[ebin/*.app]
D4 -->|app_src| D6[src/*.app.src]
D4 -->|script| D7[src/*.app.src.script]
D4 -->|mix_exs| D8[mix.exs]
D5 --> E[For each found file]
D6 --> E
D7 --> E
D8 --> E
E --> E1[Determine app directory]
E1 --> E2[Read rebar.config if exists]
E2 --> E3[Create app_info record]
E3 --> E4[Parse app resource file]
E4 --> E5{Valid app?}
E5 -->|Yes| E6[Extract app metadata]
E5 -->|No| E7[Skip this app]
E6 --> E8[Store in project_apps list]
E7 --> F{More files?}
E8 --> F
F -->|Yes| E
F -->|No| G[define_root_app]
G --> G1{App at root dir?}
G1 -->|Yes| G2[Return app name as root]
G1 -->|No| G3[Return 'root' atom]
G2 --> H[Parse dependencies per profile]
G3 --> H
H --> H1[For each profile]
H1 --> H2[Get deps from config]
H2 --> H3[Deduplicate deps]
H3 --> H4[Parse with parse_profile_deps]
H4 --> H5[Store as parsed_deps]
H5 --> I[Merge app configurations]
I --> I1[For each discovered app]
I1 --> I2{Is top-level app?}
I2 -->|Yes| I3[Use state opts directly]
I2 -->|No| I4[Apply overrides]
I3 --> I5[Apply profiles]
I4 --> I5
I5 --> I6[Verify OTP version]
I6 --> I7[Handle app deps per profile]
I7 --> I8[Set out_dir to _build/PROFILE/lib/APPNAME]
I8 --> I9[Add to project_apps]
I9 --> I10{More apps?}
I10 -->|Yes| I1
I10 -->|No| J[Install project app plugins]
J --> K[Return updated state]
style A fill:#e1f5ff
style K fill:#e1ffe1
Detailed Steps
-
Directory Scanning (
find_apps/4)- Get
lib_dirsfrom configuration (default:["apps"]) - Get
src_dirsfrom configuration (default:["src"]) - For each lib directory, search for application resource files
- Build file patterns:
lib_dir/src_dir/*.{app,app.src,app.src.script},lib_dir/ebin/*.app,lib_dir/mix.exs
- Get
-
Application Resource File Detection
.app: Compiled application resource file (inebin/).app.src: Application resource source file (insrc/or other src_dir).app.src.script: Scriptable app.src (evaluated as Erlang code)mix.exs: Elixir Mix project file (for Elixir interop)
-
Application Directory Determination
- From file path, extract parent directory as app directory
- Example:
apps/my_app/src/my_app.app.src→ app dir isapps/my_app/
-
Configuration Loading
- For each app directory, check for
rebar.config - If exists, parse and merge with parent configuration
- Create
rebar_app_info:t()record
- For each app directory, check for
-
Application Parsing (
find_app_/5)- Parse application resource file
- Extract application metadata:
- Name (atom)
- Version
- Applications (runtime dependencies)
- Included applications
- Modules list
- Other application-specific settings
- Validate application structure
-
Root App Detection (
define_root_app/2)- Check if any discovered app is at project root directory
- If yes: single-app project, return app name
- If no: umbrella project, return
rootatom
-
Dependency Parsing Per Profile (
parse_profile_deps/5)- For each active profile (default, test, prod, etc.)
- Get dependencies from
{deps, Profile}configuration - Parse into
rebar_app_info:t()records - Store as
{parsed_deps, Profile}in state
-
Configuration Merging (
merge_opts/3)- For each discovered application:
- If top-level app: use state opts directly
- If sub-app: apply overrides from parent config
- Apply active profiles
- Verify OTP version requirements
- Merge dependencies from app and parent
- For each discovered application:
-
Output Directory Assignment
- Set
out_dirfor each app - Usually:
_build/PROFILE/lib/APPNAME/ - Ensures compiled artifacts go to correct location
- Set
Functions & API Calls
rebar_prv_app_discovery:do/1
Purpose: Main provider entry point for application discovery
Signature:
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
Arguments:
State(rebar_state:t()): Current state
Returns: {ok, State} with discovered applications
Flow:
- Get
lib_dirsfrom state - Call
rebar_app_discover:do/2 - Install project app plugins
- Return updated state
rebar_app_discover:do/2
Purpose: Main discovery logic
Signature:
-spec do(rebar_state:t(), [file:filename()]) -> rebar_state:t() | no_return().
Arguments:
State(rebar_state:t()): Current stateLibDirs([file:filename()]): Library directories to scan
Returns: Updated state with project apps
Flow:
- Get
src_dirsfrom opts - Call
find_apps/4to discover applications - Call
define_root_app/2to determine project type - Parse dependencies per profile
- For each app, call
merge_opts/3and update state - Return state with
project_appsset
find_apps/4
Purpose: Find all applications in given directories
Signature:
-spec find_apps(LibDirs, SrcDirs, Validate, State) -> [rebar_app_info:t()] when
LibDirs :: [file:filename_all()],
SrcDirs :: [file:filename_all()],
Validate :: valid | invalid | all,
State :: rebar_state:t().
Arguments:
LibDirs: Directories to search (e.g.,["apps"])SrcDirs: Source directories within each lib dir (e.g.,["src"])Validate: Filter criterion (all,valid,invalid)State: Current state
Returns: List of discovered applications
Flow:
- Call
all_app_dirs/3to get app directories - For each app directory, call
find_app/5 - Filter based on validation criterion
- Return list of app info records
all_app_dirs/3
Purpose: Find all directories containing applications
Signature:
-spec all_app_dirs([file:name()], [file:name()], rebar_state:t()) ->
[{file:name(), [file:name()]}].
Arguments:
- Library directories
- Source directories
- State
Returns: List of {AppDir, SrcDirs} tuples
Flow:
- For each lib directory:
- Build file patterns for app resources
- Patterns:
lib_dir/src_dir/*.{app,app.src,app.src.script} - Also:
lib_dir/ebin/*.app,lib_dir/mix.exs
- Use
filelib:wildcard/1to find matching files - Extract app directory from file path
- Return unique app directories with their src dirs
find_app/5
Purpose: Discover and validate a single application
Signature:
-spec find_app(AppInfo, AppDir, SrcDirs, Validate, State) ->
{true, rebar_app_info:t()} | false when
AppInfo :: rebar_app_info:t(),
AppDir :: file:filename_all(),
SrcDirs :: [file:filename_all()],
Validate :: valid | invalid | all,
State :: rebar_state:t().
Arguments:
AppInfo: Empty or partially-filled app infoAppDir: Application directorySrcDirs: Source directoriesValidate: Validation criterionState: Current state
Returns:
{true, AppInfo}if app found and matches validationfalseif app not found or doesn't match validation
Flow:
- Read app's
rebar.config(if exists) - Update app info opts with config
- Call
find_app_/5to locate and parse app resource file - Return result
find_app_/5
Purpose: Internal app discovery with resource file handling
Signature:
-spec find_app_(AppInfo, AppDir, SrcDirs, Validate, State) ->
{true, rebar_app_info:t()} | false when
AppInfo :: rebar_app_info:t(),
AppDir :: file:filename_all(),
SrcDirs :: [file:filename_all()],
Validate :: valid | invalid | all,
State :: rebar_state:t().
Flow:
- Get application resource extensions from state (default:
[".app", ".app.src", ".app.src.script"]) - Search for resource files in:
ebin/*.appsrc_dir/*.app.srcsrc_dir/*.app.src.scriptmix.exs
- Flatten resource files (preferring .app > .script > .app.src)
- Call
try_handle_resource_files/4to parse
try_handle_resource_files/4
Purpose: Parse application resource file and create app info
Signature:
-spec try_handle_resource_files(AppInfo, AppDir, ResourceFiles, Validate) ->
{true, rebar_app_info:t()} | false when
AppInfo :: rebar_app_info:t(),
AppDir :: file:filename_all(),
ResourceFiles :: [{app_resource_type(), file:filename()}],
Validate :: valid | invalid | all.
Flow:
- Select first available resource file
- Based on type:
.app: Callrebar_app_info:discover/1to parse.app.src: Callcreate_app_info_src/3.app.src.script: Evaluate script, then parsemix.exs: Parse Elixir project
- Validate application based on criterion
- Return app info or false
define_root_app/2
Purpose: Determine if project is single-app or umbrella
Signature:
-spec define_root_app([rebar_app_info:t()], rebar_state:t()) -> root | binary().
Arguments:
Apps: Discovered applicationsState: Current state
Returns:
- App name (binary) if single-app project
rootatom if umbrella project
Logic:
- Check if any app's directory equals project root directory
- If match found: single-app (return app name)
- If no match: umbrella (return
root)
parse_profile_deps/5
Purpose: Parse dependencies for a specific profile
Signature:
-spec parse_profile_deps(Profile, Name, Deps, Opts, State) -> [rebar_app_info:t()] when
Profile :: atom(),
Name :: binary() | root,
Deps :: [term()],
Opts :: rebar_dict(),
State :: rebar_state:t().
Arguments:
Profile: Profile name (default, test, etc.)Name: Application name orrootDeps: Dependency specificationsOpts: Application optionsState: Current state
Returns: List of parsed dependency app infos
Flow:
- Get dependency directory for profile
- Get locks from state
- Call
rebar_app_utils:parse_deps/6 - Return list of dependency app info records
merge_opts/3
Purpose: Merge top-level and app-specific configuration
Signature:
-spec merge_opts(root | binary(), rebar_app_info:t(), rebar_state:t()) ->
{rebar_app_info:t(), rebar_state:t()}.
Arguments:
TopLevelApp: Root app name orrootAppInfo: Application to configureState: Current state
Returns: {UpdatedAppInfo, UpdatedState}
Flow:
- Reset hooks/plugins if top-level app
- Apply overrides if not top-level app
- Apply profiles to app opts
- Verify OTP version requirements
- For each profile, handle app dependencies
- Return updated app info and state
State Modification
Fields Modified
| Field | Operation | Value | Purpose |
|---|---|---|---|
project_apps | Set | List of rebar_app_info:t() | Discovered project applications |
{parsed_deps, Profile} | Set per profile | List of parsed deps | Dependencies per profile |
{deps, Profile} | Updated per profile | Merged dep list | Combined top-level and app deps |
App Info Fields Set
| Field | Value | Purpose |
|---|---|---|
name | Application name (binary) | Application identifier |
dir | Application source directory | Where source files are |
out_dir | Build output directory | Where compiled files go |
ebin_dir | out_dir/ebin | Where .beam files go |
opts | Merged configuration | App-specific config |
app_details | Parsed .app data | Application metadata |
applications | Runtime dependencies | From .app file |
deps | Build dependencies | From rebar.config |
Configuration
Library Directories
Configuration Key: lib_dirs
Default: []
Purpose: Additional directories to search for applications
Example:
{lib_dirs, ["apps", "my_libs"]}.
Result: Searches apps/*/ and my_libs/*/ for applications
Source Directories
Configuration Key: src_dirs
Default: ["src"]
Purpose: Directories within each app containing source files
Example:
{src_dirs, ["src", "lib"]}.
Result: Searches APP/src/ and APP/lib/ for .app.src files
Application Resource Extensions
Configuration Key: application_resource_extensions
Default: [".app", ".app.src", ".app.src.script"]
Purpose: File extensions to recognize as application resources
Rarely Changed: Standard OTP conventions
Application Structure
Single-App Project
my_app/
├── rebar.config
├── src/
│ ├── my_app.app.src
│ └── *.erl
└── include/
└── *.hrl
Discovery: One app found at root → single-app project
Umbrella Project
my_project/
├── rebar.config
└── apps/
├── app1/
│ ├── src/
│ │ ├── app1.app.src
│ │ └── *.erl
├── app2/
│ └── src/
│ ├── app2.app.src
│ └── *.erl
Discovery: Multiple apps in apps/ → umbrella project
Application Resource File Format
.app.src
Location: src/APP.app.src
Format: Erlang term
Example:
{application, my_app, [
{description, "My Application"},
{vsn, "1.0.0"},
{registered, []},
{applications, [kernel, stdlib]},
{mod, {my_app_app, []}},
{env, []}
]}.
.app
Location: ebin/APP.app
Format: Same as .app.src but with modules list
Generated: Usually created from .app.src during compilation
.app.src.script
Location: src/APP.app.src.script
Format: Erlang code that returns application term
Example:
case os:getenv("PROD") of
"true" ->
{application, my_app, [{vsn, "1.0.0"}, ...]};
_ ->
{application, my_app, [{vsn, "dev"}, ...]}
end.
File System Operations
Files Read
| File | Purpose | Required |
|---|---|---|
APP/src/*.app.src | Application resource | At least one |
APP/ebin/*.app | Compiled app resource | Alternative to .app.src |
APP/src/*.app.src.script | Dynamic app resource | Alternative |
APP/mix.exs | Elixir Mix project | Alternative |
APP/rebar.config | App-specific config | No |
Directories Scanned
| Directory Pattern | Purpose |
|---|---|
lib_dirs/* | Find application directories |
*/src/, */lib/ | Search for app resources |
*/ebin/ | Search for compiled apps |
Error Conditions
Multiple App Files
Condition: More than one .app.src file in same directory
Error Format:
{multiple_app_files, Files}
Error Message:
Multiple app files found in one app dir: my_app.app.src and other.app.src
Recovery: Remove duplicate files
Invalid App File
Condition: Application resource file has syntax errors
Error Format:
{invalid_app_file, File, Reason}
Error Message:
Invalid app file apps/my_app/src/my_app.app.src at line 5: syntax error before: '}'
Recovery: Fix syntax in app file
Missing App File
Condition: Application directory has no valid resource file
Behavior: Application skipped, not an error
Log: Debug message only
OTP Version Mismatch
Condition: Application requires different OTP version
Error: Thrown from rebar_app_info:verify_otp_vsn/1
Configuration: Use minimum_otp_vsn in app's rebar.config
Edge Cases
Hidden Directories
Behavior: Directories starting with . are ignored
Example: .git/, .rebar3/ not scanned
Symbolic Links
Behavior: Followed during directory scanning
Use Case: Link to shared libraries
Warning: Can cause duplicates if not careful
Mixed Erlang/Elixir Projects
Scenario: Both .app.src and mix.exs in same directory
Behavior: Prefers .app > .app.src.script > .app.src > mix.exs
Application Without rebar.config
Behavior: Uses parent/state configuration
Common: Dependencies often don't have own config
Disabled Applications
Configuration: Use enable option
Example:
{app_name, [
{enable, false}
]}.
Behavior: Application discovered but not built
Cross References
Dependencies
- Initialization & Configuration: Initial state setup
- Dependency Acquisition: Uses discovered apps
Dependents
- Dependency Resolution & Locking: Uses project apps
- Compilation Order Determination: Orders apps
- Source File Compilation: Compiles apps
Related
- State Management: App info structure
- Configuration Reference: Configuration options
Example Scenarios
Scenario 1: Single-App Project
Structure:
my_app/
├── rebar.config
└── src/
├── my_app.app.src
└── my_app.erl
Discovery:
- Scan root directory
- Find
src/my_app.app.src - Parse app resource
- App dir = root directory
define_root_appreturns<<"my_app">>- Single-app project detected
Result: One application in project_apps
Scenario 2: Umbrella Project
Structure:
my_project/
├── rebar.config
└── apps/
├── web/
│ └── src/web.app.src
└── db/
└── src/db.app.src
Discovery:
- Scan
apps/directory (fromlib_dirs) - Find
apps/web/src/web.app.src - Find
apps/db/src/db.app.src - Parse both app resources
define_root_appreturnsroot(no app at root)- Umbrella project detected
Result: Two applications in project_apps
Scenario 3: Custom Source Directories
rebar.config:
{src_dirs, ["src", "lib", "core"]}.
Structure:
my_app/
├── src/my_app.app.src
├── lib/helper.erl
└── core/engine.erl
Discovery:
- Search
src/,lib/,core/for app resources - Find
src/my_app.app.src - Associate all three directories with this app
Result: App discovered with multiple source directories
Scenario 4: Mix.exs Compatibility
Structure:
my_elixir_app/
├── mix.exs
└── lib/
└── my_elixir_app.ex
Discovery:
- Find
mix.exs - Parse Elixir project configuration
- Extract application metadata
- Create app info compatible with rebar3
Result: Elixir app discoverable by rebar3
Scenario 5: Application-Specific Dependencies
apps/web/rebar.config:
{deps, [cowboy]}.
apps/db/rebar.config:
{deps, [epgsql]}.
Discovery:
- Discover both apps
- Read each app's
rebar.config - Merge deps:
webgetscowboy,dbgetsepgsql - Top-level deps also added to both
Result: Each app has appropriate dependencies
Compilation Order Determination
Purpose
The compilation order determination stage takes the list of resolved dependencies and project applications and determines the correct sequence in which they must be compiled. This stage uses graph-based topological sorting to ensure that each application is compiled after all of its dependencies, preventing compilation errors due to missing dependencies. This applies to both dependency compilation and project application compilation separately.
When It Executes
This stage executes within the compile provider in two distinct contexts:
- Dependency Compilation: During Dependency Resolution & Locking, the
find_cycles/1function determines the order for dependencies - Project App Compilation: In
rebar_prv_compile:do/1, thecompile_order/1function determines order for project applications
Prerequisites
- Dependencies resolved with complete dependency information
- Project applications discovered with metadata
- Application dependencies extracted (both runtime
applicationsand build-timedeps) - No circular dependencies (or detection ready to report them)
Outputs
- Topologically sorted list of applications
- Dependencies listed before dependents
- Error if circular dependencies detected
- Order ready for sequential compilation
Execution Flow
graph TD
A[Start: Determine Compilation Order] --> B{Context?}
B -->|Dependencies| C[find_cycles in install_deps]
B -->|Project Apps| D[compile_order in compile provider]
C --> E[rebar_digraph:compile_order/1]
D --> E
E --> E1[Create new digraph]
E1 --> E2[For each application]
E2 --> E3[Get app name]
E3 --> E4[Get app dependencies]
E4 --> E5[all_apps_deps: Union of applications + deps]
E5 --> E6[Add vertex for app name]
E6 --> E7[For each dependency]
E7 --> E8[Add vertex for dep name if not exists]
E8 --> E9[Add edge: app -> dep]
E9 --> E10{More deps?}
E10 -->|Yes| E7
E10 -->|No| E11{More apps?}
E11 -->|Yes| E2
E11 -->|No| F[digraph_utils:topsort/1]
F --> F1{Sort successful?}
F1 -->|Yes| G[Reverse the sorted list]
F1 -->|No| H[Check if acyclic]
G --> I[Map names back to app_info records]
I --> J[Delete digraph]
J --> K[Return sorted apps]
H --> H1{Is acyclic?}
H1 -->|Yes| H2[Return no_sort error]
H1 -->|No| H3[Find strongly connected components]
H3 --> H4[Filter components: length > 1]
H4 --> H5[Sort cycles]
H5 --> H6[Return cycles error]
H2 --> J
H6 --> J
K --> L{Context?}
L -->|Dependencies| M[cull_compile: Filter deps]
L -->|Project Apps| N[Use sorted list directly]
M --> M1[Remove project apps from list]
M1 --> M2[Drop deps that don't need compile]
M2 --> O[Return final compile order]
N --> O
style A fill:#e1f5ff
style O fill:#e1ffe1
Detailed Steps
-
Graph Construction (
compile_order/1)- Create empty directed graph with
digraph:new/0 - For each application in the input list:
- Extract application name
- Get all dependencies (see
all_apps_deps/1) - Add vertex for application name
- For each dependency: add vertex and edge
- Create empty directed graph with
-
Dependency Collection (
all_apps_deps/1)- Get
applicationslist from.appfile (runtime dependencies) - Get
depslist fromrebar.config(build-time dependencies) - Convert both to binaries
- Sort and merge (union of both lists)
- This ensures all relevant dependencies are considered
- Get
-
Topological Sort (
digraph_utils:topsort/1)- Standard Erlang digraph utility performs topological sort
- Returns list in dependency order (dependencies first)
- Returns
falseif graph contains cycles
-
Cycle Detection (on sort failure)
- Check if graph is acyclic with
digraph_utils:is_acyclic/1 - If acyclic but sort failed: return
no_sorterror (rare edge case) - If not acyclic: find strongly connected components
- Strongly connected components with length > 1 are cycles
- Sort and return cycle information
- Check if graph is acyclic with
-
List Reversal
topsortreturns dependencies first- Reverse the list for compilation (dependencies last)
- Why reversed: Original sort gives evaluation order; we want build order
-
Name to AppInfo Mapping (
names_to_apps/2)- Sorted list contains application names (atoms/binaries)
- Map back to full
rebar_app_info:t()records - Preserve sort order
- Skip any apps not found in original list
-
Graph Cleanup
- Delete digraph with
digraph:delete/1 - Free memory
- Return final sorted list
- Delete digraph with
-
Compilation Filtering (
cull_compile/2- for dependencies only)- Remove project applications from sorted dependency list
- Drop dependencies that don't need compilation
- Determine "needs compile" based on:
- Checkout dependencies: always need compile
- Source dependencies (Git, Hg): need compile
- Package dependencies (Hex): may not need compile (pre-built)
Functions & API Calls
rebar_digraph:compile_order/1
Purpose: Sort applications in topological order for compilation
Signature:
-spec compile_order([rebar_app_info:t()]) ->
{ok, [rebar_app_info:t()]} | {error, no_sort | {cycles, [[binary()]]}}.
Arguments:
Apps([rebar_app_info:t()]): Unsorted list of applications
Returns:
{ok, SortedApps}: Applications sorted in compilation order{error, {cycles, Cycles}}: Circular dependencies detected{error, no_sort}: Sort failed for other reason
Example Usage:
{ok, Sorted} = rebar_digraph:compile_order(AllApps)
Called From:
- Dependency Resolution & Locking:
find_cycles/1 - Stage 05 (this stage):
handle_project_apps/2inrebar_prv_compile
all_apps_deps/1
Purpose: Get all dependencies for an application
Signature:
-spec all_apps_deps(rebar_app_info:t()) -> [binary()].
Arguments:
App(rebar_app_info:t()): Application to analyze
Returns: List of dependency names (binaries)
Flow:
- Get
applicationslist viarebar_app_info:applications/1- Returns list of atoms from
.appfile - These are runtime OTP dependencies
- Returns list of atoms from
- Get
depslist viarebar_app_info:deps/1- Returns list from
rebar.config - Format:
[{Name, Spec}]or[Name] - These are build-time rebar3 dependencies
- Returns list from
- Convert all to binaries
- Sort each list
- Merge with
lists:umerge/2(union merge) - Return combined unique list
Example:
% my_app.app:
{applications, [kernel, stdlib, cowboy]}
% rebar.config:
{deps, [jsx, {ranch, "1.8.0"}]}
% Result:
[<<"cowboy">>, <<"jsx">>, <<"kernel">>, <<"ranch">>, <<"stdlib">>]
add/2 (internal)
Purpose: Add application and its dependencies to digraph
Signature:
-spec add(digraph:graph(), {PkgName, [Dep]}) -> ok when
PkgName :: binary(),
Dep :: {Name, term()} | Name,
Name :: atom() | iodata().
Arguments:
Graph: Existing digraph{PkgName, Deps}: Application name and dependency list
Returns: ok
Flow:
- Check if vertex exists for
PkgName - If not: add vertex with
digraph:add_vertex/2 - For each dependency:
- Extract dependency name (handle tuple format)
- Convert to binary
- Add vertex for dependency if not exists
- Add edge:
PkgName → DependencyName
Edge Direction: Application points TO its dependency
names_to_apps/2 (internal)
Purpose: Map sorted names back to app info records
Signature:
-spec names_to_apps([atom()], [rebar_app_info:t()]) -> [rebar_app_info:t()].
Arguments:
Names: Sorted list of application namesApps: Original unsorted app info records
Returns: Sorted app info records
Flow:
- For each name in sorted list:
- Find corresponding app info via
find_app_by_name/2 - If found, include in result
- If not found, skip
- Find corresponding app info via
- Return list preserving sort order
find_app_by_name/2 (internal)
Purpose: Find app info by name
Signature:
-spec find_app_by_name(atom(), [rebar_app_info:t()]) ->
{ok, rebar_app_info:t()} | error.
Arguments:
Name: Application name to findApps: List of app info records
Returns:
{ok, AppInfo}: App founderror: App not found
rebar_prv_install_deps:find_cycles/1
Purpose: Wrapper for cycle detection in dependency resolution
Signature:
-spec find_cycles([rebar_app_info:t()]) ->
{no_cycle, Sorted} | {cycles, Cycles} | {error, Error} when
Sorted :: [rebar_app_info:t()],
Cycles :: [[binary()]],
Error :: term().
Arguments:
Apps: Applications to check
Returns:
{no_cycle, Sorted}: No cycles, returns sorted list{cycles, Cycles}: Circular dependencies found{error, Error}: Other error
Flow:
- Call
rebar_digraph:compile_order/1 - Transform result format
- Return appropriate tuple
rebar_prv_install_deps:cull_compile/2
Purpose: Filter sorted dependencies to those needing compilation
Signature:
-spec cull_compile([rebar_app_info:t()], [rebar_app_info:t()]) ->
[rebar_app_info:t()].
Arguments:
TopSortedDeps: All apps in compilation orderProjectApps: Project's own applications
Returns: Filtered list of dependencies to compile
Flow:
- Remove project apps from sorted list (using
--operator) - Drop dependencies from beginning until finding one that needs compilation
- Use
lists:dropwhile/2withnot_needs_compile/1predicate - Return remaining list
Logic: Once we hit a dep that needs compile, compile all remaining deps
not_needs_compile/1 (internal)
Purpose: Determine if dependency doesn't need compilation
Criteria:
- Package from Hex with pre-built artifacts: doesn't need compile
- Source dependency (Git, Hg): needs compile
- Checkout dependency: needs compile
Implementation: Checks rebar_app_info fields and source type
State Modification
No Direct State Modifications
This stage is purely computational - it doesn't modify state directly. Instead:
- Takes input list of applications
- Returns reordered list
- Calling code updates state with sorted list
Where Sorted Lists Are Stored
For Dependencies:
- Returned from
find_cycles/1 - Stored in temporary variable
- Passed to
cull_compile/2 - Final result stored as
deps_to_buildin state
For Project Apps:
- Returned from
compile_order/1 - Used immediately for compilation
- Not permanently stored in state
Configuration
No Direct Configuration
This stage doesn't have configuration options. However, the dependency information it uses comes from:
From .app or .app.src:
{applications, [kernel, stdlib, dependency1, dependency2]}
From rebar.config:
{deps, [
dependency3,
{dependency4, "1.0.0"}
]}.
Dependency Types That Affect Ordering
Runtime Dependencies (applications in .app):
- Required for application to run
- Must be loaded before application starts
- Affect compilation order (may need headers)
Build Dependencies (deps in rebar.config):
- Required for compilation
- Parse transforms, behaviors, include files
- Always affect compilation order
Example:
% In my_app.app.src:
{applications, [kernel, stdlib, cowboy]}.
% In rebar.config:
{deps, [{cowboy, "2.9.0"}]}.
Both sources contribute to dependency graph for compilation ordering.
File System Operations
This stage is purely in-memory graph manipulation. No files are read or written.
Error Conditions
Circular Dependencies Detected
Condition: Dependency graph contains one or more cycles
Detection: digraph_utils:is_acyclic/1 returns false
Error Format:
{cycles, [[<<"app1">>, <<"app2">>, <<"app3">>], [<<"app4">>, <<"app5">>]]}
Error Message (from rebar_prv_install_deps):
Dependency cycle(s) detected:
applications: app1 app2 app3 depend on each other
applications: app4 app5 depend on each other
Example Cycle:
app1 depends on app2
app2 depends on app3
app3 depends on app1
Recovery: None; must break the cycle in dependencies
Common Causes:
- Circular
applicationslists in.appfiles - Circular
depsinrebar.configfiles - Mix of runtime and build-time circular dependencies
Topological Sort Failed (Non-Cyclic)
Condition: Graph is acyclic but sort fails anyway
Error Format:
{error, no_sort}
Likelihood: Very rare; theoretical edge case
Possible Causes:
- Erlang digraph library issue
- Malformed graph structure
Recovery: Report as bug
Missing Dependency in Graph
Condition: Application references dependency not in graph
Behavior: Not an error at this stage
Handling:
- Digraph adds vertex automatically for missing deps
- Vertex exists but no app info record associated
names_to_apps/2skips missing apps- Error likely caught in earlier stage (dependency resolution)
Edge Cases
Empty Application List
Input: []
Behavior: Returns {ok, []}
Use Case: No applications to compile
Single Application
Input: Single app with no dependencies
Behavior: Returns {ok, [App]}
Graph: Single vertex, no edges
Application Depends on Itself
Behavior: Creates self-loop in graph
Detection: Caught by cycle detection
Error: Reported as single-app cycle
Transitive Dependencies
Scenario:
my_app → lib_a → lib_b → lib_c
Behavior: All transitive dependencies included in graph
Ordering: [lib_c, lib_b, lib_a, my_app]
Why It Works: Each level adds its dependencies to graph
Diamond Dependency Pattern
Scenario:
my_app → [lib_a, lib_b]
lib_a → lib_common
lib_b → lib_common
Behavior: lib_common appears once in sorted output
Ordering: [lib_common, lib_a, lib_b, my_app] (or lib_b before lib_a)
Note: Order between lib_a and lib_b is arbitrary (no dependency between them)
Dependency Referenced but Not Present
Scenario: App lists dependency that wasn't resolved
Example:
% my_app.app.src
{applications, [kernel, stdlib, missing_lib]}
Behavior:
- Vertex created for
missing_lib - No corresponding app info
names_to_appsskips it- Won't appear in final sorted list
- Compilation will likely fail later
OTP Applications in Dependency List
Scenario: Dependencies include OTP apps like kernel, stdlib
Behavior: Included in graph but not in final compile list
Reason: OTP apps aren't in the input app list, so names_to_apps skips them
Example:
Input: [my_app]
my_app depends on: [kernel, stdlib, jsx]
Graph vertices: my_app, kernel, stdlib, jsx
Sorted: [kernel, stdlib, jsx, my_app]
names_to_apps result: [jsx, my_app] % kernel, stdlib not in input
Multiple Independent Apps
Scenario: Multiple apps with no inter-dependencies
Example:
app1 → [kernel, stdlib]
app2 → [kernel, stdlib]
app3 → [kernel, stdlib]
Behavior: Arbitrary order between app1, app2, app3
Ordering: Any permutation valid, e.g., [app1, app2, app3] or [app3, app1, app2]
Cross References
Dependencies
- Dependency Resolution & Locking: Calls
find_cycles/1 - Application Discovery: Provides app metadata
Dependents
- Source File Compilation: Uses sorted order for compilation
Related
- State Management: App info structure details
- Error Handling: Error handling for cycles
Example Scenarios
Scenario 1: Simple Linear Dependencies
Applications:
my_app:
- depends on: [jsx]
jsx:
- depends on: [kernel, stdlib]
Graph Construction:
Vertices: my_app, jsx, kernel, stdlib
Edges:
my_app → jsx
jsx → kernel
jsx → stdlib
Topological Sort Result:
[kernel, stdlib, jsx, my_app]
After Reversal (compilation order):
[my_app, jsx, stdlib, kernel] % Actually reversed already by topsort output
Wait, correction: topsort gives [kernel, stdlib, jsx, my_app], we reverse to get [my_app, jsx, stdlib, kernel]?
Actually: topsort gives dependencies first, so we get [kernel, stdlib, jsx, my_app], then we reverse to... wait, that doesn't make sense.
Clarification from code:
V -> {ok, names_to_apps(lists:reverse(V), Apps)}
So if topsort returns [my_app, jsx, stdlib, kernel], we reverse to [kernel, stdlib, jsx, my_app].
Correct Interpretation:
- Topsort returns:
[my_app, jsx, stdlib, kernel](reverse topological order) - We reverse to:
[kernel, stdlib, jsx, my_app](topological order - dependencies first) - Final:
jsx, thenmy_app(OTP apps filtered out bynames_to_apps)
Scenario 2: Diamond Dependencies
Applications:
my_app:
- depends on: [web, db]
web:
- depends on: [cowboy, common]
db:
- depends on: [epgsql, common]
common:
- depends on: [kernel, stdlib]
Graph:
Vertices: my_app, web, db, cowboy, epgsql, common, kernel, stdlib
Edges:
my_app → web
my_app → db
web → cowboy
web → common
db → epgsql
db → common
common → kernel
common → stdlib
Possible Topological Sorts (dependencies first):
[kernel, stdlib, common, cowboy, epgsql, web, db, my_app]
[kernel, stdlib, common, epgsql, cowboy, web, db, my_app]
[kernel, stdlib, common, cowboy, web, epgsql, db, my_app]
... (many valid orderings)
Key Property: common always before web and db; web and db always before my_app
After Filtering OTP Apps:
[common, cowboy, epgsql, web, db, my_app]
Scenario 3: Circular Dependency
Applications:
app_a:
- depends on: [app_b]
app_b:
- depends on: [app_c]
app_c:
- depends on: [app_a]
Graph:
Vertices: app_a, app_b, app_c
Edges:
app_a → app_b
app_b → app_c
app_c → app_a
Topological Sort: Fails
Cycle Detection:
digraph_utils:strong_components(Graph)
% Returns: [[app_a, app_b, app_c], ...other components...]
Filter length > 1: [[app_a, app_b, app_c]]
Error:
{error, {cycles, [[<<"app_a">>, <<"app_b">>, <<"app_c">>]]}}
User Message:
Dependency cycle(s) detected:
applications: app_a app_b app_c depend on each other
Scenario 4: Multiple Independent Cycles
Applications:
Group 1:
app1 → app2 → app3 → app1
Group 2:
app4 → app5 → app4
Group 3:
app6 (no cycle, depends on kernel only)
Cycle Detection:
Strongly connected components:
[[app1, app2, app3], [app4, app5], [app6], [kernel], [stdlib]]
Filter length > 1:
[[app1, app2, app3], [app4, app5]]
Error:
{error, {cycles, [[<<"app1">>, <<"app2">>, <<"app3">>],
[<<"app4">>, <<"app5">>]]}}
User Message:
Dependency cycle(s) detected:
applications: app1 app2 app3 depend on each other
applications: app4 app5 depend on each other
Scenario 5: Dependencies vs Project Apps
Project Structure:
Project Apps:
- my_web
- my_db
Dependencies:
- cowboy
- epgsql
- ranch
Dependency Compilation Order:
- Resolve all dependencies (cowboy, epgsql, ranch)
- Determine order:
[ranch, cowboy, epgsql](ranch is cowboy's dep) - Call
cull_compile([ranch, cowboy, epgsql], [my_web, my_db]) - Result:
[ranch, cowboy, epgsql](project apps already removed) - Compile in that order
Project App Compilation Order:
- Get project apps:
[my_web, my_db] - Determine order: Check if my_web depends on my_db or vice versa
- If my_web depends on my_db:
[my_db, my_web] - If independent: arbitrary order, e.g.,
[my_web, my_db] - Compile in that order
Scenario 6: Build vs Runtime Dependencies
my_app.app.src:
{applications, [kernel, stdlib, cowboy]}. % Runtime deps
rebar.config:
{deps, [{parse_trans, "3.3.0"}]}. % Build-time only
Dependency Graph:
- Includes both
cowboy(runtime) andparse_trans(build-time) all_apps_deps/1unions both sources- Result:
[cowboy, kernel, parse_trans, stdlib]
Compilation Order:
[parse_trans, cowboy, my_app]
Why: parse_trans might be needed during my_app compilation (as parse transform), cowboy needed for runtime
Scenario 7: Umbrella Project with Inter-App Dependencies
Project Structure:
apps/
├── common/ (no deps)
├── api/ (depends on common)
└── web/ (depends on common, api)
Compilation Order Determination:
Input: [common, api, web]
Graph:
common → [kernel, stdlib]
api → [common, kernel, stdlib]
web → [common, api, kernel, stdlib]
Sorted: [common, api, web]
Result: common compiled first, then api, then web
Critical: Without proper ordering, web would fail to compile due to missing common and api
Source File Compilation
Purpose
The source file compilation stage is the core of the build process. It compiles source files (.erl, .yrl, .xrl, .mib, etc.) into their target artifacts (.beam, .bin, .hrl), managing dependencies between files, determining what needs recompilation, executing compilers in the correct order, supporting parallel compilation where possible, and tracking compiled artifacts for incremental builds.
When It Executes
This stage executes within rebar_prv_compile after application discovery and ordering:
- Dependency Compilation:
copy_and_build_deps/4compiles all dependencies - Project App Compilation:
copy_and_build_project_apps/3compiles project applications - Root Extras Compilation:
build_root_extras/2compiles extra source directories
Within each context, the compile/4 function orchestrates multiple sub-stages.
Prerequisites
- Applications discovered and ordered
- Dependencies fetched and ready
- Output directories created
- Code paths configured
- DAGs (Dependency Graphs) initialized for each compiler
Outputs
- Compiled
.beamfiles inebin/directories - Generated header files (
.hrlfrom.yrl,.mib) - Generated source files (
.erlfrom.yrl,.xrl) - Updated DAGs with artifact metadata and timestamps
- Code available for subsequent compilation stages
Execution Flow
graph TD
A[Start: compile/4] --> B[prepare_compile: Pre-hooks]
B --> C[prepare_compilers: Pre-erlc hooks]
C --> D[run_compilers]
D --> D1[Load DAGs for all compilers]
D1 --> D2[For each compiler module]
D2 --> E[build_apps: Separate rebar3 vs custom]
E --> E1{Project type?}
E1 -->|rebar3| F[build_rebar3_apps]
E1 -->|custom| G[build_custom_builder_app]
G --> G1[Call Module:build/1]
G1 --> D3
F --> F1[analyze_all: Analyze all apps]
F1 --> F2[For each app]
F2 --> F3[Get compiler context]
F3 --> F4[Find source files]
F4 --> F5[Populate DAG with sources]
F5 --> F6{More apps?}
F6 -->|Yes| F2
F6 -->|No| F7[Prune deleted files from DAG]
F7 --> F8[populate_deps: Scan dependencies]
F8 --> F9[propagate_stamps: Update timestamps]
F9 --> F10[compile_order: Inter-app order]
F10 --> F11[compile_analyzed: For each app]
F11 --> H[run: Execute compilation]
H --> H1[Find source files]
H1 --> H2[needed_files: Determine what to compile]
H2 --> H3[Split into FirstFiles, RestFiles]
H3 --> H4{Parse transforms need compile?}
H4 -->|Yes| H5[Recompile all files]
H4 -->|No| H6[Compile only needed files]
H5 --> I[Separate sequential/parallel]
H6 --> I
I --> I1[Compile FirstFiles sequentially]
I1 --> I2[Build subgraph of dependencies]
I2 --> I3[Topological sort needed files]
I3 --> I4[Partition: sequential vs parallel]
I4 --> I5[Compile sequential files]
I5 --> I6[Compile parallel files in worker pool]
I6 --> J[store_artifacts: Track in DAG]
J --> D3{More compilers?}
D3 -->|Yes| D2
D3 -->|No| D4[Save DAGs to disk]
D4 --> K[finalize_compilers: Post-erlc hooks]
K --> L[prepare_app_file: Pre-app hooks]
L --> M[compile_app_files: Generate .app]
M --> N[finalize_app_file: Post-app hooks]
N --> O[finalize_compile: Post-hooks]
O --> P[Verify artifacts exist]
P --> Q[End: Compiled]
style A fill:#e1f5ff
style Q fill:#e1ffe1
Compilation Sub-Stages
The compile/4 function executes these steps for each application:
- prepare_compile: Run
pre_hooksforcompilestage - prepare_compilers: Run
pre_hooksforerlc_compilestage - run_compilers: Execute all compiler modules (see below)
- finalize_compilers: Run
post_hooksforerlc_compilestage - prepare_app_file: Run
pre_hooksforapp_compilestage - compile_app_files: Generate
.appfrom.app.src(see Application File Generation) - finalize_app_file: Run
post_hooksforapp_compilestage - finalize_compile: Run
post_hooksforcompilestage, verify artifacts
Compiler Execution Order
Compilers run in sequence (not parallel with each other):
- rebar_compiler_xrl - Leex lexer (
.xrl→.erl) - rebar_compiler_yrl - Yecc parser (
.yrl→.erl) - rebar_compiler_mib - SNMP MIB (
.mib→.bin+.hrl) - rebar_compiler_erl - Erlang source (
.erl→.beam) - Custom compilers - Plugin-provided compilers
Why Sequential: Generated files from early compilers (e.g., .erl from .yrl) must be available for later compilers.
Compiler Architecture
Compiler Behavior
All compilers implement the rebar_compiler behavior:
-callback context(rebar_app_info:t()) -> Context :: map().
-callback needed_files(Graph, FoundFiles, Mappings, AppInfo) -> NeededFiles.
-callback dependencies(Source, SourceDir, InDirs) -> [Dependency].
-callback dependencies(Source, SourceDir, InDirs, DepOpts) -> [Dependency].
-callback compile(Source, Mappings, Config, Opts) -> Result.
-callback compile_and_track(Source, Mappings, Config, Opts) -> TrackedResult.
-callback clean(Files, AppInfo) -> ok.
Compiler Context
The context/1 callback provides configuration:
#{src_dirs => ["src", "lib"], % Where to find sources
include_dirs => ["/abs/path/include"], % Include directories
src_ext => ".erl", % Source extension
out_mappings => [{".beam", "ebin"}], % Output extension → directory
dependencies_opts => [...]} % Options for dependency scanning
Needed Files
The needed_files/4 callback determines what to compile:
Returns:
{{FirstFiles, FirstOpts}, % Must compile first (sequentially)
{RestFiles, RestOpts}} % Can compile after FirstFiles
or:
{{FirstFiles, FirstOpts},
{{Sequential, Parallel}, RestOpts}} % Split for parallelization
FirstFiles typically include:
- Parse transforms
- Files listed in
erl_first_filesconfiguration
Sequential files have incoming dependencies (other files depend on them)
Parallel files are independent and can compile concurrently
Functions & API Calls
rebar_prv_compile:compile/4
Purpose: Main compilation orchestrator
Signature:
-spec compile(State, Providers, Apps, Tag) -> [rebar_app_info:t()] when
State :: rebar_state:t(),
Providers :: [providers:t()],
Apps :: [rebar_app_info:t()],
Tag :: atom() | undefined.
Arguments:
State: Current rebar stateProviders: Registered providers (for hooks)Apps: Applications to compileTag: DAG label (apps,project_apps, orundefined)
Returns: List of compiled app infos
Flow: Executes all sub-stages listed in overview
run_compilers/4
Purpose: Execute all compiler modules for given applications
Signature:
-spec run_compilers(State, Providers, Apps, Tag) -> [rebar_app_info:t()] when
State :: rebar_state:t(),
Providers :: [providers:t()],
Apps :: [rebar_app_info:t()],
Tag :: atom() | undefined.
Flow:
- Load DAGs for all compilers via
load_dags/2 - Call
build_apps/3with DAGs and Apps - Store DAGs to disk
- Terminate DAGs
- Return Apps unchanged
load_dags/2
Purpose: Initialize or restore dependency graphs for all compilers
Signature:
-spec load_dags(State, Tag) -> [{Module, {Graph, Metadata}}] when
State :: rebar_state:t(),
Tag :: atom() | undefined,
Module :: module(),
Graph :: digraph:graph(),
Metadata :: {Dir, Label, CritMeta}.
Returns: List of {CompilerModule, {DAG, {Dir, Label, CritMeta}}} tuples
Flow:
- Get compiler modules from state
- For each compiler:
- Determine DAG directory (deps_dir)
- Determine DAG label from Tag
- Build critical metadata (e.g., compiler version)
- Call
rebar_compiler_dag:init/4to load or create DAG
- Return list of DAG info
build_rebar3_apps/3
Purpose: Compile applications using rebar3 compilation system
Signature:
-spec build_rebar3_apps(DAGs, Apps, State) -> ok when
DAGs :: [{Module, digraph:graph()}],
Apps :: [rebar_app_info:t()],
State :: rebar_state:t().
Flow:
- For each compiler (DAG):
- Call
rebar_compiler:analyze_all/2 - Get compilation context and reordered apps
- For each app in order:
- Print "Compiling APP_NAME"
- Call
rebar_compiler:compile_analyzed/3
- Handle extra_src_dirs separately
- Call
- Return ok
rebar_compiler:analyze_all/2
Purpose: Analyze all applications to build dependency information
Signature:
-spec analyze_all(DAG, Apps) -> {Context, ReorderedApps} when
DAG :: {Module, Graph},
Apps :: [rebar_app_info:t()],
Context :: map(),
ReorderedApps :: [rebar_app_info:t()].
Flow:
- Prepare compiler environment (add paths)
- Gather contexts for all apps via
Compiler:context/1 - For each app:
- Find source files
- Populate DAG with sources via
rebar_compiler_dag:populate_sources/5
- Prune deleted files from DAG
- Populate dependencies via
rebar_compiler_dag:populate_deps/3 - Propagate timestamps via
rebar_compiler_dag:propagate_stamps/1 - Determine inter-app compile order
- Return context and reordered apps
rebar_compiler:compile_analyzed/3
Purpose: Compile a single application using pre-analyzed context
Signature:
-spec compile_analyzed(DAG, AppInfo, Context) -> ok when
DAG :: {Module, Graph},
AppInfo :: rebar_app_info:t(),
Context :: map().
Flow: Calls run/4 to execute compilation
run/4 (internal to rebar_compiler)
Purpose: Execute compiler for one application
Signature:
-spec run(Graph, CompilerMod, AppInfo, Contexts) -> ok when
Graph :: digraph:graph(),
CompilerMod :: module(),
AppInfo :: rebar_app_info:t(),
Contexts :: map().
Flow:
- Get compiler context for this app
- Find source files
- Call
CompilerMod:needed_files/4 - Split into
FirstFilesandRestFiles - Compile FirstFiles sequentially via
compile_each/5 - Check if RestFiles is
{Sequential, Parallel}or flat list - If split: compile Sequential, then Parallel
- If flat: compile all sequentially
- Store artifacts in DAG via
store_artifacts/2
rebar_compiler_erl:context/1
Purpose: Provide Erlang compiler configuration
Returns:
#{src_dirs => ExistingSrcDirs,
include_dirs => AbsIncl,
src_ext => ".erl",
out_mappings => [{".beam", EbinDir}],
dependencies_opts => [{includes, AbsIncl},
{macros, Macros},
{parse_transforms, PTrans}]}
Include Directories (in order):
APP/include/(standard)- Directories from
{i, Dir}inerl_opts - All source directories (including recursive)
- Top-level app directory
rebar_compiler_erl:needed_files/4
Purpose: Determine which .erl files need compilation
Flow:
- Split sources into parse transforms vs rest
- Check if any parse transform needs recompilation
- If yes: recompile ALL files (parse transforms affect everything)
- If no: determine needed files via
needed_files/6 - Extract
erl_first_filesconfiguration - Build subgraph of needed files
- Topological sort the subgraph
- Partition into files with incoming deps (sequential) vs independent (parallel)
- Return
{{FirstFiles, FirstOpts}, {{Sequential, Parallel}, RestOpts}}
rebar_compiler_erl:dependencies/4
Purpose: Find dependencies for an .erl file
Uses: rebar_compiler_epp - enhanced Erlang preprocessor
Returns: List of dependency file paths
Dependencies Include:
-include("file.hrl")directives-include_lib("app/include/file.hrl")directives-parse_transform(module)directives → module source file-behaviour(module)directives → behavior source file
rebar_compiler_erl:compile/4
Purpose: Compile single .erl file to .beam
Signature:
-spec compile(Source, Mappings, Config, ErlOpts) -> Result when
Source :: file:filename(),
Mappings :: [{Ext, Dir}],
Config :: rebar_dict(),
ErlOpts :: [term()],
Result :: ok | {ok, Warnings} | error | {error, Errors, Warnings}.
Flow:
- Extract output directory from Mappings
- Build options:
[{outdir, OutDir}, no_spawn_compiler_process | ErlOpts] - Call
compile:file(Source, Options) - Handle result:
{ok, Mod}: Success{ok, Mod, Warnings}: Success with warnings{error, Errors, Warnings}: Compilation failed
- Format errors/warnings for display
- Return result
rebar_compiler_erl:compile_and_track/4
Purpose: Compile and track artifact metadata
Returns:
{ok, [{Source, Target, AllOpts}]}
{ok, [{Source, Target, AllOpts}], Warnings}
{error, Errors, Warnings}
Tracked Data:
- Source file path
- Target artifact path
- Complete compilation options (for change detection)
Used By: DAG system to detect option changes requiring recompilation
compile_each/5 (internal)
Purpose: Compile list of files sequentially
Flow:
- For each file:
- Check if compiler exports
compile_and_track/4 - If yes: call and accumulate artifacts
- If no: call
compile/4(no tracking)
- Check if compiler exports
- Return list of tracked artifacts
compile_parallel/5 (internal)
Purpose: Compile list of files in parallel
Uses: rebar_parallel:queue/4
Flow:
- Create worker pool
- Queue compilation tasks
- Each worker calls
compile/4orcompile_and_track/4 - Collect results
- Return list of tracked artifacts
Worker Count: Configured by jobs option, defaults to number of CPUs
State Modification
No Direct State Modifications
Compilation doesn't modify rebar state directly. Changes are to:
- File system (creates
.beamfiles, etc.) - DAGs (updated and saved to disk)
- App info records (may be updated with compilation metadata)
DAG Updates
For each compiled file:
- Add/update artifact vertices in DAG
- Store artifact metadata (options, compiler version)
- Update timestamps
- Save DAG to
_build/PROFILE/.rebar3/COMPILER/source[_LABEL].dag
Configuration
Erlang Compiler Options
Configuration Key: erl_opts
Location: rebar.config or app-specific config
Common Options:
{erl_opts, [
debug_info, % Include debug information
warnings_as_errors, % Treat warnings as errors
{parse_transform, Module}, % Apply parse transform
{d, 'MACRO'}, % Define macro
{d, 'MACRO', Value}, % Define macro with value
{i, "include"}, % Include directory
{platform_define, Regex, Def}, % Platform-specific defines
{src_dirs, ["src", "lib"]}, % Additional source directories
no_debug_info, % Exclude debug information
inline_list_funcs % Inline list functions
]}.
First Files
Configuration Key: erl_first_files
Purpose: Compile specific files before others
Example:
{erl_opts, [
{erl_first_files, ["src/parser.erl", "src/types.erl"]}
]}.
Use Cases:
- Parse transforms (must compile before files using them)
- Type definitions (for other modules)
- Macros (defined in one file, used in others)
Extra Source Directories
Configuration Key: extra_src_dirs
Purpose: Additional source directories with separate compilation
Example:
{extra_src_dirs, [
"test",
{"scripts", [{recursive, true}]}
]}.
Behavior:
- Compiled to
_build/PROFILE/extras/DIR/ - Not included in application's
.appfile modules list - Use for tests, scripts, examples
Parallel Compilation
Configuration Key: jobs
Default: Number of logical CPU cores
Example:
{jobs, 4}. % Use 4 parallel workers
File System Operations
Files Read
| File Pattern | Purpose | When |
|---|---|---|
src/**/*.erl | Source files | Compilation |
src/**/*.yrl | Parser specs | Before .erl compilation |
src/**/*.xrl | Lexer specs | Before .erl compilation |
src/**/*.mib | MIB files | SNMP compilation |
include/**/*.hrl | Header files | Dependency scanning |
| DAG files | Cached dependencies | Start of compilation |
Files Written
| File Pattern | Source | Compiler |
|---|---|---|
ebin/*.beam | .erl | rebar_compiler_erl |
src/*.erl | .yrl | rebar_compiler_yrl |
src/*.erl | .xrl | rebar_compiler_xrl |
priv/mibs/*.bin | .mib | rebar_compiler_mib |
include/*.hrl | .mib | rebar_compiler_mib |
.rebar3/COMPILER/*.dag | - | All compilers (DAG cache) |
Directories Created
| Directory | Purpose |
|---|---|
_build/PROFILE/lib/APP/ebin/ | Compiled .beam files |
_build/PROFILE/lib/APP/priv/ | Private resources |
_build/PROFILE/extras/DIR/ | Extra source dirs output |
_build/PROFILE/.rebar3/COMPILER/ | DAG cache storage |
Error Conditions
Compilation Error
Condition: Source file has syntax or semantic errors
Error Format (from compile:file/2):
{error, [{File, [{Line, Module, Description}]}], Warnings}
Example:
Compiling src/my_module.erl failed
src/my_module.erl:10: syntax error before: '}'
Recovery: Fix source file and recompile
Missing Include File
Condition: -include("file.hrl") file not found
Error:
src/my_module.erl:5: can't find include file "missing.hrl"
Common Causes:
- File doesn't exist
- Not in include path
- Typo in filename
Recovery: Add file or fix include path
Parse Transform Not Found
Condition: Parse transform module not compiled or not in path
Error:
src/my_module.erl:1: parse transform 'my_transform' undefined
Recovery:
- Ensure parse transform compiled first
- Add to
erl_first_filesif needed - Check module name spelling
Circular Dependencies
Condition: Files depend on each other in a cycle
Detection: DAG topological sort fails
Example:
mod_a.erl includes mod_b.hrl
mod_b.erl includes mod_a.hrl
Impact: Compilation may fail or produce incorrect results
Recovery: Refactor to break cycle
DAG Version Mismatch
Condition: Cached DAG from different rebar3/compiler version
Behavior: DAG discarded, full recompilation
Log: Debug message about DAG invalidation
Missing Artifact
Condition: Expected artifact not created after compilation
Error:
Missing artifact path/to/expected/file.beam
Causes:
- Compiler failed silently
- Custom artifact configuration incorrect
Recovery: Check compiler output, fix artifact configuration
Edge Cases
Parse Transform Recompilation
Scenario: Parse transform source changed
Behavior: ALL files recompiled (parse transforms affect all files)
Detection: needed_files/6 checks parse transform timestamps
ERL_OPTS Change
Condition: Compiler options changed in rebar.config
Behavior: Affected files recompiled
Detection: DAG compares stored options with current options
Include File Modified
Condition: .hrl file changed
Behavior: All files including it (directly or transitively) recompiled
Detection: DAG timestamp propagation
Generated Source Files
Scenario: .yrl generates .erl, which must then compile to .beam
Handling:
rebar_compiler_yrlruns first, generates.erlrebar_compiler_erlruns second, finds generated.erl- Compiles generated
.erlto.beam
Critical: Compiler order matters
Parallel Compilation Failures
Scenario: One of parallel compilations fails
Behavior:
- Worker reports error
- Main process collects error
- Compilation aborts with error
Impact: Some files may be partially compiled
Large Projects
Optimization: Parallel compilation significantly speeds up builds
Example: 100 independent .erl files
- Sequential: ~100 seconds
- Parallel (8 cores): ~15 seconds
Cross References
Dependencies
- Application Discovery: Provides applications to compile
- Compilation Order Determination: Orders applications
- Dependency Graph Management: DAG system details
Dependents
- Application File Generation: Follows compilation
Related
- Hooks & Extensibility: Hooks during compilation
- Configuration Reference: Compilation options
Example Scenarios
Scenario 1: Simple Erlang Module Compilation
Files:
src/my_module.erl
Flow:
- Compiler:
rebar_compiler_erl - Find:
[src/my_module.erl] - Check DAG: not compiled yet
- Needed files:
[src/my_module.erl] - No first files, no dependencies
- Parallel compilation: single file, no benefit
- Compile:
compile:file("src/my_module.erl", [{outdir, "ebin"}, ...]) - Result:
ebin/my_module.beam
Scenario 2: Module with Include Files
Files:
src/my_module.erl
include/types.hrl
my_module.erl:
-module(my_module).
-include("types.hrl").
Flow:
- Dependency scan finds
include/types.hrl - Add dependency:
my_module.erl→types.hrl - Store in DAG
- Compile
my_module.erl - Store artifact with timestamp
Next Run:
- Check timestamps
- If
types.hrlmodified: recompilemy_module.erl - If not: skip compilation
Scenario 3: Parse Transform
Files:
src/my_transform.erl (parse transform)
src/user_module.erl (uses transform)
user_module.erl:
-compile([{parse_transform, my_transform}]).
Flow:
- Split sources:
[my_transform.erl]vs[user_module.erl] - FirstFiles:
[my_transform.erl] - Compile
my_transform.erlfirst - Dependency:
user_module.erl→my_transform.erl - Compile
user_module.erl(transform applied)
Scenario 4: Yecc Parser
Files:
src/parser.yrl
Flow:
- Compiler:
rebar_compiler_yrl - Find:
[src/parser.yrl] - Compile:
yecc:file("src/parser.yrl") - Generates:
src/parser.erl - Compiler:
rebar_compiler_erl - Find:
[src/parser.erl](generated) - Compile:
compile:file("src/parser.erl", ...) - Result:
ebin/parser.beam
Scenario 5: Parallel Compilation
Files:
src/mod_a.erl (independent)
src/mod_b.erl (independent)
src/mod_c.erl (independent)
src/mod_d.erl (uses mod_a)
Dependencies:
- mod_d → mod_a
- mod_a, mod_b, mod_c independent
Compilation Order:
Sequential: [mod_a] (mod_d depends on it)
Parallel: [mod_b, mod_c] (independent)
Then Sequential: [mod_d] (after mod_a done)
Workers: 2 parallel workers
- Worker 1: compile mod_b
- Worker 2: compile mod_c
- Main: compile mod_a (sequential)
- Main: compile mod_d (after mod_a)
Scenario 6: Incremental Build
Initial Build:
Compile: mod_a, mod_b, mod_c
DAG: stored with timestamps
Modify mod_b.erl:
Find: mod_b.erl timestamp changed
DAG: mod_b needs recompilation
Compile: only mod_b
Result: 1 file compiled vs 3 initial
Modify include/common.hrl (used by mod_a, mod_c):
DAG propagation: common.hrl timestamp updated
Dependents: mod_a, mod_c use common.hrl
Compile: mod_a, mod_c (mod_b unchanged)
Result: 2 files compiled
Scenario 7: Changed Compiler Options
Initial:
{erl_opts, [debug_info]}.
Build: All files compiled with debug_info
Modified:
{erl_opts, [debug_info, warnings_as_errors]}.
Next Build:
- DAG compares stored opts vs current opts
- Mismatch detected:
warnings_as_errorsadded - All files need recompilation
- Compile all with new options
- Update DAG with new options
Dependency Graph Management
Purpose
The Dependency Graph (DAG) management system tracks relationships between source files, header files, and compiled artifacts. It enables incremental compilation by maintaining timestamps, detecting changes, propagating modifications through dependency chains, determining inter-application compilation order, and persisting graph data between builds for performance.
When It Executes
DAG operations occur throughout the compilation process within Source File Compilation:
- Initialization:
load_dags/2- Load or create DAGs before compilation - Population:
populate_sources/5- Add source files and scan dependencies - Pruning:
prune/5- Remove deleted files - Dependency Scanning:
populate_deps/3- Scan header files - Timestamp Propagation:
propagate_stamps/1- Update timestamps through chains - Compilation Ordering:
compile_order/4- Determine inter-app order - Artifact Tracking:
store_artifact/4- Record compiled outputs - Persistence:
maybe_store/5- Save DAG if modified
Prerequisites
- Output directory exists for DAG storage
- Compiler modules loaded
- Applications discovered with source files
Outputs
- In-memory directed graphs tracking dependencies
- Persisted DAG files in
_build/PROFILE/.rebar3/COMPILER/source[_LABEL].dag - Compilation order based on dependencies
- List of files needing recompilation
Execution Flow
graph TD
A[Start: DAG Operations] --> B[init: Load or Create DAG]
B --> B1{DAG file exists?}
B1 -->|Yes| B2[Read DAG file]
B1 -->|No| B3[Create new DAG]
B2 --> B4{Version matches?}
B4 -->|Yes| B5{CritMeta matches?}
B4 -->|No| B6[Discard DAG]
B5 -->|Yes| B7[Restore graph from file]
B5 -->|No| B6
B6 --> B3
B7 --> C[prune: Remove Deleted Files]
B3 --> C
C --> C1[Get all vertices in DAG]
C1 --> C2[For each vertex not in Sources]
C2 --> C3{Is source file?}
C3 -->|Yes| C4[Check if in app paths]
C3 -->|No| C5{Is artifact?}
C5 -->|Yes| C6[Skip]
C5 -->|No| C7{Has incoming edges?}
C7 -->|No| C8[Delete vertex: header file removed]
C7 -->|Yes| C6
C4 --> C9{In app paths?}
C9 -->|Yes| C10[Delete vertex + artifacts]
C9 -->|No| C6
C10 --> D[populate_sources: Add Sources]
C8 --> D
C6 --> D
D --> D1[Create parallel worker pool]
D1 --> D2[For each source file]
D2 --> D3{Vertex exists?}
D3 -->|Yes| D4[Get stored timestamp]
D3 -->|No| D5[Add vertex with current timestamp]
D4 --> D6{File modified?}
D6 -->|Yes| D7[Update timestamp]
D6 -->|No| D8[Skip unchanged]
D7 --> D9[Queue dependency scan]
D5 --> D9
D9 --> D10{More sources?}
D10 -->|Yes| D2
D10 -->|No| D11[Wait for parallel scans]
D11 --> D12[Process scan results]
D12 --> D13[Add/update dependency edges]
D13 --> D14[Mark DAG dirty]
D14 --> E[populate_deps: Scan Headers]
D8 --> E
E --> E1[Find header files in DAG]
E1 --> E2[Refresh timestamps for headers]
E2 --> F[propagate_stamps: Propagate Timestamps]
F --> F1{DAG dirty?}
F1 -->|No| F2[Skip propagation]
F1 -->|Yes| F3[Topological sort vertices]
F3 --> F4[Reverse sort order]
F4 --> F5[For each vertex, end to start]
F5 --> F6[Get out-neighbors: dependencies]
F6 --> F7[Find max timestamp of dependencies]
F7 --> F8{Current < max dependency?}
F8 -->|Yes| F9[Update to max timestamp]
F8 -->|No| F10[Keep current]
F9 --> F11{More vertices?}
F10 --> F11
F11 -->|Yes| F5
F11 -->|No| G[compile_order: Inter-App Order]
F2 --> G
G --> G1[Build app-level DAG]
G1 --> G2[For each file dependency edge]
G2 --> G3{Is artifact edge?}
G3 -->|Yes| G4[Skip]
G3 -->|No| G5[Resolve files to apps]
G5 --> G6{Same app?}
G6 -->|Yes| G4
G6 -->|No| G7[Add inter-app dependency]
G7 --> G8{More edges?}
G4 --> G8
G8 -->|Yes| G2
G8 -->|No| G9[Interleave sort]
G9 --> H[Compilation Proceeds]
H --> I[store_artifact: Track Outputs]
I --> I1[For each compiled file]
I1 --> I2[Add artifact vertex]
I2 --> I3[Add artifact edge: artifact → source]
I3 --> I4[Mark DAG dirty]
I4 --> J[maybe_store: Save DAG]
J --> J1{DAG dirty?}
J1 -->|Yes| J2[Serialize DAG to disk]
J1 -->|No| J3[Skip save]
J2 --> K[terminate: Cleanup]
J3 --> K
K --> L[Delete in-memory DAG]
style A fill:#e1f5ff
style L fill:#e1ffe1
DAG Structure
Vertices
Three Types:
-
Source Files:
.erl,.yrl,.xrl,.mib- Label:
LastModified(timestamp) - Example:
{"/path/to/file.erl", 1234567890}
- Label:
-
Header/Include Files:
.hrl, other included files- Label:
LastModified(timestamp) - Example:
{"/path/to/file.hrl", 1234567890}
- Label:
-
Artifact Files:
.beam,.bin, compiled outputs- Label:
{artifact, Metadata} - Metadata includes: compiler options, compiler version
- Example:
{"/path/to/file.beam", {artifact, [{compiler_version, "8.0"}, ...]}}
- Label:
Edges
Two Types:
-
Dependency Edges: Source/header → dependency
- Label:
[](empty list) - Direction: File points TO what it depends ON
- Example:
my_module.erl → types.hrl
- Label:
-
Artifact Edges: Artifact → source
- Label:
artifact(atom) - Direction: Artifact points TO source that created it
- Example:
my_module.beam → my_module.erl
- Label:
Special Vertex
Dirty Bit: '$r3_dirty_bit'
- Presence indicates DAG has been modified
- Used to determine if save is needed
- Not a real file
Functions & API Calls
rebar_compiler_dag:init/4
Purpose: Initialize or restore DAG from disk
Signature:
-spec init(Dir, Compiler, Label, CritMeta) -> Graph when
Dir :: file:filename_all(),
Compiler :: atom(),
Label :: string() | undefined,
CritMeta :: term(),
Graph :: digraph:graph().
Arguments:
Dir: Directory for DAG storage (typically deps_dir)Compiler: Compiler module name (e.g.,rebar_compiler_erl)Label: Optional label (e.g., "apps", "project_apps")CritMeta: Critical metadata for invalidation (compiler version, etc.)
Returns: Digraph handle
Flow:
- Create new acyclic digraph
- Determine DAG file path
- Try to restore from file
- If restoration fails: delete invalid file, return empty graph
- Return graph
DAG File Path:
- With label:
_build/PROFILE/.rebar3/COMPILER/source_LABEL.dag - Without label:
_build/PROFILE/.rebar3/COMPILER/source.dag
rebar_compiler_dag:status/4
Purpose: Quick validation of DAG file without loading
Signature:
-spec status(Dir, Compiler, Label, CritMeta) -> Status when
Dir :: file:filename_all(),
Compiler :: atom(),
Label :: string() | undefined,
CritMeta :: term(),
Status :: valid | bad_format | bad_vsn | bad_meta | not_found.
Returns:
valid: DAG file valid and compatiblebad_format: File corrupted or wrong formatbad_vsn: DAG version mismatch (current: 4)bad_meta: Critical metadata mismatchnot_found: DAG file doesn't exist
Use: Determines if full rebuild needed before actual compilation
rebar_compiler_dag:prune/5
Purpose: Remove deleted files from DAG
Signature:
-spec prune(G, SrcExt, ArtifactExt, Sources, AppPaths) -> ok when
G :: digraph:graph(),
SrcExt :: string(),
ArtifactExt :: [string()],
Sources :: [file:filename()],
AppPaths :: [{AppDir, OutDir}].
Arguments:
G: DAG graphSrcExt: Source extension (e.g., ".erl")ArtifactExt: Artifact extensions (e.g., [".beam"])Sources: Current list of source filesAppPaths: App directories and output directories
Flow:
- Find vertices not in
Sources - For source files: check if in app paths, delete if so
- For header files with no incoming edges: delete
- For deleted sources: delete associated artifacts
- Mark DAG dirty if anything deleted
rebar_compiler_dag:populate_sources/5
Purpose: Add sources and scan their dependencies
Signature:
-spec populate_sources(G, Compiler, InDirs, Sources, DepOpts) -> ok when
G :: digraph:graph(),
Compiler :: module(),
InDirs :: [file:filename()],
Sources :: [file:filename()],
DepOpts :: term().
Arguments:
G: DAG graphCompiler: Compiler module (fordependencies/4callback)InDirs: Include directories to searchSources: Source files to addDepOpts: Options for dependency scanning
Flow:
- Create parallel worker pool
- For each source:
- Check if vertex exists
- Compare timestamps
- If new or modified: queue dependency scan
- If unchanged: skip
- Wait for all scans to complete
- Add dependency edges to graph
- Delete obsolete edges
- Mark dirty if changes made
Parallel Scanning: Uses rebar_parallel for performance
rebar_compiler_dag:populate_deps/3
Purpose: Scan header files for timestamp updates
Signature:
-spec populate_deps(G, SourceExt, ArtifactExts) -> ok when
G :: digraph:graph(),
SourceExt :: string(),
ArtifactExts :: [string()].
Flow:
- Find all vertices that are neither sources nor artifacts (headers)
- Refresh timestamp for each header file
- Update vertex labels
rebar_compiler_dag:propagate_stamps/1
Purpose: Propagate timestamps through dependency chains
Signature:
-spec propagate_stamps(G :: digraph:graph()) -> ok.
Algorithm:
Given: A → B → C → D with timestamps [0, 1, 3, 2]
Process in reverse topological order: [D, C, B, A]
For D (timestamp 2):
Dependencies: none
Keep timestamp: 2
For C (timestamp 3):
Dependencies: [D with timestamp 2]
Max dependency: 2
Current: 3
Keep timestamp: 3 (already newer)
For B (timestamp 1):
Dependencies: [C with timestamp 3]
Max dependency: 3
Current: 1 < 3
Update timestamp: 3
For A (timestamp 0):
Dependencies: [B with timestamp 3]
Max dependency: 3
Current: 0 < 3
Update timestamp: 3
Result: [3, 3, 3, 2]
Why: Ensures dependent files know when their dependencies changed
rebar_compiler_dag:compile_order/4
Purpose: Determine inter-application compilation order
Signature:
-spec compile_order(G, AppDefs, SrcExt, ArtifactExt) -> [AppName] when
G :: digraph:graph(),
AppDefs :: [{AppName, AppPath}],
SrcExt :: string(),
ArtifactExt :: [string()],
AppName :: atom().
Returns: Ordered list of application names
Flow:
- Create new app-level DAG
- For each file dependency edge in G:
- Skip artifact edges
- Resolve both files to apps
- If different apps: add inter-app edge
- Use interleave sort (respects original order + DAG constraints)
- Return sorted app names
- Delete app DAG
Interleave Sort: Preserves rebar.config order while respecting hard dependencies
rebar_compiler_dag:store_artifact/4
Purpose: Track compiled artifact in DAG
Signature:
-spec store_artifact(G, Source, Target, Meta) -> ok when
G :: digraph:graph(),
Source :: file:filename(),
Target :: file:filename(),
Meta :: term().
Arguments:
G: DAG graphSource: Source file pathTarget: Artifact file pathMeta: Artifact metadata (options, versions)
Flow:
- Add artifact vertex with metadata
- Add artifact edge:
Target → Source - Check for duplicate edge (artifact compilation may run multiple times)
- Mark DAG dirty
rebar_compiler_dag:maybe_store/5
Purpose: Save DAG to disk if modified
Signature:
-spec maybe_store(G, Dir, Compiler, Label, CritMeta) -> ok when
G :: digraph:graph(),
Dir :: file:filename_all(),
Compiler :: atom(),
Label :: string() | undefined,
CritMeta :: term().
Flow:
- Check if DAG is dirty
- If dirty:
- Clear dirty bit
- Serialize DAG to binary
- Write to file
- If not dirty: skip save
DAG File Format:
#dag{
vsn = 4,
meta = CritMeta,
vtab = Vertices,
etab = Edges,
ntab = NeighborTable
}
rebar_compiler_dag:terminate/1
Purpose: Clean up in-memory DAG
Signature:
-spec terminate(G :: digraph:graph()) -> true.
Flow: Calls digraph:delete/1
Note: Does not delete disk files
State Modification
DAG Internal State
Dirty Bit: Special vertex '$r3_dirty_bit'
- Added when DAG modified
- Checked before saving
- Cleared after save
Modification Operations:
- Adding/updating vertices
- Adding/deleting edges
- Pruning files
- Storing artifacts
Persistence
Saved Data:
- All vertices with labels (files + timestamps/metadata)
- All edges with labels (dependencies + artifacts)
- Version number (currently 4)
- Critical metadata
Not Saved:
- Dirty bit
- Transient state
Configuration
No Direct Configuration
DAGs are managed automatically. However, behavior affected by:
DAG Version: ?DAG_VSN = 4
- Hardcoded in
rebar_compiler_dag.erl - Change invalidates all cached DAGs
Critical Metadata:
- For
rebar_compiler_erl: includes compiler version - Mismatch triggers rebuild
File System Operations
Files Read
| File | When | Purpose |
|---|---|---|
_build/PROFILE/.rebar3/COMPILER/source*.dag | init/4 | Restore cached DAG |
| Source files | populate_sources/5 | Check timestamps |
| Header files | populate_deps/3 | Check timestamps |
Files Written
| File | When | Content |
|---|---|---|
_build/PROFILE/.rebar3/COMPILER/source*.dag | maybe_store/5 | Serialized DAG |
Directories Created
| Directory | When |
|---|---|
_build/PROFILE/.rebar3/COMPILER/ | Before saving DAG |
Error Conditions
DAG Restoration Failure
Condition: Corrupted or incompatible DAG file
Handling:
- Log warning
- Delete corrupt file
- Create new empty DAG
- Full rebuild triggered
Not Fatal: Compilation continues
Version Mismatch
Condition: DAG file has different version number
Handling:
- DAG discarded
- Full rebuild
Common Cause: rebar3 upgrade
Critical Metadata Mismatch
Condition: Compiler version or important options changed
Example: Erlang/OTP upgraded from 24 to 25
Handling:
- DAG discarded
- Full rebuild
Ensures: Artifacts always match current environment
Circular Dependencies (File-Level)
Condition: Files depend on each other in cycle
Detection: digraph_utils:topsort/1 fails during timestamp propagation
Handling:
- Timestamps may not propagate correctly
- Compilation may fail later
- Not specifically caught at DAG level
Edge Cases
Header File Deleted
Scenario: .hrl file removed from project
Handling:
prune/5finds header with no incoming edges- Delete vertex
- Files including it will fail compilation (caught later)
Source File Deleted
Scenario: .erl file removed
Handling:
prune/5finds source not in current list- Check if in app paths
- Delete source vertex
- Find and delete associated artifact (
.beam) - Mark dirty
Timestamp Regression
Scenario: File restored from backup with older timestamp
Behavior: Change not detected
Limitation: Timestamp-based system can't detect regression
Workaround: Force rebuild (rebar3 clean && rebar3 compile)
Parse Transform Affects All Files
Scenario: Parse transform modified
Handling:
- Compiler (
rebar_compiler_erl) detects parse transform change - Returns all files as needing compilation
- DAG not specifically involved
Multiple Artifacts from One Source
Scenario: .mib generates .bin and .hrl
Handling:
- Call
store_artifact/4twice - Two artifact vertices
- Two artifact edges to same source
Concurrent Builds
Scenario: Two rebar3 processes running simultaneously
Risk: DAG file corruption
Mitigation: None built-in; avoid concurrent builds
Large Projects
Performance: DAG operations scale linearly
Optimization: Parallel dependency scanning
Typical: 1000+ files handled efficiently
Cross References
Dependencies
- Source File Compilation: Uses DAGs for all compilation
Dependents
- None; DAG is a supporting system
Related
- Configuration Reference: Compiler options affect DAG metadata
Example Scenarios
Scenario 1: Initial Build
State: No DAG file exists
Flow:
init/4: Create empty DAGpopulate_sources/5: Add all sources- All files new
- Scan all dependencies
- Build complete graph
propagate_stamps/1: Propagate timestamps- Compilation proceeds
store_artifact/4: Track each.beammaybe_store/5: Save DAG to disk
Result: Full compilation, DAG saved
Scenario 2: Incremental Build (No Changes)
State: Valid DAG exists, no files modified
Flow:
init/4: Restore DAG from diskpopulate_sources/5: Check all sources- All timestamps match
- No scans needed
propagate_stamps/1: Skip (not dirty)needed_files/4: No files need compilation- Compilation skipped
maybe_store/5: Skip save (not dirty)
Result: No compilation, instant completion
Scenario 3: Single File Modified
Files:
src/mod_a.erl (modified)
src/mod_b.erl (unchanged)
src/mod_c.erl (unchanged, includes mod_a.hrl)
include/mod_a.hrl (unchanged)
DAG:
mod_a.erl (timestamp updated)
mod_b.erl → ...
mod_c.erl → mod_a.hrl
mod_a.hrl (unchanged)
Flow:
populate_sources/5: Detect mod_a.erl changed- Update vertex timestamp
propagate_stamps/1: No propagation (mod_a.hrl unchanged)- Compilation: Only mod_a.erl
store_artifact/4: Update mod_a.beam- Save DAG
Result: One file compiled
Scenario 4: Header File Modified
Files:
include/types.hrl (modified)
src/mod_a.erl → types.hrl
src/mod_b.erl → types.hrl
src/mod_c.erl (independent)
Flow:
populate_deps/3: Refresh types.hrl timestamppropagate_stamps/1:- types.hrl timestamp: 100 (new)
- mod_a.erl depends on types.hrl
- Update mod_a.erl timestamp: 100
- mod_b.erl depends on types.hrl
- Update mod_b.erl timestamp: 100
- mod_c.erl independent: unchanged
- Compilation: mod_a.erl, mod_b.erl
- Save DAG
Result: Two files compiled
Scenario 5: Transitive Dependencies
Files:
include/base.hrl (modified)
include/types.hrl → base.hrl
src/mod_a.erl → types.hrl
Flow:
- Refresh base.hrl: timestamp 100
- Propagate:
- types.hrl depends on base.hrl: update to 100
- mod_a.erl depends on types.hrl: update to 100
- Compilation: mod_a.erl
Result: Change propagates through chain
Scenario 6: Compiler Options Changed
Initial:
{erl_opts, [debug_info]}.
DAG Metadata: [{compiler_version, "8.0"}, {options, [debug_info]}]
Changed:
{erl_opts, [debug_info, inline]}.
Flow:
init/4: Restore DAG- Check critical metadata
- New options:
[debug_info, inline] - Mismatch detected (in compiler, not DAG module)
- All files marked for recompilation
- DAG updated with new metadata
- Save DAG
Result: Full rebuild with new options
Scenario 7: Inter-App Dependencies
Apps:
app_common: no deps
app_api: depends on app_common (parse_transform)
app_web: depends on app_api
DAG (file-level):
app_api/src/api_mod.erl → app_common/src/my_transform.erl
app_web/src/web_mod.erl → app_api/include/api.hrl
Flow:
compile_order/4: Build app-level DAG- Resolve file deps to apps:
- api_mod.erl in app_api
- my_transform.erl in app_common
- Add edge: app_api → app_common
- Similar for app_web → app_api
- Interleave sort
- Result:
[app_common, app_api, app_web]
Result: Apps compiled in correct order
Scenario 8: DAG Version Upgrade
Scenario: rebar3 upgraded, DAG version changes from 3 to 4
Flow:
init/4: Try restore DAG- Read file:
#dag{vsn = 3, ...} - Version mismatch: 3 ≠ 4
status/4returnsbad_vsn- Delete old DAG file
- Create new empty DAG
- Full rebuild
Result: Clean rebuild after upgrade
Application File Generation
Purpose
Application file generation transforms .app.src (application resource source) files into .app (application resource) files by substituting variables, adding module lists, determining version numbers, ensuring required fields exist, and writing the final application specification to the ebin/ directory. This stage makes applications ready for deployment and release generation.
When It Executes
This stage executes within the compile/4 function in Source File Compilation, specifically in the compile_app_files/3 sub-stage:
Sequence:
- Source files compiled →
.beamfiles created prepare_app_file: Pre-hooks executedcompile_app_files: This stagefinalize_app_file: Post-hooks executed
Prerequisites
.app.srcor.app.src.scriptfile exists- Source files compiled to
.beamfiles inebin/ - Version information available (from config, git, or other sources)
- Application info record initialized
Outputs
.appfile inebin/directory- Complete application specification with:
- Accurate module list
- Resolved version number
- All required fields (
registered,description) - Variable substitutions applied
Execution Flow
graph TD
A[Start: compile_app_files] --> B{Source file exists?}
B -->|app.src.script| C[Load and execute .app.src.script]
B -->|app.src| D[Read .app.src]
B -->|None| E[Error: missing app file]
C --> F[Parse application term]
D --> F
F --> F1[Extract application, AppName, AppData]
F1 --> G[load_app_vars Load external variables]
G --> G1{app_vars_file configured?}
G1 -->|Yes| G2[Read vars from file]
G1 -->|No| G3[Empty vars list]
G2 --> H[ebin_modules Generate module list]
G3 --> H
H --> H1[Find all .beam files in ebin/]
H1 --> H2[Get extra_src_dirs]
H2 --> H3[Filter out extra dir modules]
H3 --> H4[Convert .beam to module names]
H4 --> H5[Add to vars modules]
H5 --> I[apply_app_vars Substitute variables]
I --> I1[For each Key Value in AppVars]
I1 --> I2[Replace Key in AppData with Value]
I2 --> J[app_vsn Determine version]
J --> J1{vsn value?}
J1 -->|git| J2[Extract from git tags]
J1 -->|String| J3[Use literal string]
J1 -->|semver| J4[Use semver calculation]
J2 --> K[Update vsn in AppData]
J3 --> K
J4 --> K
K --> L[ensure_registered Check registered field]
L --> L1{registered exists?}
L1 -->|No| L2[Add registered empty list]
L1 -->|Yes| L3[Keep existing]
L2 --> M[ensure_description Check description]
L3 --> M
M --> M1{description exists?}
M1 -->|No| M2[Add description empty string]
M1 -->|Yes| M3[Keep existing]
M2 --> N[Format application spec]
M3 --> N
N --> N1[io_lib format application term]
N1 --> O[Determine .app file path]
O --> O1[OutDir/ebin/AppName.app]
O1 --> P[write_file_if_contents_differ]
P --> P1{Contents changed?}
P1 -->|Yes| P2[Write .app file]
P1 -->|No| P3[Skip write preserve timestamp]
P2 --> Q[validate_app Validate result]
P3 --> Q
Q --> Q1[Read .app file]
Q1 --> Q2[Parse application term]
Q2 --> Q3[validate_name Check name matches]
Q3 --> Q4{Name matches filename?}
Q4 -->|No| Q5[Error invalid name]
Q4 -->|Yes| Q6[validate_app_modules]
Q6 --> Q7{validate_app_modules = true?}
Q7 -->|Yes| Q8[Check all modules exist]
Q7 -->|No| Q9[Skip validation]
Q8 --> Q10{All modules found?}
Q10 -->|No| Q11[Error missing module]
Q10 -->|Yes| R[Update AppInfo with vsn]
Q9 --> R
R --> S[End .app file generated]
style A fill:#e1f5ff
style S fill:#e1ffe1
style E fill:#ffe1e1
style Q5 fill:#ffe1e1
style Q11 fill:#ffe1e1
Functions & API Calls
rebar_otp_app:compile/2
Purpose: Main entry point for .app file generation
Signature:
-spec compile(State, App) -> {ok, UpdatedApp} | {error, Reason} when
State :: rebar_state:t(),
App :: rebar_app_info:t(),
UpdatedApp :: rebar_app_info:t(),
Reason :: term().
Flow:
- Check for
.app.src.scriptfile - If not found, check for
.app.srcfile - If found, call
preprocess/3 - Call
validate_app/2to verify result - Return updated app info
preprocess/3
Purpose: Process .app.src into .app file
Signature:
-spec preprocess(State, AppInfo, AppSrcFile) -> UpdatedAppInfo when
State :: rebar_state:t(),
AppInfo :: rebar_app_info:t(),
AppSrcFile :: file:filename(),
UpdatedAppInfo :: rebar_app_info:t().
Arguments:
State: Rebar stateAppInfo: Application information recordAppSrcFile: Path to.app.srcor.app.src.script
Returns: Updated app info with .app file path and version
Flow:
- Read and parse
AppSrcFile - Load app vars via
load_app_vars/1 - Generate module list via
ebin_modules/2 - Apply vars via
apply_app_vars/2 - Determine version via
app_vsn/4 - Ensure
registeredfield exists - Ensure
descriptionfield exists - Format as Erlang term
- Write to
.appfile - Return updated app info
load_app_vars/1
Purpose: Load variables for substitution
Signature:
-spec load_app_vars(State :: rebar_state:t()) -> [{Key, Value}] when
Key :: atom(),
Value :: term().
Configuration: app_vars_file in rebar.config
Example:
% rebar.config:
{app_vars_file, "config/app.vars"}.
% config/app.vars:
{copyright, "Copyright (c) 2024 My Company"}.
{author, "John Doe"}.
Returns: List of {Key, Value} tuples
ebin_modules/2
Purpose: Generate list of compiled modules
Signature:
-spec ebin_modules(AppInfo, Dir) -> [Module] when
AppInfo :: rebar_app_info:t(),
Dir :: file:filename(),
Module :: atom().
Flow:
- Find all
.beamfiles inDir/ebin/ - Get
extra_src_dirsconfiguration - Filter out modules from extra directories
- Convert beam file names to module atoms
- Return sorted module list
Why Filter Extra Dirs: Modules in extra_src_dirs shouldn't be in the main .app file's modules list (they're for tests, scripts, etc.)
extra_dirs/1
Purpose: Get extra source directories (excluding normal src_dirs)
Returns: List of directories like ["test", "scripts"]
in_extra_dir/3
Purpose: Check if beam file originated from extra directory
Uses: beam_lib:chunks/2 to read compile_info and find original source path
apply_app_vars/2
Purpose: Substitute variables in application data
Signature:
-spec apply_app_vars(Vars, AppData) -> UpdatedAppData when
Vars :: [{Key, Value}],
AppData :: [tuple()],
UpdatedAppData :: [tuple()].
Logic: For each {Key, Value}, replace {Key, _} in AppData with {Key, Value}
Example:
AppVars = [{modules, [mod1, mod2]}, {custom_key, "value"}]
AppData = [{vsn, "1.0.0"}, {modules, []}, {custom_key, undefined}]
Result = [{vsn, "1.0.0"}, {modules, [mod1, mod2]}, {custom_key, "value"}]
app_vsn/4
Purpose: Determine application version
Signature:
-spec app_vsn(AppInfo, AppData, AppFile, State) -> Version when
AppInfo :: rebar_app_info:t(),
AppData :: [tuple()],
AppFile :: file:filename(),
State :: rebar_state:t(),
Version :: string().
Calls: rebar_utils:vcs_vsn/3
Version Specifications:
-
Literal String:
{vsn, "1.2.3"}Returns:
"1.2.3" -
Git Tag:
{vsn, git}Returns: Latest git tag, e.g.,
"v1.2.3"or"1.2.3-15-gabc123"if commits after tag -
Git Short/Long:
{vsn, {git, short}} % 7-char SHA {vsn, {git, long}} % 40-char SHA -
Semver from Git:
{vsn, semver}Calculates semantic version from git history
-
Command:
{vsn, {cmd, "git describe --tags"}}Executes command, uses output
ensure_registered/1
Purpose: Ensure registered field exists
Signature:
-spec ensure_registered(AppData) -> UpdatedAppData when
AppData :: [tuple()],
UpdatedAppData :: [tuple()].
Logic:
- If
registeredkey exists: keep it - If not: add
{registered, []}
Reason: Required by systools:make_relup/4
ensure_description/1
Purpose: Ensure description field exists
Similar to ensure_registered/1
Adds: {description, ""} if missing
Reason: Required for releases
write_file_if_contents_differ/3
Purpose: Write file only if contents changed
Benefit: Preserves timestamp if content unchanged, preventing unnecessary recompilation
validate_app/2
Purpose: Verify generated .app file is valid
Flow:
- Read
.appfile - Parse as Erlang term
- Verify format:
{application, AppName, AppData} - Validate name matches filename
- Optionally validate modules exist
validate_name/2
Purpose: Ensure app name matches filename
Example:
- File:
ebin/my_app.app - Expected name:
my_app - Actual name in file: must be
my_app
Error if mismatch: Prevents deployment issues
validate_app_modules/3
Purpose: Verify all listed modules exist as .beam files
Configuration: validate_app_modules (default: true)
Checks:
- Every module in
moduleslist has corresponding.beamfile - No missing modules
- No extra unlisted modules (warning only)
State Modification
App Info Updates
| Field | Updated To | When |
|---|---|---|
app_file | Path to generated .app | After write |
vsn | Resolved version | After version determination |
original_vsn | Same as vsn | If not already set |
Configuration
Application Source File Format
File: src/APP.app.src
Structure:
{application, my_app, [
{description, "My Application"},
{vsn, "1.0.0"},
{registered, [my_app_sup]},
{applications, [kernel, stdlib, sasl]},
{included_applications, []},
{mod, {my_app_app, []}},
{env, [
{config_key, default_value}
]},
{modules, []}, % Filled automatically
{licenses, ["Apache 2.0"]},
{links, [{"GitHub", "https://github.com/user/my_app"}]}
]}.
Required Fields
| Field | Required | Default if Missing |
|---|---|---|
description | Yes | "" (added automatically) |
vsn | Yes | Error if missing |
registered | Yes | [] (added automatically) |
applications | Yes | Error if missing |
modules | No | Generated automatically |
mod | No | None (library app) |
Variable Substitution
Syntax: Use atoms as placeholders
Example:
% .app.src:
{application, my_app, [
{vsn, "1.0.0"},
{modules, modules}, % Placeholder
{custom_field, custom_value} % Placeholder
]}.
% Substitution:
AppVars = [
{modules, [mod1, mod2, mod3]},
{custom_value, "Actual Value"}
]
% Result in .app:
{application, my_app, [
{vsn, "1.0.0"},
{modules, [mod1, mod2, mod3]},
{custom_field, "Actual Value"}
]}.
App Vars File
Configuration:
{app_vars_file, "config/app.vars"}.
File Format (Erlang terms):
{copyright, "Copyright (c) 2024"}.
{build_date, "2024-01-15"}.
{custom_value, some_atom}.
Validate App Modules
Configuration:
{validate_app_modules, true}. % Default
{validate_app_modules, false}. % Skip validation
When to Disable:
- Dynamic module loading
- Modules generated at runtime
- NIF-based applications with special requirements
File System Operations
Files Read
| File | Purpose | Required |
|---|---|---|
src/APP.app.src | Application specification | Yes (or .script) |
src/APP.app.src.script | Dynamic specification | Alternative |
config/app.vars | Variable substitution | No |
ebin/*.beam | Module list generation | Yes |
Files Written
| File | Content | When |
|---|---|---|
ebin/APP.app | Application resource | Always (if changed) |
Beam Inspection
Uses beam_lib:chunks/2 to read:
compile_info: Find source file path- Used to filter extra directory modules
Error Conditions
Missing .app.src File
Condition: No .app.src or .app.src.script found
Error: {missing_app_file, Filename}
Message: "App file is missing: src/my_app.app.src"
Recovery: Create .app.src file
Invalid Application Name
Condition: Name in file doesn't match filename
Example:
% File: ebin/my_app.app
% Content:
{application, wrong_name, [...]}.
Error: {invalid_name, File, AppName}
Message: "Invalid ebin/my_app.app: name of application (wrong_name) must match filename."
Recovery: Fix name in .app.src
Missing Module
Condition: Module listed in .app but no .beam file exists
Error: From validate_application_info/2
Example: .app lists my_module but ebin/my_module.beam missing
Common Causes:
- Compilation failed for that module
- Module manually added to
.app.src - Typo in module name
Recovery: Ensure module compiles successfully
Parse Error in .app.src
Condition: Syntax error in .app.src
Error: {file_read, AppName, ".app.src", Reason}
Example: Missing comma, unmatched bracket
Recovery: Fix syntax
Git VCS Command Failure
Condition: {vsn, git} but git command fails
Possible Issues:
- Not a git repository
- No tags exist
- Git not installed
Fallback: May use "0" or error
Edge Cases
Empty ebin Directory
Scenario: No .beam files compiled yet
Behavior: modules list will be empty []
Valid: For library applications without modules
.app.src.script Evaluation
File: .app.src.script
Content: Erlang code that returns application term
Example:
%% .app.src.script
Env = os:getenv("BUILD_ENV"),
Vsn = case Env of
"prod" -> "1.0.0";
_ -> "dev"
end,
{application, my_app, [
{vsn, Vsn},
{description, "My App"}
]}.
Evaluation: Executed as Erlang code, result used
Extra Src Dirs Modules
Configuration:
{extra_src_dirs, ["test"]}.
Behavior:
- Modules in
test/compiled to separate location - Not included in main
.appmodules list - Prevents test modules from being included in releases
Git Describe Format
Command: git describe --tags
Possible Outputs:
"v1.2.3": Exact tag"v1.2.3-5-gabc123": 5 commits after tag, commit abc123"abc123": No tags, just commit SHA
Handling: rebar3 parses and formats appropriately
Missing Version in .app.src
Scenario: No vsn field in .app.src
Error: Failed to get app value 'vsn' from 'src/my_app.app.src'
Required: vsn must be present
Module List with Placeholder
Common Pattern:
{modules, []} % Will be filled automatically
Also Valid:
{modules, modules} % Placeholder atom
Both replaced with actual module list
Cross References
Dependencies
- Source File Compilation: Compiles source files before this stage
Dependents
- Release generation tools require valid
.appfiles
Related
- Application Discovery: Reads
.app.srcduring discovery - Configuration Reference: Configuration options
Example Scenarios
Scenario 1: Simple Application
.app.src:
{application, my_app, [
{description, "My Application"},
{vsn, "1.0.0"},
{registered, []},
{applications, [kernel, stdlib]},
{mod, {my_app_app, []}},
{modules, []}
]}.
After Compilation:
ebin/my_app_app.beamexistsebin/my_app_sup.beamexists
Generated .app:
{application, my_app, [
{description, "My Application"},
{vsn, "1.0.0"},
{registered, []},
{applications, [kernel, stdlib]},
{mod, {my_app_app, []}},
{modules, [my_app_app, my_app_sup]}
]}.
Scenario 2: Git-Based Version
.app.src:
{application, my_app, [
{description, "My Application"},
{vsn, git},
{applications, [kernel, stdlib]},
{modules, []}
]}.
Git State: Tag v1.2.3, 5 commits ahead
Generated .app:
{application, my_app, [
{description, "My Application"},
{vsn, "1.2.3-5-gabc123"},
{applications, [kernel, stdlib]},
{modules, [my_app_app]}
]}.
Scenario 3: Variable Substitution
rebar.config:
{app_vars_file, "vars/build.vars"}.
vars/build.vars:
{build_time, "2024-01-15T10:30:00Z"}.
{build_user, "jenkins"}.
.app.src:
{application, my_app, [
{vsn, "1.0.0"},
{applications, [kernel, stdlib]},
{modules, []},
{env, [
{build_time, build_time},
{build_user, build_user}
]}
]}.
Generated .app:
{application, my_app, [
{vsn, "1.0.0"},
{applications, [kernel, stdlib]},
{modules, [my_app_app]},
{env, [
{build_time, "2024-01-15T10:30:00Z"},
{build_user, "jenkins"}
]}
]}.
Scenario 4: Extra Source Directories
rebar.config:
{extra_src_dirs, ["test"]}.
Files:
src/my_app.erl→ebin/my_app.beamtest/my_test.erl→_build/test/extras/test/my_test.beam
Generated .app (modules list):
{modules, [my_app]} % my_test NOT included
Scenario 5: Dynamic .app.src.script
File: src/my_app.app.src.script
%% Dynamic version based on environment
Version = case os:getenv("RELEASE_VERSION") of
false -> "dev";
V -> V
end,
%% Dynamic applications based on profile
ExtraApps = case os:getenv("PROFILE") of
"prod" -> [recon];
_ -> []
end,
{application, my_app, [
{description, "My App"},
{vsn, Version},
{applications, [kernel, stdlib | ExtraApps]},
{modules, []}
]}.
Production Build (RELEASE_VERSION=1.0.0, PROFILE=prod):
{application, my_app, [
{description, "My App"},
{vsn, "1.0.0"},
{applications, [kernel, stdlib, recon]},
{modules, [my_app]}
]}.
Development Build (no env vars):
{application, my_app, [
{description, "My App"},
{vsn, "dev"},
{applications, [kernel, stdlib]},
{modules, [my_app]}
]}.
Hooks & Extensibility
Rebar3's extensibility system allows customization at multiple levels: pre/post hooks execute shell commands or providers at specific compilation stages, custom compilers handle new file types, plugins add entirely new functionality, and project builders support non-Erlang build systems.
Hook System
Hook Types
1. Shell Hooks: Execute shell commands
Configuration:
{pre_hooks, [
{compile, "echo 'Starting compilation'"},
{compile, "./scripts/pre_compile.sh"}
]}.
{post_hooks, [
{compile, "echo 'Compilation complete'"}
]}.
2. Provider Hooks: Run other rebar3 providers
Configuration:
{provider_hooks, [
{pre, [
{compile, {my_namespace, my_provider}}
]},
{post, [
{compile, clean}
]}
]}.
3. Platform-Specific Hooks:
{pre_hooks, [
{linux, compile, "./configure"},
{darwin, compile, "./configure.macos"},
{win32, compile, "configure.bat"}
]}.
Hook Execution Points
Within compilation (Source File Compilation):
- compile: Around entire compilation
- erlc_compile: Around source file compilation
- app_compile: Around
.appfile generation
Example Flow:
pre compile hooks
pre erlc_compile hooks
[Source compilation]
post erlc_compile hooks
pre app_compile hooks
[.app file generation]
post app_compile hooks
post compile hooks
API: run_all_hooks/5,6
Signature:
-spec run_all_hooks(Dir, Type, Command, Providers, AppInfo, State) -> AppInfo when
Dir :: file:filename(),
Type :: pre | post,
Command :: atom(),
Providers :: [providers:t()],
AppInfo :: rebar_app_info:t(),
State :: rebar_state:t().
Flow:
- Run provider hooks for this command
- Run shell hooks for this command
- Return updated app info
Custom Compilers
Compiler Behavior
Implement rebar_compiler behavior (see Source File Compilation):
-module(my_compiler).
-behaviour(rebar_compiler).
-export([context/1, needed_files/4, dependencies/3, compile/4, clean/2]).
context(AppInfo) ->
#{src_dirs => ["priv/templates"],
include_dirs => [],
src_ext => ".template",
out_mappings => [{".html", "priv/static"}],
dependencies_opts => []}.
needed_files(Graph, Files, Mappings, AppInfo) ->
% Determine which files need compilation
{{[], []}, {Files, []}}.
dependencies(Source, SourceDir, InDirs) ->
% Return list of dependencies
[].
compile(Source, Mappings, Config, Opts) ->
% Compile source to target
% Return: ok | {ok, Warnings} | error | {error, Errors, Warnings}
ok.
clean(Files, AppInfo) ->
% Clean compiled artifacts
ok.
Register Custom Compiler
In Plugin:
init(State) ->
State1 = rebar_state:prepend_compilers(State, [my_compiler]),
{ok, State1}.
Compiler Order:
- Custom compilers can be prepended or appended
- Prepend: Run before built-in compilers (for source generation)
- Append: Run after built-in compilers (for final artifacts)
Plugins
Plugin Structure
rebar3_my_plugin/
├── src/
│ ├── rebar3_my_plugin.erl % Plugin entry point
│ ├── rebar3_my_plugin_prv.erl % Provider implementation
│ └── my_custom_compiler.erl % Optional custom compiler
├── rebar.config
└── README.md
Plugin Entry Point
-module(rebar3_my_plugin).
-export([init/1]).
init(State) ->
% Register providers
{ok, State1} = rebar3_my_plugin_prv:init(State),
% Register custom compilers
State2 = rebar_state:append_compilers(State1, [my_custom_compiler]),
{ok, State2}.
Provider Implementation
-module(rebar3_my_plugin_prv).
-behaviour(provider).
-export([init/1, do/1, format_error/1]).
init(State) ->
Provider = providers:create([
{name, my_command},
{module, ?MODULE},
{bare, true},
{deps, [compile]}, % Run after compile
{example, "rebar3 my_command"},
{short_desc, "My custom command"},
{desc, "Longer description"},
{opts, []}
]),
{ok, rebar_state:add_provider(State, Provider)}.
do(State) ->
% Implement provider logic
rebar_api:info("Running my_command", []),
{ok, State}.
format_error(Reason) ->
io_lib:format("~p", [Reason]).
Install Plugin
Global (~/.config/rebar3/rebar.config):
{plugins, [rebar3_my_plugin]}.
Project (rebar.config):
{project_plugins, [rebar3_my_plugin]}.
From Source:
{plugins, [
{rebar3_my_plugin, {git, "https://github.com/user/rebar3_my_plugin.git", {tag, "1.0.0"}}}
]}.
Configuration
Hook Configuration
% Shell hooks
{pre_hooks, [
{compile, "make -C c_src"}
]}.
{post_hooks, [
{clean, "make -C c_src clean"}
]}.
% Provider hooks
{provider_hooks, [
{pre, [{compile, {pc, compile}}]},
{post, [{clean, {pc, clean}}]}
]}.
% Platform-specific
{pre_hooks, [
{linux, compile, "echo 'Linux build'"},
{darwin, compile, "echo 'macOS build'"}
]}.
Custom Compiler Registration
% In plugin init/1:
State1 = rebar_state:prepend_compilers(State, [my_gen_compiler]),
State2 = rebar_state:append_compilers(State1, [my_post_compiler]).
Project Builders
For non-rebar3 projects (e.g., Mix, Make):
% Register builder
State1 = rebar_state:add_project_builder(State, mix, rebar3_mix_builder).
Builder Module:
-module(rebar3_mix_builder).
-export([build/1]).
build(AppInfo) ->
% Build using Mix
rebar_utils:sh("mix compile", [{cd, rebar_app_info:dir(AppInfo)}]),
ok.
Cross References
- Initialization & Configuration: Plugin loading
- Source File Compilation: Hook execution points
- Configuration Reference: Hook configuration options
State Management
Rebar3 uses two primary data structures to manage build state: rebar_state:t() holds global build configuration and state, while rebar_app_info:t() holds per-application information. Both are immutable records passed through the entire compilation chain.
State Flow
Through Compilation Chain
[Initialization]
↓ rebar_state:new(Config)
State: {dir, opts, ...}
[Dependency Resolution]
↓ Update with resolved deps
State: {..., all_deps, deps_to_build}
[Application Discovery]
↓ Add discovered apps
State: {..., project_apps}
[Compilation]
↓ For each app
State: {..., current_app}
↓ Compile sources
↓ Generate .app
State: {..., updated project_apps}
[Final State]
State with all apps compiled
State Immutability
Pattern: Always return new state
% GOOD:
{ok, NewState} = provider:do(State)
% BAD (not possible):
provider:do_mutate(State) % No in-place mutation
State Threading
Example through providers:
State0 = rebar3:init_config(),
{ok, State1} = rebar_prv_app_discovery:do(State0),
{ok, State2} = rebar_prv_install_deps:do(State1),
{ok, State3} = rebar_prv_compile:do(State2)
rebar_state Structure
Record Definition
-record(state_t, {
dir :: file:name(),
opts = dict:new() :: rebar_dict(),
code_paths = dict:new() :: rebar_dict(),
default = dict:new() :: rebar_dict(),
escript_path :: undefined | file:filename_all(),
lock = [],
current_profiles = [default] :: [atom()],
namespace = default :: atom(),
command_args = [],
command_parsed_args = {[], []},
current_app :: undefined | rebar_app_info:t(),
project_apps = [] :: [rebar_app_info:t()],
deps_to_build = [] :: [rebar_app_info:t()],
all_plugin_deps = [] :: [rebar_app_info:t()],
all_deps = [] :: [rebar_app_info:t()],
compilers = [] :: [module()],
project_builders = [] :: [{project_type(), module()}],
resources = [],
providers = [],
allow_provider_overrides = false :: boolean()
}).
Key Fields
| Field | Type | Purpose |
|---|---|---|
dir | file:name() | Project root directory |
opts | rebar_dict() | Configuration from rebar.config |
default | rebar_dict() | Original opts before profile application |
current_profiles | [atom()] | Active profiles (e.g., [default, test]) |
project_apps | [rebar_app_info:t()] | Project applications |
deps_to_build | [rebar_app_info:t()] | Dependencies needing compilation |
all_deps | [rebar_app_info:t()] | All resolved dependencies |
compilers | [module()] | Registered compiler modules |
providers | [providers:t()] | Available commands/providers |
lock | [lock_entry()] | Lock file data |
Common Operations
Create New State:
State = rebar_state:new(Config)
Get/Set Configuration:
Value = rebar_state:get(State, Key, Default),
State1 = rebar_state:set(State, Key, Value)
Manage Applications:
Apps = rebar_state:project_apps(State),
State1 = rebar_state:project_apps(State, UpdatedApps),
Deps = rebar_state:all_deps(State),
State2 = rebar_state:update_all_deps(State1, UpdatedDeps)
Apply Profiles:
State1 = rebar_state:apply_profiles(State, [test])
Manage Compilers:
State1 = rebar_state:prepend_compilers(State, [my_compiler]),
State2 = rebar_state:append_compilers(State1, [another_compiler]),
Compilers = rebar_state:compilers(State2)
rebar_app_info Structure
Record Definition
-record(app_info_t, {
name :: binary() | undefined,
app_file_src :: file:filename_all() | undefined,
app_file_src_script:: file:filename_all() | undefined,
app_file :: file:filename_all() | undefined,
original_vsn :: app_vsn() | undefined,
vsn :: app_vsn() | undefined,
parent = root :: binary() | root,
app_details = [] :: list(),
applications = [] :: list(),
included_applications = [] :: [atom()],
optional_applications = [] :: [atom()],
deps = [] :: list(),
profiles = [default] :: [atom()],
default = dict:new() :: rebar_dict(),
opts = dict:new() :: rebar_dict(),
dep_level = 0 :: integer(),
fetch_dir :: file:name(),
dir :: file:name(),
out_dir :: file:name(),
ebin_dir :: file:name(),
source :: source_spec(),
is_lock = false :: boolean(),
is_checkout = false :: boolean(),
valid :: boolean() | undefined,
project_type :: rebar3 | mix | undefined,
is_available = false :: boolean()
}).
Key Fields
| Field | Type | Purpose |
|---|---|---|
name | binary() | Application name |
dir | file:name() | Source directory |
out_dir | file:name() | Build output directory |
ebin_dir | file:name() | Compiled .beam files location |
app_file_src | file:filename() | Path to .app.src |
app_file | file:filename() | Path to .app |
vsn | string() | Application version |
applications | [atom()] | Runtime dependencies |
deps | [term()] | Build dependencies |
opts | rebar_dict() | App-specific configuration |
dep_level | integer() | Depth in dependency tree |
is_checkout | boolean() | Whether from _checkouts/ |
project_type | atom() | rebar3, mix, etc. |
Common Operations
Create New AppInfo:
AppInfo = rebar_app_info:new(AppName, Vsn, Dir)
Get/Set Fields:
Name = rebar_app_info:name(AppInfo),
AppInfo1 = rebar_app_info:name(AppInfo, NewName),
Dir = rebar_app_info:dir(AppInfo),
AppInfo2 = rebar_app_info:dir(AppInfo1, NewDir)
Manage Configuration:
Opts = rebar_app_info:opts(AppInfo),
AppInfo1 = rebar_app_info:opts(AppInfo, NewOpts),
Value = rebar_app_info:get(AppInfo, Key, Default),
AppInfo2 = rebar_app_info:set(AppInfo1, Key, Value)
Apply Profiles:
AppInfo1 = rebar_app_info:apply_profiles(AppInfo, [test, prod])
Functions & API Calls
rebar_state API
Creation:
new/0,1,2,3: Create state with configuration
Configuration:
get/2,3: Get configuration valueset/3: Set configuration valueopts/1,2: Get/set options dictdefault/1,2: Get/set default options
Applications:
project_apps/1,2: Get/set project applicationsall_deps/1,2: Get/set all dependenciesdeps_to_build/1,2: Get/set dependencies to compileupdate_all_deps/2: Update dependency list
Compilers:
compilers/1,2: Get/set compiler listprepend_compilers/2: Add compilers at startappend_compilers/2: Add compilers at end
Providers:
providers/1,2: Get/set providersadd_provider/2: Register new provider
Profiles:
current_profiles/1,2: Get/set active profilesapply_profiles/2: Apply profile configurations
Paths:
dir/1,2: Get/set project directorycode_paths/2,3: Get/set code paths
rebar_app_info API
Creation:
new/0,1,2,3,4,5: Create app info with varying detail
Basic Fields:
name/1,2: Get/set application namevsn/1,2: Get/set versiondir/1,2: Get/set source directoryout_dir/1,2: Get/set output directoryebin_dir/1,2: Get/set ebin directory
Application Files:
app_file/1,2: Get/set.apppathapp_file_src/1,2: Get/set.app.srcpathapp_file_src_script/1,2: Get/set.app.src.scriptpath
Dependencies:
deps/1,2: Get/set build dependenciesapplications/1,2: Get/set runtime dependenciesdep_level/1,2: Get/set dependency depth
Configuration:
opts/1,2: Get/set optionsget/2,3: Get config valueset/3: Set config valueapply_profiles/2: Apply profiles
Status:
valid/1,2: Get/set validityis_available/1,2: Get/set availabilityis_checkout/1,2: Get/set checkout statusis_lock/1,2: Get/set lock status
Type:
project_type/1,2: Get/set project type (rebar3, mix, etc.)
Cross References
- Initialization & Configuration: State initialization
- Application Discovery: AppInfo creation
- Source File Compilation: State usage during compilation
- All other stages: Use and modify state
Paths & Directories
Rebar3 uses a well-defined directory structure for organizing source files, compiled artifacts, dependencies, and cached data. Understanding this structure is essential for navigating builds and troubleshooting issues.
Directory Structure
Project Layout
Single-App Project:
my_app/
├── rebar.config % Project configuration
├── rebar.lock % Locked dependencies
├── src/ % Source files
│ ├── my_app.app.src % Application resource
│ └── *.erl % Erlang modules
├── include/ % Header files
│ └── *.hrl
├── priv/ % Private resources
├── test/ % Test files (extra_src_dirs)
└── _build/ % Build output (gitignored)
└── default/ % Default profile
├── lib/ % Dependencies + project
│ ├── my_app/ % Project output
│ │ ├── ebin/ % Compiled .beam files
│ │ ├── priv/ % Copied priv resources
│ │ └── ...
│ └── dep_name/ % Each dependency
├── extras/ % Extra src_dirs output
│ └── test/ % Test modules
└── .rebar3/ % Rebar3 metadata
└── */ % Compiler DAGs
Umbrella Project:
my_project/
├── rebar.config
├── apps/ % Multiple applications
│ ├── app1/
│ │ ├── src/
│ │ │ ├── app1.app.src
│ │ │ └── *.erl
│ │ └── include/
│ └── app2/
│ └── src/
└── _build/
└── default/
└── lib/
├── app1/
├── app2/
└── deps.../
Build Directories
Base Directory: _build/
- Default location for all build outputs
- Configurable via
base_diroption orREBAR_BASE_DIRenv var
Profile Directories: _build/PROFILE/
- Separate output for each profile (default, test, prod, etc.)
- Isolates artifacts between profiles
Lib Directory: _build/PROFILE/lib/
- Contains both dependencies and project applications
- Each app in its own subdirectory
Extras Directory: _build/PROFILE/extras/
- Output for
extra_src_dirs(test, scripts, etc.) - Not included in releases
Metadata Directory: _build/PROFILE/.rebar3/
- Compiler DAG files
- Other rebar3 metadata
Cache Directories
Global Cache: ~/.cache/rebar3/
- Default location (configurable via
REBAR_CACHE_DIR) - Hex packages
- Git repositories
Structure:
~/.cache/rebar3/
├── hex/
│ └── default/
│ └── packages/
│ ├── package-1.0.0.tar
│ └── package-1.0.0.etag
├── git/
│ └── repo-hash/ % Git clones
└── plugins/ % Global plugins
Configuration Directories
Global Config: ~/.config/rebar3/
~/.config/rebar3/
└── rebar.config % Global configuration
Path Resolution
Source Directories
Default Source Directories:
{src_dirs, ["src"]}.
Multiple Source Directories:
{src_dirs, ["src", "lib", "core"]}.
Recursive Source Directories:
{src_dirs, [
"src",
{"lib", [{recursive, true}]}
]}.
Include Directories
Include Path Priority (for rebar_compiler_erl):
- Application's
include/directory - Directories from
{i, Dir}inerl_opts - All source directories (recursively if configured)
- Top-level application directory
- All project apps' include directories (during compilation)
Example:
{erl_opts, [
{i, "custom_include"},
{i, "/absolute/path/include"}
]}.
Output Directories
Application Output: _build/PROFILE/lib/APPNAME/
- Mirrors source structure
ebin/: Compiled artifactspriv/: Copied private resourcesinclude/: Available to other apps
Dependency Output: _build/PROFILE/lib/DEPNAME/
- Same structure as application output
- Fetched from cache or remote
Library Directories
lib_dirs Configuration:
{lib_dirs, ["apps", "components"]}.
Purpose: Where to find application directories during discovery
Default: [] (only scan project root)
Extra Source Directories
Configuration:
{extra_src_dirs, ["test", "scripts"]}.
Output: _build/PROFILE/extras/DIRNAME/
Module List: Not included in .app file
Code Paths
Path Management
Code Path Contexts:
deps: Dependency ebinsplugins: Plugin ebinsall_deps: All dependencies including project apps
Setting Paths:
rebar_paths:set_paths([deps], State)
rebar_paths:set_paths([deps, plugins], State)
Path Priority
During Compilation:
- Current application's ebin (prepended)
- All dependencies' ebins
- All project apps' ebins
- Rebar3's own dependencies (controlled)
Erlang Code Path:
- Managed via
code:add_patha/1andcode:add_pathz/1 patha: Front of path (higher priority)pathz: End of path (lower priority)
Include Path Resolution
-include_lib("app/include/file.hrl") Resolution:
- Find
appin code path - Look for
include/file.hrlrelative to app directory
Example:
% In code:
-include_lib("cowboy/include/cowboy.hrl").
% Resolves to:
_build/default/lib/cowboy/include/cowboy.hrl
Cross References
- Initialization & Configuration: Directory configuration
- Application Discovery: lib_dirs usage
- Source File Compilation: Path usage during compilation
Error Handling
Rebar3 uses consistent error handling patterns throughout the compilation chain. Errors are reported with formatted messages, stack traces preserved for debugging, and recovery suggestions provided where possible.
Error Patterns
Provider Error Macro
Definition:
-define(PRV_ERROR(Reason), {error, {?MODULE, Reason}}).
Usage:
throw(?PRV_ERROR({missing_file, Filename}))
Format Error Callback:
format_error({missing_file, Filename}) ->
io_lib:format("File not found: ~ts", [Filename]).
Error Tuple Returns
Pattern:
-spec do(State) -> {ok, NewState} | {error, Reason}.
Providers return:
{ok, State}: Success{error, Reason}: Failure
Throws vs Returns
Throws: Immediate abortion
throw(?PRV_ERROR(Reason))
Returns: Allow caller to decide
{error, Reason}
Common Errors
Compilation Errors
Syntax Error:
Compiling src/my_module.erl failed
src/my_module.erl:10: syntax error before: '}'
Missing Include:
src/my_module.erl:5: can't find include file "missing.hrl"
Undefined Function:
src/my_module.erl:15: function foo/1 undefined
Dependency Errors
Circular Dependencies:
Dependency cycle(s) detected:
applications: app1 app2 app3 depend on each other
Missing Package:
Package not found in registry: nonexistent-1.0.0
Bad Constraint:
Unable to parse version for package jsx: ~~>3.1.0
Configuration Errors
Invalid rebar.config:
Error reading config file rebar.config: {Line, erl_parse, ["syntax error before: ", "']'"]}
Invalid Profile:
Profile config must be a list but for profile 'test' config given as: {test, value}
Application Errors
Invalid App Name:
Invalid ebin/my_app.app: name of application (wrong_name) must match filename.
Missing Module:
Module my_module listed in my_app.app but beam file not found
DAG/Build Errors
DAG Restoration Failure:
Failed to restore _build/default/.rebar3/rebar_compiler_erl/source.dag file. Discarding it.
(Warning only, triggers rebuild)
Missing Artifact:
Missing artifact path/to/expected/file.beam
Recovery Strategies
Compilation Failures
Strategy: Fix source code, recompile
Incremental: Only failed files recompile
Clean Build:
rebar3 clean
rebar3 compile
Dependency Issues
Update Registry:
rebar3 update
Unlock Dependency:
rebar3 unlock DEPNAME
rebar3 compile
Clear Dependency Cache:
rm -rf _build/default/lib/DEPNAME
rebar3 compile
Configuration Errors
Validate Syntax:
erl -eval "file:consult(\"rebar.config\")." -noshell -s init stop
Check Profile Format:
% Correct:
{profiles, [{test, [{deps, [meck]}]}]}.
% Incorrect:
{profiles, [{test, {deps, [meck]}}]}. % Not a list!
DAG/Cache Issues
Clear DAG Cache:
rm -rf _build/default/.rebar3/
rebar3 compile
Clear All Caches:
rm -rf _build/
rm -rf ~/.cache/rebar3/
rebar3 compile
Network Issues
Offline Mode (use only cached):
rebar3 compile --offline
Retry with Timeout:
% In ~/.config/rebar3/rebar.config:
{hex, [{http_timeout, 300000}]}. % 5 minutes
Cross References
- Initialization & Configuration: Configuration errors
- Dependency Resolution & Locking: Dependency errors
- Source File Compilation: Compilation errors
- Dependency Graph Management: DAG errors
- All stages: Error conditions documented per stage
Configuration Reference
This document provides a comprehensive reference for all configuration options related to compilation in rebar3. Configuration is specified in rebar.config at the project root.
Compilation Options
erl_opts
Purpose: Erlang compiler options
Type: [term()]
Common Options:
{erl_opts, [
debug_info, % Include debug information
warnings_as_errors, % Treat warnings as errors
warn_export_vars, % Warn about exported variables
warn_unused_import, % Warn about unused imports
warn_missing_spec, % Warn about missing specs
{parse_transform, Module}, % Apply parse transform
{d, 'MACRO'}, % Define macro
{d, 'MACRO', Value}, % Define macro with value
{i, "include"}, % Include directory
{i, "/abs/path/include"}, % Absolute include path
no_debug_info, % Exclude debug information
inline_list_funcs, % Inline list functions
{platform_define, Regex, Def}, % Platform-specific defines
{src_dirs, ["src", "lib"]}, % Source directories
compressed, % Compress beam files
warn_export_all, % Warn on export_all
warn_shadow_vars, % Warn about shadowed variables
warn_obsolete_guard, % Warn about obsolete guards
warn_unused_record, % Warn about unused records
warn_unused_vars, % Warn about unused variables
nowarn_deprecated_function, % Don't warn on deprecated functions
{nowarn_unused_function, [{F,A}]}, % Specific functions
{nowarn_unused_type, [{T,A}]} % Specific types
]}.
Warning Control:
{erl_opts, [
{warn_format, 2}, % Warning level for format strings
{error_location, column}, % Include column in errors
{feature, maybe_expr, enable} % Enable experimental features
]}.
src_dirs
Purpose: Source directories to search for .erl files
Type: [Dir :: string()] or [{Dir, Opts}]
Default: ["src"]
Examples:
{src_dirs, ["src", "lib", "core"]}.
{src_dirs, [
"src",
{"lib", [{recursive, true}]}
]}.
extra_src_dirs
Purpose: Additional source directories (compiled separately, not in .app modules list)
Type: [Dir :: string()]
Default: []
Common Use: Test, script, example directories
Example:
{extra_src_dirs, ["test", "scripts"]}.
erl_first_files
Purpose: Files to compile before others
Type: [File :: string()]
Use Cases: Parse transforms, macros, type definitions
Example:
{erl_opts, [
{erl_first_files, ["src/my_transform.erl", "src/types.erl"]}
]}.
validate_app_modules
Purpose: Validate all modules in .app exist as .beam files
Type: boolean()
Default: true
Example:
{validate_app_modules, false}.
Directory Configuration
lib_dirs
Purpose: Directories to search for applications
Type: [Dir :: string()]
Default: []
Example:
{lib_dirs, ["apps", "components"]}.
base_dir
Purpose: Build output directory
Type: string()
Default: "_build"
Environment: REBAR_BASE_DIR
Example:
{base_dir, "build"}.
deps_dir
Purpose: Where to place dependencies
Type: string()
Default: "lib" (within profile directory)
Example:
{deps_dir, "external"}.
% Results in: _build/default/external/
Dependency Configuration
deps
Purpose: Project dependencies
Type: [Dep] where Dep is:
atom(): Package from Hex (latest version){atom(), binary()}: Package with version{atom(), VCS}: From version control
Examples:
{deps, [
% Hex packages
jsx, % Latest
{cowboy, "2.9.0"}, % Specific version
{cowlib, "~> 2.11"}, % Semver constraint
% Git
{my_dep, {git, "https://github.com/user/my_dep.git", {tag, "1.0.0"}}},
{my_dep, {git, "...", {branch, "main"}}},
{my_dep, {git, "...", {ref, "abc123"}}},
% Mercurial
{my_dep, {hg, "https://bitbucket.org/user/my_dep", {tag, "1.0.0"}}}
]}.
overrides
Purpose: Override dependency configuration
Types:
{override, Dep, Config}: Override specific dep{add, Dep, Config}: Add to dep's config{del, Dep, Config}: Delete from dep's config
Examples:
{overrides, [
% Override specific dependency
{override, cowboy, [
{erl_opts, [nowarn_export_all]}
]},
% Add to dependency config
{add, ranch, [
{erl_opts, [debug_info]}
]},
% Delete from dependency config
{del, jsx, [
{erl_opts, [warnings_as_errors]}
]}
]}.
Profile Configuration
profiles
Purpose: Environment-specific configuration
Type: [{ProfileName, Config}]
Built-in Profiles: default, test, prod
Example:
{profiles, [
{test, [
{deps, [meck, proper]},
{erl_opts, [debug_info, nowarn_export_all]}
]},
{prod, [
{erl_opts, [no_debug_info, inline_list_funcs]},
{relx, [{dev_mode, false}, {include_erts, true}]}
]},
{custom, [
{deps, [recon]},
{erl_opts, [{d, 'CUSTOM_BUILD'}]}
]}
]}.
Usage:
rebar3 as test compile
rebar3 as prod release
rebar3 as test,custom ct
Merging: Profile config merges with default, later profiles override earlier
Hook Configuration
pre_hooks / post_hooks
Purpose: Execute shell commands before/after providers
Type: [{Provider, Command}] or [{Arch, Provider, Command}]
Examples:
{pre_hooks, [
{compile, "make -C c_src"},
{clean, "make -C c_src clean"}
]}.
{post_hooks, [
{compile, "./scripts/post_build.sh"}
]}.
% Platform-specific
{pre_hooks, [
{linux, compile, "./configure"},
{darwin, compile, "./configure.macos"},
{win32, compile, "configure.bat"}
]}.
provider_hooks
Purpose: Run providers before/after other providers
Type: [{pre | post, [{Provider, HookProvider}]}]
Example:
{provider_hooks, [
{pre, [
{compile, {pc, compile}},
{clean, {pc, clean}}
]},
{post, [
{compile, {my_plugin, post_compile}}
]}
]}.
Advanced Configuration
minimum_otp_vsn
Purpose: Minimum OTP version required
Type: string()
Example:
{minimum_otp_vsn, "24"}.
Also Supports Regex:
{minimum_otp_vsn, "24|25"}.
artifacts
Purpose: Additional artifacts to verify after compilation
Type: [File :: string()]
Variables: {{profile}}, {{priv_dir}}
Example:
{artifacts, [
"priv/my_nif.so",
"priv/{{profile}}/custom_artifact"
]}.
project_plugins
Purpose: Plugins for this project only (not global)
Type: Same as deps
Example:
{project_plugins, [
rebar3_hex,
{rebar3_custom, {git, "...", {tag, "1.0.0"}}}
]}.
plugins
Purpose: Global plugins
Type: Same as deps
Example:
{plugins, [
rebar3_proper,
rebar3_ex_doc
]}.
app_vars_file
Purpose: External file for .app.src variable substitution
Type: string()
Example:
{app_vars_file, "config/app.vars"}.
File Contents (config/app.vars):
{copyright, "Copyright (c) 2024 Company"}.
{build_date, "2024-01-15"}.
jobs
Purpose: Number of parallel compilation workers
Type: pos_integer()
Default: Number of CPU cores
Example:
{jobs, 4}.
xrl_opts / yrl_opts
Purpose: Leex/Yecc compiler options
Type: [term()]
Example:
{xrl_opts, [{includefile, "custom.hrl"}]}.
{yrl_opts, [{includefile, "parser.hrl"}]}.
mib_opts
Purpose: SNMP MIB compiler options
Type: [term()]
Example:
{mib_opts, [
{i, ["priv/mibs"]},
{outdir, "priv/mibs"}
]}.
shell
Purpose: Rebar3 shell configuration
Type: [{Key, Value}]
Example:
{shell, [
{config, "config/sys.config"},
{apps, [my_app]},
{script_file, "scripts/shell.escript"}
]}.
Configuration Precedence
Order (later overrides earlier):
- Built-in defaults
~/.config/rebar3/rebar.config(global)rebar.config(project)- Profile-specific configuration
- Environment variables
- Command-line options
Example:
% rebar.config (default):
{erl_opts, [debug_info]}.
% Profile overrides:
{profiles, [{prod, [{erl_opts, [no_debug_info]}]}]}.
% Result with `rebar3 as prod compile`:
{erl_opts, [no_debug_info]} % prod profile wins
Cross References
Cross-References
- All stages use configuration options documented here
- Initialization & Configuration: Configuration loading
- Source File Compilation: Compiler options usage
- Hooks & Extensibility: Hook configuration
Part VII - Guides
Great software is built not just with technical skill, but with shared understanding of what constitutes quality, clarity, and community. Part VII addresses the human elements of LFE programming - how to write code that others can read, understand, and maintain, and how to participate constructively in the broader LFE community. While the previous parts focused on what LFE can do, Part VII explores how it should be done.
The Style Guide represents the accumulated wisdom of the LFE community about writing maintainable, readable code. Lisp has changed since its early days, and many dialects have existed over its history, and each dialect has developed its own conventions for expressing ideas clearly. LFE inherits both Lisp traditions and Erlang practices, creating a unique synthesis that requires its own stylistic guidelines.
Good style in LFE goes far beyond superficial formatting concerns. While consistent indentation and whitespace certainly matter for readability, the deeper principles involve how to structure programs for maximum clarity and maintainability. The naming conventions section explores how to choose identifiers that communicate intent effectively - a particularly important skill in a language where functions and data structures are manipulated as first-class objects. When code can be treated as data, the names you choose become even more significant, as they often appear in contexts where their meaning must be immediately clear.
The documentation guidelines reflect LFE's dual heritage as both a practical programming language and a tool for exploring complex ideas. Docstrings serve not just as API documentation, but as executable specifications that can be tested and verified. Comments become opportunities to explain not just what the code does, but why particular approaches were chosen - crucial information in a language that often offers multiple elegant solutions to the same problem.
Data representation choices reveal one of LFE's greatest strengths and potential pitfalls. With multiple ways to structure information - lists, tuples, property lists, maps, records - choosing the right representation for each situation becomes a critical skill. The style guide provides principled guidance for these decisions, helping you leverage LFE's flexibility without creating unnecessarily complex code.
The sections on processes, servers, and messages address uniquely Erlang-influenced aspects of LFE style. Writing clear concurrent code requires different skills than writing sequential programs, and the style guide explores how to structure process interactions, design message protocols, and organize supervision trees for maximum clarity and robustness.
Software engineering principles tie together all these specific guidelines into a coherent philosophy of LFE development. The emphasis on simplicity, composability, and explicit rather than implicit behavior reflects both Lisp's mathematical heritage and Erlang's systems programming requirements. Understanding these principles helps you make good decisions even in situations not explicitly covered by specific style rules.
The Code of Conduct acknowledges that technical communities are, fundamentally, human communities. The most sophisticated programming language and the most elegant technical practices mean nothing if the community around them is unwelcoming, exclusive, or hostile. The code of conduct establishes expectations for how community members should interact, ensuring that LFE remains accessible to programmers from all backgrounds and experience levels.
This dual focus - technical excellence and community health - reflects a mature understanding of what makes programming languages successful in the long term. Languages thrive not just because they solve technical problems elegantly, but because they foster communities where knowledge is shared generously, newcomers are welcomed warmly, and diverse perspectives are valued highly.
Part VII recognizes that becoming an effective LFE programmer involves more than mastering syntax and semantics. It requires developing judgment about code quality, learning to communicate effectively through code and documentation, and participating constructively in a community of practice. These skills, while harder to quantify than technical knowledge, are often what distinguish truly excellent programmers from merely competent ones.
By the end of Part VII, you'll understand not just how to write LFE code, but how to write LFE code that others will want to read, understand, and build upon. You'll have internalized the community standards that make collaborative development possible and enjoyable. Most importantly, you'll be equipped to contribute positively to the ongoing evolution of LFE as both a technical platform and a human community.
Style Guide
Introduction
What is good style?
Good style in any language consists of code that is:[^1]
- Understandable
- Reusable
- Extensible
- Efficient
- Easy to develop and debug
It also helps ensure correctness, robustness, and compatibility. Maxims of good style are:
- Be explicit
- Be specific
- Be concise
- Be consistent
- Be helpful (anticipate the reader's needs)
- Be conventional (don't be obscure)
- Build abstractions at a usable level
- Allow tools to interact (referential transparency)
Know the context when reading code:
- Who wrote it and when?
- What were the business needs?
- What other factors contributed to the design decisions?
Sources and Inspiration
The LFE Style Guide takes inspiration (and often times actual content) directly from key sources in the Lisp, Erlang, and even Clojure developer communities. These are as follows
- Google Common Lisp Style Guide
- A Guide to Writing Good, Maintainable Common Lisp Code
- Tutorial on Good Lisp Programming Style
- Erlang Programming Rules and Conventions
- Erlang Coding Standards & Guidelines
- The Clojure Style Guide
- How to ns
Note, however, that these are not considered sacrosanct sources of ultimate truth; (and neither is this guide). Instead, they contain practices that we have either adopted as-is, modified to some extent, or simply rejected (e.g., due to prior conventions established in MACLISP and LMI Lisp, their inapplicability due to LFE's unique status as a Lisp and Erlang dialect, etc.).
In general we suggest following the LFE style as outlined here if you are creating a new project. If you are contributing to a project maintained by someonoe in the community, we recommend consistency: using the style adopted by that project (for any contributions to that project).
Above all, enjoy the parenthesis.
Notes
[^1] This section was adapted from the Tutorial on Good Lisp Programming Style by Peter Norvig and Kent Pitman.
Formatting
Topics related to the manner of formatter LFE code.
File Headers
Every source file should begin with a brief description of the contents of that file.
After that description, every file should start the code itself with a (defmodule ...) form.
;;;; Variable length encoding for integers and floating point numbers.
(defmodule num-encode
...)
It is not necessary to include copyright info in every file as long as the project has a LICENSE file in its top-level directory. Files which differ in license from that file should get have a copyright notice in their header section.
If you are contributing to a project that has established a convention of adding copyright headers to all files, simply follow that convention.
Indentation
In general, use your text editor's indentation capabilities. If you are contributing to a particular library, be sure to ask the maintainers what standard they use, and follow those same guidelines, thus saving everyone from the drudgery of whitespace fixes later.
In particular, you'll want to do everything you can to follow the conventions laid out in the Emacs LFE mode supplied in the LFE source. Instructions for use are given in the LFE Github wiki, but we'll repeat it here. Simply edit your ~/.emacs file to include the following:
;; Prevent tabs being added:
(setq-default indent-tabs-mode nil)
;; LFE mode.
;; Set lfe-dir to point to where the lfe emacs files are.
(defvar lfe-dir (concat (getenv "HOME") "/git/lfe/emacs"))
(setq load-path (cons lfe-dir load-path))
(require 'lfe-start)
In general though, indentation is two columns per form, for instance:
(defun f ()
(let ((x 1)
(y 2))
(lfe_io:format "X=~p, Y=~p~n" (list x y))))
Note that LFE has many exceptions to this rule, given the complex forms it defines for features inherited from Erlang (e.g., pattern-matching in function heads). A few examples for the number exceptions to the two-space indentation rule above:
(cond ((lists:member x '(1 2 3)) "First three")
((=:= x 4) "Is four")
((>= x 5) "More than four")
('true "You chose poorly"))
(defun ackermann
((0 n)
(+ n 1))
((m 0)
(ackermann (- m 1) 1))
((m n)
(ackermann (- m 1) (ackermann m (- n 1)))))
The last function would actually be better written as follows, but the form above demonstrates the indentation point:
(defun ackermann
((0 n) (+ n 1))
((m 0) (ackermann (- m 1) 1))
((m n) (ackermann (- m 1) (ackermann m (- n 1)))))
Maintain a consistent indentation style throughout a project.
Indent carefully to make the code easier to understand.
Use indentation to make complex function applications easier to read. When an application does not fit on one line or the function takes many arguments, consider inserting newlines between the arguments so that each one is on a separate line. However, do not insert newlines in a way that makes it hard to tell how many arguments the function takes or where an argument form starts and ends.
Bad:
(do-something first-argument second-argument (lambda (x)
(frob x)) fourth-argument last-argument)
Better:
(do-something first-argument
second-argument
(lambda (x) (frob x))
fourth-argument
last-argument)
Vertical White Space
You should include one blank line between top-level forms, such as function definitions. Exceptionally, blank lines can be omitted between simple, closely related defining forms of the same kind, such as a group of related type declarations or constant definitions.
(defun +my-pi+ () 3.14)
(defun +my-e+ () 2.72)
(defun factorial (n)
(factorial n 1))
(defun factorial
((0 acc) acc)
((n acc) (when (> n 0))
(factorial (- n 1) (* n acc))))
Horizontal White Space
Do not include extra horizontal whitespace before or after parentheses or around symbols.
Furthermore, do not place right parentheses by themselves on a line. A set of consecutive trailing parentheses must appear on the same line.
Very bad:
( defun factorial
(
( 0 acc)
acc
)
(
( n acc)
( when ( > n 0)
)
( factorial ( - n 1)
( * n acc
)
)
)
)
Much better:
(defun factorial
((0 acc) acc)
((n acc) (when (> n 0))
(factorial (- n 1) (* n acc))))
You should use only one space between forms.
You should not use spaces to vertically align forms in the middle of consecutive lines. An exception is made when the code possesses an important yet otherwise not visible symmetry that you want to emphasise.
Bad:
(let* ((low 1)
(high 2)
(sum (+ (* low low) (* high high))))
...)
Better:
(let* ((low 1)
(high 2)
(sum (+ (* low low) (* high high))))
...))
You should align nested forms if they occur across more than one line.
Bad:
(defun munge (a b c)
(* (+ a b)
c))
Better:
(defun munge (a b c)
(* (+ a b)
c))
Line Length
You should format source code so that no line is longer than 80 characters.
Old text terminals were standardised on 80 columns which they in turn inherited from even older punch card technology. While modern computer screens support vastly more than this, there are a couple of considerations to keep in mind that motivate us to continue supporting an 80 character limit:
- Displaying code in web pages, paste-bins, gist services, etc., is much cleaner and easier to read when the character width is limited to 80 characters.
- Most modern text editors allow for multiple panes, allowing several files to be open side-by-side, supporting the easy editing and referencing of multiple files simultaneously; limiting these files to 80 characters in width facilitates this type of workflow.
- Code that has to be examined under emergency circumstances (such as via a terminal attached to a crash cart in a data centre, or in an emergency shell session without a graphical window manager) is much easier to read quickly when character width is limited to 80.
- Lastly, such a convention encourages good naming discipline!
Spelling and Abbreviations
Use correct spelling in your comments, and most importantly in your identifiers. The LFE documentation projects (books and reference materials) use aspell and include make targets for running various spell-checking tasks across the project files. Feel free to borrow from these for your own projects.
Use common and domain-specific abbreviations, and must be consistent with these abbreviations. You may abbreviate lexical variables of limited scope in order to avoid overly-long symbol names.
If you're not sure, consult a dictionary, look up alternative spellings in a dictionary, or ask a local expert.
Here are examples of choosing the correct spelling:
- Use "complimentary" in the sense of a meal or beverage that is not paid for by the recipient, not "complementary".
- Use "existent" and "nonexistent", not "existant". Use "existence", not "existance".
- Use "hierarchy" not "heirarchy".
- Use "precede" not "preceed".
- Use "weird", not "wierd".
Make appropriate exceptions for industry standard nomenclature/jargon, including plain misspellings. For instance:
- Use "referer", not "referrer", in the context of the HTTP protocol.
Naming
On names and naming in LFE code.
Symbols
Use lower case for all symbols (Erlang "atoms"). Consistently using lower case makes searching for symbol names easier and is more readable.
Place hyphens between all the words in a symbol. If you can't easily say an identifier out loud, it is probably badly named.
Always prefer - over / or . unless you have a well-documented overarching reason to, and approval from other hackers who review your proposal.
Bad:
(defun *default-username* ()"Ann")
(defun *max-widget-cnt* () 200)
Better:
(defun *default-user-name* () "Ann")
(defun *maximum-widget-count* () 200)
Unless the scope of a variable is very small, do not use overly short names like i and zq.
Names in Modules
When naming a symbol in a module, you should not include the module name as part of the name of the symbol. Naming a symbol this way makes it awkward to use from a client module accessing the symbol by qualifying it with a module prefix, where the module name then appears twice (once as part of the module name, another time as part of the symbol name itself).
Bad:
(defmodule varint
(export
(varint-length64 0))
(defun varint-length64 () ... )
(defmodule client-code)
(defun +padding+ ()
(varint:varint-length64))
Better:
(defmodule varint
(export
(length64 0))
(defun length64 () ... )
(defmodule client-code)
(defun +padding+ ()
(varint:length64))
Global Variables and Constants
Erlang, and thus LFE, does not support global variables or mutable data. However, many projects define constants in modules. Traditionally, Lisp projects have used symbols enclosed in + for global constants and symbols enclosed in * (a.k.a. "earmuffs") for global variables.
Adapted for LFE, one could use these conventions for module constants and default values, respectively.
(defun +my-pi+ () 3.14)
(defun *default-host* () "127.0.0.1")
Predicates
There are several options for naming boolean-valued functions and variables to indicate they are predicates:
- a trailing
? - a trailing
-p - a trailing
p - a leading
is-
Modern Lisps tend to prefer ?, while classic Lisps tend to use p. Erlang code tends to use is_ which translates to is- in LFE. You should use "p" when the rest of the function name is one word and "-p" when it is more than one word.
A rationale for this convention is given in the CLtL2 chapter on predicates.
Whichever convention your project wishes to use, be consistent and use only that convention in the entire project.
Do not use these boolean indicators in functions that do not return booleans and variables that are not boolean-valued.
Intent not Content
You should name a variable according to the high-level concept that it represents (intent), not according to the low-level implementation details of how the concept is represented (content).
Thus, you should avoid embedding data structure or aggregate type names, such as list, array, or hash-table as part of the variable names, unless you're writing a generic algorithm that applies to arbitrary lists, arrays, hash-tables, etc. In that case it's perfectly OK to name a variable list or array.
For example, if a variable's value is always a row (or is either a row or NIL), it's good to call it row or first-row or something like that.
Be consistent. If a variable is named row in one function, and its value is being passed to a second function, then call it row rather than, say, value.
Documentation
Topics related to in-code LFE documentation.
Docstrings
First and foremost, document everything.
You should use document strings (a.k.a. "docstrings") on all visible functions to explain how to use your code.
Unless some bit of code is painfully self-explanatory, document it with a documentation string.
Documentation strings are destined to be read by the programmers who use your code. They can be extracted from functions, types, classes, variables and macros, and displayed by programming tools, such as IDEs, or by REPL queries; web-based documentation or other reference works can be created based on them. Documentation strings are thus the perfect locus to document your API. They should describe how to use the code (including what pitfalls to avoid), as opposed to how the code works (and where more work is needed), which is what you'll put in comments.
Supply a documentation string when defining top-level functions, records, classes, variables and macros. Generally, add a documentation string wherever the language allows.
For functions, the docstring should describe the function's contract: what the function does, what the arguments mean, what values are returned, what conditions the function can signal. It should be expressed at the appropriate level of abstraction, explaining the intended meaning rather than, say, just the syntax.
Some LFE forms do not accept docstrings, in which case a preceding code comment should be used instead.
(defun small-prime-number? (n)
"Return true if N, an integer, is a prime number. Otherwise, return false."
((n) (when (< n 4))
(>= n 2))
((n) (when (== 0 (rem n 2)))
'false)
((n)
(lists:all #'not/1
(lists:map (lambda (x) (== 0 (rem n x)))
(lists:seq 3 (trunc (math:sqrt n)))))))
(defmacro is (bool-expression)
"Assert bool-expression evaluates to 'true."
`(assert ,bool-expression))
;;; This record tracks test results and is ulimately used when reporting the
;;; status of completed tests.
(defrecord state
(status (orddict:new))
test-type
(ok 0)
(fail 0)
(err 0)
(skip 0)
(cancel 0)
(time 0))
A long docstring may usefully begin with a short, single-sentence summary, followed by the larger body of the docstring.
Text in successive lines of docstrings are indented two spaces, aligned with the open quote in the first line of documentation, not with the first character of the text.
Code Comments
Comments are explanations to the future maintainers of the code. Even if you're the only person who will ever see and touch the code, even if you're either immortal and never going to quit, or unconcerned with what happens after you leave (and have your code self-destruct in such an eventuality), you may find it useful to comment your code. Indeed, by the time you revisit your code, weeks, months or years later, you will find yourself a different person from the one who wrote it, and you will be grateful to the previous you for making the code readable.
Comment anything complicated so that the next developer can understand what's going on.
Also use comments as a way to guide those who read the code, so they know what to find where.
Code comments in LFE, as in most Lisp dialects, begin with a semi-colon, with their number having conventional semantic value:
- Four Semi-colons: These are used for file headers and important comments that apply to large sections of code in a source file.
- Three Semi-colons: These are used to begin comments that apply to just one top-level form or small group of top-level forms.
- Two Semi-colons: These are used inside a top-level form, for comments appearing between lines. For code that uses unobvious forms to accomplish a task, you must include a comment stating the purpose of the form and the task it accomplishes.
- One Semi-colon: This is used for parenthetical remark and only occurs at the end of a line. You should use spaces to separate the comment from the code it refers to so the comment stands out. You should try to vertically align consecutive related end-of-line comments.
For all comments, there should be a space between the semicolon and the text of the comment.
;;;; File-level comments or comments for large sections of code.
(defmodule math-n-things
(export
(utility-function 0)
...
(small-prime-number? 1)
(large-prime-number? 1)
...))
;;; The functions in this section are utility in nature, supporting others in
;;; the module. More details on their intended use cases are available here:
;;; * https://some.url/
(defun utility-function ()
...)
;;; Prime numbers section
(defun small-prime-number? (n)
"Return true if N, an integer, is a prime number. Otherwise, return false."
((n) (when (< n 4)) ; parenthetical remark here
(>= n 2)) ; continuation of the remark
((n) (when (== 0 (rem n 2)))
'false) ; different remark here
((n)
;; Comment that applies to a section of code.
(lists:all #'not/1
(lists:map (lambda (x) (== 0 (rem n x)))
(lists:seq 3 (trunc (math:sqrt n)))))))
(defun large-prime-number? (n)
...)
Attention Required
For comments requiring special attention, such as incomplete code, todo items, questions, breakage, and danger, include a TODO or XXX comment indicating the type of problem, its nature, and any notes on how it may be addressed.
The comments begin with TODO or XXX in all capital letters, followed by the name, e-mail address, or other identifier of the person with the best context about the problem referenced by the TODO or XXX. The main purpose is to have a consistent TODO or XXX that can be searched to find out how to get more details upon request. A TODO or XXX is not a commitment that the person referenced will fix the problem. Thus when you create a TODO or XXX, it is almost always your name that is given.
Generally, TODO and XXX commands are differentiated in that TODO items represent normal code tasks around such things as incomplete features and XXX items represent a bug, potential bug, pitfalls, incorrectness, inelegance, uncertainty about part of the code, etc. Common synonyms for XXX include BUG, FIXME and sometimes HACK (this last especially for incorrectness or inelegance).
When signing comments, you should use your username (for code within the company) or full email address (for code visible outside the company), not just initials.
;; --- TODO (alice@gmail.com): Refactor to provide a better API.
;; --- TODO (bob): Remove this code after release 1.7 or before 2012-11-30.
If there is an associated issue or bug ticket with the given TODO or XXX item, be sure to include that in a following line:
;; --- XXX (carol): There is a serious issue here, causing problems in other
;; areas of the code. We haven't decided upon the best
;; approach yet. See the following ticket for details:
;;
;; * https://github.com/examplecom/api/issues/42
;;
Data Representation
Notes on basic data structures.
Lists
Use the appropriate functions when manipulating lists.
For simple access to list data, you can use car, cdr, cadr, etc. to access list elements and segments. For common pattern-matching in function heads, receive, let, etc., use cons to access the head and tail of a list (e.g., (,head . ,tail)).
Additionally, don't forget the lists Erlang module for accessing list elements.
You should avoid using a list as anything besides a container of elements of like type. Avoid using a list as method of passing multiple separate values of different types in and out of function calls. Sometimes it is convenient to use a list as a little ad hoc structure, i.e. "the first element of the list is a foo, and the second is a bar", but this should be used minimally since it gets harder to remember the little convention. You should only use a list that way when destructuring the list of arguments from a function, or creating a list of arguments to which to apply a function.
The proper way to pass around an object comprising several values of heterogeneous types is to use a record created via defrecord.
Tuples and Proplists
Do not align keys and values in property lists; instead, simply use the standard Lisp formatting (e.g, as provided by the LFE Emacs formatter).
Bad:
'(#(k1 v1)
#(key2 value2)
#(key-the-third value-the-third)
#(another one))
Better:
'(#(k1 v1)
#(key2 value2)
#(key-the-third value-the-third)
#(another one))
Maps
Do not align keys and values in maps. Note, however, that the LFE Emacs formatter doesn't currently indent maps properly.
Bad:
'#m(k1 v1
key2 value2
key-the-third value-the-third
another one)
Also bad (formatted with the LFE Emacs formatter):
'#m(k1 v1
key2 value2
key-the-third value-the-third
another one)
Better:
#m(k1 v1
key2 value2
key-the-third value-the-third
another one)
Records
Use records as the principle data structure
Use records as the principle data structure in messages. A record is a tagged tuple and was introduced in Erlang version 4.3 and thereafter.
If the record is to be used in several modules, its definition should be placed in a header file (with suffix .lfe) that is included from the modules. If the record is only used from within one module, the definition of the record should be in the beginning of the file the module is defined in.
The record features of LFE can be used to ensure cross module consistency of data structures and should therefore be used by interface functions when passing data structures between modules.
Use selectors and constructors
Use the record macros provided by LFE for managing instances of records. Don't use matching that explicitly assumes that the record is a tuple.
Bad:
(defun demo ()
(let* ((joe (make-person name "Joe" age 29))
(`#(person ,name ,age) joe))
...
))
Good:
(defun demo ()
(let* ((joe (make-person name "Joe" age 29))
(name-2 (person-name joe)))
...
))
Errors
Separate error handling and normal case code
Don't clutter code for the "normal case" with code designed to handle exceptions. As far as possible you should only program the normal case. If the code for the normal case fails, your process should report the error and crash as soon as possible. Don't try to fix up the error and continue. The error should be handled in a different process.
Clean separation of error recovery code and normal case code should greatly simplify the overall system design.
The error logs which are generated when a software or hardware error is detected will be used at a later stage to diagnose and correct the error. A permanent record of any information that will be helpful in this process should be kept.
Identify the error kernel
One of the basic elements of system design is identifying which part of the system has to be correct and which part of the system does not have to be correct.
In conventional operating system design the kernel of the system is assumed to be, and must be, correct, whereas all user application programs do not necessarily have to be correct. If a user application program fails this will only concern the application where the failure occurred but should not affect the integrity of the system as a whole.
The first part of the system design must be to identify that part of the system which must be correct; we call this the error kernel. Often the error kernel has some kind of real-time memory resident data base which stores the state of the hardware.
Processes, Servers and Messages
Processes
Implement a process in one module
Code for implementing a single process should be contained in one module. A process can call functions in any library routines but the code for the "top loop" of the process should be contained in a single module. The code for the top loop of a process should not be split into several modules - this would make the flow of control extremely difficult to understand. This does not mean that one should not make use of generic server libraries, these are for helping structuring the control flow.
Conversely, code for no more than one kind of process should be implemented in a single module. Modules containing code for several different processes can be extremely difficult to understand. The code for each individual process should be broken out into a separate module.
Use processes for structuring the system
Processes are the basic system structuring elements. But don't use processes and message passing when a function call can be used instead.
Registered processes
Registered processes should be registered with the same name as the module. This makes it easy to find the code for a process.
Only register processes that should live a long time.
Assign exactly one parallel process to each true concurrent activity in the system
When deciding whether to implement things using sequential or parallel processes then the structure implied by the intrinsic structure of the problem should be used. The main rule is:
"Use one parallel process to model each truly concurrent activity in the real world"
If there is a one-to-one mapping between the number of parallel processes and the number of truly parallel activities in the real world, the program will be easy to understand.
Each process should only have one "role"
Processes can have different roles in the system, for example in the client-server model.
As far as possible a process should only have one role, i.e. it can be a client or a server but should not combine these roles.
Other roles which process might have are:
Supervisor: watches other processes and restarts them if they fail. Worker: a normal work process (can have errors). Trusted Worker: not allowed to have errors.
Use the process dictionary with extreme care
Do not use get and put etc. unless you know exactly what you are doing! Use get and put etc., as little as possible.
A function that uses the process dictionary can be rewritten by introducing a new argument.
Don't program like this:
(defun tokenize
((`(,head . ,tail))
...)
(('())
(case (get-characters-from-device (get 'device))
('eof
'())
(`#(value ,chars)
(tokenize chars)))))
The correct solution:
(defun tokenize
((device (,head . ,tail))
...)
((device '())
(case (get-characters-from-device device)
('eof
'())
(`#(value, chars)
(tokenize device chars)))))
The use of get and put will cause a function to behave differently when called with the same input at different occasions. This makes the code hard to read since it is non-deterministic. Debugging will be more complicated since a function using get and put is a function of not only of its argument, but also of the process dictionary. Many of the run time errors in LFE (for example bad_match) include the arguments to a function, but never the process dictionary.
Servers
Use generic functions for servers and protocol handlers wherever possible
In many circumstances it is a good idea to use generic server programs such as the generic server implemented in the standard libraries. Consistent use of a small set of generic servers will greatly simplify the total system structure.
The same is possible for most of the protocol handling software in the system.
Write tail-recursive servers
All servers must be tail-recursive, otherwise the server will consume memory until the system runs out of it.
Don't program like this:
(defun loop ()
(receive
(`#(msg1 ,msg1)
...
(loop))
('stop 'true)
(other
(logger:error "Process ~w got unknown msg ~w~n"
`(,(self) ,other))
(loop)))
;; don't do this! This is not tail-recursive!
(io:format "Server going down" '()))
This is a correct solution:
(defun loop ()
(receive
(`#(msg1 ,msg1)
...
(loop))
('stop
(io:format "Server going down" '()))
(other
(logger:error "Process ~w got unknown msg ~w~n"
`(,(self) ,other))
(loop))))
If you use some kind of server library, for example generic, you automatically avoid doing this mistake.
Messages
Tag messages
All messages should be tagged. This makes the order in the receive statement less important and the implementation of new messages easier.
Don't program like this:
(defun loop (state)
(receive
...
(`#(,mod ,funcs ,args)
(erlang:apply mod funcs args)
(loop state))
...))
The new message `#(get_status_info ,from ,option) will introduce a conflict if it is placed below the `#(,mod ,funcs ,args) message.
If messages are synchronous, the return message should be tagged with a new atom, describing the returned message. Example: if the incoming message is tagged get_status_info, the returned message could be tagged status_info. One reason for choosing different tags is to make debugging easier.
This is a good solution:
(defun loop (state)
(receive
...
(`#(execute ,mod ,funcs ,args)
(erlang:apply mod funcs args)
(loop state))
(`#(get_status_info ,from ,option)
(! from `#(status_info ,(get-status-info option state)))
(loop state))
...))
Use tagged return values
Use tagged return values.
Don't program like this:
(defun keysearch
((key `(#(,key ,value) . ,tail))
value)
((key `(cons `#(,wrong-key ,wrong-value) . ,tail))
(keysearch key '()))
((key '())
'false))
Then (tuple key, value) cannot contain the false value.
This is the correct solution:
(defun keysearch
((key `(#(,key ,value) . ,tail))
`#(value ,value))
((key `(#(,wrong-key ,wrong-value) . ,tail))
(keysearch key '()))
((key '())
'false))
Flush unknown messages
Every server should have an Other alternative in at least one receive statement. This is to avoid filling up message queues. Example:
(defun main-loop ()
(receive
(`#(msg1 ,msg1)
...
(main-loop))
(`#(msg2 ,msg2)
...
(main-loop))
(other ; flush the message queue
(logger:error "Process ~w got unknown msg ~w~n" `(,(self) ,other))
(main-loop))))
Interface functions
Use functions for interfaces whenever possible, avoid sending messages directly. Encapsulate message passing into interface functions. There are cases where you can't do this.
The message protocol is internal information and should be hidden to other modules.
Example of interface function:
(defmodulee fileserver
(export
(start 0)
(stop 0)
(open-file 1)
...))
(defun open-file (server-pid filename)
(! serever-pid `#(open-file-request ,filename))
(receive
(`#(open-file-response ,result) result)))
...
Time-outs
Be careful when using after in receive statements. Make sure that you handle the case when the message arrives later.
Trapping exits
As few processes as possible should trap exit signals. Processes should either trap exits or they should not. It is usually very bad practice for a process to "toggle" trapping exits.
Software Components
From the smallest chunks of code to a completed project.
Flow Control
if Branches
Large conditional expressions and deeply nested blocks of code are harder to read, so should be factored out into functions.
For example, this:
(if (and (fuelled? rocket)
(lists:all #'strapped-in?
(crew rocket))
(sensors-working? rocket))
(launch rocket)
(! pid `#(err "Aborting launch.")))
Should be refactored to something like this:
(defun rocket-ready? (rocket)
(and (fuelled? rocket)
(lists:all #'strapped-in?
(crew rocket))
(sensors-working? rocket)))
(if (rocket-ready-p rocket)
(launch rocket)
(! pid `#(err "Aborting launch.")))
case Branches
Don't write complex case statements with deeply nested branching. Instead, split these into functions, too, pattern-matching in the function heads.
Functions
Keep Functions Small
Keep functions small, focused on one thing. If you have six separate tasks being performed by a function, create six functions for these.
Group Functions Logically
Try to always separate unexported and exported functions in groups, with the exported ones first, unless it helps readability and code discovery.
Modules
When defining modules in LFE, put exported functions and their arities on separate lines. Optionally, functions of the same name with different arity may be put on the same line. Functions within a single export call should be sorted alphabetically.
Do not use (export all); explicitly exporting functions constitutes the conventional contract for clients utilising the module.
Very bad:
(defmodule maths
(export all))
Bad:
(defmodule maths
(export (factorial 2)
(large-prime-number? 1)
(small-prime-number? 1)
(ackermann 2)
(factorial 1)))
Better:
(defmodule maths
(export
(ackermann 2)
(factorial 1) (factorial 2)
(large-prime-number? 1)
(small-prime-number? 1)))
If you have a public API with groups of related functions in a module, you may indicate their relatedness with separate exports:
(defmodule maths
(export
(util-func 1)
(other-util 2))
(export
(ackermann 2)
(factorial 1) (factorial 2)
(large-prime-number? 1)
(small-prime-number? 1)))
With Pseudo-Packages
If you are using the LFE rebar3 plugin, then you also have the flexibility of organising your project's source code into sub-directories under your source directory (see the projects section for more information).
In that case, you would define your module like so:
(defmodule project.subdir.maths
(export
(ackermann 2)
(factorial 1) (factorial 2)
(small-prime-number? 1)
(large-prime-number? 1)))
Since there is no support in Erlang and LFE for actual packages, the dotted name is actually a module. As such, when referencing this module elsewhere, use import aliases to improve readability):
(defmodule client
(export
(some-func 0))
(import
(from project.subdir.maths
(small-prime-number? 1))))
(defun some-func ()
(small-prime-number? 100))
Or, if you need to avoid a name collision between the imported function and one in the client module:
(defmodule client
(export
(some-func 0))
(import
(rename project.subdir.maths
((small-prime-number? 1) small-prime?))))
(defun some-func ()
(small-prime? 100))
When to Create
If some portion of your code is reusable enough to be a module then the maintenance gains are really worth the overhead of splitting it out with separate tests and docs.1
Gains for separating code into separate modules include, but are not limited to:
- Easier reuse in other parts of the software being developed.
- Increased ability to reason about problems due to increased simplicity and separation of concerns.
- Great clarity and understanding of the system as a whole.
A good general workflow around module creation:
- Start small and remain focused on the problem at hand.
- Write just the functions you need.
- Keep the functions small and limit them to one specific chunk of functionality (do one thing and do it well).
- Make incremental changes as needed.
For new code:
- Experiment with in the LFE REPL by defining your function and then calling with different values (expected and otherwise).
- When it works in the REPL, create a test module in
./testand paste the function calls in a test case. - Create a new module in
./srcand paste the final form of your function from the REPL. - Ensure the tests pass successfully for the new module.
Build your libraries using this approach
Libraries
Use Before You Write
Look for libraries that solve the problems you are trying to solve before embarking on a project.[^1] Making a project with no dependencies is not some sort of virtue. It doesn’t aid portability and it doesn’t help when it comes to turning a Lisp program into an executable.
Writing a library that solves the same problem as another hurts consolidation. Generally, you should only do this if you are going to make it worth it: Good, complete documentation, examples, and a well-designed website are – taken together – a good reason to write an alternative library to an existing one.
As always, check the licensing information of the libraries you use for incompatibilities.
Writing Libraries
Before starting a project, think about its structure: Does every component have to be implemented within the project? Or are there parts that might be usable by others? If so, split the project into libraries.
If you set out to write a vast monolith and then lose interest, you will end up with 30% of an ambitious project, completely unusable to others because it’s bound to the rest of the unfinished code.
If you think of your project as a collection of independent libraries, bound together by a thin layer of domain-specific functionality, then if you lose interest in a project you will have left a short trail of useful libraries for others to use and build upon.
In short: write many small libraries.
Notes
[^1] This entire page was adapted from the lisp-lang.org Style Guide's General Guidelines.
Projects
Use the established best practices for Erlang project creation, adapted for LFE.
These have been used when defining the LFE new project templates in the rebar3 plugin. That is probably the best way to get consistent results when creating the most common types of LFE projects (e.g., main-scripts, escripts, libraries, OTP applications, and OTP releases).
With Pseudo-Packages
While Erlang and LFE do not support packages, it is possible to use the rebar3 LFE plugin to simulate packages, complete with project directory structures that consolidate specific functionality in collections of sub-directories. These will be detected at compile-time when you use rebar3 lfe compile and from these, proper Erlang-compatible modules will be compiled (with dotted names preserving the hierarchy). Consider your project's organisation carefully when creating your sub-directory structure.
Here is a good example for a game project's directory structure:
├── LICENSE
├── README.md
├── rebar.config
├── src
│ ├── title.app.src
│ └── title
│ ├── config.lfe
│ ├── db.lfe
│ ├── graphics
│ │ ├── mesh.lfe
│ │ ├── obj.lfe
│ │ └── gl.lfe
│ └── logging.lfe
├── test
...
The modules under the src/title directory can be represented in a more abstract hierarchy:
title
title.graphics
title.graphics.mesh
title.graphics.obj
title.graphics.gl
title.config
title.logging
title.db
Keep in mind that the LFE plugin will flatten this structure into sibling files compiled to the ebin directory:
title.app
title.config.beam
title.db.beam
title.graphics.mesh.beam
title.graphics.obj.beam
title.graphics.gl.beam
title.logging.beam
Software Engineering
General best practices around software engineering.
Principles
There are some basic principles for team software development that every developer must keep in mind. Whenever the detailed guidelines are inadequate, confusing or contradictory, refer back to these principles for guidance:
- Every developer's code must be easy for another developer to read, understand, and modify — even if the first developer isn't around to explain it. (This is the "hit by a truck" principle.)
- Everybody's code should look the same. Ideally, there should be no way to look at lines of code and recognize it as "Alice's code" by its style.
- Be precise.
- Be concise.
- Keep it simple.
- Use the smallest hammer for the job.
- Use common sense.
- Keep related code together. Minimize the amount of jumping around someone has to do to understand an area of code.
Priorities
When making decisions about how to write a given piece of code, aim for the following in this priority order:
- Usability by the customer
- Debuggability/Testability
- Readability/Comprehensibility
- Extensibility/Modifiability
- Efficiency (of the LFE code at runtime)
Most of these are obvious.
Usability by the customer means that the system has to do what the customer requires; it has to handle the customer's transaction volumes, uptime requirements; etc.
For the LFE efficiency point, given two options of equivalent complexity, pick the one that performs better. (This is often the same as the one that conses less, i.e. allocates less storage from the heap.)
Given two options where one is more complex than the other, pick the simpler option and revisit the decision only if profiling shows it to be a performance bottleneck.
However, avoid premature optimisation. Don't add complexity to speed up something that runs rarely, since in the long run, it matters less whether such code is fast.
Architecture
To build code that is robust and maintainable, it matters a lot how the code is divided into components, how these components communicate, how changes propagate as they evolve, and more importantly how the programmers who develop these components communicate as these components evolve.
If your work affects other groups, might be reusable across groups, adds new components, has an impact on other groups (including QA or Ops), or otherwise isn't purely local, you must write it up using at least a couple of paragraphs, and get a design approval from the other parties involved before starting to write code — or be ready to scratch what you have when they object.
If you don't know or don't care about these issues, ask someone who does.
Code of Conduct
The LFE Code of Conduct is the same as the erlang.org Code of Conduct, adopted from the original below for the LFE Community.
Overview
This Code of Conduct is a guideline of how to communicate within the LFE Community in a way we hope is easy to read, help mutual understanding and avoid flames. The LFE Community is by definition all communication in or around LFE use, development, and related discussions, including but not limited to the LFE mailing lists, Slack and Discord discussions, Github PRs and reviews, and even social media.
If you have been pointed to this page after posting a message, the reason is probably that you made some common mistake that we all made at least once. We have been there, do not feel alone, and please keep reading. It will help all of us in the LFE Community.
It is pointless to send a message that only warns about posting style. If you are trying to point someone to correct posting style guidelines, please do so while at least honestly attempting to answer their questions or comments. It is generally unhelpful to give only a warning related to posting style, as newcomers may feel unwelcome, only to leave. And that is exactly what we do not want.
Content Policy
- Be nice to each other.
- Hateful, hurtful, oppressive, or exclusionary remarks are not tolerated. (Cursing is strongly discouraged, but if you must curse, it may under no circumstances be used to target another user or in a hateful manner).
- No SPAM.
- The common language spoken in the LFE Community is English. The are various native language communities around the world. Conversely, many members of this list do not speak or write English as a first language. Be understanding of erroneous English, and don't point it out if it doesn't change the meaning or make it ambiguous.
- The mailing lists, Github issues, and Github PRs are to discuss LFE, its uses, contributions, and its community. Try to keep discussions in those areas on topic.
Violating the Code
Posters of inappropriate content will be (1) informed, (2) warned if they continue breaking the rules, (3) moderated, and ultimately (4) banned to retain the highly welcoming and friendly nature of the LFE Community. To lift a ban or otherwise contend a regulative measure, please contact maintainers@lfe.io.
AI Resources for LFE
One of the problems users of AI tools face when coding in LFE is the tendency to slip and use Clojure or Common Lisp styles and functions, sometimes even Scheme. Sometimes the slip goes the other direction and the tools attempt to use Erlang syntax mixed in with LFE. This really slows down development and has the effect of spoiling an otherwise good experience.
This section aims to fix those problems with "single page" documents that cover the following:
- LFE style and best practices
- LFE syntax
- LFE functions and macros
- Erlang functions
The manner in which this information is presented in these documents is structured per the advice of the AI tools themselves as being the most easily digested with the information presented in the most efficacious order. This is the advice we received:
- Lead with syntax conventions (what code should look like)
- Follow with semantic patterns (how to structure code)
- End with architectural guidance (how to organize systems)
Additionally, the documents should be:
- Scannable - Clear headers and sections, all in a single document
- Example-heavy - Show don't tell
- Prescriptive - Clear "Do this, not that"
- Context-minimal - Assume the reader needs quick reference, not philosophy
- Quick checklist - Provide a pre-commit verification list
LFE Style Guide - Consolidated Reference
Quick Reference for AI Coding Tools
This guide consolidates LFE style conventions in order of immediate applicability: syntax → semantics → architecture.
1. SYNTAX & FORMATTING
Indentation
Standard: 2 spaces per form
(defun f ()
(let ((x 1)
(y 2))
(lfe_io:format "X=~p, Y=~p~n" (list x y))))
Exception: Pattern matching and conditionals
;; cond - align clauses
(cond ((lists:member x '(1 2 3)) "First three")
((=:= x 4) "Is four")
((>= x 5) "More than four")
('true "You chose poorly"))
;; defun with pattern matching - compact related clauses
(defun ackermann
((0 n) (+ n 1))
((m 0) (ackermann (- m 1) 1))
((m n) (ackermann (- m 1) (ackermann m (- n 1)))))
;; Multi-line arguments
(do-something first-argument
second-argument
(lambda (x) (frob x))
fourth-argument
last-argument)
Use Emacs LFE mode for consistency:
;; Add to ~/.emacs
(setq-default indent-tabs-mode nil)
(defvar lfe-dir (concat (getenv "HOME") "/git/lfe/emacs"))
(setq load-path (cons lfe-dir load-path))
(require 'lfe-start)
Whitespace
Vertical spacing:
- One blank line between top-level forms
- Exception: Related simple definitions can be grouped without blank lines
(defun +my-pi+ () 3.14)
(defun +my-e+ () 2.72)
(defun factorial (n)
(factorial n 1))
(defun factorial
((0 acc) acc)
((n acc) (when (> n 0))
(factorial (- n 1) (* n acc))))
Horizontal spacing:
- No extra whitespace around parentheses
- One space between forms
- All closing parens on the same line
- Do NOT vertically align middle-of-line forms
;; BAD
(let* ((low 1)
(high 2)
(sum (+ (* low low) (* high high))))
...)
;; GOOD
(let* ((low 1)
(high 2)
(sum (+ (* low low) (* high high))))
...)
Line Length
Maximum 80 characters per line
Reasons:
- Web display (paste-bins, gists, documentation)
- Multiple editor panes side-by-side
- Emergency terminal access
- Encourages good naming discipline
File Headers
;;;; Brief description of file contents.
(defmodule module-name
...)
Copyright only needed if different from project LICENSE file.
2. NAMING CONVENTIONS
Symbols (Atoms)
Always lowercase with hyphens
;; BAD
*default-username*
*max-widget-cnt*
;; GOOD
*default-user-name*
*maximum-widget-count*
Rules:
- Always use
-between words (never/or.unless well-documented reason) - Must be pronounceable
- No overly short names except in tiny scopes
Predicates (Boolean Functions)
Choose ONE convention per project:
trailing?(modern Lisps) - RECOMMENDED for LFEtrailing-p(multi-word classic Lisp)trailingp(single-word classic Lisp)is-leading(Erlang style)
;; Consistent usage - pick one style
(defun prime? (n) ...)
(defun even? (n) ...)
;; OR (not both)
(defun is-prime (n) ...)
(defun is-even (n) ...)
Constants and Defaults
;; Constants: + earmuffs +
(defun +my-pi+ () 3.14)
;; Defaults/config: * earmuffs *
(defun *default-host* () "127.0.0.1")
Module-Relative Names
Do NOT repeat module name in symbols
;; BAD
(defmodule varint
(export (varint-length64 0)))
(defun varint-length64 () ...)
;; Usage becomes ugly:
(varint:varint-length64)
;; GOOD
(defmodule varint
(export (length64 0)))
(defun length64 () ...)
;; Usage is clean:
(varint:length64)
Intent Over Content
Name by concept (intent), not implementation (content)
;; BAD - names reveal implementation
user-list
row-array
config-hash-table
;; GOOD - names reveal meaning
users
active-row
configuration
Exception: Generic algorithms operating on arbitrary data structures can use type names.
3. DOCUMENTATION
Comment Levels
Four levels by semicolon count:
;;;; File headers and large section comments
(defmodule math-n-things
(export (utility-function 0)))
;;; Section headers for groups of related functions
;;; Can include multi-line explanatory text
(defun utility-function ()
;; Comment applying to following code section
(do-something)
(do-something-else) ; Parenthetical remark
(final-thing)) ; Aligned remarks
Rules:
- Always space after semicolons
- Align single-semicolon remarks vertically when consecutive
Docstrings
Document ALL visible functions
(defun small-prime-number? (n)
"Return true if N, an integer, is a prime number. Otherwise, return false."
((n) (when (< n 4))
(>= n 2))
((n) (when (== 0 (rem n 2)))
'false)
((n)
(lists:all #'not/1
(lists:map (lambda (x) (== 0 (rem n x)))
(lists:seq 3 (trunc (math:sqrt n)))))))
Format:
- First line: concise summary
- Optional: detailed explanation after blank line
- Continuation lines indented 2 spaces (align with opening quote)
- Describe contract: inputs, outputs, side effects, conditions
When forms don't support docstrings, use comments:
;;; This record tracks test results for reporting.
(defrecord state
(status (orddict:new))
test-type
(ok 0)
(fail 0))
TODO and XXX Comments
;; --- TODO (alice@gmail.com): Refactor to provide better API.
;; --- XXX (bob): Critical bug causing cascading failures.
;; See: https://github.com/project/issues/42
Conventions:
TODO: Normal tasks, incomplete featuresXXX: Bugs, potential issues, inelegance, uncertainty- Synonyms:
BUG,FIXME,HACK
- Synonyms:
- Include identifier (username/email)
- Reference issue tracker when applicable
4. MODULE STRUCTURE
Module Definition
Export functions on separate lines, alphabetically sorted
;; NEVER use this
(defmodule maths
(export all)) ;; NEVER DO THIS
;; BAD - wrong order, inconsistent arity grouping
(defmodule maths
(export (factorial 2)
(large-prime-number? 1)
(small-prime-number? 1)
(ackermann 2)
(factorial 1)))
;; GOOD
(defmodule maths
(export
(ackermann 2)
(factorial 1) (factorial 2)
(large-prime-number? 1)
(small-prime-number? 1)))
;; GOOD - separate exports for logical grouping
(defmodule maths
(export
(util-func 1)
(other-util 2))
(export
(ackermann 2)
(factorial 1) (factorial 2)
(large-prime-number? 1)
(small-prime-number? 1)))
Pseudo-Packages (Sub-directories)
;; Module in src/project/subdir/maths.lfe
(defmodule project.subdir.maths
(export
(factorial 1)))
;; Client usage - import for readability
(defmodule client
(export (some-func 0))
(import
(from project.subdir.maths
(factorial 1))))
(defun some-func ()
(factorial 5))
;; OR rename to avoid collision
(defmodule client
(export (some-func 0))
(import
(rename project.subdir.maths
((factorial 1) fact))))
(defun some-func ()
(fact 5))
When to Create Modules
Create separate modules for:
- Reusable functionality
- Clear separation of concerns
- Code that needs independent testing
Workflow:
- Start small, stay focused
- Write only needed functions
- Keep functions small (one task each)
- Make incremental changes
For new code:
- Prototype in REPL
- Paste working code into test module
- Move final version to src module
- Verify tests pass
5. FUNCTIONS
Keep Functions Small
One function = one task
If doing six things, create six functions.
Group Functions Logically
;;;; Exported Functions
(defun public-api-1 () ...)
(defun public-api-2 () ...)
;;;; Internal Functions
(defun helper-1 () ...)
(defun helper-2 () ...)
Generally put exported functions first, unless readability benefits from different ordering.
Refactor Complex Conditionals
Extract complex conditions into named predicates
;; BAD - inline complexity
(if (and (fuelled? rocket)
(lists:all #'strapped-in? (crew rocket))
(sensors-working? rocket))
(launch rocket)
(! pid `#(err "Aborting launch.")))
;; GOOD - named abstraction
(defun rocket-ready? (rocket)
(and (fuelled? rocket)
(lists:all #'strapped-in? (crew rocket))
(sensors-working? rocket)))
(if (rocket-ready? rocket)
(launch rocket)
(! pid `#(err "Aborting launch.")))
Pattern Match in Function Heads
Don't write complex case statements with deep nesting
Split into functions using pattern matching in heads.
6. DATA STRUCTURES
Lists
Appropriate access patterns:
;; Simple access
(car lst)
(cdr lst)
(cadr lst)
;; Pattern matching
(let ((`(,head . ,tail) lst))
...)
;; Erlang lists module
(lists:nth 1 lst)
(lists:reverse lst)
Avoid using lists as ad-hoc structures
- Don't use lists to pass multiple heterogeneous values
- Exception: function argument lists, apply arguments
- Use records for heterogeneous collections
Maps
Do NOT align keys and values
;; BAD
'#m(k1 v1
key2 value2
key-the-third value-the-third)
;; GOOD
#m(k1 v1
key2 value2
key-the-third value-the-third
another one)
Records
Primary data structure for messages and complex data
;; Define in header (.lfe) if used across modules
;; Define in module if used only locally
(defrecord person
name
age
occupation)
;; ALWAYS use record macros - NEVER match tuples directly
;; BAD
(let ((`#(person ,name ,age) joe))
...)
;; GOOD
(let* ((joe (make-person name "Joe" age 29))
(name (person-name joe)))
...)
Benefits:
- Cross-module consistency
- Enforced by compiler
- Self-documenting code
Tuples and Proplists
Standard formatting (no alignment)
;; BAD
'(#(k1 v1)
#(key2 value2)
#(key-the-third value-the-third))
;; GOOD
'(#(k1 v1)
#(key2 value2)
#(key-the-third value-the-third)
#(another one))
7. PROCESSES AND CONCURRENCY
One Process Per Module
Code for a process's top loop stays in ONE module
- Process can call library functions elsewhere
- But control flow must be in single module
- Conversely: one module = one process type
Process-to-Activity Mapping
One parallel process per truly concurrent real-world activity
This creates natural, understandable structure.
Process Roles
Each process has ONE role:
- Client
- Server
- Supervisor (watches/restarts others)
- Worker (can have errors)
- Trusted Worker (must not error)
Don't mix roles in one process.
Registered Processes
Register with same name as module
(defmodule fileserver
...)
;; Register as 'fileserver
(register 'fileserver pid)
Only register long-lived processes.
Process Dictionary - Avoid
Do NOT use get/put unless absolutely necessary
;; BAD - uses process dictionary
(defun tokenize
((`(,head . ,tail)) ...)
(('())
(case (get-characters-from-device (get 'device))
('eof '())
(`#(value ,chars) (tokenize chars)))))
;; GOOD - explicit parameter
(defun tokenize
((device `(,head . ,tail)) ...)
((device '())
(case (get-characters-from-device device)
('eof '())
(`#(value ,chars) (tokenize device chars)))))
Process dictionary makes functions non-deterministic and harder to debug.
8. SERVERS
Use Generic Servers
Leverage OTP gen_server or similar libraries
Consistent use of generic servers simplifies system architecture.
Tail Recursion Required
ALL servers must be tail-recursive
;; BAD - not tail recursive, will consume memory
(defun loop ()
(receive
(`#(msg1 ,msg1)
...
(loop))
('stop 'true))
(io:format "Server going down" '())) ;; DON'T DO THIS
;; GOOD - tail recursive
(defun loop ()
(receive
(`#(msg1 ,msg1)
...
(loop))
('stop
(io:format "Server going down" '()))
(other
(logger:error "Unknown msg ~w~n" `(,other))
(loop))))
9. MESSAGES
Tag All Messages
Makes receive order irrelevant and extension easier
;; BAD - untagged messages
(defun loop (state)
(receive
(`#(,mod ,funcs ,args) ;; Ambiguous!
(erlang:apply mod funcs args)
(loop state))))
;; GOOD - tagged messages
(defun loop (state)
(receive
(`#(execute ,mod ,funcs ,args)
(erlang:apply mod funcs args)
(loop state))
(`#(get_status_info ,from ,option)
(! from `#(status_info ,(get-status-info option state)))
(loop state))))
Use Tagged Return Values
;; BAD - can't distinguish false result from not-found
(defun keysearch
((key `(#(,key ,value) . ,tail)) value)
((key `(#(,_ ,_) . ,tail)) (keysearch key tail))
((key '()) 'false))
;; GOOD - tagged return
(defun keysearch
((key `(#(,key ,value) . ,tail)) `#(value ,value))
((key `(#(,_ ,_) . ,tail)) (keysearch key tail))
((key '()) 'false))
Flush Unknown Messages
Always have catch-all in receive
(defun main-loop ()
(receive
(`#(msg1 ,msg1)
...
(main-loop))
(`#(msg2 ,msg2)
...
(main-loop))
(other
(logger:error "Process ~w got unknown msg ~w~n"
`(,(self) ,other))
(main-loop))))
Use Interface Functions
Encapsulate message passing behind functions
(defmodule fileserver
(export
(start 0)
(stop 0)
(open-file 1)))
(defun open-file (filename)
(! fileserver `#(open-file-request ,filename))
(receive
(`#(open-file-response ,result) result)))
Message protocol is internal, hide it from clients.
Timeouts
Be careful with after in receive
Must handle case where message arrives late.
Exit Trapping
Minimize processes that trap exits
Processes should either always trap or never trap. Don't toggle.
10. ERROR HANDLING
Separate Error and Normal Case
Don't clutter normal case with error handling
;; Program for the happy path
;; Let it crash on errors
;; Handle errors in separate supervisor process
Clean separation simplifies system design.
Identify Error Kernel
Determine what MUST be correct
Like OS kernel vs user programs:
- Error kernel must be correct
- Application code can fail without system compromise
- Often contains critical real-time state
11. LIBRARIES AND PROJECTS
Use Before You Write
Search for existing libraries first
- No-dependency projects aren't virtuous
- Doesn't aid portability or executable creation
- Check licensing compatibility
When to Write Alternative Library
Only if you provide significant value:
- Complete documentation
- Comprehensive examples
- Well-designed website/resources
Structure Projects as Library Collections
Many small libraries > one monolith
If you abandon 30% of a monolith, it's useless to others. If you abandon a collection of libraries, you've left useful components.
Project Structure with Pseudo-Packages
├── LICENSE
├── README.md
├── rebar.config
├── src
│ ├── title.app.src
│ └── title
│ ├── config.lfe
│ ├── db.lfe
│ ├── graphics
│ │ ├── mesh.lfe
│ │ ├── obj.lfe
│ │ └── gl.lfe
│ └── logging.lfe
└── test
Compiles to flat structure:
ebin/
title.app
title.config.beam
title.db.beam
title.graphics.mesh.beam
title.graphics.obj.beam
title.graphics.gl.beam
title.logging.beam
12. GENERAL PRINCIPLES
Team Development
Every developer must remember:
- "Hit by a truck" principle: Others must understand your code
- Consistency: All code should look the same
- Precision: Be exact
- Conciseness: Say more with less
- Simplicity: Use smallest hammer for job
- Common sense: Think before coding
- Cohesion: Keep related code together
Priority Order
When making decisions, optimize in this order:
- Usability by customer
- Debuggability/Testability
- Readability/Comprehensibility
- Extensibility/Modifiability
- Efficiency (runtime performance)
Efficiency Notes
- Choose performant option when complexity is equal
- Choose simpler option when complexity differs
- Profile before optimizing
- Avoid premature optimization
- Don't optimize rarely-run code
Architecture
For non-trivial changes:
- Write design document (at least 2 paragraphs)
- Get approval from affected parties
- Consider: reusability, component communication, evolution, team coordination
QUICK SYNTAX CHECKLIST
Before committing LFE code, verify:
- 2-space indentation (use Emacs LFE mode)
- No lines > 80 characters
- Lowercase atoms with hyphens (not underscores)
- Module names match filenames
- Exported functions listed alphabetically
- Docstrings on all public functions
- Comments use appropriate semicolon count
- No (export all)
- Records used for structured data
- Pattern matching in function heads, not case
- Tail-recursive server loops
- Tagged messages
- Interface functions hiding message protocols
- Unknown messages flushed in receive
- Complex conditionals extracted to named predicates
SPELLING CONVENTIONS
Correct spellings:
- complimentary (free meal) not complementary
- existent, nonexistent, existence (not -ant/-ance)
- hierarchy (not heirarchy)
- precede (not preceed)
- weird (not wierd)
Exceptions (industry standard):
- referer (HTTP header, not referrer)
Use aspell or similar for spell-checking.
SOURCES
This guide synthesizes conventions from:
- Google Common Lisp Style Guide
- Common Lisp Style Guide (lisp-lang.org)
- Peter Norvig & Kent Pitman's Lisp Style Tutorial
- Erlang Programming Rules and Conventions
- Inaka Erlang Guidelines
- Clojure Style Guide
- James Halliday's module philosophy
Remember: These are guidelines, not laws. Consistency within a project matters most. When contributing to existing projects, match their established style.
End of Consolidated LFE Style Guide
LFE Pocket Reference
LFE function and macro inventory
Erlang function inventory
Part VIII - LFE for Contributors
This final major section of the LFE MACHINE MANUAL serves as a definitive technical reference for the LFE programming language implementation. It synthesizes findings from an exhaustive analysis of the entire LFE codebase, providing the depth and rigor required for:
- The Motivated Newcomer to understand LFE's complete architecture and design philosophy
- Academics to conduct formal research and write papers on LFE's implementation
- Contributors to identify precisely where and how to add new features
- Maintainers to diagnose bugs by understanding system-wide interactions
- Integrators to embed or extend LFE within larger systems
- Language implementers to study a production-quality Lisp-on-VM implementation
This analysis assumes graduate-level computer science knowledge, including familiarity with:
- Compiler construction (lexing, parsing, AST transformation, code generation)
- Programming language theory (lambda calculus, type systems, evaluation strategies)
- Functional programming concepts (closures, higher-order functions, immutability)
- Virtual machine architectures (BEAM/Erlang VM in particular)
- Systems programming and concurrent/distributed computing
Document Navigation Guide
This document can be creativly re-organized for multiple reading paths, per focused need:
For newcomers (understanding the system), focus on these sections:
- LFE Architecture
- Module Reference
- Erlang Integration
For contributors (adding features), focus on these sections:
- Module Reference
- Component Relationship Graphs
- Design Patterns & Idioms
For maintainers (debugging), focus on these sections:
- Language Subsystems
- Data Structure Catalog
- Performance Considerations
For academics (research), focus on these sections:
- LFE Architecture
- Language Subsystems
- Language Features Matrix
For integrators (embedding/extending), focus on these sections:
- Erlang Integration
- Tooling
- Future Directions
LFE Architecture
What is LFE?
Lisp Flavoured Erlang (LFE) is a production-ready Lisp-2 dialect that compiles to BEAM bytecode, running natively on the Erlang VM. It is not a toy language, a proof-of-concept, or a syntax overlay—it is a sophisticated, self-hosting compiler and runtime system that achieves true zero-overhead interoperability with Erlang and OTP while providing the full power of Lisp's homoiconic macro system.
The Core Achievement
LFE accomplishes something remarkable: complete semantic equivalence with Erlang at the bytecode level while providing complete syntactic and metaprogramming capabilities of Lisp. This is achieved through a two-stage compilation strategy:
LFE Source → Macro Expansion → Core Erlang → BEAM Bytecode
Unlike foreign function interfaces (FFI) or runtime interop layers, LFE code is Erlang code. A function compiled from LFE generates identical BEAM bytecode to an equivalent function written in Erlang. There is no marshaling, no type conversion, no performance penalty. LFE functions are Erlang functions, LFE processes are Erlang processes, and LFE modules are Erlang modules.
Language Classification
LFE is:
- A Lisp-2: Functions and variables inhabit separate namespaces (like Common Lisp, unlike Scheme)
- Homoiconic: Code is data; S-expressions are the native AST
- Eagerly evaluated: Call-by-value semantics with left-to-right argument evaluation
- Lexically scoped: All bindings use lexical scope (no dynamic scoping)
- Macro-powered: Compile-time metaprogramming with full code-as-data manipulation
- Pattern-matching native: Pattern matching is pervasive across all binding forms
- Concurrency-oriented: Full access to Erlang's actor model and OTP framework
- Functionally pure (within Erlang's constraints): Immutable data, pure functions, side effects explicit
LFE provides:
- Complete Erlang/OTP compatibility (processes, supervision, behaviors, gen_server, etc.)
- Self-hosting compilation (LFE compiler written in LFE)
- Dual execution modes (compiled to BEAM or interpreted in REPL)
- Three Lisp dialect compatibility layers (Common Lisp, Clojure, Scheme)
- Full Dialyzer support for static type analysis
- Comprehensive standard library
- Production-grade tooling (REPL, compiler, build integration, documentation system)
High-Level Architecture
System Layers
LFE is organized into nine architectural layers, from foundational utilities to user-facing applications:
┌─────────────────────────────────────────────────────────┐
│ Layer 9: Applications & CLI Tools │
│ lfec, lfescript, lfe (REPL) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 8: Shell & Interactive Environment │
│ lfe_shell, lfe_edlin_expand │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 7: Documentation System │
│ lfe_docs, lfe_shell_docs │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 6: Compilation Pipeline │
│ lfe_comp (orchestrator) │
│ lfe_scan → lfe_parse → lfe_macro → lfe_lint → │
│ lfe_codegen → lfe_translate │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 5: Runtime & Evaluation │
│ lfe_eval, lfe_env │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 4: Standard Library │
│ lfe_io, lfe_lib, lfe_gen, lfe_types │
│ lfe_bits, lfe_ms, lfe_qlc │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 3: Compatibility Layers │
│ cl (Common Lisp), clj (Clojure), scm (Scheme) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 2: Language Internals │
│ lfe_internal (core forms, special forms, intrinsics) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Layer 1: Foundation │
│ lfe.hrl, lfe_comp.hrl, lfe_macro.hrl (shared defs) │
└─────────────────────────────────────────────────────────┘
Compilation Pipeline Architecture
The compilation pipeline is a linear, acyclic flow with distinct phases:
Source File (.lfe)
↓
┌──────────────────┐
│ lfe_scan.erl │ Lexical Analysis: Text → Tokens
│ (897 LOC) │ Handles: strings, atoms, numbers, symbols, sigils
└──────────────────┘
↓ Tokens
┌──────────────────┐
│ lfe_parse.erl │ Syntactic Analysis: Tokens → S-expressions
│ (284 LOC) │ LL(1) parser, trivial due to S-expr syntax
└──────────────────┘
↓ S-expressions
┌──────────────────┐
│ lfe_macro.erl │ Macro Expansion: Transform AST
│ (1,432 LOC) │ Recursive expansion, hygiene, quasiquotation
└──────────────────┘
↓ Expanded Forms
┌──────────────────┐
│ lfe_lint.erl │ Semantic Analysis: Validate & Check
│ (2,532 LOC) │ Variable binding, type checking, pattern validation
└──────────────────┘
↓ Validated Forms
┌──────────────────┐
│ lfe_docs.erl │ Documentation Extraction: EEP-48 chunks
│ (362 LOC) │ Extract docstrings, specs, metadata
└──────────────────┘
↓ Forms + Docs
┌──────────────────┐
│ lfe_codegen.erl │ Code Generation: Lambda lifting, AST building
│ (499 LOC) │ Prepare Erlang Abstract Format
└──────────────────┘
↓ Erlang AST (pre-translation)
┌──────────────────┐
│ lfe_translate.erl│ AST Translation: LFE Forms → Erlang Abstract Format
│ (2,182 LOC) │ Pattern/guard/expression translation
└──────────────────┘
↓ Complete Erlang AST
┌──────────────────┐
│ compile:forms/2 │ Erlang Compiler: AST → BEAM
│ (Erlang stdlib) │ Optimizations, code generation, BEAM emission
└──────────────────┘
↓
BEAM Bytecode (.beam)
Key metrics:
- Total compiler code: ~8,000 LOC (39.5% of codebase)
- Largest module:
lfe_lint.erl(2,532 LOC) - semantic analysis - Second largest:
lfe_translate.erl(2,182 LOC) - AST translation - Macro system:
lfe_macro.erl(1,432 LOC) - the heart of LFE's power - Parser simplicity:
lfe_parse.erl(284 LOC) - S-expressions make parsing trivial
Runtime System Architecture
LFE provides a complete interpreter alongside its compiler, enabling dual execution modes:
┌─────────────────────────────────────────────────────────┐
│ Compiled Execution Path │
│ Source → Compile → BEAM → Execute on VM │
│ (Fast, optimized, production) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Interpreted Execution Path │
│ S-expr → Macro Expand → lfe_eval → Value │
│ (Interactive, REPL, eval-when-compile) │
└─────────────────────────────────────────────────────────┘
Runtime components:
- lfe_eval.erl (2,004 LOC): Complete interpreter with all special forms
- lfe_env.erl (252 LOC): Environment management (bindings, scoping)
- lfe_shell.erl (1,188 LOC): Interactive REPL with job control
- lfe_edlin_expand.erl (175 LOC): Tab completion system
Shell architecture uses a two-process model:
┌──────────────────┐ ┌─────────────────────┐
│ Shell Process │ <-----> │ Evaluator Process │
│ - I/O handling │ msgs │ - Expression eval │
│ - History │ │ - Pattern matching │
│ - State mgmt │ │ - Isolated crashes │
└──────────────────┘ └─────────────────────┘
This isolation ensures evaluator crashes don't kill the shell, enabling robust interactive development.
Key Design Decisions
Two-Stage Compilation Strategy
Decision: Compile LFE → Core Erlang → BEAM, rather than LFE → BEAM directly.
Rationale:
- Leverage existing infrastructure: Reuse Erlang's battle-tested optimizer and code generator
- Pattern compilation: Exploit Core Erlang's efficient case statement representation
- Tool compatibility: Dialyzer, Cover, Xref all understand Core Erlang
- Maintenance reduction: No need to maintain BEAM code generation
- Optimization inheritance: Automatically benefit from Erlang compiler improvements
Trade-off: Extra translation layer adds compile-time overhead, but enables perfect interop and reduces implementation complexity dramatically.
S-Expression Syntax (Homoiconicity)
Decision: Use pure S-expressions rather than inventing syntax.
Impact:
- Parser is trivial: 284 LOC for complete parser vs. 1000s in typical languages
- Macros are natural: Code-as-data means macro manipulation is just list processing
- No AST library needed: S-expressions are the AST
- Metaprogramming is first-class: Quasiquotation and manipulation are built-in
- Tooling simplification: Pretty-printing, code formatting, analysis all easier
Cost: Parentheses-heavy syntax unfamiliar to non-Lisp programmers (mitigated by Lisp's large existing community).
Macro System Design
Decision: Provide both procedural macros (lambda/match-lambda) and pattern-based macros (syntax-rules).
Capabilities:
- Procedural macros: Full Turing-complete transformation (receive environment as
$ENV) - Pattern macros: Declarative Scheme-style syntax-rules with ellipsis
- Hygiene mechanism: Manual via gensym (not automatic like Scheme)
- Compile-time eval:
eval-when-compilefor computation during macro expansion - Macro export: Cross-module macro sharing
50+ built-in macros provide Common Lisp-style convenience (defun, defmodule, cond, etc.).
Dual Execution Philosophy
Decision: Support both compilation and interpretation.
Compiled mode:
- Production use
- Maximum performance
- Full optimization
- Dialyzer integration
Interpreted mode:
- REPL development
- Dynamic code loading (slurp)
- Compile-time evaluation
- Interactive debugging
Both paths share the same frontend (scanner, parser, macro expander), ensuring semantic consistency.
Environment as Core Abstraction
Decision: Use a single unified environment representation (lfe_env.erl) across all subsystems.
Usage:
- Compiler: Tracks macro definitions, record definitions during expansion/linting
- Runtime: Tracks variable/function bindings during evaluation
- Shell: Maintains persistent state across REPL interactions
Implementation: Immutable record with maps/orddict for bindings, enabling functional threading through compilation/evaluation passes.
Pattern Matching Everywhere
Decision: Make pattern matching a first-class, pervasive feature.
Contexts:
- Function clauses (
match-lambda) - Let bindings (
let) - Case expressions (
case) - Message receiving (
receive) - Comprehension generators (
lc,bc) - Record/struct access
- Match specifications (ETS/trace)
Consistency: Same pattern language across all contexts (with context-appropriate restrictions for guards).
Three-Way Compatibility
Decision: Provide separate compatibility modules for Common Lisp, Clojure, and Scheme rather than trying to "be" any of them.
Modules:
- cl.lfe (767 LOC): Common Lisp functions, symbol properties, sequences
- clj.lfe (842 LOC): Clojure threading macros, predicates, lazy sequences
- scm.erl (276 LOC): Scheme syntax-rules, begin, define
Philosophy: Explicit namespacing ((: cl member ...)) makes semantics clear. No hidden behavior changes. Programmers from any Lisp background can be productive immediately.
Records vs. Structs Duality
Decision: Support both Erlang-style records (tuples) and Elixir-style structs (maps).
Records:
- Tuple-based:
{record_name, field1, field2, ...} - Fixed size, positional access
- Erlang/OTP compatible
- Used in gen_server callbacks, OTP behaviors
Structs:
- Map-based:
#{__struct__ => module, field => value, ...} - Flexible size, named access
- Modern developer ergonomics
- Elixir-inspired
Rationale: Provide the right tool for each job—legacy compatibility vs. modern ergonomics.
Relationship to Erlang and Other Lisps
Relationship to Erlang
LFE is not:
- A syntax overlay (it has different semantics via macros)
- An FFI to Erlang (there is no foreign function interface)
- A separate runtime (it is the BEAM VM)
- An Erlang dialect (it's a different language that compiles to same target)
LFE is:
- Semantically equivalent at the bytecode level
- Perfectly interoperable with zero overhead
- OTP-native (all behaviors, supervision, applications work)
- Tool-compatible (Dialyzer, Observer, Debugger, etc. work seamlessly)
Code equivalence example:
;; LFE
(defun factorial (n)
(factorial n 1))
(defun factorial (0 acc) acc)
(defun factorial (n acc) (when (> n 0))
(factorial (- n 1) (* n acc)))
%% Equivalent Erlang
factorial(N) -> factorial(N, 1).
factorial(0, Acc) -> Acc;
factorial(N, Acc) when N > 0 ->
factorial(N - 1, N * Acc).
Both compile to identical BEAM bytecode.
Relationship to Common Lisp
LFE borrows from Common Lisp:
- Lisp-2 namespace separation (functions and variables distinct)
- Macro style (lambda-based, not pattern-based)
- Quote/backquote/comma syntax
- Many function names (car, cdr, mapcar, etc.)
LFE differs from Common Lisp:
- No CLOS (use Erlang records/behaviors instead)
- No condition system (use Erlang try/catch/exit)
- No mutable data structures (Erlang immutability)
- Pattern matching is primary (vs. destructuring-bind)
- Processes instead of threads
- Message passing instead of shared state
Compatibility: The cl module provides 60+ CL functions with correct semantics (including () = false).
Relationship to Clojure
LFE borrows from Clojure:
- Threading macros (
->,->>,as->,some->, etc.) - Conditional macros (
if-let,when-let,condp) - Rich set of predicates
- Lazy sequence concepts
- Functional composition style
LFE differs from Clojure:
- No persistent data structures (Erlang uses copying immutability)
- No STM (Erlang uses actors/message passing)
- Pattern matching is primary (vs. destructuring)
- Lisp-2 (Clojure is Lisp-1)
- S-expressions (Clojure has reader macros for vectors/maps)
Compatibility: The clj module provides threading macros and predicates with Clojure-like semantics.
Relationship to Scheme
LFE borrows from Scheme:
- Syntax-rules (macro-by-example system)
- Hygienic macro concepts
- Simple, elegant core
- Lexical scoping only
LFE differs from Scheme:
- Lisp-2 (Scheme is Lisp-1)
- Eager evaluation (Scheme encourages laziness)
- Pattern matching pervasive
- No continuations (Erlang has processes instead)
- Macro expansion is explicit, not automatic hygiene
Compatibility: The scm module provides full syntax-rules implementation with ellipsis support.
Unique Position in Language Ecosystem
LFE occupies a unique niche:
Lisp Family BEAM VM Family
│ │
│ │
├─ Common Lisp ├─ Erlang
├─ Scheme ├─ Elixir
├─ Clojure (JVM) │
│ │
└─────────┬────────────┘
│
└─ LFE (intersection)
- Lisp syntax & macros
- BEAM semantics & concurrency
- Three-dialect compatibility
- Zero-overhead interop
No other language provides:
- Lisp metaprogramming on BEAM VM
- Pattern matching as primary paradigm in Lisp
- Actor model concurrency in Lisp
- OTP supervision/behaviors in Lisp
- Erlang tool compatibility in Lisp
Architectural Quality and Maturity
Code Organization Metrics
Codebase statistics:
- 39 modules total (37 Erlang, 2 LFE)
- 20,272 lines of code
- 4 header files (shared definitions)
- ~350 exported functions across all modules
Complexity distribution:
| Category | Modules | LOC | % of Total |
|---|---|---|---|
| Compiler | 13 | 8,011 | 39.5% |
| Library | 9 | 3,404 | 16.8% |
| Runtime | 4 | 2,595 | 12.8% |
| I/O | 4 | 1,601 | 7.9% |
| Shell | 2 | 1,402 | 6.9% |
| Compatibility | 3 | 1,885 | 9.3% |
| Other | 4 | 1,374 | 6.8% |
Module size distribution:
- Top 3 modules: 30% of codebase (lfe_lint, lfe_translate, lfe_eval)
- Top 10 modules: 61% of codebase
- 28% of modules: Under 100 LOC
- Median module size: 419 LOC
Power law distribution: 20% of modules contain 61% of code, indicating good separation of concerns with a few complex core modules.
Architectural Assessment
Strengths:
- ✅ Clear module responsibilities (each module has well-defined purpose)
- ✅ Consistent naming (
lfe_*prefix, subsystem grouping) - ✅ Minimal circular dependencies (clean layer separation)
- ✅ Focused modules (28% under 100 LOC)
- ✅ Clean subsystem boundaries (compiler, runtime, library distinct)
- ✅ Excellent extensibility (macro system, compatibility layers)
- ✅ Perfect interoperability (zero-overhead Erlang integration)
Overall ratings:
| Aspect | Grade | Justification |
|---|---|---|
| Architecture | A | Excellent layering and separation of concerns |
| Code Quality | A- | Consistent, well-written; some large modules could be split |
| Maintainability | A | Easy to navigate, clear purposes, good documentation |
| Extensibility | A+ | Multiple extension points, powerful macro system |
| Interoperability | A+ | Zero-overhead Erlang integration, tool compatibility |
| Maturity | A | Production-ready, self-hosting, comprehensive testing |
Production Readiness
Evidence of maturity:
- Self-hosting: LFE compiler is written in LFE (dogfooding)
- Comprehensive testing: Extensive test suites (property-based, unit, integration)
- Real-world usage: Deployed in production systems for years
- Complete tooling: REPL, compiler, build integration, documentation
- OTP integration: Full support for all standard behaviors
- Type system support: Complete Dialyzer integration
- Documentation: EEP-48 compliant, comprehensive user guide
- Stability: Mature codebase with predictable evolution
Not a toy language: LFE demonstrates that implementation quality matters. This is thoughtfully designed, carefully implemented, and well-architected.
Key Findings Summary
Top 20 Architectural Insights
Compiler Architecture:
- Two-stage translation (LFE → Core Erlang → BEAM) leverages Erlang's optimizer
- S-expression advantage: Parser is only 284 LOC, vs. 1000s in typical languages
- Macro system centrality: 1,432 LOC, 50+ built-in macros, full code transformation
- Comprehensive linting: Largest module (2,532 LOC) performs deep semantic analysis
- Lambda lifting: Nested closures lifted to module-level functions for BEAM compatibility
- Pattern compilation: Optimized translation to Core Erlang case statements
Runtime System:
- Dual execution modes: Compiled (fast) and interpreted (interactive) share frontend
- Complete interpreter: 2,004 LOC implementing all special forms
- Environment design: Immutable, layered, used by both compiler and runtime
- Closure implementation: Erlang funs capture environment, support lexical scoping
- Shell architecture: Two-process model isolates crashes for robustness
Standard Library:
- Three Lisp dialects: CL, Clojure, Scheme compatibility layers (unique in Lisp world)
- Dual data systems: Records (Erlang-compatible tuples) + Structs (modern maps)
- Match spec DSL: Readable LFE syntax for ETS/trace patterns
- Type integration: Bidirectional LFE ↔ Erlang type conversion for Dialyzer
Integration & Quality:
- Zero-overhead interop: Identical BEAM bytecode to equivalent Erlang code
- EEP-48 documentation: Standard Erlang docs format, tool integration
- OTP compatibility: All behaviors, processes, supervision work natively
- Self-hosting: Compiler written in LFE (proof of maturity)
- Clean architecture: 9 layers, power-law distribution, minimal cycles
Novel Contributions to PL Implementation
LFE demonstrates several techniques valuable to language implementers:
1. Homoiconicity enables simplicity:
- 284 LOC parser (vs. 1000s typical)
- Natural macro system
- Trivial code generation/analysis tools
2. Leveraging existing infrastructure:
- Use host language's optimizer (don't reinvent)
- Reuse tool ecosystem (Dialyzer, etc.)
- Compile to intermediate representation (Core Erlang)
3. Environment as universal abstraction:
- Same data structure for compile-time and runtime
- Immutable threading through all passes
- Enables simple reasoning about scoping
4. Multiple compatibility without compromise:
- Separate namespace modules (cl, clj, scm)
- Explicit invocation (
(cl:member ...)) - No hidden semantic changes
5. Dual execution as development accelerator:
- Compilation for production
- Interpretation for development
- Same semantics, different performance characteristics
Language Subsystems
This section provides comprehensive technical details on LFE's three core subsystems: the compilation pipeline, the runtime system, and the macro system. Each subsystem is documented at the implementation level, with data structures, algorithms, and concrete examples.
Compilation Pipeline
The LFE compiler is a multi-stage pipeline where each stage performs a distinct transformation. The pipeline is strictly linear and acyclic—there are no back-edges or iterative refinement loops. This design simplifies reasoning about compilation and makes the pipeline highly composable.
graph TB
A[Source Text<br/>.lfe file] -->|String| B[lfe_scan.erl<br/>Lexical Analysis]
B -->|Token Stream| C[lfe_parse.erl<br/>Syntactic Analysis]
C -->|S-expressions| D[lfe_comp.erl<br/>File Splitting]
D -->|Module Forms| E[lfe_macro.erl<br/>Macro Expansion]
E -->|Expanded Forms| F[lfe_lint.erl<br/>Semantic Analysis]
F -->|Validated Forms| G[lfe_docs.erl<br/>Doc Extraction]
G -->|Forms + Docs| H[lfe_codegen.erl<br/>Code Generation]
H -->|Erlang AST| I[lfe_translate.erl<br/>AST Translation]
I -->|Complete Erlang AST| J[compile:forms/2<br/>Erlang Compiler]
J -->|BEAM Bytecode| K[.beam File]
style A fill:#e1f5ff
style K fill:#ffe1e1
style E fill:#f0e1ff
style F fill:#e1ffe1
style I fill:#ffe1f5
Key characteristics:
- One-way flow: Each stage consumes input from previous stage, produces output for next
- Explicit state threading: Compilation state (
#comp{}record) threaded through all stages - Error accumulation: Errors collected but don't stop compilation (report all errors at once)
- Incremental optimization: Each stage can be stopped early for debugging/analysis
Stage 1: Lexical Analysis (lfe_scan.erl)
Purpose: Convert source text into a token stream.
Module: lfe_scan.erl (897 LOC)
Entry point:
string(String, StartLine) -> {ok, Tokens, LastLine} | {error, Error, LastLine}
string(String, StartLine, Options) -> {ok, Tokens, LastLine} | {error, Error, LastLine}
Token format:
Token :: {TokenType, Line} | {TokenType, Line, Value}
TokenType ::
'(' | ')' | '[' | ']' | '.' | % Delimiters
'\'' | '`' | ',' | ',@' | % Quote operators
'#(' | '#.' | '#\'' | '#B(' | '#M(' | % Hash forms
symbol | number | string | binary % Literals
Example transformation:
;; Input
(defun hello (name)
(io:format "Hello, ~s!~n" (list name)))
;; Tokens produced
[{'(', 1},
{symbol, 1, defun},
{symbol, 1, hello},
{'(', 1},
{symbol, 1, name},
{')', 1},
{'(', 2},
{':', 2},
{symbol, 2, io},
{symbol, 2, format},
{string, 2, "Hello, ~s!~n"},
{'(', 2},
{symbol, 2, list},
{symbol, 2, name},
{')', 2},
{')', 2},
{')', 2}]
Special features:
- Triple-quoted strings:
"""multi\nline"""(lines 450-500) - Sigils:
#"binary string",#B(binary),#M(map) - Based numbers:
#2r1010(binary),#16rFF(hex),#8r777(octal) - Character literals:
#\a,#\newline - Quoted symbols:
|complex-symbol-name| - Elixir module hack:
#Emodule→'Elixir.module'
Scanner state:
-record(lfe_scan, {
% Currently unused, reserved for future stateful scanning
}).
The scanner is continuation-based (like Erlang's erl_scan), supporting incremental parsing:
token(Cont, Chars) ->
{done, Result, Rest} | {more, Continuation}
This enables:
- REPL interaction (partial input buffering)
- Streaming compilation
- Error recovery
Stage 2: Syntactic Analysis (lfe_parse.erl)
Purpose: Convert token stream into S-expressions (LFE's native AST).
Module: lfe_parse.erl (284 LOC)
Why so small? S-expressions are trivial to parse. The grammar is:
Sexpr → Atom | Number | String | Binary | List | Tuple | Map | Quote
List → '(' Sexpr* ')' | '[' Sexpr* ']'
Quote → "'" Sexpr | "`" Sexpr | "," Sexpr | ",@" Sexpr
Hash → "#(" Sexpr* ")" | "#B(" Bitsegs ")" | "#M(" Pairs ")"
Parser type: LL(1) shift-reduce parser with explicit state stack.
Parser state:
-record(spell1, {
line = none, % Current line number
st = [], % State stack
vs = [] % Value stack
}).
Parsing algorithm (from spell1/2 at line 147-192):
1. Read token
2. Consult parse table: table(State, Token) → Action
3. Action = shift → Push state, push value, continue
4. Action = reduce → Pop values, apply reduction rule, push result
5. Repeat until accept or error
Parse table excerpt (lines 195-227):
%% State transitions
table([start|_], {'(', _}) -> {shift, 1, s_list};
table([start|_], {'[', _}) -> {shift, 1, s_bracket};
table([start|_], {symbol, _, S}) -> {shift_reduce, 0, 1, S};
table([start|_], {number, _, N}) -> {shift_reduce, 0, 1, N};
table([start|_], {string, _, S}) -> {shift_reduce, 0, 1, S};
...
Example transformation:
;; Tokens
[{'(', 1}, {symbol, 1, hello}, {symbol, 1, world}, {')', 1}]
;; S-expression produced
[hello, world]
Special forms handling:
%% Quote: 'expr → [quote, expr]
table([start|_], {'\'', _}) -> {shift, 2, s_quote};
%% Backquote: `expr → [backquote, expr]
table([start|_], {'`', _}) -> {shift, 2, s_backquote};
%% Comma: ,expr → [comma, expr]
table([start|_], {',', _}) -> {shift, 2, s_comma};
%% Comma-at: ,@expr → [comma-at, expr]
table([start|_], {',@', _}) -> {shift, 2, s_comma_at};
Stage 3: Macro Expansion (lfe_macro.erl)
Purpose: Recursively expand all macros, transforming user-level syntax into core forms.
Module: lfe_macro.erl (1,432 LOC) - Location: src/lfe_macro.erl
This is the heart of LFE's power. The macro system enables metaprogramming, DSL creation, and syntactic abstraction.
Macro state:
-record(mac, {
deep = true, % Deep expand everything
keep = true, % Keep all forms
module = '-no-module-', % Current module
line = 1, % Line no of current form
vc = 0, % Variable counter
fc = 0, % Function counter
file = [], % File name
opts = [], % Compiler options
ipath = [], % Include path
errors = [], % Errors
warnings = [], % Warnings
unloadable = [] % Macro modules we can't load
}).
Expansion strategy (from pass_form/3 at src/lfe_macro.erl:180-213):
1. Handle progn → recursively expand all forms
2. Handle eval-when-compile → evaluate at compile time
3. Handle include-file/include-lib → file inclusion
4. Handle define-macro → collect macro definitions
5. Expand other forms recursively
Macro types:
1. User-defined macros (from define-macro):
(define-macro my-when (test . body)
`(if ,test (progn ,@body) 'false))
Stored in environment as {macro, Definition} where Definition is a lambda or match-lambda that receives [Args, $ENV].
2. Built-in macros (50+ total, from exp_predef/3 at src/lfe_macro.erl:290-800):
;; Common Lisp style
defmodule, defun, defmacro
;; Convenience
cond, let*, flet*, list*
;; Data structures
defrecord, defstruct
;; Syntactic sugar
:module:function → (call 'module 'function ...)
;; Pattern matching
match-spec (ETS/trace DSL)
;; Query comprehensions
qlc
;; Special
MODULE, LINE (compile-time constants)
Backquote (quasiquotation) expansion:
From exp_backquote/2 (src/lfe_macro.erl:1343-1408):
Implements R6RS-compliant quasiquotation:
`(a ,b ,@c) → [list, [quote, a], b | c]
Algorithm:
1. Scan expression for comma and comma-at
2. Build list construction for comma (unquote)
3. Build list splicing for comma-at (unquote-splicing)
4. Handle nested backquotes recursively
5. Special handling for tuples and maps
Hygiene mechanism:
LFE macros are unhygienic (like Common Lisp, unlike Scheme). LFE does not provide gensym - there is no built-in facility for generating unique symbols.
Manual hygiene techniques:
- Use sufficiently unique variable names in macro-generated code
- Rely on lexical scoping to avoid most capture issues
- Use the
$ENVparameter to check for name conflicts at expansion time - For Scheme-style hygiene, use the optional
scmmodule (see below)
Macro expansion order:
1. Top-level forms expanded first
2. Nested forms expanded recursively (inside-out)
3. User macros shadow built-in macros
4. Most recently defined macro wins (LIFO)
Stage 4: Semantic Analysis (lfe_lint.erl)
Purpose: Validate code for semantic correctness without executing it.
Module: lfe_lint.erl (2,532 LOC - largest module) - Location: src/lfe_lint.erl
Lint state:
-record(lfe_lint, {
module = [], % Module name
mline = 0, % Module definition line
exports = orddict:new(), % Exported function-line
imports = orddict:new(), % Imported function-{module,func}
aliases = orddict:new(), % Module-alias
onload = [], % Onload function
funcs = orddict:new(), % Defined function-line
types = [], % Known types
texps = orddict:new(), % Exported types
specs = [], % Known func specs
records = orddict:new(), % Record definitions
struct = undefined, % Struct definition
env = [], % Top-level environment
func = [], % Current function
file = "no file", % File name
opts = [], % Compiler options
errors = [], % Errors
warnings = [] % Warnings
}).
What gets checked (comprehensive list from src/lfe_lint.erl:200-500):
1. Module Definition:
- Bad attributes
- Bad metadata
- Redefining imports/exports
- Multiple module definitions
2. Functions:
- Undefined functions (called but not defined)
- Redefining functions
- Redefining core forms/BIFs
- Importing then defining same function (conflict)
- Head arity mismatches (clauses with different arities)
- Bad function definitions
3. Variables:
- Unbound symbols (used but not bound)
- Multiple variable definitions in patterns (same var bound twice)
- Unused variables (bound but never used) - warning only
4. Patterns:
- Illegal patterns (non-pattern forms in pattern position)
- Bad pattern aliases (
=operator misuse) - Invalid record patterns
- Invalid binary patterns
5. Guards:
- Illegal guard expressions (only BIFs allowed)
- Bad guard forms
- Non-boolean guard results
6. Records/Structs:
- Undefined records/structs (used but not defined)
- Undefined fields (accessing non-existent fields)
- Bad definitions (malformed define-record/define-struct)
- Redefining records/structs
7. Types/Specs:
- Undefined types (type references to non-existent types)
- Redefining types
- Bad type definitions (syntax errors)
- Singleton type variables (defined but used only once) - warning only
- Spec arity mismatch (spec doesn't match function arity)
8. Operators:
- Illegal operator calls (operators used incorrectly)
9. Literals:
- Illegal literal values
- Illegal map keys (non-constant keys)
- Illegal bit segments (invalid bitstring specifications)
Example error detection:
;; Unbound variable
(defun foo (x)
(+ x y)) % ERROR: unbound symbol y
;; Undefined function
(defun bar ()
(baz 42)) % ERROR: undefined function baz/1
;; Arity mismatch
(defun multi
([x] x)
([x y] (+ x y))
([x] (* x 2))) % ERROR: clause arity mismatch
;; Pattern error
(defun bad ([x x] x)) % ERROR: variable x bound multiple times
Stage 5: Documentation Extraction (lfe_docs.erl)
Purpose: Extract documentation from code and format it per EEP-48 (Erlang documentation standard).
Module: lfe_docs.erl (362 LOC) - Location: src/lfe_docs.erl
EEP-48 format:
-record(docs_v1, {
anno :: erl_anno:anno(),
beam_language :: atom(),
format :: binary(),
module_doc :: doc_content(),
metadata :: map(),
docs :: [doc_element()]
}).
Documentation extraction process:
- Module docs: From module metadata
(defmodule name "docstring" ...) - Function docs: From function metadata
(defun name "docstring" ...) - Type docs: From type definitions
- Spec extraction: Function specifications
Docstring formats supported:
- Binary:
#"Documentation string" - Charlist:
"Documentation string" - Hidden:
false(function exists but not documented)
Integration: Docs stored in .beam file as chunk "Docs", accessible via code:get_doc/1.
Stage 6: Code Generation (lfe_codegen.erl)
Purpose: Convert validated LFE forms to Erlang Abstract Format structure.
Module: lfe_codegen.erl (499 LOC) - Location: src/lfe_codegen.erl
Code generation state:
-record(lfe_cg, {
module = [], % Module name
mline = 0, % Module definition line
exports = ordsets:new(), % Exports
imports = orddict:new(), % Imports
aliases = orddict:new(), % Aliases
onload = [], % Onload function
records = [], % Records
struct = undefined, % Struct definition
attrs = [], % Attributes
metas = [], % Meta data (types, specs)
funcs = orddict:new(), % Defined functions
opts = [], % Compiler options
file = [], % File name
func = [], % Current function
errors = [],
warnings = []
}).
Processing steps:
1. Collect Module Definitions (collect_mod_defs/2):
- Extract module name, exports, imports
- Collect attributes, records, structs
- Collect type definitions and specs
- Store function definitions
2. Build Struct Definition (build_struct_def/1):
If struct defined, create __struct__/0 and __struct__/1 functions:
__struct__() -> #{__struct__ => module_name, field1 => default1, ...}.
__struct__(Fields) -> maps:merge(__struct__(), maps:from_list(Fields)).
Auto-export these functions.
3. Build Info Function (build_info_func/1):
Create __info__/1 function (like Elixir):
__info__(module) -> module_name;
__info__(functions) -> [{func, arity}, ...];
__info__(macros) -> [];
__info__(attributes) -> [...];
...
Auto-exported.
4. Compile Attributes (compile_attributes/1):
Generate Erlang attributes:
- Module name:
{attribute, Line, module, Name} - Exports:
{attribute, Line, export, [{F, A}, ...]} - Imports:
{attribute, Line, import, {Mod, [{F, A}, ...]}} - on_load:
{attribute, Line, on_load, {F, A}} - Custom attributes
- Type declarations
- Specs
- Records
5. Compile Functions (compile_functions/1):
For each function:
- Lambda lift (via
lfe_codelift) - nested functions become module-level - Translate (via
lfe_translate) - LFE → Erlang AST - Generate clauses - multiple clauses for match-lambda
Stage 7: AST Translation (lfe_translate.erl)
Purpose: Translate between LFE forms and Erlang Abstract Format.
Module: lfe_translate.erl (2,182 LOC - second largest) - Location: src/lfe_translate.erl
Bidirectional translation:
%% LFE → Erlang
to_expr(LFEForm, Line) -> ErlangAST
to_expr(LFEForm, Line, {Imports, Aliases}) -> ErlangAST
%% Erlang → LFE
from_expr(ErlangAST) -> LFEForm
from_expr(ErlangAST, Line) -> LFEForm
Key translation examples:
%% Data constructors
[quote, E] → {atom, Line, E} (for atoms)
[cons, H, T] → {cons, Line, H', T'}
[list | Es] → nested cons / {nil, Line}
[tuple | Es] → {tuple, Line, [E1', ...]}
[binary | Segs] → {bin, Line, Segments}
[map | KVs] → {map, Line, Assocs}
%% Functions
[lambda, Args, Body] → {'fun', Line, {clauses, [Clause]}}
[match-lambda | Clauses] → {'fun', Line, {clauses, Clauses}}
%% Control flow
[if, Test, Then, Else] → {'case', Line, Test, [...]}
[case, Expr, Clauses] → {'case', Line, Expr, Clauses}
[receive | Clauses] → {'receive', Line, Clauses}
[try, Expr, ...] → {'try', Line, ...}
%% Function calls
[funcall, F | Args] → {call, Line, F', Args'}
[call, M, F | Args] → {call, Line, {remote, Line, M', F'}, Args'}
[F | Args] → {call, Line, {atom, Line, F}, Args'}
Pattern translation:
Patterns use similar syntax but different semantics:
- Variables become
{var, Line, Var} [= Pat1 Pat2]becomes alias patterns- Maps become
{map_pat, Line, [...]}
Guard translation:
Guards have restricted expressions:
- BIFs only (no user functions)
- Comparisons:
>,>=,<,=<,==,=:=,/=,=/= - Arithmetic:
+,-,*,/,div,rem,band,bor,bxor,bnot,bsl,bsr - Boolean:
and,or,xor,not,andalso,orelse - Type tests:
is_atom,is_integer,is_list, etc.
Import/alias resolution:
During translation, the {Imports, Aliases} context resolves:
- Imported function calls → remote calls
- Module aliases → full module names
Example:
;; With import: (from lists (map 2))
(map fun list) → {call, Line, {remote, Line, {atom, Line, lists}, {atom, Line, map}}, [Fun', List']}
Complete Transformation Example
Let's trace a complete example from LFE source through to BEAM bytecode, showing every intermediate representation.
Source code (factorial.lfe):
(defmodule factorial
(export (fac 1)))
(defun fac (n)
(fac-helper n 1))
(defun fac-helper
([0 acc] acc)
([n acc] (when (> n 0))
(fac-helper (- n 1) (* n acc))))
Stage 1: Tokens (abbreviated):
[{'(', 1}, {symbol, 1, defmodule}, {symbol, 1, factorial},
{'(', 2}, {symbol, 2, export}, {'(', 2}, {symbol, 2, fac}, {number, 2, 1}, ...
Stage 2: S-expressions:
[defmodule, factorial,
[export, [fac, 1]]]
[defun, fac, [n],
['fac-helper', n, 1]]
[defun, 'fac-helper',
[[0, acc], acc],
[[n, acc], [when, ['>', n, 0]],
['fac-helper', ['-', n, 1], ['*', n, acc]]]]
Stage 3: After Macro Expansion (lfe_macro.erl):
The macros defmodule and defun expand to core forms:
;; defmodule expands to define-module
['define-module', factorial,
[], % Metadata
[[export, [fac, 1]]]] % Attributes
;; defun expands to define-function
['define-function', fac,
[], % Metadata
[lambda, [n], % Single-clause function
['fac-helper', n, 1]]]
;; defun with multiple clauses expands to match-lambda
['define-function', 'fac-helper',
[], % Metadata
['match-lambda', % Multi-clause function
[[0, acc], acc],
[[n, acc], [when, ['>', n, 0]],
['fac-helper', ['-', n, 1], ['*', n, acc]]]]]
Stage 4: After Linting (lfe_lint.erl):
Forms are validated (variables bound, exports match definitions, etc.). No transformation, just error checking.
Stage 5: After Code Generation (lfe_codegen.erl):
Module structure is built, ready for translation:
%% Module attributes
{module, factorial}
{exports, [{fac, 1}]}
%% Function definitions (still LFE forms, but organized)
{function, fac, 1,
[lambda, [n], ['fac-helper', n, 1]]}
{function, 'fac-helper', 2,
['match-lambda',
[[0, acc], acc],
[[n, acc], [when, ['>', n, 0]],
['fac-helper', ['-', n, 1], ['*', n, acc]]]]}
Stage 6: After Translation to Erlang AST (lfe_translate.erl):
This is where LFE becomes Erlang Abstract Format:
%% Module attribute
{attribute, 1, module, factorial}
%% Export attribute
{attribute, 2, export, [{fac, 1}]}
%% Function: fac/1
{function, 4, fac, 1,
[{clause, 4,
[{var, 4, 'N'}], % Parameters
[], % Guards
[{call, 5, % Body
{atom, 5, 'fac-helper'},
[{var, 5, 'N'}, {integer, 5, 1}]}]}]}
%% Function: fac-helper/2
{function, 7, 'fac-helper', 2,
[%% Clause 1: base case
{clause, 8,
[{integer, 8, 0}, {var, 8, 'Acc'}], % Pattern
[], % No guard
[{var, 8, 'Acc'}]}, % Body
%% Clause 2: recursive case
{clause, 9,
[{var, 9, 'N'}, {var, 9, 'Acc'}], % Pattern
[[{op, 9, '>', {var, 9, 'N'}, {integer, 9, 0}}]], % Guard
[{call, 10, % Body: recursive call
{atom, 10, 'fac-helper'},
[{op, 10, '-', {var, 10, 'N'}, {integer, 10, 1}},
{op, 10, '*', {var, 10, 'N'}, {var, 10, 'Acc'}}]}]}]}
Stage 7: Core Erlang (intermediate representation before BEAM):
The Erlang compiler (compile:forms/2) translates Abstract Format to Core Erlang:
module 'factorial' ['fac'/1, 'module_info'/0, 'module_info'/1]
attributes []
'fac'/1 =
fun (N) ->
call 'fac-helper'(N, 1)
'fac-helper'/2 =
fun (0, Acc) -> Acc
fun (N, Acc) when call 'erlang':'>'(N, 0) ->
let <_2> = call 'erlang':'-'(N, 1)
<_3> = call 'erlang':'*'(N, Acc)
in call 'fac-helper'(_2, _3)
Stage 8: BEAM Bytecode:
Core Erlang is compiled to BEAM assembly, then to bytecode. The .beam file contains:
- Module name:
factorial - Exports:
[{fac,1}] - Attributes:
[] - Compiled code: native BEAM instructions
- Metadata: line numbers, debug info
Key observation: The final BEAM bytecode for this LFE module is identical to what would be produced from equivalent Erlang code. This is LFE's zero-overhead interoperability.
Runtime System
LFE provides a complete interpreter alongside its compiler. This dual execution mode enables:
- REPL (interactive development)
- lfescript (scripting without compilation)
- eval-when-compile (compile-time code execution)
- Dynamic code loading (slurp command in shell)
graph LR
A[Source Code] --> B{Execution Mode}
B -->|Compile| C[Compiler Pipeline]
C --> D[BEAM Bytecode]
D --> E[BEAM VM]
B -->|Interpret| F[lfe_eval]
F --> G[Direct Execution]
H[lfe_shell] --> F
I[lfescript] --> F
J[eval-when-compile] --> F
style C fill:#e1f5ff
style F fill:#ffe1e1
Shared components:
lfe_scanandlfe_parse- same frontendlfe_macro- same macro expansionlfe_env- same environment model
Different backends:
- Compiler: lfe_lint → lfe_codegen → lfe_translate → BEAM
- Interpreter: lfe_eval (direct execution)
The Evaluator (lfe_eval.erl)
Purpose: Interpret and execute LFE code at runtime without compilation.
Module: lfe_eval.erl (2,004 LOC) - Location: src/lfe_eval.erl
Public API:
expr(Sexpr) -> Value
expr(Sexpr, Env) -> Value
exprs([Sexpr]) -> [Value]
exprs([Sexpr], Env) -> [Value]
body([Sexpr]) -> Value % Evaluates sequence, returns last
body([Sexpr], Env) -> Value
apply(Function, Args) -> Value
apply(Function, Args, Env) -> Value
match(Pattern, Value, Env) -> {yes, Bindings} | no
match_when(Pattern, Value, Guards, Env) -> {yes, Body, Bindings} | no
Evaluation strategy:
From src/lfe_eval.erl:185-349:
- Eager evaluation (call-by-value, not lazy)
- Left-to-right argument evaluation
- Tail call optimization via Erlang's TCO
Core evaluation loop:
eval_expr(Sexpr, Env) -> Value
% Literals
eval_expr(Atom, Env) when is_atom(Atom) -> Atom;
eval_expr(Number, Env) when is_number(Number) -> Number;
% Variables
eval_expr(Var, Env) when is_atom(Var) ->
lfe_env:fetch_vbinding(Var, Env);
% Special forms
eval_expr([quote, E], _) -> E;
eval_expr([cons, H, T], Env) -> [eval_expr(H, Env) | eval_expr(T, Env)];
eval_expr([lambda|_]=L, Env) -> eval_lambda(L, Env);
eval_expr([if, Test, Then, Else], Env) -> eval_if(Test, Then, Else, Env);
eval_expr([case, Expr|Clauses], Env) -> eval_case(Expr, Clauses, Env);
% Function calls
eval_expr([Fun|Args], Env) when is_atom(Fun) ->
Vals = eval_list(Args, Env), % Eager evaluation
eval_fun_call(Fun, length(Args), Vals, Env);
Data constructors:
eval_expr([quote, E], _) -> E
eval_expr([cons, H, T], Env) ->
[eval_expr(H, Env) | eval_expr(T, Env)]
eval_expr([list|Es], Env) ->
eval_list(Es, Env)
eval_expr([tuple|Es], Env) ->
list_to_tuple(eval_list(Es, Env))
eval_expr([binary|Segs], Env) ->
eval_binary(Segs, Env)
eval_expr([map|KVs], Env) ->
eval_map(KVs, Env)
Closures:
From src/lfe_eval.erl:743-792:
eval_lambda([lambda, Args|Body], Env) ->
% Capture current environment
Apply = fun (Vals) -> apply_lambda(Args, Body, Vals, Env) end,
Arity = length(Args),
make_lambda(Arity, Apply)
make_lambda(Arity, Apply) ->
case Arity of
0 -> fun () -> Apply([]) end;
1 -> fun (A) -> Apply([A]) end;
2 -> fun (A, B) -> Apply([A, B]) end;
% ... up to arity 20
_ -> eval_error({argument_limit, Arity})
end
Key insight: Closures are Erlang funs that capture the environment.
Closure application:
apply_lambda(Args, Body, Vals, CapturedEnv) ->
% Extend captured environment with arguments
Env1 = bind_args(Args, Vals, CapturedEnv),
eval_body(Body, Env1)
Match-lambda (multi-clause functions):
eval_match_lambda(['match-lambda'|Clauses], Env) ->
Apply = fun (Vals) -> apply_match_lambda(Clauses, Vals, Env) end,
make_lambda(match_lambda_arity(Clauses), Apply)
apply_match_lambda([[Pats|Body]|Clauses], Vals, Env) ->
% Try to match patterns against values
case match_when([list|Pats], Vals, Body, Env) of
{yes, BodyExprs, Bindings} ->
eval_body(BodyExprs, lfe_env:add_vbindings(Bindings, Env));
no ->
apply_match_lambda(Clauses, Vals, Env) % Try next clause
end;
apply_match_lambda([], _, _) ->
eval_error(function_clause)
Let bindings:
eval_let([Bindings|Body], Env0) ->
Env1 = eval_let_bindings(Bindings, Env0),
eval_body(Body, Env1)
% Each binding: [Pattern, Expr] or [Pattern, (when Guards), Expr]
Val = eval_expr(Expr, Env0),
{yes, NewBindings} = match(Pattern, Val, Env0),
Env1 = add_vbindings(NewBindings, Env0)
Control flow:
eval_expr(['progn'|Body], Env) ->
eval_body(Body, Env) % Sequence evaluation
eval_expr(['if', Test, Then, Else], Env) ->
case eval_expr(Test, Env) of
true -> eval_expr(Then, Env);
false -> eval_expr(Else, Env);
_ -> error(badarg) % Non-boolean test
end
eval_expr(['case', Expr|Clauses], Env) ->
Val = eval_expr(Expr, Env),
eval_case_clauses(Val, Clauses, Env)
eval_expr(['receive'|Clauses], Env) ->
eval_receive(Clauses, Env)
eval_expr(['try'|Body], Env) ->
eval_try(Body, Env)
Function calls:
eval_expr([call, Mod, Fun|Args], Env) ->
M = eval_expr(Mod, Env),
F = eval_expr(Fun, Env),
Vals = eval_list(Args, Env),
erlang:apply(M, F, Vals)
eval_expr([funcall, F|Args], Env) ->
Fun = eval_expr(F, Env),
Vals = eval_list(Args, Env),
eval_apply_expr(Fun, Vals, Env)
eval_expr([Fun|Args], Env) when is_atom(Fun) ->
Arity = length(Args),
Vals = eval_list(Args, Env),
eval_fun_call(Fun, Arity, Vals, Env)
Function binding lookup (src/lfe_eval.erl:598-612):
Priority order:
- Locally bound functions (from
let-function,letrec-function) - Imported functions (from module imports)
- LFE BIFs (auto-imported from
lfemodule) - Erlang BIFs (auto-imported from
erlangmodule) - Error if not found
Environment Management (lfe_env.erl)
Purpose: Manage variable, function, macro, and record bindings.
Module: lfe_env.erl (252 LOC) - Location: src/lfe_env.erl
Environment structure:
-record(env, {
vars = null, % Variable bindings (map or orddict)
funs = null, % Function/macro bindings (map or orddict)
recs = null % Record definitions (map or orddict)
}).
Implementation:
Conditional compilation based on map availability:
-ifdef(HAS_MAPS).
% Use maps module
-else.
% Use orddict
-endif.
Variable operations:
add_vbinding(Name, Value, Env) -> Env
is_vbound(Name, Env) -> boolean()
get_vbinding(Name, Env) -> {yes, Value} | no
fetch_vbinding(Name, Env) -> Value % throws if not found
del_vbinding(Name, Env) -> Env
add_vbindings([{Name, Value}], Env) -> Env % Bulk add
Function operations:
Functions stored as {function, [{Arity, Definition}]}:
add_fbinding(Name, Arity, Value, Env) -> Env
is_fbound(Name, Arity, Env) -> boolean()
get_fbinding(Name, Arity, Env) ->
{yes, Value} | % Local function
{yes, Mod, Func} | % Imported function
no
Imported functions stored as {Arity, Module, RemoteName}.
Macro operations:
Macros stored as {macro, Definition}:
add_mbinding(Name, Macro, Env) -> Env
is_mbound(Name, Env) -> boolean()
get_mbinding(Name, Env) -> {yes, Macro} | no
Important: Adding a macro shadows ALL function definitions with that name (because macros are expanded at compile-time).
Record operations:
add_record(Name, Fields, Env) -> Env
get_record(Name, Env) -> {yes, Fields} | no
Environment merging:
add_env(Env1, Env2) -> Env
Merges two environments, preferring Env1 bindings in case of conflicts.
Environment threading:
Environments are immutable:
Env0 = lfe_env:new(),
Env1 = lfe_env:add_vbinding(x, 42, Env0),
Env2 = lfe_env:add_vbinding(y, 100, Env1),
% Env0, Env1 unchanged
This enables:
- Safe concurrency
- Easy backtracking
- Clear scoping rules
Shell/REPL Architecture (lfe_shell.erl)
Purpose: Interactive Read-Eval-Print Loop for development.
Module: lfe_shell.erl (1,188 LOC) - Location: src/lfe_shell.erl
Shell state:
-record(state, {
curr, % Current environment
save, % Saved environment (before slurp)
base, % Base environment (initial state)
slurp = false % Are we in slurp mode?
}).
Three-environment model:
- Base environment: Initial pristine state with shell functions/macros/vars
- Current environment: Active environment for evaluation
- Save environment: Checkpoint for
slurp/unslurp
Shell startup (src/lfe_shell.erl:119-131):
server() ->
St = new_state("lfe", []), % Create default state
process_flag(trap_exit, true), % Must trap exits
display_banner(),
% Set up tab completion
io:setopts([{expand_fun, fun(B) -> lfe_edlin_expand:expand(B) end}]),
Eval = start_eval(St), % Start evaluator process
server_loop(Eval, St)
Two-process architecture:
┌──────────────────┐ ┌─────────────────────┐
│ Shell Process │ <-----> │ Evaluator Process │
│ - I/O handling │ msgs │ - Expression eval │
│ - History │ │ - Pattern matching │
│ - State mgmt │ │ - Isolated crashes │
└──────────────────┘ └─────────────────────┘
Why separate processes?
- Isolation: Evaluator crashes don't kill shell
- Restart: Can restart evaluator after error
- Stack traces: Cleaner error reporting
Read-Eval-Print loop:
server_loop(Eval0, St0) ->
Prompt = prompt(),
{Ret, Eval1} = read_expression(Prompt, Eval0, St0),
case Ret of
{ok, Form} ->
{Eval2, St1} = shell_eval(Form, Eval1, St0),
server_loop(Eval2, St1);
{error, E} ->
list_errors([E]),
server_loop(Eval1, St0)
end
Shell variables (src/lfe_shell.erl:302-320):
add_shell_vars(Env0) ->
% Add special shell expression variables
Env1 = foldl(fun (Symb, E) -> lfe_env:add_vbinding(Symb, [], E) end,
Env0,
['+', '++', '+++', % Previous expressions
'-', % Current expression
'*', '**', '***']), % Previous values
lfe_env:add_vbinding('$ENV', Env1, Env1) % Environment itself
update_shell_vars(Form, Value, Env0) ->
% Rotate history
Env1 = lfe_env:add_vbinding('+++', fetch('++', Env0), Env0),
Env2 = lfe_env:add_vbinding('++', fetch('+', Env1), Env1),
Env3 = lfe_env:add_vbinding('+', Form, Env2),
Env4 = lfe_env:add_vbinding('***', fetch('**', Env3), Env3),
Env5 = lfe_env:add_vbinding('**', fetch('*', Env4), Env4),
Env6 = lfe_env:add_vbinding('*', Value, Env5),
Env7 = lfe_env:add_vbinding('-', Form, Env6),
update_env(Env7)
Shell-specific forms:
(set Pattern Expr) ; Pattern matching assignment
(slurp File) ; Load file into shell
(unslurp) ; Revert to pre-slurp state
(run File) ; Execute shell commands from file
(reset-environment) ; Reset to base environment
Shell functions (builtin commands):
% Compilation
(c file) % Compile and load LFE file
(ec file) % Compile and load Erlang file
(l modules) % Load/reload modules
% Information
(help) % Show help
(h module), (h module function) % Documentation
(i), (i pids) % Process information
(m), (m module) % Module information
(memory) % Memory statistics
% File system
(cd dir) % Change directory
(pwd) % Print working directory
(ls [dir]) % List files
% Printing
(p expr) % Print in LFE format
(pp expr) % Pretty-print in LFE format
(ep expr) % Print in Erlang format
(epp expr) % Pretty-print in Erlang format
% Utilities
(clear) % Clear screen
(flush) % Flush messages
(q), (exit) % Quit shell
Slurp mechanism (src/lfe_shell.erl:559-601):
slurp([File], St0) ->
{ok, St1} = unslurp(St0), % Reset first
Name = lfe_eval:expr(File, Curr),
case slurp_file(Name) of
{ok, Mod, Forms, Env, Warnings} ->
% Collect functions, imports, records
Sl = collect_module_forms(Forms),
% Add to environment
Env1 = add_imports(Sl#slurp.imps, Env),
Env2 = add_functions(Sl#slurp.funs, Env1),
Env3 = add_records(Sl#slurp.recs, Env2),
% Merge with current environment
Env4 = lfe_env:add_env(Env3, Curr),
% Save old environment
{{ok, Mod}, St1#state{save = Curr, curr = Env4, slurp = true}};
{error, ...} ->
{error, St1}
end
Slurp Flow:
flowchart TD
A[Start: slurp File] --> B[Compile file<br/>to_split pass]
B --> C[Macro expand<br/>all forms]
C --> D[Lint the forms]
D --> E[Collect module elements]
E --> F[Function definitions]
E --> G[Import declarations]
E --> H[Record definitions]
F --> I[Add to environment]
G --> I
H --> I
I --> J[Save previous environment<br/>for unslurp]
J --> K[Set slurp = true]
K --> L[End: Return updated state]
Macro System
The macro system is the distinguishing feature of LFE. It provides:
- Compile-time metaprogramming: Transform code before compilation
- Syntactic abstraction: Create new language constructs
- Code generation: Generate repetitive code automatically
- DSL creation: Embed domain-specific languages
Two macro styles:
- Procedural macros (Common Lisp style): Full Turing-complete transformations
- Pattern macros (Scheme style): Declarative syntax-rules with pattern matching
Procedural Macros
Definition:
(define-macro name (arg1 arg2 ... $ENV)
body)
Special argument $ENV: The macro receives the environment at the call site, enabling hygiene and context-aware transformation.
Example:
(define-macro unless (test . body)
`(if ,test 'false (progn ,@body)))
;; Usage:
(unless (< x 0)
(print "positive")
(inc x))
;; Expands to:
(if (< x 0)
'false
(progn
(print "positive")
(inc x)))
Quasiquotation (backquote):
`(list ,x ,@xs)
;; With x=42, xs=[1,2,3]:
[list, 42, 1, 2, 3]
Backquote operators:
`(backquote): Quote with holes,(comma / unquote): Insert value,@(comma-at / unquote-splicing): Splice list
Example of manual hygiene:
;; CAREFUL: This macro can capture variables
(define-macro my-swap (a b)
`(let ((temp ,a))
(set ,a ,b)
(set ,b temp))) ; 'temp' might collide with user code
;; Better: use a very unlikely variable name
(define-macro my-swap (a b)
`(let ((___swap_temp_internal___ ,a))
(set ,a ,b)
(set ,b ___swap_temp_internal___)))
Pattern Macros (syntax-rules)
From Scheme (provided by scm module).
Syntax:
(define-syntax name
(syntax-rules (keywords...)
(pattern1 template1)
(pattern2 template2)
...))
Example:
(define-syntax when
(syntax-rules ()
((when test body ...)
(if test (progn body ...) 'false))))
;; Usage:
(when (> x 10)
(print x)
(inc-counter))
;; Expands to:
(if (> x 10)
(progn (print x) (inc-counter))
'false)
Ellipsis (...): Matches zero or more repetitions.
Pattern matching:
mbe_match_pat(Pattern, Args, Keywords) -> true | false
From scm.erl:110-139:
;; Patterns:
'quoted % Matches exactly
keyword % Matches keyword (from syntax-rules)
variable % Matches anything (binds variable)
(p ...) % Matches list where all elements match p
(p1 p2 ...) % Matches list structure
Binding extraction:
mbe_get_bindings(Pattern, Args, Keywords) -> Bindings
Template expansion:
mbe_expand_pattern(Template, Bindings, Keywords) -> Expansion
Implementation (src/scm.erl:242-276):
mbe_syntax_rules_proc(Patterns, Keywords, Args, Env, St) ->
case find_matching_pattern(Patterns, Args, Keywords) of
{ok, Pattern, Template} ->
Bindings = mbe_get_bindings(Pattern, Args, Keywords),
Expansion = mbe_expand_pattern(Template, Bindings, Keywords),
{ok, Expansion, Env, St};
no_match ->
{error, no_matching_pattern}
end
Built-in Macros Catalog
LFE provides 50+ built-in macros. Key categories:
Common Lisp style:
defmodule, defun, defmacro % Module/function definition
cond % Multi-branch conditional
let*, flet*, letrec* % Binding forms
list* % List construction
Data structures:
defrecord % Record definition (generates 9+ macros)
defstruct % Struct definition (generates functions)
Pattern matching:
match-spec % ETS/trace match specification DSL
Comprehensions:
lc % List comprehension
bc % Binary comprehension
qlc % Query list comprehension
Convenience:
c*r macros % car, cdr, caar, cadr, cddr, etc.
++, !=, ===, !== % Operator synonyms
Special:
MODULE % Current module name (compile-time constant)
LINE % Current line number (compile-time constant)
:module:function % Call syntax → (call 'module 'function ...)
Macro Expansion Algorithm
Expansion process (from src/lfe_macro.erl:pass_form/3):
1. Check if form is progn → recursively expand all subforms
2. Check if form is eval-when-compile → evaluate at compile time
3. Check if form is include-file/include-lib → read and expand file
4. Check if form is define-macro → add macro to environment
5. Check if form head is macro → expand macro call
6. Otherwise: recursively expand subforms
7. Repeat until no macros remain
Macro lookup (priority order):
- Local macros (defined in current module)
- Imported macros (from other modules)
- Built-in macros (predefined)
Most recently defined wins (LIFO).
Expansion safety:
- Infinite loop detection: Track expansion depth, error if exceeds limit
- Error handling: Collect errors, continue expanding other forms
- Hygiene: Manual (LFE has no
gensym- developers are required to protect their macro variables from conflicting with user-space names)
Record Macro Generation
One define-record generates 9+ macros:
(define-record person name age city)
;; Generates:
make-person % Constructor
person? % Type predicate
person-name % Field accessor
person-age % Field accessor
person-city % Field accessor
set-person-name % Field setter
set-person-age % Field setter
set-person-city % Field setter
person-name-index % Field index (for element/2)
Implementation (src/lfe_macro_record.erl):
Each record definition is stored in environment, and accessor macros are generated as wrappers around tuple operations:
;; person-name expands to:
(element 2 person-record) % name is field 2 (1-indexed after record tag)
Usage:
(let ((p (make-person name "Alice" age 30 city "NYC")))
(person-name p)) ; → "Alice"
Match Specification DSL
Purpose: Make ETS/trace match specifications readable.
Module: lfe_ms.erl (387 LOC)
Problem: Erlang match specs are cryptic:
%% Erlang match spec
[{{'$1', '$2'}, [{'>', '$2', 10}], ['$1']}]
Solution: LFE DSL:
(ets-ms
([(tuple key val)]
(when (> val 10))
key))
Compilation (src/lfe_ms.erl:52-67):
flowchart TD
A["LFE Clauses<br/>[Pattern, (when Guards), Body]"] --> B["clause/2"]
B --> C["Expand head<br/>(pattern matching)"]
C --> D["Expand guards<br/>(test expressions)"]
D --> E["Expand body<br/>(action expressions)"]
E --> F["Generate:<br/>{tuple, Head, Guard, Body}"]
Variable binding:
- First occurrence of variable → Assign new
$N, store binding. - Subsequent occurrences → Use same
$N.
Dollar variables numbered from 1 (Erlang-compatible).
Example:
(ets-ms
([(tuple key val)]
(when (> val 10))
val))
;; Expands to:
[{{'$1', '$2'}, [{'>', '$2', 10}], ['$2']}]
Module Reference
This section provides a comprehensive reference for all 39 modules in the LFE codebase. Each module is documented with its purpose, API surface, internal structure, dependencies, and key implementation details. Line number references point to specific locations in the source code to aid contributors in navigating the codebase.
Module Statistics
The LFE codebase consists of 39 modules totaling 20,272 lines of code:
- 37 Erlang modules (.erl files)
- 2 LFE modules (.lfe files:
cl.lfe,clj.lfe)
Module Distribution by Category:
| Category | Module Count | LOC | % of Codebase |
|---|---|---|---|
| Compiler | 13 | 8,011 | 39.5% |
| Runtime | 4 | 2,595 | 12.8% |
| Library | 9 | 3,404 | 16.8% |
| I/O | 4 | 1,601 | 7.9% |
| Shell | 2 | 1,402 | 6.9% |
| Support Utilities | 6 | 1,174 | 5.8% |
| Compatibility | 3 | 1,885 | 9.3% |
| Documentation | 2 | 372 | 1.8% |
| Integration | 1 | 51 | 0.3% |
Top 10 Modules by Size (comprising 60.9% of the codebase):
lfe_lint.erl- 2,532 LOC (semantic analysis)lfe_translate.erl- 2,182 LOC (AST translation)lfe_eval.erl- 2,004 LOC (runtime evaluator)lfe_macro.erl- 1,432 LOC (macro system)lfe_shell.erl- 1,188 LOC (REPL)lfe_scan.erl- 897 LOC (lexer)clj.lfe- 842 LOC (Clojure compatibility)lfe_codelift.erl- 785 LOC (lambda lifting)cl.lfe- 767 LOC (Common Lisp compatibility)lfe_comp.erl- 711 LOC (compilation orchestrator)
Architectural Insights
1. Modular Design:
- Clean separation of concerns
- Minimal inter-module dependencies
- Clear interface boundaries
2. Layered Structure:
- Foundation: lfe_lib, lfe_internal, lfe_env, lfe_error
- Infrastructure: lfe_bits, lfe_struct, lfe_types
- I/O Subsystem: lfe_io, lfe_io_write, lfe_io_pretty, lfe_io_format
- Compiler Pipeline: scan → parse → macro → lint → translate → codegen
- Runtime: lfe_eval, lfe_eval_bits, lfe_init, lfescript
- Shell: lfe_shell, lfe_edlin_expand
- Libraries: cl, clj, scm, lfe_ms, lfe_qlc, lfe_gen
- Documentation: lfe_docs, lfe_shell_docs
3. Code Concentration:
- Top 3 modules: 30.3% of codebase (lfe_lint, lfe_translate, lfe_eval)
- Top 10 modules: 60.9% of codebase
- 11 modules < 100 LOC (focused, single-purpose)
4. Implementation Languages:
- 37 Erlang modules (95%)
- 2 LFE modules (5% - cl.lfe, clj.lfe)
5. Common Patterns:
- Facade: lfe.erl, lfe_io.erl
- Delegation: lfe_io delegates to lfe_io_write, lfe_io_pretty, lfe_io_format
- Pipeline: Compiler modules form processing chain
- Environment Threading: lfe_env passed through evaluation
- Two-Process: lfe_shell separates reading from evaluation
Critical Modules
By Importance:
- lfe_env - Environment management (imported by lfe_macro, used everywhere)
- lfe_eval - Runtime evaluation (complete interpreter)
- lfe_translate - Core Erlang translation (compilation backend)
- lfe_lint - Semantic analysis (validation)
- lfe_macro - Macro expansion (transformation layer)
- lfe_shell - Interactive REPL (user interface)
- lfe_comp - Compilation orchestration (coordinates pipeline)
By Usage Frequency:
- lfe_env - Used by 7+ modules
- lfe_internal - Used by 5+ modules
- lfe_io - Used by all modules (error reporting)
- lfe_lib - Used by 4+ modules
Extensibility Points
Adding Features:
- New special form: Update lfe_internal, lfe_eval, lfe_translate, lfe_lint
- New macro: Add to lfe_macro or user code
- New compatibility layer: Create new .lfe module
- New I/O format: Create new lfe_io_* module
- New library: Create new module, no core changes needed
Performance Characteristics
Fast Paths:
- Compiled code (via lfe_translate → Erlang compiler)
- Simple I/O (lfe_io_write)
- Environment lookups (map-based or orddict)
Slow Paths:
- Interpreted code (lfe_eval)
- Macro expansion (lfe_macro - 30-40% of compilation)
- Pretty printing (lfe_io_pretty - layout calculations)
- Slurping (lfe_shell - full file parsing + expansion)
Quality Indicators
Strengths:
- Well-documented modules
- Clear module boundaries
- Minimal circular dependencies
- Consistent naming conventions
- Comprehensive error handling
Areas for Improvement:
- Large modules could be split (lfe_lint, lfe_translate, lfe_eval)
- Some tight coupling (lfe_env heavily used)
- Limited test coverage visibility from module structure alone
Compiler Modules
The compiler pipeline transforms LFE source code into BEAM bytecode through a series of well-defined passes. These 13 modules constitute 39.5% of the codebase and represent the core complexity of the LFE system.
lfe_comp.erl - Compilation Orchestrator
Purpose: Coordinate all compilation passes and manage the overall compilation pipeline.
Location: src/lfe_comp.erl
Size: 711 LOC, 27KB
Module Classification: Compiler core, orchestration layer
Public API
Primary Entry Points:
file(FileName) -> CompRet
file(FileName, Options) -> CompRet
CompRet = {ok, ModuleName, Binary, Warnings}
| {ok, ModuleName, Warnings}
| error | {error, Errors, Warnings}
Entry point for compiling LFE files. Located at lfe_comp.erl:100-114.
forms(Forms) -> CompRet
forms(Forms, Options) -> CompRet
Compile pre-parsed forms directly. Located at lfe_comp.erl:116-129.
Specialized Compilation:
is_lfe_file(FileName) -> boolean()
Check if file has .lfe extension. Located at lfe_comp.erl:132.
format_error(Error) -> Chars
Format compiler errors for display. Located at lfe_comp.erl:175-178.
Compilation Pipeline
The do_forms/1 function at lfe_comp.erl:236-267 orchestrates seven distinct passes:
- File Splitting (
do_split_file/1at line 298): Separate multi-module files - Export Macros (
do_export_macros/1at line 346): Process macro exports - Macro Expansion (
do_expand_macros/1at line 369): Full macro expansion - Linting (
do_lfe_lint/1at line 398): Semantic analysis - Documentation (
do_get_docs/1at line 412): Extract EEP-48 docs - Code Generation (
do_lfe_codegen/1at line 426): Generate Erlang AST - Erlang Compilation (
do_erl_comp/1at line 458): Call Erlang compiler
Each pass can be stopped early via options: to_split, to_expand, to_lint, to_core0, to_core, to_kernel, to_asm.
Internal Structure
Key Records (defined in lfe_comp.hrl):
-record(comp, {
base="", % Base filename
ldir=".", % LFE source directory
lfile="", % LFE filename
odir=".", % Output directory
opts=[], % Compiler options
ipath=[], % Include path
cinfo=none, % Compiler info (#cinfo{})
module=[], % Module name
code=[], % Generated code
return=[], % Return mode
errors=[], % Accumulated errors
warnings=[], % Accumulated warnings
extra=[] % Pass-specific options
}).
File Splitting Algorithm (do_split_file/1 at lines 298-318):
The file splitter handles LFE's unique feature of allowing multiple modules per file:
- Top-level macros are expanded to identify
define-moduleforms - Forms before the first module become "pre-forms" available to all modules
- Each module receives:
PreForms ++ ModuleForms - A
FILEmacro is automatically injected with the source file path
Dependencies
Direct Dependencies:
lfe_io(7 calls) - Error/warning reportinglfe_macro(4 calls) - Macro expansionlfe_lib(3 calls) - Utility functionslfe_env(2 calls) - Environment managementlfe_macro_export(1 call) - Export macro processinglfe_lint(1 call) - Semantic analysislfe_docs(1 call) - Documentation generationlfe_codegen(1 call) - Code generation
Erlang Dependencies:
compile(Erlang compiler) - Final BEAM generationlists,ordsets,orddict- Data structures
Used By
lfec(compiler script)lfe_shell(for:ccommand)rebar3hooks- Any tool compiling LFE files
Key Algorithms
Option Processing (lfe_comp.erl:180-217):
% Options can be:
% - Atoms: verbose, report, return, binary
% - Tuples: {outdir, Dir}, {i, IncludeDir}
% - Stop flags: to_expand, to_lint, to_core, etc.
Error Aggregation:
Errors and warnings are accumulated through all passes in the #comp.errors and #comp.warnings lists, then formatted and returned together.
Special Considerations
- Multi-module files: LFE allows multiple modules in one file, unlike Erlang
- Pre-forms: Forms before the first module are shared across all modules in the file
- FILE macro: Automatically defined to the source file path for each module
- Incremental compilation: Not currently supported; each compilation is full recompilation
- Pass control: Compilation can stop at intermediate stages for debugging
Performance Note: The compiler is dominated by macro expansion (30-40%) and Erlang compilation (40-50%) time.
lfe_scan.erl - Lexical Scanner
Purpose: Convert raw LFE source text into a stream of tokens for parsing.
Location: src/lfe_scan.erl
Size: 897 LOC, 35KB
Module Classification: Compiler frontend, lexical analysis
Public API
Primary Scanning Functions:
string(String) -> {ok, Tokens, Line} | {error, ErrorInfo, Line}
string(String, StartLine) -> Result
string(String, StartLine, Options) -> Result
Tokens = [Token]
Token = {Type, Line, Value}
Type = symbol | number | string | binary | '(' | ')' | '[' | ']' | ...
Scan a complete string. Located at lfe_scan.erl:66-68.
Incremental Scanning (for REPL):
token(Continuation, Chars) -> {more, Continuation1}
| {done, Result, RestChars}
tokens(Continuation, Chars) -> {more, Continuation1}
| {done, Result, RestChars}
Continuation-based scanning for streaming input. Located at lfe_scan.erl:45-54.
Token Types
Delimiters: '(', ')', '[', ']', '.'
Special Syntactic Markers:
'\''- Quote ('expr)''- Backquote (``expr ``)','- Unquote (,expr)',@'- Unquote-splicing (,@expr)
Hash Forms:
'#('- Tuple literal#(a b c)'#.'- Eval-at-read#.(+ 1 2)'#B('- Binary literal#B(42 (f 32))'#M('- Map literal#M(a 1 b 2)'#\''- Function reference#'module:function/arity
Literals:
symbol- Atoms and identifiers (foo,foo-bar,|complex symbol|)number- Integers and floats (42,3.14,#2r1010,#16rFF)string- String literals ("hello","""multi\nline""")binary- Binary strings (#"bytes")
Token Structure
Each token is a tuple: {Type, Line, Value} or {Type, Line} for delimiters.
Examples:
{symbol, 1, foo}
{number, 1, 42}
{string, 2, "hello"}
{'(', 1}
Special Features
Triple-Quoted Strings (lfe_scan.erl:394-450):
Supports Python/Elixir-style triple-quoted strings:
"""
Multi-line string
with "quotes" inside
"""
Quoted Symbols (lfe_scan.erl:350-365):
Allows arbitrary characters in symbol names:
|complex-symbol-name!@#$|
|with spaces|
Based Numbers (lfe_scan.erl:473-493):
Supports bases 2-36:
#2r1010 ; Binary
#8r755 ; Octal
#16rDEADBEEF ; Hexadecimal
#36rZZZ ; Base-36
Character Literals (lfe_scan.erl:552-573):
#\a ; Character 'a'
#\n ; Newline
#\x41 ; Hex character code
Elixir Module Name Hack (lfe_scan.erl:542-549):
Transforms #Emodule → 'Elixir.module' for Elixir interop:
#EEnum ; Becomes 'Elixir.Enum'
Comment Handling
Line Comments (lfe_scan.erl:277-282):
; This is a comment
;; Also a comment
Block Comments (lfe_scan.erl:283-311):
#|
Block comment
can span multiple lines
|#
Nested Block Comments: Supported via counter tracking.
Internal Structure
Scanner State:
-record(lfe_scan, {}). % Currently unused, reserved for future
The scanner uses functional continuation passing for incremental parsing rather than explicit state records.
Key Functions:
scan/3(line 136): Main dispatch loopscan_symbol/4(line 236): Symbol scanningscan_number/4(line 452): Number parsingscan_string/5(line 375): String literal handlingscan_comment/3(line 277): Comment skipping
Dependencies
Erlang stdlib:
lists- List operationsstring- String manipulationunicode- UTF-8 handling
No LFE module dependencies - Scanner is self-contained.
Used By
lfe_parse- Consumes token streamlfe_comp- Via parse (indirectly)lfe_shell- For REPL inputlfe_io:read*functions
Key Algorithms
Continuation-Based Scanning (lfe_scan.erl:45-54):
The scanner supports incremental input for REPL use:
{more, Continuation} % Need more input
{done, {ok, Token, Line}, RestChars} % Token complete
This allows reading from a terminal where input arrives character-by-character.
Symbol Validation (lfe_scan.erl:601-645):
Symbols must:
- Not start with digits (unless quoted)
- Not contain special delimiters (unless quoted)
- Support Unicode characters
- Handle reserved characters in quoted form
Number Parsing (lfe_scan.erl:452-535):
Supports:
- Decimal integers:
42,-17 - Floats:
3.14,1.5e10,6.022e23 - Based integers:
#16rFF,#2r1010 - Sign handling for all numeric forms
Special Considerations
Performance:
- Scanner is O(n) in input length
- Minimal memory allocation (continuation-based)
- Hot path: symbol scanning (most common token)
Unicode Support:
- Full UTF-8 support in strings and comments
- Symbol names can include Unicode characters
- Proper handling of multi-byte sequences
Error Recovery:
- Errors include line numbers for reporting
- Invalid characters reported with context
- Unterminated strings/comments detected
Compatibility:
- Inherited structure from Erlang's
erl_scan - Extended with LFE-specific features (hash forms, quoted symbols)
lfe_parse.erl - S-Expression Parser
Purpose: Transform token stream into s-expression Abstract Syntax Trees (ASTs).
Location: src/lfe_parse.erl
Size: 284 LOC, 11KB
Module Classification: Compiler frontend, syntactic analysis
Public API
form(Tokens) -> {ok, Line, Sexpr, RestTokens}
| {more, Continuation}
| {error, ErrorInfo, RestTokens}
Parse a single top-level form from tokens. Located at lfe_parse.erl:46-48.
forms(Tokens) -> {ok, Forms}
| {error, ErrorInfo, RestTokens}
Parse all forms from a token stream. Located at lfe_parse.erl:50-52.
Parser Type
LL(1) shift-reduce parser with explicit state and value stacks.
Parser State Record:
-record(spell1, {
line=none, % Current line number
st=[], % State stack
vs=[] % Value stack
}).
Located at lfe_parse.erl:38-43.
Grammar
The parser handles standard S-expression grammar with LFE extensions:
Basic Forms:
- Lists:
(a b c),[a b c] - Atoms:
foo,foo-bar,|quoted| - Numbers:
42,3.14 - Strings:
"hello" - Binaries:
#"bytes"
Special Syntax:
- Quote:
'expr→(quote expr) - Backquote:
`expr→(backquote expr) - Comma:
,expr→(comma expr) - Comma-at:
,@expr→(comma-at expr) - Tuple:
#(a b)→ Tuple{a, b} - Binary:
#B(42)→ Binary construction - Map:
#M(a 1)→ Map#{a => 1} - Function ref:
#'mod:func/2→(function mod func 2)
Improper Lists (dotted pairs):
(a . b) ; Cons cell
(a b . c) ; Improper list
Parsing Algorithm
Shift-Reduce with Table (lfe_parse.erl:107-229):
The parser uses a table/2 function encoding the parsing table:
table(State, Token) -> {shift, NewState}
| {reduce, RuleNumber}
| {accept, RuleNumber}
| {error, Error}
Reduction Rules (reduce/2 at lines 238-262):
- Rule 0: Accept top-level form
- Rules 1-4: Extract token values
- Rule 5: Function literal
#'F/A - Rule 6: Eval literal
#.expr - Rules 7-10: Quote transformations
- Rules 11-12: List construction
- Rule 13: Tuple construction
- Rule 14: Binary construction
- Rule 15: Map construction
- Rules 16-22: Cons cell handling
Special Form Processing
Function References (make_fun/1 at lines 277-295):
Transforms #'function/arity and #'module:function/arity:
#'foo/2 → (function foo 2)
#'lists:map/2 → (function lists map 2)
#'Mod:func/1 → (function Mod func 1)
Eval-at-Read (eval_expr/2 at lines 307-320):
The #. form evaluates expressions at read time:
#.(+ 1 2) → 3
#.(list 'a 'b) → (a b)
Note: Eval-at-read is limited - only safe operations allowed.
Binary Construction (make_bin/2 at lines 324-334):
Transforms binary syntax:
#B(42) → Binary with byte 42
#B(42 (f 32)) → 32-bit float
#B((str "hello")) → Binary string
Map Construction (make_map/2 at lines 338-361):
Transforms map syntax:
#M(a 1 b 2) → #{a => 1, b => 2}
Dependencies
Erlang stdlib:
lists- List manipulation- No other dependencies
LFE modules: None (parser is self-contained)
Used By
lfe_comp- Compilation pipelinelfe_shell- REPL input parsinglfe_io:read*- Reading s-expressions from text
Key Algorithms
Parse Stack Management:
The parser maintains two stacks:
- State stack (
st): Parsing states - Value stack (
vs): Reduced values
parse_1(Tokens, St0) ->
case scan_token(Tokens, St0) of
{shift, NewState, St1} ->
parse_1(RestTokens, St1);
{reduce, Rule, St1} ->
St2 = reduce(Rule, St1),
parse_1(Tokens, St2);
{accept, St1} ->
{ok, value(St1)};
{error, Error} ->
{error, Error}
end.
Error Recovery:
The parser provides error messages with line numbers and context:
{error, {Line, lfe_parse, ["unexpected ", T]}, RestTokens}
Special Considerations
No Macro Expansion:
The parser produces raw s-expressions. Macros are not expanded here - that happens in lfe_macro.
Improper List Handling:
Dotted pairs are represented as two-element tuples:
(a . b) → [cons, a, b]
Tuple vs List:
- Parentheses:
(a b c)→ List - Brackets:
[a b c]→ List - Hash-paren:
#(a b c)→ Tuple
Quote Simplification:
Multiple quote forms are preserved:
''a → (quote (quote a))
`',a → (backquote (comma a))
Performance:
- O(n) parsing time
- Minimal memory usage
- No backtracking (LL(1) grammar)
lfe_macro.erl - Macro Expansion Engine
Purpose: Expand macros into core LFE forms. This is the largest and most complex module in the compiler pipeline.
Location: src/lfe_macro.erl
Size: 1,432 LOC, 55KB
Module Classification: Compiler core, transformation layer
Public API
Form Expansion:
expand_form(Form, Line, Env, State) ->
{ok, Form, Env, State}
| {error, Errors, Warnings, State}
Expand a single form. Located at lfe_macro.erl:131-139.
File Form Expansion:
expand_fileform({Form, Line}, Env, State) ->
{ok, {Form, Line}, Env, State}
| {error, Errors, Warnings, State}
expand_fileforms(FileForms, Env, State) -> ...
Expand forms with line number annotations. Located at lfe_macro.erl:141-151.
Macro State Management:
new() -> State
format_error(Error) -> Chars
Create new macro state and format errors. Located at lfe_macro.erl:114-117.
Macro State
State Record (defined in lfe_macro.hrl):
-record(mac, {
deep=true, % Deep recursive expansion
keep=true, % Keep all forms (even unexpanded)
module='-no-module-', % Current module name
line=1, % Current line number
vc=0, % Variable counter (for gensym)
fc=0, % Function counter (for gensym)
file=[], % Source file name
opts=[], % Compiler options
ipath=[], % Include path
errors=[], % Accumulated errors
warnings=[], % Accumulated warnings
unloadable=[] % Unloadable macro modules
}).
Macro Types
1. User-Defined Macros (via define-macro):
(define-macro when-positive (x body)
`(if (> ,x 0) ,body 'undefined))
; Expands to macro function:
; (lambda (x body $ENV) ...)
Macros receive arguments + $ENV parameter for environment access.
2. Pattern-Based Macros (via match-lambda):
(define-macro foo
([a b] `(list ,a ,b))
([a b c] `(tuple ,a ,b ,c)))
3. Built-in Macros (50+ macros):
Located at lfe_macro.erl:700-1340:
Convenience Macros:
c*rfamily:caar,cadr,cdar,cddr, etc. (lines 1118-1184)- Comparison operators:
!=,===,!==,/=(lines 885-900) list*- List construction with tail (lines 908-917)let*,flet*,fletrec*- Sequential bindings (lines 927-1000)
Common Lisp Style:
defmodule,defun,defmacro- CL-style definitions (lines 779-835)defsyntax- Syntax macro definition (line 837)
Records and Structs:
defrecord- Record definition (callslfe_macro_record, line 843)defstruct- Struct definition (callslfe_macro_struct, line 849)
Control Flow:
doloops - Iteration macro (lines 1024-1090)funshortcuts - Lambda syntax sugar (lines 1095-1111)
Match Specifications:
ets-ms,trace-ms- DSL for match specs (callslfe_ms, lines 1186-1198)
Query List Comprehensions:
qlc- QLC syntax (callslfe_qlc, lines 1202-1221)
Module Information:
MODULE- Current module name (line 1224)LINE- Current line number (line 1225)FILE- Current file path (line 1226)
Call Syntax:
:module:function→(call 'module 'function ...)(lines 1230-1268)
Expansion Algorithm
Main Expansion Loop (pass_form/3 at lines 180-213):
pass_form([progn | Forms], Env, St) ->
% Expand all forms in progn
expand_forms(Forms, Env, St);
pass_form([eval-when-compile | Forms], Env, St) ->
% Evaluate forms at compile time
eval_forms(Forms, Env),
{ok, [progn], Env, St};
pass_form(['include-file', File], Env, St) ->
% Load and expand file contents
include_file(File, Env, St);
pass_form(['define-macro', Name, Meta, Def], Env, St) ->
% Add macro to environment
add_macro(Name, Def, Env, St);
pass_form(Form, Env, St) ->
% Expand form recursively
expand_expr(Form, Env, St).
Backquote Expansion (exp_backquote/2 at lines 1343-1408):
Implements R6RS-compliant quasiquotation:
`(a ,b ,@c)
→ (list 'a b | c)
`(a b ,(+ 1 2))
→ (list 'a 'b 3)
`#(a ,b)
→ (tuple 'a b)
Key feature: Nested backquotes are handled correctly.
Macro Application (exp_macro/4 at lines 574-641):
When a macro is encountered:
- Look up macro definition in environment
- Apply macro function to arguments +
$ENV - Recursively expand the result
- Track expansion depth to prevent infinite loops
Environment Threading:
The environment (lfe_env) is threaded through expansion:
{ok, Form1, Env1, St1} = expand_form(Form0, Env0, St0),
{ok, Form2, Env2, St2} = expand_form(Next0, Env1, St1),
...
Special Forms (Not Expanded)
Core forms are preserved and passed through:
- Data:
quote,cons,car,cdr,list,tuple,binary,map - Functions:
lambda,match-lambda,let,let-function,letrec-function - Control:
progn,if,case,receive,catch,try - Calls:
function,call,funcall
Located at lfe_macro.erl:216-268.
Dependencies
LFE modules:
lfe_env- Imported (39 functions imported! - lines 79-87)lfe_io- I/O operationslfe_lib- Utilitieslfe_internal- Form validationlfe_macro_record- Record macro generationlfe_macro_struct- Struct macro generationlfe_macro_include- File inclusionlfe_eval- Foreval-when-compile
Erlang stdlib:
lists,ordsets,orddict
Used By
lfe_comp- Compilation pipelinelfe_shell- REPL macro expansionlfe_eval- Runtime macro expansion (for macros in interpreted code)
Key Algorithms
Macro Hygiene (or lack thereof):
LFE macros are unhygienic - they can capture variables from the calling context:
(define-macro bad-swap (a b)
`(let ((tmp ,a))
(set ,a ,b)
(set ,b tmp)))
; If caller has variable 'tmp', it will be captured!
Solution: Manual gensym using environment's variable counter:
gen_variable(St) ->
{erlang:list_to_atom("_G" ++ integer_to_list(St#mac.vc)),
St#mac{vc = St#mac.vc + 1}}.
Macro Recursion Detection (lfe_macro.erl:574-641):
The expander tracks expansion depth and detects cycles:
-define(MAX_EXPAND, 1000).
expand_with_depth(Form, Depth, Env, St) when Depth > ?MAX_EXPAND ->
{error, "Macro expansion depth exceeded", St};
expand_with_depth(Form, Depth, Env, St) ->
...
File Inclusion (lfe_macro_include.erl):
The include-file and include-lib forms:
- Resolve file path (absolute or relative to include path)
- Read and parse file
- Expand all forms from file
- Insert into current expansion
Eval-When-Compile:
Forms in eval-when-compile are evaluated using lfe_eval:
(eval-when-compile
(defun helper () 'compiled)
(io:format "Compiling!~n"))
The side effects occur at compile time, but forms are also included in output.
Special Considerations
Performance Hotspot:
Macro expansion is 30-40% of compilation time. Deeply nested macros or heavy quasiquotation can slow compilation significantly.
Environment Import:
lfe_macro is the only module that imports from lfe_env:
-import(lfe_env, [new/0, add_vbinding/3, is_vbound/2, ...]).
This tight coupling reflects the heavy use of environment operations.
Error Messages:
Macro expansion errors can be cryptic because:
- Original source location is lost after expansion
- Generated code may not correspond to source
- Nested macro expansions obscure the error source
Macro Export (lfe_macro_export.erl):
Macros can be exported between modules in the same file:
(defmodule foo
(export-macro when-positive))
(defmodule bar
(import-macro foo when-positive))
Record and Struct Macros:
These delegate to specialized modules:
lfe_macro_record- Generates 9+ macros per recordlfe_macro_struct- Generates struct access macros
lfe_macro_export.erl - Export Macro Processor
Purpose: Handle macro exports between modules in the same file.
Location: src/lfe_macro_export.erl
Size: 288 LOC, 8.8KB
Module Classification: Compiler support, macro system
Public API
export(Modules, Env, State) -> {ok, Env, State}
| {error, Errors, Warnings, State}
Process macro exports across modules. Located at lfe_macro_export.erl:42-47.
Export Mechanism
Syntax:
(defmodule foo
(export-macro when-test my-cond))
(defmodule bar
(import-macro foo when-test))
Process:
- Collect all
export-macrodeclarations - Collect all
import-macrodeclarations - For each import, copy macro binding from exporting module to importing module
- Validate: exported macros must exist, no circular dependencies
Implementation (lfe_macro_export.erl:67-158):
collect_exports(Modules) ->
% Build map: ModuleName → ExportedMacros
...
collect_imports(Modules) ->
% Build map: ModuleName → ImportedMacros
...
validate_imports(Imports, Exports) ->
% Check all imports have corresponding exports
...
merge_macros(FromEnv, ToEnv, MacroNames) ->
% Copy macro bindings between environments
...
Dependencies
lfe_env- Environment operationslfe_lib- Utilitieslists,orddict
Used By
lfe_comp- Called during compilation if multi-module file
Special Considerations
Scope: Only works within a single file. Macros cannot be exported across file boundaries (by design - macros are compile-time only).
Order Independence: Modules can import macros from modules defined later in the same file.
lfe_macro_include.erl - Include File Processor
Purpose: Handle include-file and include-lib macro forms.
Location: src/lfe_macro_include.erl
Size: 502 LOC, 18KB
Module Classification: Compiler support, macro system
Public API
file(Name, St) -> {ok, Forms, State}
| {error, Error, State}
Include a file by name. Located at lfe_macro_include.erl:45.
lib(Name, St) -> {ok, Forms, State}
| {error, Error, State}
Include a library file. Located at lfe_macro_include.erl:55.
Include Forms
Include File:
(include-file "common-macros.lfe")
Searches:
- Current directory
- Directories in
-Iinclude path option
Include Lib:
(include-lib "lfe/include/clj.lfe")
Searches:
- Erlang lib directories (
code:lib_dir/1) - Standard OTP application paths
File Resolution
Include File Resolution (lfe_macro_include.erl:115-140):
resolve_include(File, IncludePath) ->
% Try each directory in include path
case try_paths(["."|IncludePath], File) of
{ok, Path} -> Path;
error -> {error, {include_file, File}}
end.
Include Lib Resolution (lfe_macro_include.erl:142-178):
resolve_include_lib("app/include/file.lfe") ->
case code:lib_dir(app) of
{error, bad_name} ->
{error, {include_lib, "app not found"}};
Dir ->
Path = filename:join([Dir, "include", "file.lfe"]),
check_file(Path)
end.
File Reading and Parsing
Process (lfe_macro_include.erl:89-113):
- Resolve file path
- Read file contents
- Parse into tokens (
lfe_scan:string/1) - Parse tokens into forms (
lfe_parse:forms/1) - Return forms for expansion
Dependencies
lfe_scan- Lexical analysislfe_parse- Parsinglfe_io- File I/Ofilename,code(Erlang) - Path resolution
Used By
lfe_macro- Expandsinclude-fileandinclude-libforms
Special Considerations
Recursive Includes: Supported (include files can include other files)
Include Guards: Not built-in; user must avoid circular includes manually
Compile-Time Evaluation: Included forms are expanded at compile time, so changes to included files require recompilation
Performance: Large include files slow compilation; prefer modular design
lfe_macro_record.erl - Record Macro Generator
Purpose: Generate accessor macros for record definitions.
Location: src/lfe_macro_record.erl
Size: 122 LOC, 4.5KB
Module Classification: Compiler support, code generation
Public API
define(Name, FieldDefs, Env, Line) ->
{[{Name, Macros}], Env}
Generate record macros. Located at lfe_macro_record.erl:44-56.
Generated Macros
For a record definition:
(define-record person
name
age
(city "Unknown")) ; With default
9+ Macros Generated:
-
make-person- Constructor(make-person name "Alice" age 30) → (tuple 'person "Alice" 30 "Unknown") -
is-person- Type test(is-person x) → (andalso (is_tuple x) (=:= (tuple-size x) 4) (=:= (element 1 x) 'person)) -
match-person- Pattern matcher(match-person name n age a) → (= (tuple 'person n a _) x) -
update-person- Updater(update-person p age 31 city "NYC") → (setelement 3 (setelement 4 p "NYC") 31) -
set-person- Updater (deprecated alias forupdate-person) -
fields-person- Field list(fields-person) → (list 'name 'age 'city) -
size-person- Record size(size-person) → 4 -
person-field(one per field) - Getter(person-name p) → (element 2 p) (person-age p) → (element 3 p) -
set-person-field(one per field) - Setter(set-person-name p "Bob") → (setelement 2 p "Bob")
Tuple Layout
Records compile to tuples with tag as first element:
(make-person name "Alice" age 30)
→ #(person "Alice" 30 "Unknown")
^^^^^^ ^^^^^^^ ^^ ^^^^^^^^^
tag name age city
Field Indexing: 1-based (element 1 is tag, element 2 is first field, etc.)
Erlang Compatibility
LFE records are identical to Erlang records:
% Erlang code can use LFE records:
-include("person.hrl"). % If generated
P = {person, "Alice", 30, "Unknown"},
Name = P#person.name, % "Alice"
Dependencies
lfe_lib- Utilitieslists- List operations
Used By
lfe_macro- Expandsdefrecordforms
Special Considerations
Macro Count:
For an N-field record:
- Base macros: 7
- Field accessors: N getters + N setters
- Total: 7 + 2N macros
For 10-field record: 27 macros generated!
Naming Conflicts:
If you define:
(define-record foo bar)
The following names are reserved:
make-foo,is-foo,match-foo,update-foo,set-foo,fields-foo,size-foofoo-bar,set-foo-bar
Default Values:
(define-record person
name ; No default (will be 'undefined' if not specified)
(age 0) ; Default: 0
(city "Stockholm")) ; Default: "Stockholm"
Performance: Record access compiles to direct element/2 calls - very fast.
lfe_macro_struct.erl - Struct Macro Generator
Purpose: Generate accessor macros for struct (map-based) definitions.
Location: src/lfe_macro_struct.erl
Size: 46 LOC, 1.5KB
Module Classification: Compiler support, code generation
Public API
define(Name, FieldDefs, Env, Line) ->
{[{Name, Macros}], Env}
Generate struct macros. Located at lfe_macro_struct.erl:42.
Struct vs Record
Structs use maps instead of tuples:
(define-struct [name age city])
(struct person name "Alice" age 30)
→ #{__struct__ => person, name => "Alice", age => 30, city => undefined}
Key Difference: Structs include a __struct__ key identifying the struct type (Elixir-inspired).
Generated Macros
Fewer macros than records (structs use runtime functions):
-
Module functions (not macros):
__struct__/0- Return default struct__struct__/1- Create struct from proplist
-
Accessor macros:
struct- Constructor macrois-struct- Type test macrostruct-field- Field access macro
Struct Runtime
Struct operations are defined in lfe_struct.erl (33 LOC):
new(Mod) -> Mod:'__struct__'().
new(Mod, Fields) -> Mod:'__struct__'(Fields).
is(Struct, Mod) -> maps:get('__struct__', Struct) =:= Mod.
fetch(Struct, Mod, Field) -> maps:get(Field, Struct).
...
Dependencies
lfe_struct(runtime)maps(Erlang)
Used By
lfe_macro- Expandsdefstructforms
Special Considerations
Flexibility: Maps are more flexible than tuples:
- Can add fields dynamically
- Pattern matching on maps
- Better introspection
Performance: Map access is slower than tuple element access
OTP Compatibility: Less compatible with OTP than records (some behaviors expect records)
lfe_lint.erl - Semantic Analyzer
Purpose: Validate LFE code for semantic correctness. This is the largest module in the codebase.
Location: src/lfe_lint.erl
Size: 2,532 LOC, 94KB
Module Classification: Compiler core, validation layer
Public API
module(Forms) -> {ok, Warnings}
| {error, Errors, Warnings}
module(Forms, CompilerInfo) -> Result
Lint an entire module. Located at lfe_lint.erl:109-115.
format_error(Error) -> Chars
Format lint errors for display. Located at lfe_lint.erl:128-297.
Lint State
State Record (lines 58-79):
-record(lfe_lint, {
module=[], % Module name
mline=0, % Module definition line
exports=orddict, % Exported functions
imports=orddict, % Imported functions
aliases=orddict, % Module aliases
onload=[], % On-load function
funcs=orddict, % Defined functions
types=[], % Type definitions
texps=orddict, % Exported types
specs=[], % Function specs
records=orddict, % Record definitions
struct=undefined, % Struct definition
env=[], % Current environment
func=[], % Current function
file="no file", % Source file
opts=[], % Compiler options
errors=[], % Accumulated errors
warnings=[] % Accumulated warnings
}).
Validation Checks
Module-Level Checks (module_forms/2 at lines 322-401):
-
Module Definition:
- Exactly one
define-modulerequired - Module name must be atom
- Valid attributes only
- Exactly one
-
Exports:
- Exported functions must be defined
- Duplicate exports detected
- Export syntax validation
-
Imports:
- Import syntax validation
- Conflicting imports detected
- Can't import and define same function
-
Attributes:
- Valid attribute names
- Attribute value types
- Duplicates detected
Function-Level Checks (check_function/3 at lines 673-736):
-
Function Definitions:
- Arity consistency across clauses
- Duplicate definitions
- Redefining imports
- Redefining core forms
-
Variable Bindings:
- Unbound variables detected
- Duplicate bindings in patterns
- Shadowing warnings
-
Pattern Matching:
- Illegal patterns
- Improper list patterns
- Binary segment validation
- Map key patterns
Expression Checks (check_expr/2 at lines 891-1105):
-
Special Forms:
- Correct special form syntax
- Argument counts
- Valid sub-expressions
-
Function Calls:
- Undefined function calls
- Arity mismatches
- Core function validation
-
Guards:
- Only guard-safe expressions
- No function calls in guards (except BIFs)
- Type test validity
Type and Spec Checks (check_type_def/2 at lines 1406-1593):
-
Type Definitions:
- Valid type syntax
- Undefined type references
- Recursive type detection
- Type variable usage
-
Function Specs:
- Spec matches function arity
- Type validity in specs
- Return type specified
Record and Struct Checks (check_record_def/2 at lines 1651-1722):
-
Record Definitions:
- Valid field names
- Default value types
- Duplicate field names
-
Struct Definitions:
- One struct per module
- Valid field list
Error Categories
Errors (compilation fails):
- Undefined functions
- Arity mismatches
- Invalid patterns
- Unbound variables
- Syntax errors in forms
Warnings (compilation succeeds):
- Unused variables
- Shadowed variables
- Deprecated features
- Unused functions (with opt-in flag)
Dependencies
LFE modules:
lfe_env- Heavy use for tracking bindingslfe_internal- Form validationlfe_lib- Utilitieslfe_bits- Binary segment validation
Erlang stdlib:
lists,orddict,ordsets
Used By
lfe_comp- Compilation pipeline
Key Algorithms
Environment Tracking:
The linter maintains an environment (lfe_env) to track:
- Variable bindings in scope
- Function definitions
- Macro definitions (though macros are already expanded)
- Record definitions
Example (check_let/3 at lines 1108-1145):
check_let([Bindings|Body], Env, St) ->
% Check binding expressions
{Env1, St1} = check_bindings(Bindings, Env, St),
% Check body with new environment
{_Env2, St2} = check_body(Body, Env1, St1),
{Env, St2}. % Return original env (let is scoped)
Pattern Validation (check_pat/2 at lines 1737-1886):
Patterns are checked for:
- Valid constructors (cons, tuple, binary, map, record)
- Literal values
- Variable bindings (no duplicates)
- Proper nesting
Guard Validation (check_guard/2 at lines 1941-2014):
Guards can only contain:
- BIFs from
lfe_internal:is_guard_func/2 - Comparisons
- Boolean operators
- Type tests
- Arithmetic
No user-defined functions allowed in guards.
Special Considerations
Complexity:
This is the most complex module in LFE:
- 2,532 LOC
- Handles all language constructs
- Deep pattern matching
- Environment threading
Performance:
Linting is ~5-10% of compilation time (much less than macro expansion or Erlang compilation).
Error Messages:
The format_error/1 function (lines 128-297) provides user-friendly error messages with context.
Conservative Validation:
The linter may produce false positives (e.g., flagging valid dynamic code as errors) but avoids false negatives.
lfe_translate.erl - Core Erlang Translator
Purpose: Translate LFE forms to Core Erlang intermediate representation. This is the second-largest module in the codebase.
Location: src/lfe_translate.erl
Size: 2,182 LOC, 84KB
Module Classification: Compiler backend, code generation
Public API
to_expr(LfeExpr, Line) -> CoreExpr
to_expr(LfeExpr, Line, {Imports, Aliases}) -> CoreExpr
Translate LFE expression to Core Erlang. Located at lfe_translate.erl:82-86.
from_expr(CoreExpr) -> LfeExpr
from_expr(CoreExpr, Options) -> LfeExpr
Translate Core Erlang back to LFE. Located at lfe_translate.erl:96-99.
Pattern and Guard Translation:
to_pat(LfePattern, Line) -> CorePattern
to_guard(LfeGuard, Line) -> CoreGuard
Located at lines 88-92.
Translation Targets
Core Erlang is Erlang's intermediate representation:
- Explicit pattern matching
- Simplified control flow
- SSA-like structure
- Direct mapping to BEAM
Translation Rules
Atomic Values (to_expr/2 at lines 273-295):
LFE Core Erlang
--- -----------
42 #c_literal{val=42}
3.14 #c_literal{val=3.14}
"hello" #c_literal{val="hello"}
(quote foo) #c_literal{val=foo}
Data Constructors (lines 297-385):
[cons H T] #c_cons{hd=H', tl=T'}
[list A B C] #c_cons{hd=A', tl=#c_cons{...}}
[tuple A B] #c_tuple{es=[A', B']}
[binary Seg1 Seg2] #c_binary{segments=[Seg1', Seg2']}
[map K1 V1 K2 V2] #c_map{es=[K1=>V1, K2=>V2]}
Functions (lines 450-582):
[lambda [x y] Body]
→ #c_fun{vars=[Vx, Vy], body=Body'}
[match-lambda
[[x y] Body1]
[[a b c] Body2]]
→ #c_fun{vars=[V1,V2,...], body=#c_case{...}}
Let Bindings (lines 630-708):
[let [[x Expr1] [y Expr2]] Body]
→ #c_let{vars=[Vx,Vy], arg=#c_values{[Expr1',Expr2']}, body=Body'}
Control Flow (lines 752-890):
[if Test Then Else]
→ #c_case{arg=Test',
clauses=[#c_clause{pats=[#c_literal{val=true}], body=Then'},
#c_clause{pats=[#c_literal{val=false}], body=Else'}]}
[case Expr [Pat1 Body1] [Pat2 Body2]]
→ #c_case{arg=Expr',
clauses=[#c_clause{pats=[Pat1'], body=Body1'},
#c_clause{pats=[Pat2'], body=Body2'}]}
[receive [Pat1 Body1] [Pat2 Body2] [after Timeout AfterBody]]
→ #c_receive{clauses=[...], timeout=Timeout', action=AfterBody'}
Try/Catch (lines 934-1023):
[try Expr
[case Pat1 Body1]
[catch [[Class Reason Stacktrace] Handler]]
[after AfterBody]]
→ #c_try{arg=Expr',
vars=[V1], body=Body',
evars=[Eclass,Ereason,Etrace], handler=Handler',
...}
Function Calls (lines 1077-1165):
[foo Arg1 Arg2]
→ #c_apply{op=#c_var{name={foo,2}}, args=[Arg1', Arg2']}
[call Mod Fun Arg1]
→ #c_call{module=Mod', name=Fun', args=[Arg1']}
[funcall FunExpr Arg1]
→ #c_apply{op=FunExpr', args=[Arg1']}
Pattern Translation
Pattern Contexts (lines 1267-1453):
Patterns have special translation rules:
- Variables become
#c_var{} - Literals must match exactly
- No expressions allowed
- Maps allow both matching and association patterns
Pattern Core Pattern
------- ------------
x #c_var{name=X}
42 #c_literal{val=42}
[= Pat1 Pat2] #c_alias{var=..., pat=...}
[cons H T] #c_cons{hd=H', tl=T'}
[binary [x [size 8]]] #c_bitstr{val=X, size=8, ...}
Guard Translation
Guard Restrictions (lines 1489-1621):
Guards can only contain:
- BIFs from
lfe_internal:is_guard_func/2 - Boolean operators:
and,or,andalso,orelse - Comparisons:
<,>,=:=,==, etc. - Type tests:
is_atom,is_list, etc.
Guard Core Guard
----- ----------
[(> x 10)] #c_call{module=erlang, name='>', args=[X,10]}
[(andalso (> x 0) (< x 100))]
Conjunction of comparisons
Import and Alias Resolution
When translating with imports/aliases (to_expr/3):
Context = {Imports, Aliases}
Imports = [{FuncName, {Module, RemoteName}}]
Aliases = [{Alias, ActualModule}]
Example:
(import (from lists (map list-map)))
(import (rename lists (reverse rev)))
(module-alias (long-module-name short))
(list-map Fn List) ; Translates to: lists:map(Fn, List)
(rev List) ; Translates to: lists:reverse(List)
(short:func A) ; Translates to: long-module-name:func(A)
Dependencies
LFE modules:
lfe_internal(13 calls) - Form validationlfe_lib(5 calls) - Utilitieslfe_bits- Binary segment translation
Erlang compiler:
cerl- Core Erlang constructioncore_lib- Core Erlang utilities
Used By
lfe_codegen- Code generation pipeline
Key Algorithms
SSA Construction:
Core Erlang uses Single Static Assignment (SSA) form. The translator generates fresh variables:
new_var(St) ->
N = St#trans.vc,
{#c_var{name={var,N}}, St#trans{vc=N+1}}.
Pattern Compilation:
Complex patterns are compiled to nested case expressions:
(match-lambda
[[x x] 'same] ; Repeated variable
[[x _] 'different])
→ Compiles to Core with equality check:
case {Arg1, Arg2} of
{X, X} -> 'same';
{X, _} -> 'different'
end
Binary Optimization (lfe_translate.erl:1623-1789):
Binary construction is optimized:
- Constant-size segments
- UTF-8/UTF-16/UTF-32 encoding
- Bit alignment tracking
- Size expression evaluation
Let-Optimization (lines 630-708):
Sequential let bindings are compiled to nested #c_let forms for efficiency.
Special Considerations
Core Erlang Validation:
Generated Core Erlang is validated by the Erlang compiler. If translation produces invalid Core, compilation fails with cryptic errors.
Optimization Opportunities:
The translator produces straightforward Core Erlang. The Erlang compiler's optimization passes handle:
- Dead code elimination
- Inlining
- Constant folding
Performance:
Translation is ~10% of compilation time. Complex pattern matching and binary construction are the slowest parts.
Tail Call Optimization:
The translator marks tail positions. Core Erlang and BEAM handle actual TCO.
lfe_codegen.erl - Code Generator
Purpose: Generate Erlang Abstract Format from validated LFE forms. Coordinates lambda lifting and translation to Core Erlang.
Location: src/lfe_codegen.erl
Size: 499 LOC, 18KB
Module Classification: Compiler backend, orchestration
Public API
forms(Forms, CompilerInfo) ->
{ok, ErlangAST, State}
| {error, Errors, Warnings, State}
Generate Erlang AST from LFE forms. Located at lfe_codegen.erl:82-89.
Code Generation Pipeline
Process (comp_forms/2 at lines 155-235):
-
Collect Module Definitions (
collect_mod_defs/2):- Extract module name, exports, imports
- Collect attributes, records, types, specs
- Store function definitions
-
Build Struct Definition (
build_struct_def/1):- If struct defined, generate
__struct__/0and__struct__/1 - Auto-export struct functions
- If struct defined, generate
-
Build Info Function (
build_info_func/1):- Generate
__info__/1function - Returns: module, functions, macros, attributes, etc.
- Auto-export
__info__/1
- Generate
-
Compile Attributes (
compile_attributes/1):- Module name, exports, imports
- Custom attributes
- Type definitions, specs
- Records
-
Compile Functions (
compile_functions/1):- Lambda lift each function (
lfe_codelift) - Translate to Erlang AST (
lfe_translate) - Build function clauses
- Lambda lift each function (
State Record
-record(lfe_cg, {
module=[], % Module name
mline=0, % Module line
exports=ordsets, % Exported functions
imports=orddict, % Imported functions
aliases=orddict, % Module aliases
onload=[], % On-load function
records=[], % Records
struct=undefined, % Struct definition
attrs=[], % Attributes
metas=[], % Type/spec metadata
funcs=orddict, % Function definitions
opts=[], % Compiler options
file=[], % Source file
func=[], % Current function
errors=[],
warnings=[]
}).
Generated Functions
Struct Functions (build_struct_def/1 at lines 394-434):
If module defines a struct, two functions are generated:
__struct__() ->
#{__struct__ => ModuleName, field1 => undefined, ...}.
__struct__(Fields) ->
maps:merge(__struct__(), maps:from_list(Fields)).
Info Function (build_info_func/1 at lines 436-492):
__info__(module) -> ModuleName;
__info__(functions) -> [{func,arity}, ...];
__info__(macros) -> []; % Always empty (macros are compile-time)
__info__(attributes) -> [{attr, value}, ...];
...
Erlang AST Format
The generated AST uses Erlang's abstract format:
Module Attribute:
{attribute, Line, module, ModuleName}
Export Attribute:
{attribute, Line, export, [{FuncName, Arity}, ...]}
Function Definition:
{function, Line, Name, Arity, Clauses}
Clause:
{clause, Line, Patterns, Guards, Body}
Dependencies
LFE modules:
lfe_codelift- Lambda liftinglfe_translate- Core Erlang translationlfe_internal- Validationlfe_lib- Utilities
Erlang compiler:
compile:forms/2- Final compilation
Used By
lfe_comp- Compilation pipeline
Key Algorithms
Lambda Lifting (via lfe_codelift):
Nested lambdas are hoisted to module level:
(defun outer (x)
(lambda (y) (+ x y)))
; After lifting:
; outer/1: calls lifted lambda
; -lfe-outer-lambda-1/2: the lifted function taking x and y
Import Translation (build_imports/1 at lines 321-356):
LFE import forms are transformed to Erlang imports:
(import (from lists map filter))
→ -import(lists, [map/2, filter/2]).
(import (rename lists (reverse rev)))
→ Stored in context, calls to rev/1 translate to lists:reverse/1
Record Translation (compile_records/1 at lines 289-319):
(define-record person name age)
→ -record(person, {name, age}).
Special Considerations
Auto-Exports:
These functions are automatically exported:
__info__/1(always)__struct__/0,__struct__/1(if struct defined)
Compile-Time vs Runtime:
- Macros are not included in
__info__/1(they're compile-time only) - Functions include metadata from specs
OTP Compatibility:
Generated code is standard Erlang AST, fully compatible with OTP tools.
lfe_codelift.erl - Lambda Lifter
Purpose: Transform closures into top-level functions by lifting nested lambdas and converting them to pass captured variables as explicit arguments.
Location: src/lfe_codelift.erl
Size: 785 LOC, 30KB
Module Classification: Compiler transformation, closure conversion
Public API
lift(Forms, State) -> {ok, Forms, State}
| {error, Errors, Warnings, State}
Lift all lambdas in forms to top-level. Located at lfe_codelift.erl:82-88.
Lambda Lifting Algorithm
Process:
- Identify Closures: Find all
lambdaandmatch-lambdaforms - Capture Analysis: Determine which free variables are captured
- Generate Top-Level Function: Create new function with captured vars as parameters
- Transform Call Sites: Replace lambda with call to lifted function passing captured vars
Example:
(defun make-adder (x)
(lambda (y) (+ x y)))
; After lifting:
(defun make-adder (x)
(lambda (y) (-lfe-make-adder-lambda-1 x y)))
(defun -lfe-make-adder-lambda-1 (x y)
(+ x y))
Closure Representation
Before Lifting:
[lambda [y] [+ x y]] ; x is free (captured)
After Lifting:
; Lambda becomes call to lifted function
[lambda [y] [-lfe-outer-lambda-1 x y]]
; Lifted function
[-lfe-outer-lambda-1 [x y] [+ x y]]
Captured Variable Analysis
Free Variable Detection (free_vars/2 at lines 234-389):
free_vars([lambda, Args, Body], Env) ->
% Variables in Args are bound
BodyFree = free_vars(Body, add_bindings(Args, Env)),
% Remove Args from free vars
subtract(BodyFree, Args);
free_vars([let, Bindings, Body], Env) ->
BindingFree = free_vars_bindings(Bindings, Env),
BodyFree = free_vars(Body, add_bindings(bound_vars(Bindings), Env)),
union(BindingFree, BodyFree);
free_vars(Var, Env) when is_atom(Var) ->
case is_bound(Var, Env) of
true -> [];
false -> [Var]
end.
Lifted Function Naming
Naming Convention (lifted_name/2 at lines 154-168):
-lfe-<OriginalFunc>-lambda-<Counter>
Examples:
make-adder→-lfe-make-adder-lambda-1- Top-level lambda →
-lfe-top-lambda-1 - Nested lambda →
-lfe-outer-lambda-2-lambda-1
Counter: Ensures unique names for multiple lambdas in same function.
Match-Lambda Handling
Match-Lambda Lifting (lift_match_lambda/3 at lines 456-523):
(match-lambda
[[x] (+ x 1)]
[[x y] (+ x y)])
; Lifted to:
(lambda Args
(-lfe-lifted-match-lambda-1 CapturedVars Args))
(defun -lfe-lifted-match-lambda-1 (CapturedVars Args)
(case Args
[x] (+ x 1)
[x y] (+ x y)))
Recursive Lifting
Nested Lambdas are lifted recursively:
(defun foo (x)
(lambda (y)
(lambda (z)
(+ x y z))))
; Lifts to:
; foo/1
; -lfe-foo-lambda-1/2 (takes x, y)
; -lfe-foo-lambda-1-lambda-1/3 (takes x, y, z)
Dependencies
LFE modules:
lfe_lib- Utilitieslfe_env- Environment tracking
Erlang stdlib:
lists,ordsets
Used By
lfe_codegen- Before translation to Erlang AST
Key Algorithms
Environment Tracking:
The lifter maintains:
- Bound variables (function parameters, let-bindings)
- Free variables (captured from outer scope)
Closure Conversion:
Standard compiler transformation:
- Analyze free variables
- Extend function signature with captured vars
- Update call sites to pass captured vars
Optimization: Lift Only When Necessary:
If a lambda has no free variables, it can be lifted without capturing:
(lambda (x) (* x x))
; Lifts to simple top-level function (no captures)
Special Considerations
Performance Impact:
Lambda lifting is necessary because:
- Erlang doesn't have true closures at BEAM level
- All functions must be at module level
- BEAM funs capture by creating new fun objects
Call Overhead:
Lifted functions have overhead:
- Extra parameters (captured vars)
- Indirect call (fun object wraps call)
Generated Code Size:
Each closure generates a new top-level function, increasing module size.
Debugging:
Lifted functions have generated names (-lfe-...-lambda-N), making stack traces harder to read.
lfe_abstract_code.erl - Debug Info Provider
Purpose: Provide debug_info callback for Erlang tools (Dialyzer, debugger, cover, xref).
Location: src/lfe_abstract_code.erl
Size: 51 LOC, 1.7KB
Module Classification: Integration, tool support
Public API
debug_info(Format, Module, Data, Options) ->
{ok, Result}
| {error, Reason}
Erlang compiler callback for debug information. Located at lfe_abstract_code.erl:37-52.
Supported Formats
erlang_v1 - Erlang Abstract Format:
Returns the LFE-generated Erlang AST directly.
core_v1 - Core Erlang:
Converts AST to Core Erlang using compile:noenv_forms/2.
Implementation
debug_info(_Format, _Module, {none, _CompOpts}, _Opts) ->
{error, missing}; % No debug info stored
debug_info(erlang_v1, _Mod, {AbstrCode, _CompilerOpts}, _Opts) ->
{ok, AbstrCode}; % Return AST as-is
debug_info(core_v1, _Mod, {AbstrCode, CompilerOpts}, Opts) ->
% Convert to Core Erlang
CoreOpts = add_core_returns(delete_reports(CompilerOpts ++ Opts)),
try compile:noenv_forms(AbstrCode, CoreOpts) of
{ok, _, Core, _} -> {ok, Core};
_Error -> {error, failed_conversion}
catch
error:_E -> {error, failed_conversion}
end;
debug_info(_Format, _, _Data, _) ->
{error, unknown_format}. % Unknown format requested
Storage in BEAM
Debug Info Chunk:
When compiling with debug info, LFE stores:
{debug_info, {lfe_abstract_code, {AbstractCode, CompilerOpts}}}
This is stored in the BEAM file's debug_info chunk.
Tool Integration
Dialyzer:
Requests erlang_v1 or core_v1 format for type analysis:
{ok, Core} = lfe_abstract_code:debug_info(core_v1, Mod, DebugInfo, [])
% Dialyzer analyzes Core Erlang for types
Debugger:
Requests erlang_v1 for step debugging:
{ok, AST} = lfe_abstract_code:debug_info(erlang_v1, Mod, DebugInfo, [])
% Debugger uses AST for breakpoints, stepping
Cover (Code Coverage):
Requests erlang_v1 for coverage analysis:
{ok, AST} = lfe_abstract_code:debug_info(erlang_v1, Mod, DebugInfo, [])
% Cover instruments AST for coverage tracking
Dependencies
Erlang compiler:
compile:noenv_forms/2- Core Erlang conversion
Used By
- Dialyzer
- Erlang debugger
- Cover
- Xref
- Any tool querying debug_info
Special Considerations
Compile Options:
To include debug info:
lfec +debug_info mymodule.lfe
Without +debug_info, tools cannot analyze the module.
Format Evolution:
If Erlang adds new debug_info formats, this module needs updates.
Security:
Debug info includes source-level information. Strip it for production releases:
lfec mymodule.lfe # No debug info by default
Runtime Modules
The runtime system executes LFE code dynamically without compilation. These 4 modules constitute 12.8% of the codebase and provide the complete interpreter.
lfe_eval.erl - Expression Evaluator
Purpose: Interpret and execute LFE expressions at runtime. This is the third-largest module in the codebase and implements a complete LFE interpreter.
Location: src/lfe_eval.erl
Size: 2,004 LOC, 68KB
Module Classification: Runtime core, interpreter
Public API
Expression Evaluation:
expr(Sexpr) -> Value
expr(Sexpr, Env) -> Value
Evaluate a single expression. Located at lfe_eval.erl:107-109.
Multiple Expressions:
exprs([Sexpr]) -> [Value]
exprs([Sexpr], Env) -> [Value]
Evaluate list of expressions. Located at lfe_eval.erl:113-115.
Body Evaluation:
body([Sexpr]) -> Value
body([Sexpr], Env) -> Value
Evaluate sequence, return last value. Located at lfe_eval.erl:119-121.
Function Application:
apply(Function, Args) -> Value
apply(Function, Args, Env) -> Value
Apply function to arguments. Located at lfe_eval.erl:125-127.
Pattern Matching:
match(Pattern, Value, Env) -> {yes, Bindings} | no
match_when(Pattern, Value, Guards, Env) -> {yes, Body, Bindings} | no
Pattern matching with optional guards. Located at lfe_eval.erl:131-133.
Evaluation Strategy
Call-by-Value Semantics:
All arguments are evaluated before function application:
eval_expr([Fun|Args], Env) when is_atom(Fun) ->
Vals = eval_list(Args, Env), % Evaluate args first
eval_fun_call(Fun, length(Args), Vals, Env).
Left-to-Right Evaluation:
Arguments evaluated in order:
eval_list([E|Es], Env) ->
[eval_expr(E, Env) | eval_list(Es, Env)].
Eager Evaluation:
No lazy evaluation. All sub-expressions evaluated immediately.
Core Form Evaluation
Data Constructors (eval_expr/2 at lines 191-224):
eval_expr([quote, E], _Env) -> E;
eval_expr([cons, H, T], Env) ->
[eval_expr(H, Env) | eval_expr(T, Env)];
eval_expr([list|Es], Env) ->
eval_list(Es, Env);
eval_expr([tuple|Es], Env) ->
list_to_tuple(eval_list(Es, Env));
eval_expr([binary|Segs], Env) ->
eval_binary(Segs, Env);
eval_expr([map|Assocs], Env) ->
eval_map(Assocs, Env);
Closures (eval_lambda/2 at lines 743-792):
eval_lambda([lambda, Args|Body], Env) ->
% Capture current environment
Apply = fun (Vals) -> apply_lambda(Args, Body, Vals, Env) end,
Arity = length(Args),
make_lambda(Arity, Apply).
% Create Erlang fun with specific arity
make_lambda(0, Apply) -> fun () -> Apply([]) end;
make_lambda(1, Apply) -> fun (A) -> Apply([A]) end;
make_lambda(2, Apply) -> fun (A,B) -> Apply([A,B]) end;
...
% Up to arity 20
make_lambda(20, Apply) -> fun (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T) ->
Apply([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T]) end.
Note: Arity limit of 20 (matches Erlang's erl_eval).
Let Bindings (eval_let/2 at lines 846-868):
eval_let([Bindings|Body], Env0) ->
Env1 = eval_let_bindings(Bindings, Env0),
eval_body(Body, Env1).
eval_let_bindings([[Pat, Expr]|Rest], Env0) ->
Val = eval_expr(Expr, Env0), % Evaluate in OLD env
{yes, NewBindings} = match(Pat, Val, Env0),
Env1 = add_vbindings(NewBindings, Env0),
eval_let_bindings(Rest, Env1).
Control Flow (eval_if/2 at lines 812-828):
eval_if([Test, Then, Else], Env) ->
case eval_expr(Test, Env) of
true -> eval_expr(Then, Env);
false -> eval_expr(Else, Env);
_ -> error(badarg) % Non-boolean test
end.
Case Expression (eval_case/2 at lines 916-951):
eval_case([Expr|Clauses], Env) ->
Val = eval_expr(Expr, Env),
eval_case_clauses(Clauses, Val, Env).
eval_case_clauses([[Pat|Body]|Clauses], Val, Env) ->
case match_when(Pat, Val, Body, Env) of
{yes, BodyExprs, Bindings} ->
eval_body(BodyExprs, add_vbindings(Bindings, Env));
no ->
eval_case_clauses(Clauses, Val, Env)
end.
Receive Expression (eval_receive/2 at lines 1001-1068):
eval_receive([['after', Timeout|TimeoutBody]], Env) ->
T = eval_expr(Timeout, Env),
receive after T -> eval_body(TimeoutBody, Env) end;
eval_receive(Clauses, Env) ->
receive
Msg -> eval_receive_clauses(Clauses, Msg, Env)
end.
eval_receive_clauses([[Pat|Body]|Clauses], Msg, Env) ->
case match_when(Pat, Msg, Body, Env) of
{yes, BodyExprs, Bindings} ->
eval_body(BodyExprs, add_vbindings(Bindings, Env));
no ->
% Put message back, try next clause
self() ! Msg,
eval_receive_clauses(Clauses, Msg, Env)
end.
Note: Failed pattern matches put message back in mailbox!
Function Calls
Local Function Call (eval_fun_call/4 at lines 394-413):
eval_fun_call(Fun, Arity, Args, Env) ->
case lfe_env:get_fbinding(Fun, Arity, Env) of
{yes, M, F} ->
% Imported function
erlang:apply(M, F, Args);
{yes, Def} ->
% Local function binding
eval_apply(Def, Args, Env);
no ->
% Try LFE BIFs, then Erlang BIFs
eval_bif(Fun, Arity, Args, Env)
end.
BIF Lookup Priority:
- Locally bound functions (let-function, letrec-function)
- Imported functions
- LFE BIFs (lfe module)
- Erlang BIFs (erlang module)
Remote Call (eval_expr/2 at lines 317-322):
eval_expr([call, Mod, Fun|Args], Env) ->
M = eval_expr(Mod, Env),
F = eval_expr(Fun, Env),
Vals = eval_list(Args, Env),
erlang:apply(M, F, Vals).
Funcall (eval_expr/2 at lines 329-333):
eval_expr([funcall, FunExpr|Args], Env) ->
Fun = eval_expr(FunExpr, Env),
Vals = eval_list(Args, Env),
eval_apply_expr(Fun, Vals, Env).
Pattern Matching
Pattern Match (match/3 at lines 1104-1242):
% Literals must match exactly
match([quote, Lit], Val, _Env) ->
if Lit =:= Val -> {yes, []};
true -> no
end;
% Variables bind
match(Var, Val, _Env) when is_atom(Var) ->
{yes, [{Var, Val}]};
% Don't-care
match('_', _Val, _Env) ->
{yes, []};
% Cons cells
match([cons, H, T], [Hval|Tval], Env) ->
case match(H, Hval, Env) of
{yes, Hbs} ->
case match(T, Tval, Env) of
{yes, Tbs} -> {yes, Hbs ++ Tbs};
no -> no
end;
no -> no
end;
% Tuples
match([tuple|Pats], Val, Env) when is_tuple(Val) ->
match_list(Pats, tuple_to_list(Val), Env);
% Maps
match([map|Assocs], Val, Env) when is_map(Val) ->
match_map_assocs(Assocs, Val, Env);
% Binaries
match([binary|Segs], Val, Env) when is_binary(Val) ->
match_binary(Segs, Val, Env);
% Aliases
match(['=', Pat1, Pat2], Val, Env) ->
case match(Pat1, Val, Env) of
{yes, Bs1} ->
case match(Pat2, Val, Env) of
{yes, Bs2} -> {yes, Bs1 ++ Bs2};
no -> no
end;
no -> no
end.
Guard Evaluation (eval_guard/2 at lines 1279-1335):
eval_guard([['andalso'|Tests]], Env) ->
eval_guard_and(Tests, Env);
eval_guard([['orelse'|Tests]], Env) ->
eval_guard_or(Tests, Env);
eval_guard([[Test]], Env) ->
eval_guard_test(Test, Env).
eval_guard_test(Test, Env) ->
Val = eval_expr(Test, Env),
Val =:= true. % Must be exactly 'true'
Records and Structs
Record Operations (eval_expr/2 at lines 226-241):
% Create record
eval_expr([record, Name|FieldVals], Env) ->
{ok, Fields} = lfe_env:get_record(Name, Env),
make_record_tuple(Name, Fields, FieldVals, Env);
% Access field
eval_expr(['record-field', Rec, Name, Field], Env) ->
RecVal = eval_expr(Rec, Env),
get_record_field(RecVal, Name, Field, Env);
% Update record
eval_expr(['record-update', Rec, Name|Updates], Env) ->
RecVal = eval_expr(Rec, Env),
update_record_tuple(RecVal, Name, Updates, Env).
Struct Operations (eval_expr/2 at lines 242-256):
% Create struct
eval_expr([struct, Mod|FieldVals], Env) ->
Mod:'__struct__'(eval_pairs(FieldVals, Env));
% Test struct
eval_expr(['is-struct', Struct, Mod], Env) ->
StructVal = eval_expr(Struct, Env),
lfe_struct:is(StructVal, Mod);
% Access field
eval_expr(['struct-field', Struct, Mod, Field], Env) ->
StructVal = eval_expr(Struct, Env),
lfe_struct:fetch(StructVal, Mod, Field).
Comprehensions
List Comprehension (eval_list_comp/3 at lines 1367-1452):
eval_list_comp(Qualifiers, Expr, Env) ->
eval_lc_quals(Qualifiers, Expr, Env, []).
eval_lc_quals([['<-', Pat, ListExpr]|Quals], Expr, Env, Acc) ->
List = eval_expr(ListExpr, Env),
lists:foldr(fun (Elem, Acc1) ->
case match(Pat, Elem, Env) of
{yes, Bindings} ->
Env1 = add_vbindings(Bindings, Env),
eval_lc_quals(Quals, Expr, Env1, Acc1);
no -> Acc1
end
end, Acc, List);
eval_lc_quals([TestExpr|Quals], Expr, Env, Acc) ->
case eval_expr(TestExpr, Env) of
true -> eval_lc_quals(Quals, Expr, Env, Acc);
false -> Acc
end;
eval_lc_quals([], Expr, Env, Acc) ->
[eval_expr(Expr, Env) | Acc].
Binary Comprehension: Similar structure, builds binaries instead of lists.
Dependencies
LFE modules:
lfe_env(39 calls!) - Heavy environment usagelfe_io(18 calls) - I/O operationslfe_internal(14 calls) - Type checkinglfe_lib(10 calls) - Utilitieslfe_macro(4 calls) - Macro expansionlfe_eval_bits(4 calls) - Binary evaluationlfe_bits(1 call) - Bitstring specs
Erlang stdlib:
lists,maps,erlang
Used By
lfe_shell- REPL evaluationlfe_macro-eval-when-compilelfescript- Script executionlfe_init- Runtime initialization
Key Algorithms
Closure Application (apply_lambda/4 at lines 800-802):
apply_lambda(Args, Body, Vals, CapturedEnv) ->
Env1 = bind_args(Args, Vals, CapturedEnv),
eval_body(Body, Env1).
bind_args(['_'|As], [_|Vals], Env) ->
bind_args(As, Vals, Env); % Ignore don't-care
bind_args([Arg|As], [Val|Vals], Env) ->
bind_args(As, Vals, lfe_env:add_vbinding(Arg, Val, Env));
bind_args([], [], Env) ->
Env.
Let-Function (eval_let_function/2 at lines 870-883):
eval_let_function([Bindings|Body], Env0) ->
% Add function definitions to environment
Fun = fun ([Name, Lambda], E) ->
add_lexical_func(Name, Lambda, Env0, E)
end,
Env1 = lists:foldl(Fun, Env0, Bindings),
eval_body(Body, Env1).
Letrec-Function (eval_letrec_function/2 at lines 889-899):
eval_letrec_function([Bindings|Body], Env0) ->
% Create recursive environment
Fbs = extract_function_bindings(Bindings),
Env1 = make_letrec_env(Fbs, Env0),
eval_body(Body, Env1).
make_letrec_env(FuncBindings, Env) ->
% Each function sees the full environment (mutual recursion)
...
Special Considerations
Performance:
Interpreted code is 10-100x slower than compiled code:
- No optimization
- Environment lookups
- Pattern matching overhead
Arity Limit:
Closures support up to arity 20 (lines 758-792).
Tail Call Optimization:
TCO relies on Erlang's TCO (body-recursive functions in tail position).
Memory Usage:
Each closure captures its environment → potential memory leaks if not careful.
Error Handling:
Runtime errors throw exceptions with stack traces pointing to evaluator, not source.
Use Cases:
- REPL (interactive development)
eval-when-compile(compile-time code execution)- Scripts (lfescript)
- Testing (eval test expressions)
lfe_eval_bits.erl - Binary Evaluation
Purpose: Evaluate binary construction and pattern matching at runtime.
Location: src/lfe_eval_bits.erl
Size: 318 LOC, 11KB
Module Classification: Runtime support, specialized evaluator
Public API
eval_binary(Segments, Env) -> Binary
Evaluate binary construction. Located at lfe_eval_bits.erl:35-38.
match_binary(Segments, Binary, Env) -> {yes, Bindings} | no
Match binary pattern. Located at lfe_eval_bits.erl:42-45.
Binary Segment Types
Segment Syntax:
[binary [Value Spec1 Spec2 ...]]
Specs:
size N ; Bit size
unit N ; Unit size (default 1)
integer ; Integer value
float ; Float value
binary ; Binary value
bytes ; Alias for binary
bitstring ; Bitstring value
utf8, utf16, utf32 ; UTF encoding
signed, unsigned ; Sign (default unsigned)
big, little, native ; Endianness (default big)
Example:
#B(42) ; Single byte
#B(42 (f 32)) ; 32-bit float
#B((s "hello")) ; Binary string
#B((u16 65)) ; UTF-16 character
#B((x integer (size 16))) ; 16-bit integer
Binary Construction
Process (eval_binary/2 at lines 52-121):
- Evaluate each segment value
- Apply segment type/size specifications
- Construct binary from segments
eval_segment([Value|Specs], Env) ->
Val = lfe_eval:expr(Value, Env),
Type = get_spec_type(Specs),
Size = get_spec_size(Specs, Env),
Unit = get_spec_unit(Specs),
Sign = get_spec_sign(Specs),
Endian = get_spec_endian(Specs),
% Build binary segment
<<(convert_value(Val, Type)):Size/unit:Unit-Sign-Endian>>.
Binary Matching
Process (match_binary/3 at lines 129-248):
- Iterate through segments
- Match each segment against binary
- Accumulate bindings
match_segment([Pat|Specs], Binary, Offset, Env) ->
Type = get_spec_type(Specs),
Size = get_spec_size(Specs, Env),
Unit = get_spec_unit(Specs),
Sign = get_spec_sign(Specs),
Endian = get_spec_endian(Specs),
% Extract value from binary
<<_:Offset, Value:Size/unit:Unit-Sign-Endian, _/binary>> = Binary,
% Match pattern against value
case lfe_eval:match(Pat, Value, Env) of
{yes, Bindings} -> {yes, Bindings, Offset+Size*Unit};
no -> no
end.
UTF Encoding
UTF-8/16/32 Support (eval_utf_segment/2 at lines 156-189):
eval_utf8(Char) ->
unicode:characters_to_binary([Char], unicode, utf8).
eval_utf16(Char, Endian) ->
unicode:characters_to_binary([Char], unicode, {utf16, Endian}).
eval_utf32(Char, Endian) ->
unicode:characters_to_binary([Char], unicode, {utf32, Endian}).
Example:
#B((u8 65)) ; UTF-8 'A'
#B((u16 65 big)) ; UTF-16 big-endian 'A'
#B((u32 65 little)); UTF-32 little-endian 'A'
Spec Defaults
Default Specifications (lfe_eval_bits.erl:267-301):
- Type:
integer - Size: 8 bits (for integer), full value (for binary/bitstring)
- Unit: 1
- Sign:
unsigned - Endian:
big
Dependencies
LFE modules:
lfe_eval- Expression evaluation, pattern matchinglfe_bits- Bitstring specification parsing
Erlang stdlib:
unicode- UTF encoding/decoding
Used By
lfe_eval- Delegated binary operations
Special Considerations
Performance:
Binary operations are relatively fast even in interpreted mode (use Erlang's native binary operations).
Size Expressions:
Sizes can be expressions:
(let ([n 16])
#B((x integer (size n)))) ; Size evaluated at runtime
Alignment:
Bitstrings need not be byte-aligned:
#B((x integer (size 3)) ; 3 bits
(y integer (size 5))) ; 5 bits
; Total: 8 bits (1 byte)
String Shorthand:
#"hello" ; Shorthand for: #B((s "hello"))
lfe_init.erl - Runtime Initialization
Purpose: Initialize LFE runtime environment when starting Erlang with LFE as the default shell.
Location: src/lfe_init.erl
Size: 118 LOC, 4.1KB
Module Classification: Runtime infrastructure, launcher
Public API
start() -> ok
Entry point for -user lfe_init. Located at lfe_init.erl:49-74.
Startup Modes
Interactive Shell:
erl -user lfe_init
Starts LFE REPL as default shell.
Eval Mode:
erl -user lfe_init -e "(+ 1 2)"
Evaluates expression and exits.
Script Mode:
erl -user lfe_init script.lfe arg1 arg2
Runs script with arguments.
Implementation
Main Entry (start/0 at lines 49-74):
start() ->
OTPRelease = erlang:system_info(otp_release),
Repl = case init:get_argument(repl) of
{ok, [[R|_]]} -> list_to_atom(R);
_Other -> ?DEFAULT_REPL % lfe_shell
end,
case collect_args(init:get_plain_arguments()) of
{[],[]} ->
% Start interactive shell
if OTPRelease >= "26" ->
user_drv:start(#{initial_shell => {Repl,start,[]}});
true ->
user_drv:start(['tty_sl -c -e',{Repl,start,[]}])
end;
{Evals,Script} ->
% Run evals and/or script
if OTPRelease >= "26" ->
user_drv:start(#{initial_shell => noshell});
true ->
user:start()
end,
run_evals_script(Repl, Evals, Script)
end.
OTP Version Handling:
OTP 26 changed user_drv API:
- OTP <26:
user_drv:start(['tty_sl -c -e', {Repl,start,[]}]) - OTP ≥26:
user_drv:start(#{initial_shell => {Repl,start,[]}})
Argument Collection (collect_args/1 at lines 76-86):
collect_args(Args) ->
{Evals, Rest} = collect_evals(Args, []),
{Evals, Rest}.
collect_evals(["-e", Eval|Args], Evals) ->
collect_evals(Args, [Eval|Evals]);
collect_evals(["-lfe_eval", Eval|Args], Evals) ->
collect_evals(Args, [Eval|Evals]);
collect_evals(Args, Evals) ->
{lists:reverse(Evals), Args}.
Script Execution (run_evals_script/3 at lines 88-97):
run_evals_script(Repl, Evals, Script) ->
Run = fun () ->
St = Repl:run_strings(Evals), % Run eval strings
case Script of
[F|As] -> Repl:run_script(F, As, St);
[] -> {[],St}
end
end,
spawn_link(fun () -> run_script(Repl, Run) end).
Error Handling (run_script/2 at lines 102-118):
run_script(_Repl, Run) ->
try
Run(),
timer:sleep(1),
init:stop(?OK_STATUS) % Exit 0
catch
?CATCH(Class, Error, Stack)
Skip = fun (M, _F, _A) -> M =/= lfe_eval end,
Format = fun (T, I) -> lfe_io:prettyprint1(T, 15, I, 80) end,
Cs = lfe_error:format_exception(Class, Error, Stack,
Skip, Format, 1),
io:put_chars(Cs),
halt(?ERROR_STATUS) % Exit 127
end.
Exit Codes:
0- Success127- Error
Custom REPL
Specify Alternative REPL:
erl -user lfe_init -repl my_custom_repl
Default: lfe_shell
Dependencies
LFE modules:
lfe_shell(or custom REPL)lfe_error- Error formattinglfe_io- Pretty printing
Erlang stdlib:
user_drv- Terminal driverinit- System initialization
Used By
erl- When started with-user lfe_initlfescript- Indirectly for script execution
Special Considerations
OTP Compatibility:
The OTP 26 check ensures compatibility across Erlang versions.
Process Spawning:
Script execution spawns a separate process to isolate crashes.
Exit Codes:
Standard Unix convention:
0= success- Non-zero = error (127 for exceptions)
Terminal Setup:
-c -e flags enable proper terminal handling (command-line editing, escape sequences).
lfescript.erl - Script Runner
Purpose: Execute LFE scripts (lfescript files) with command-line argument handling.
Location: src/lfescript.erl
Size: 189 LOC, 6.3KB
Module Classification: Runtime infrastructure, script execution
Public API
start() -> ok
start(Args) -> ok
Main entry point for lfescript execution. Located at lfescript.erl:47-52.
Script Format
Lfescript File:
#!/usr/bin/env lfescript
;; -*- mode: lfe -*-
;;! -smp enable -setcookie mycookie
(defun main (args)
(lfe_io:format "Args: ~p~n" (list args)))
Shebang Line: #!/usr/bin/env lfescript
Mode Line: ;; -*- mode: lfe -*- (optional, for editors)
Emulator Flags: ;;! followed by erl flags (optional, line 2 or 3)
Script Execution
Process (start/1 at lines 54-89):
- Parse script file
- Extract emulator flags (
;;!line) - Read and expand forms
- Execute
main/1function with arguments
start(Args) ->
[Script|ScriptArgs] = Args,
{ok, Forms} = lfe_io:read_file(Script),
% Expand macros
{ok, EForms, Env, _} = lfe_macro:expand_forms(Forms, lfe_env:new(), ...),
% Create environment with main/1
Env1 = eval_script_forms(EForms, Env),
% Call main/1
Main = lfe_env:get_fbinding(main, 1, Env1),
lfe_eval:apply(Main, [ScriptArgs], Env1).
Emulator Flags
Flag Parsing (extract_emulator_flags/1 at lines 101-134):
% Shebang line must be first
parse_flags(["#!/usr/bin/env lfescript" | Lines]) ->
parse_flags_rest(Lines);
% Skip mode line
parse_flags_rest([";; -*- mode: lfe -*-" | Lines]) ->
parse_flags_rest(Lines);
% Extract flags
parse_flags_rest([";;!" ++ Flags | _Lines]) ->
string:tokens(Flags, " \t");
parse_flags_rest(_) ->
[]. % No flags
Example:
#!/usr/bin/env lfescript
;;! -smp enable -setcookie secret -name node1
; Script runs with: erl -smp enable -setcookie secret -name node1
Error Handling
Script Errors:
Exceptions during script execution are caught and formatted:
try
run_script(Script, Args)
catch
Class:Reason:Stack ->
lfe_error:format_exception(Class, Reason, Stack),
halt(127)
end.
Exit Codes:
0- Success127- Error
Dependencies
LFE modules:
lfe_io- File readinglfe_macro- Macro expansionlfe_eval- Script executionlfe_env- Environmentlfe_error- Error formatting
Erlang stdlib:
file,string,lists
Used By
lfescriptshell script (wrapper)- Direct execution via shebang
Special Considerations
Compilation:
Scripts are not compiled to BEAM. They're interpreted via lfe_eval.
Performance:
Script startup is slower than compiled modules:
- Parsing
- Macro expansion
- Interpretation overhead
Use Cases:
- Build scripts
- Admin tasks
- One-off utilities
main/1 Required:
Script must define main/1:
(defun main (args)
; Script logic here
)
Arguments:
args is a list of strings:
./script.lfe foo bar baz
; args = ["foo", "bar", "baz"]
I/O Modules
The I/O subsystem provides a clean, modular interface for reading and writing LFE S-expressions in various formats. These 4 modules constitute 7.9% of the codebase and demonstrate excellent separation of concerns.
lfe_io.erl - I/O Interface
Purpose: Unified interface for reading and writing LFE S-expressions with delegation to specialized formatters.
Location: src/lfe_io.erl
Size: 275 LOC, 9.6KB
Module Classification: I/O core, API facade
Public API
File Reading:
parse_file(FileName) -> {ok, [{Sexpr, Line}]} | {error, Error}
parse_file(FileName, StartLine) -> Result
Parse file returning sexprs with line numbers. Located at lfe_io.erl:72-77.
read_file(FileName) -> {ok, [Sexpr]} | {error, Error}
read_file(FileName, StartLine) -> Result
Read file returning sexprs only. Located at lfe_io.erl:98-103.
Interactive Reading:
read() -> {ok, Sexpr} | {error, Error} | eof
read(Prompt) -> Result
read(IoDevice, Prompt) -> Result
Read single sexpr with optional prompt. Located at lfe_io.erl:146-154.
read_line() -> {ok, Sexpr} | {error, Error} | eof
read_line(Prompt) -> Result
read_line(IoDevice, Prompt) -> Result
Line-oriented reading (for REPL). Located at lfe_io.erl:165-182.
String Reading:
read_string(String) -> {ok, [Sexpr]} | {error, Error}
Parse string into sexprs. Located at lfe_io.erl:221-226.
Writing:
print(Sexpr) -> ok
print(IoDevice, Sexpr) -> ok
print1(Sexpr) -> [char()]
print1(Sexpr, Depth) -> [char()]
Compact printing. Delegates to lfe_io_write. Lines 233-237.
prettyprint(Sexpr) -> ok
prettyprint(IoDevice, Sexpr) -> ok
prettyprint1(Sexpr) -> [char()]
prettyprint1(Sexpr, Depth, Indentation, LineLength) -> [char()]
Pretty printing. Delegates to lfe_io_pretty. Lines 244-253.
Formatted Output:
format(Format, Args) -> ok
format(IoDevice, Format, Args) -> ok
format1(Format, Args) -> [char()]
Formatted output (LFE's printf). Delegates to lfe_io_format. Lines 261-273.
Reading Architecture
Token-Based Reading (with_token_file/3 at lines 123-138):
with_token_file(File, DoFunc, Line) ->
% 1. Open file
{ok, Fd} = file:open(File, [read]),
% 2. Scan tokens
{ok, Tokens, LastLine} = io:request(Fd, {get_until, lfe_scan, tokens, [Line]}),
% 3. Apply processing function
Result = DoFunc(Tokens, LastLine),
% 4. Close file
file:close(Fd),
Result.
Incremental Scanning (scan_sexpr/2 at lines 192-216):
Continuation-based scanning for interactive use:
scan_sexpr({ScanCont, ParseCont}, Chars, Line) ->
case lfe_scan:token(ScanCont, Chars, Line) of
{done, {ok, Token, Line1}, Rest} ->
% Got token, try parsing
case lfe_parse:sexpr(ParseCont, [Token]) of
{ok, _, Sexpr, _} -> {done, {ok, Sexpr}, Rest};
{more, ParseCont1} -> scan_sexpr_1([], ParseCont1, Rest, Line1)
end;
{more, ScanCont1} ->
{more, {ScanCont1, ParseCont}}
end.
Dependencies
LFE modules:
lfe_scan- Lexical scanninglfe_parse- Parsinglfe_io_write- Compact writinglfe_io_pretty- Pretty printinglfe_io_format- Formatted output
Erlang stdlib:
file,io,unicode
Used By
lfe_comp- File readinglfe_shell- REPL I/Olfe_eval- Reading expressions- User code - Public API
Special Considerations
Line Numbering: Parse functions preserve line numbers for error reporting.
Unicode: Full UTF-8 support in reading and writing.
History: The get_line/2 function integrates with Erlang's line history for the REPL.
Facade Pattern: This module doesn't implement formatting itself; it delegates to specialized modules.
lfe_io_write.erl - Compact Writer
Purpose: Write LFE terms in compact (non-pretty) format with depth limiting.
Location: src/lfe_io_write.erl
Size: 156 LOC, 5.3KB
Module Classification: I/O support, compact formatting
Public API
term(Term) -> [char()]
term(Term, Depth) -> [char()]
Convert term to compact string representation. Located at lfe_io_write.erl:36-38.
Output Format
Data Types:
; Atoms
foo → "foo"
'foo-bar' → "foo-bar"
'|complex|' → "|complex|"
; Numbers
42 → "42"
3.14 → "3.14"
; Strings
"hello" → "\"hello\""
; Lists
(1 2 3) → "(1 2 3)"
'() → "()"
; Improper lists
(a . b) → "(a . b)"
; Tuples
#(a b c) → "#(a b c)"
; Binaries
#"bytes" → "#\"bytes\""
; Maps
#M(a 1 b 2) → "#M(a 1 b 2)"
Depth Limiting:
term([a, [b, [c, [d, [e]]]]], 3)
→ "(a (b (c ...)))"
The ... indicates truncation at depth limit.
Implementation
Main Dispatch (term/2 at lines 45-92):
term(Atom, _D) when is_atom(Atom) ->
write_atom(Atom);
term(Number, _D) when is_number(Number) ->
write_number(Number);
term(List, D) when is_list(List) ->
write_list(List, D);
term(Tuple, D) when is_tuple(Tuple) ->
write_tuple(Tuple, D);
term(Binary, D) when is_binary(Binary) ->
write_binary(Binary, D);
term(Map, D) when is_map(Map) ->
write_map(Map, D).
List Writing (write_list/2 at lines 95-123):
Handles:
- Empty lists:
() - Proper lists:
(a b c) - Improper lists:
(a . b) - Depth limiting
Atom Quoting (write_atom/1 at lines 126-141):
Quotes atoms when necessary:
- Contains special characters
- Starts with uppercase
- Is a reserved word
Dependencies
lists,io_lib(Erlang stdlib)
Used By
lfe_io- Viaprint1/1,2lfe_error- Error formatting- Internal modules - Debugging output
Special Considerations
No Line Breaks: Output is single-line (compact).
Performance: Optimized for speed over readability.
Erlang Compatibility: Output can be read by Erlang's erl_scan (mostly).
lfe_io_pretty.erl - Pretty Printer
Purpose: Write LFE terms in formatted, indented, multi-line layout for readability.
Location: src/lfe_io_pretty.erl
Size: 405 LOC, 15KB
Module Classification: I/O support, pretty formatting
Public API
term(Term) -> [char()]
term(Term, Depth) -> [char()]
term(Term, Depth, Indentation, LineLength) -> [char()]
Pretty print term with specified formatting parameters. Located at lfe_io_pretty.erl:47-49.
Formatting Strategy
Layout Modes:
-
Horizontal - Single line:
(a b c) -
Vertical - Multi-line with indentation:
(defun foo (x) (let ((y (* x 2))) (+ y 1)))
Line Length Tracking:
The pretty printer tries to fit output within LineLength (default 80) by:
- Attempting horizontal layout
- Falling back to vertical if too long
- Intelligent indentation based on form type
Special Form Formatting (format_form/4 at lines 178-267):
Different forms get custom layouts:
; Function definitions - indent body
(defun name (args)
body)
; Let bindings - align bindings
(let ((x 1)
(y 2))
body)
; Conditionals - indent branches
(if test
then-branch
else-branch)
; Case - indent clauses
(case expr
(pattern1 result1)
(pattern2 result2))
Indentation Rules
By Form Type:
defun,defmacro- Indent 2 after function headlet,let*,letrec- Align binding pairsif,cond- Indent brancheslambda- Indent body- Default lists - Indent 1 from opening paren
Column Tracking (write_tail/4 at lines 298-356):
Maintains current column position to:
- Decide horizontal vs vertical layout
- Calculate indentation
- Enforce line length limits
Dependencies
lists,io_lib,string(Erlang stdlib)
Used By
lfe_io- Viaprettyprint1/*lfe_shell- Value displaylfe_error- Error formatting
Special Considerations
Performance: Slower than compact writing due to layout calculations.
Configurable: LineLength and initial indentation customizable.
Readable Output: Prioritizes human readability over machine parsing.
lfe_io_format.erl - Formatted Output
Purpose: Printf-style formatted output for LFE with support for LFE-specific format directives.
Location: src/lfe_io_format.erl
Size: 494 LOC, 17KB
Module Classification: I/O support, formatted output
Public API
fwrite1(Format, Args) -> [char()]
Format string with arguments. Located at lfe_io_format.erl:52-56.
Format Directives
From Erlang's io:format:
~w - Write term
~p - Pretty print term
~s - String
~c - Character
~d - Decimal integer
~f - Float
~e - Exponential float
~g - General float
~b - Integer in arbitrary base
~x - Hexadecimal
~n - Newline
~~ - Literal tilde
LFE Extensions:
~a - Write atom (without quotes)
~l - Write list (without parens)
Modifiers:
~Nw - Width N
~N.Mf - Width N, precision M
~-Nw - Left-align in width N
~0Nd - Pad with zeros
Examples
(lfe_io:format "Hello ~s!~n" '("world"))
→ "Hello world!\n"
(lfe_io:format "Number: ~d, Float: ~.2f~n" '(42 3.14159))
→ "Number: 42, Float: 3.14\n"
(lfe_io:format "~w ~p~n" '((a b c) (x y z)))
→ "(a b c) (x y z)\n"
(lfe_io:format "Atom: ~a, List: ~l~n" '(foo (1 2 3)))
→ "Atom: foo, List: 1 2 3\n"
Implementation
Parser (parse_format/1 at lines 89-156):
Parses format string into directive list:
parse_format("~d ~s") →
[{int, [], []}, {string, " "}, {string, [], []}]
Formatter (format_directive/2 at lines 178-312):
Applies each directive:
format_directive({int, Width, Prec}, [Arg|Args]) ->
S = integer_to_list(Arg),
Padded = pad_string(S, Width, Prec),
{Padded, Args}.
LFE-Specific (write_atom/1 at lines 387-401, write_list/1 at lines 403-419):
Special handling for LFE atoms and lists.
Dependencies
lfe_io_write- Term writinglists,io_lib,string(Erlang stdlib)
Used By
lfe_io- Viaformat1/2- LFE user code - Formatted output
Special Considerations
Error Handling: Throws badarg if format/args mismatch.
Performance: Parsing format string has overhead; cache results for repeated formats.
Compatibility: Most Erlang ~ directives work identically.
Shell Modules
The shell system provides an interactive REPL environment with full support for LFE evaluation, history, and command-line editing. These 2 modules constitute 6.9% of the codebase.
lfe_shell.erl - REPL Implementation
Purpose: Interactive Read-Eval-Print Loop with environment management, command execution, and module slurping.
Location: src/lfe_shell.erl
Size: 1,188 LOC, 41KB
Module Classification: Shell core, interactive environment
This is the 5th largest module in the codebase and provides the primary user interface to LFE.
Public API
Server Mode:
start() -> Pid
start(State) -> Pid
server() -> no_return()
server(State) -> no_return()
Start shell server process. Located at lfe_shell.erl:110-131.
Script Execution:
run_script(File, Args) -> no_return()
run_script(File, Args, State) -> no_return()
run_strings(Strings) -> {Value, State}
run_strings(Strings, State) -> {Value, State}
Execute scripts or string expressions. Lines 66-105.
State Management:
new_state(ScriptName, Args) -> State
new_state(ScriptName, Args, Env) -> State
upd_state(ScriptName, Args, State) -> State
Create and update shell state. Lines 279-300.
Shell State
State Record (line 63-64):
-record(state, {
curr, % Current environment
save, % Saved environment (before slurp)
base, % Base environment (predefined)
slurp=false % Are we in slurped state?
}).
Three Environments:
- Base: Predefined shell functions, macros, variables (never changes)
- Current: Active environment with user definitions
- Save: Snapshot before
(slurp ...)for(unslurp)
Shell Commands
Built-in Functions (added at lines 322-359):
(c file) ; Compile and load
(cd dir) ; Change directory
(ec file) ; Compile Erlang file
(ep expr) ; Print in Erlang format
(epp expr) ; Pretty print in Erlang format
(help) ; Show help (alias: h)
(h mod) ; Show module documentation
(h mod func) ; Show function documentation
(h mod func arity) ; Show specific arity documentation
(i) ; System information
(l modules) ; Load modules
(ls dir) ; List directory
(clear) ; Clear screen
(m) ; List loaded modules
(m mod) ; Module information
(memory) ; Memory stats
(p expr) ; Print expression
(pp expr) ; Pretty print expression
(pwd) ; Print working directory
(q) ; Quit (alias: exit)
(flush) ; Flush messages
(regs) ; Registered processes
(nregs) ; Named registered processes
(uptime) ; Node uptime
Built-in Forms (added at lines 365-389):
(set pattern expr) ; Pattern match and bind
(set pattern (when guard) expr) ; With guard
(slurp file) ; Load file into shell
(unslurp) ; Revert slurp
(run file) ; Execute shell script
(reset-environment) ; Reset to base environment
Built-in Macros:
(c ...)- Compile with arguments(doc mod),(doc mod func),(doc mod func arity)- Documentation(l ...),(ls ...),(m ...)- Variadic list handling
Shell Variables
Expression History (lines 302-320):
+ ; Last expression entered
++ ; Second-to-last expression
+++ ; Third-to-last expression
* ; Last value
** ; Second-to-last value
*** ; Third-to-last value
- ; Current expression (being evaluated)
$ENV ; Current environment
Updated after each evaluation via update_shell_vars/3.
Evaluation Architecture
Two-Process Design (server_loop/2 and eval_loop/2 at lines 133-440):
Shell Process Evaluator Process
------------- -----------------
Read input
↓
{eval_expr, Form} -------→ Receive form
Evaluate
↓
{eval_value, Val, St} ←--- Send result
↓
Display value
Update state
Why Two Processes?
- Isolation: Evaluator crashes don't kill shell
- Interruption: Can kill evaluator (Ctrl-C) without losing shell state
- Timeout: Can timeout long-running evaluations
Error Handling (shell_eval/3 at lines 150-167):
shell_eval(Form, Eval0, St0) ->
Eval0 ! {eval_expr, self(), Form},
receive
{eval_value, Eval0, _Value, St1} ->
{Eval0, St1};
{eval_error, Eval0, {Class, Reason, Stack}} ->
% Evaluator crashed
receive {'EXIT', Eval0, normal} -> ok end,
report_exception(Class, Reason, Stack),
Eval1 = start_eval(St0),
{Eval1, St0};
{'EXIT', Eval0, Reason} ->
% Evaluator was killed
report_exception(exit, Reason, []),
Eval1 = start_eval(St0),
{Eval1, St0}
end.
Slurping
Purpose: Load an LFE file's functions and macros into the shell without compiling.
Slurp Process (slurp/2 at lines 559-601):
- Save current environment
- Read and parse file
- Macro expand forms
- Lint forms
- Extract functions, macros, imports, records
- Add to environment
- Mark state as slurped
Unslurp (unslurp/1 at lines 550-557):
Revert to saved environment before slurp.
Module Collection (collect_module/2 at lines 628-642):
Extracts from parsed forms:
- Module name
- Exported functions
- Function definitions
- Imports (from, rename)
- Record definitions
Example:
lfe> (slurp "mylib.lfe")
{ok, mylib}
lfe> (my-function 42) ; Now available
84
lfe> (unslurp)
ok
lfe> (my-function 42)
** exception error: undefined function my-function/1
Banner and Prompt
Banner (banner/0,1,2 at lines 243-259):
Colorful ASCII art LFE banner displayed on startup:
..-~.~_~---..
( \\ ) | A Lisp-2+ on the Erlang VM
|`-.._/_\\_.-': | Type (help) for usage info.
| g |_ \ |
| n | | | Docs: http://docs.lfe.io/
| a / / | Source: http://github.com/lfe/lfe
\ l |_/ |
\ r / | LFE v2.2.0 (abort with ^G)
`-E___.-'
Prompt (prompt/0 at lines 169-200):
Customizable via -prompt flag:
erl -user lfe_init -prompt "my-prompt> "
erl -user lfe_init -prompt classic # Old-style "> "
Supports ~node placeholder for distributed nodes.
Documentation Display
Paged Output (paged_output/2 at lines 1154-1176):
Documentation output is paged (30 lines per page) with "More (y/n)?" prompts.
Format Selection (get_module_doc/2 etc. at lines 1108-1148):
Handles both:
- LFE format docs (via
lfe_shell_docs) - Erlang native format docs (via
shell_docsin OTP 23+)
Dependencies
LFE modules:
lfe_eval- Expression evaluationlfe_env- Environment managementlfe_macro- Macro expansionlfe_comp- File compilationlfe_lint- Lintinglfe_io- I/O operationslfe_edlin_expand- Command-line expansionlfe_docs,lfe_shell_docs- Documentationlfe_error- Error formatting
Erlang stdlib:
cmodule - Many shell commands delegate toccode,file,io
Used By
lfe_init- Default shellerl -user lfe_init- Interactive sessionslfescript- Script execution
Key Algorithms
REPL Loop (server_loop/2 at lines 133-144):
server_loop(Eval0, St0) ->
Prompt = prompt(),
{Ret, Eval1} = read_expression(Prompt, Eval0, St0),
case Ret of
{ok, Form} ->
{Eval2, St1} = shell_eval(Form, Eval1, St0),
server_loop(Eval2, St1);
{error, E} ->
list_errors([E]),
server_loop(Eval1, St0)
end.
Form Evaluation (eval_form_1/2 at lines 465-501):
Special handling for:
(set ...)- Pattern matching with bindings(slurp ...)- File slurping(unslurp)- Revert slurp(run ...)- Run script file(reset-environment)- Revert to base(define-record ...)- Add record(define-function ...)- Add function(define-macro ...)- Add macro- Regular expressions - Evaluate
Special Considerations
Slurp vs Compile:
(slurp "file.lfe")- Load into shell (interpreted)(c "file.lfe")- Compile to BEAM (faster)
Slurping is useful for interactive development; compilation for production.
History Storage: Not persistent across sessions (unlike some Erlang shells).
Command Completion: Integrated with lfe_edlin_expand for tab completion.
Performance: Slurped code runs slower (interpreted) than compiled code.
lfe_edlin_expand.erl - Command Line Expansion
Purpose: Provide tab completion and expansion for the LFE shell.
Location: src/lfe_edlin_expand.erl
Size: 232 LOC, 7.8KB
Module Classification: Shell support, command completion
Public API
expand(Before) -> {yes, Expansion, Matches} | no
Expand command-line input. Located at lfe_edlin_expand.erl:42-45.
Parameters:
Before- String before cursor- Returns:
{yes, Expansion, Matches}- Possible expansionsno- No completions available
Completion Types
Module Name Completion:
lfe> (lists:m<TAB>
→ lists:map lists:max lists:member ...
Function Name Completion:
lfe> (list<TAB>
→ lists list-comprehension ...
Variable Name Completion:
lfe> my-var<TAB>
→ my-variable-name
Local Bindings: Completes from current environment.
Implementation
Tokenization (expand/1 at lines 47-89):
- Parse
Beforestring into tokens - Identify last token (completion target)
- Determine context (module, function, variable)
- Find matches
- Return common prefix + alternatives
Module/Function Matching (match_module_functions/2 at lines 134-178):
match_module_functions(Prefix, Env) ->
% Get all loaded modules
Modules = code:all_loaded(),
% Get module exports
Exports = [Mod:module_info(exports) || {Mod, _} <- Modules],
% Filter by prefix
filter_matches(Prefix, flatten(Exports)).
Local Binding Matching (match_local_bindings/2 at lines 182-214):
Searches current environment for matching variable names.
Dependencies
LFE modules:
lfe_env- Access to bindingslfe_scan- Tokenization
Erlang stdlib:
code,lists,string
Used By
lfe_shell- Viaio:setopts([{expand_fun, Fun}])
Special Considerations
Performance: Completion must be fast (< 100ms) for responsive UX.
Context Sensitivity: Understands LFE syntax to provide relevant completions.
Module Loading: Only completes for loaded modules (doesn't search filesystem).
Library Modules
The library modules provide utility functions, compatibility layers, and specialized DSLs. These 9 modules constitute 16.8% of the codebase and extend LFE's capabilities.
cl.lfe - Common Lisp Compatibility
Purpose: Provide Common Lisp-style functions and macros for LFE.
Location: src/cl.lfe
Size: 767 LOC, 21KB
Module Classification: Compatibility layer, CL functions
This is the 9th largest module in the codebase.
Exported Functions (60+)
List Functions:
(adjoin item list) ; Add if not member
(acons key value alist) ; Add to association list
(pairlis keys values) ; Create association list
(assoc key alist) ; Association list lookup
(rassoc value alist) ; Reverse assoc lookup
(subst new old tree) ; Substitute in tree
(sublis alist tree) ; Multiple substitutions
(union list1 list2) ; Set union
(intersection list1 list2) ; Set intersection
(set-difference list1 list2) ; Set difference
(member item list) ; Membership test (returns tail)
(remove item list) ; Remove all occurrences
(remove-if pred list) ; Remove matching
(delete item list) ; Destructive remove
Tree Functions:
(tree-equal tree1 tree2) ; Deep equality
(subst new old tree) ; Substitute in tree
(copy-tree tree) ; Deep copy
Sequence Functions:
(some pred list) ; Any element satisfies
(every pred list) ; All elements satisfy
(notany pred list) ; No element satisfies
(notevery pred list) ; Not all satisfy
(reduce func list) ; Fold
(fill list item) ; Fill with item
(replace list1 list2) ; Replace elements
Mapping Functions:
(mapcar func list) ; Map (same as lists:map)
(maplist func list) ; Map over tails
(mapc func list) ; Map for side effects
(mapcan func list) ; Map and concatenate
Property Lists:
(get-properties plist indicators) ; Get first matching property
(getf plist key [default]) ; Get property value
(putf plist key value) ; Set property value
(remf plist key) ; Remove property
Implementation Style
Written in pure LFE (not Erlang).
Example Function (from cl.lfe):
(defun adjoin (item list)
"Add ITEM to LIST if not already member."
(if (member item list)
list
(cons item list)))
(defun pairlis (keys values)
"Make association list from KEYS and VALUES."
(zipwith #'tuple/2 keys values))
(defun union (list1 list2)
"Set union of LIST1 and LIST2."
(++ list1 (foldl (lambda (elem acc)
(if (member elem list1)
acc
(cons elem acc)))
'()
list2)))
Dependencies
Erlang stdlib:
lists,ordsets
LFE core functions: Uses LFE's built-in functions heavily.
Used By
- User code wanting Common Lisp-style functions
- Porting CL code to LFE
Special Considerations
Naming: Function names follow Common Lisp conventions (kebab-case, CL names).
Semantics: Attempts to match CL semantics where reasonable, but adapted to Erlang/LFE.
Performance: Some functions less efficient than Erlang equivalents (e.g., member walks entire list).
clj.lfe - Clojure Compatibility
Purpose: Provide Clojure-style macros and functions for LFE, including threading macros.
Location: src/clj.lfe
Size: 842 LOC, 26KB
Module Classification: Compatibility layer, Clojure macros
This is the 7th largest module in the codebase.
Exported Macros (40+)
Threading Macros:
(-> x forms...) ; Thread-first
(->> x forms...) ; Thread-last
(as-> x name forms...) ; Thread with explicit name
Examples:
(-> 5
(+ 3)
(* 2)
(- 1))
→ (- (* (+ 5 3) 2) 1) → 15
(->> '(1 2 3)
(map (lambda (x) (* x 2)))
(filter (lambda (x) (> x 2))))
→ (2 4 6) filtered to (4 6)
(as-> 10 x
(+ x 5)
(* x 2)
(list x x))
→ (list (* (+ 10 5) 2) (* (+ 10 5) 2)) → (30 30)
Conditional Macros:
(if-let [binding test] then else)
(when-let [binding test] body...)
(if-not test then else)
(when-not test body...)
(cond clauses...)
Looping Macros:
(doseq [var sequence] body...)
(dotimes [var n] body...)
(while test body...)
Destructuring:
(let [[a b & rest] '(1 2 3 4 5)]
(list a b rest))
→ (1 2 (3 4 5))
(let [{:keys [name age]} #M(name "Alice" age 30)]
(list name age))
→ ("Alice" 30)
Collection Functions:
(get map key [default]) ; Map lookup
(assoc map k v ...) ; Add/update keys
(dissoc map k ...) ; Remove keys
(conj coll item ...) ; Add to collection
(into to from) ; Pour from into to
(merge map ...) ; Merge maps
Sequence Functions:
(comp f g ...) ; Function composition
(partial f args ...) ; Partial application
(complement f) ; Negate predicate
(constantly x) ; Constant function
Threading Macro Implementation
From clj.lfe:
(defmacro -> (x . forms)
"Thread-first: insert X as first argument in each form."
(fletrec ((thread
([y ()] y)
([y ((cons f args) . rest)]
(thread (list* f y args) rest))
([y (f . rest)] (when (is_atom f))
(thread (list f y) rest))))
(thread x forms)))
(defmacro ->> (x . forms)
"Thread-last: insert X as last argument in each form."
(fletrec ((thread
([y ()] y)
([y ((cons f args) . rest)]
(thread (++ (list f) args (list y)) rest))
([y (f . rest)] (when (is_atom f))
(thread (list f y) rest))))
(thread x forms)))
Dependencies
LFE core: Heavy use of LFE macros and special forms.
Used By
- User code preferring Clojure style
- Porting Clojure code to LFE
Special Considerations
Macro Heavy: Almost all exports are macros (compile-time transformations).
Map-Centric: Many functions designed for LFE maps (Erlang maps).
Immutability: Follows Clojure's immutable data philosophy (natural fit for Erlang/LFE).
Performance: Threading macros have zero runtime overhead (pure macro expansion).
scm.erl - Scheme Compatibility
Purpose: Provide Scheme-style syntax-rules macro system for pattern-based macros.
Location: src/scm.erl
Size: 276 LOC, 9.3KB
Module Classification: Compatibility layer, Scheme syntax
Public API
define_syntax_rules(Name, Rules, Env) -> {[{Name, MacroFun}], Env}
Define a pattern-based macro. Located at scm.erl:45-49.
Syntax Rules
Scheme's syntax-rules:
; Scheme example
(define-syntax when
(syntax-rules ()
((_ test body ...)
(if test (begin body ...) #f))))
LFE Equivalent:
(define-syntax when
(syntax-rules ()
((_ test body ...)
(if test (progn body ...) 'false))))
Pattern Matching: Matches macro call against patterns, expands according to template.
Ellipsis (...): Represents zero or more repetitions.
Implementation
Pattern Compiler (compile_rule/2 at lines 98-156):
Converts syntax rules to LFE pattern-matching macro:
- Parse pattern
- Extract pattern variables
- Generate match code
- Generate template expansion code
- Return lambda form
Ellipsis Handling (expand_ellipsis/2 at lines 178-234):
Complex pattern matching for ...:
; Pattern: (foo a ...)
; Matches: (foo 1), (foo 1 2), (foo 1 2 3), ...
; Binds: a = [1], a = [1, 2], a = [1, 2, 3], ...
Template Expansion (expand_template/2 at lines 256-298):
Substitutes pattern variables in template:
; Template: (list a ...)
; With a = [1, 2, 3]
; Expands to: (list 1 2 3)
Dependencies
LFE modules:
lfe_macro- Macro expansion frameworklfe_env- Environment management
Erlang stdlib:
lists
Used By
- User code wanting Scheme-style macros
- Porting Scheme code to LFE
Special Considerations
Limited Adoption: Less commonly used than Clojure or CL styles in LFE.
Power: syntax-rules is very powerful for pattern-based macros.
Hygiene: LFE macros are unhygienic; syntax-rules doesn't change this.
lfe_bits.erl - Bitstring Specification
Purpose: Parse and validate bitstring segment specifications.
Location: src/lfe_bits.erl
Size: 137 LOC, 5.0KB
Module Classification: Library support, binary handling
Public API
get_bitspecs(Specs) -> {Type, Size, Unit, Sign, Endian}
Parse bitstring specifications. Located at lfe_bits.erl:37-41.
Specification Format
Segment Specs (from binary syntax):
#B(value) ; Default: 8-bit unsigned int
#B((value integer)) ; Explicit type
#B((value (size 16))) ; 16-bit
#B((value integer (size 16) big unsigned)) ; Full spec
Types:
integer- Integer valuefloat- Floating-pointbinary- Binary/bitstringbytes- Alias for binarybitstring- Bitstringutf8,utf16,utf32- UTF encoding
Modifiers:
(size N)- Bit size(unit N)- Unit size (for binary/bitstring)signed,unsigned- Signbig,little,native- Endianness
Parsing
Spec Parser (parse_spec/1 at lines 78-134):
parse_spec([integer|Rest]) ->
parse_spec(Rest, {integer, default, 1, unsigned, big});
parse_spec([float|Rest]) ->
parse_spec(Rest, {float, 64, 1, unsigned, big});
parse_spec([(size, N)|Rest], {T, _, U, S, E}) ->
parse_spec(Rest, {T, N, U, S, E});
parse_spec([signed|Rest], {T, Sz, U, _, E}) ->
parse_spec(Rest, {T, Sz, U, signed, E});
...
Defaults:
| Type | Size | Unit | Sign | Endian |
|---|---|---|---|---|
| integer | 8 | 1 | unsigned | big |
| float | 64 | 1 | unsigned | big |
| binary | all | 8 | - | - |
| bitstring | all | 1 | - | - |
| utf8/16/32 | - | - | - | - |
Dependencies
lists(Erlang stdlib)
Used By
lfe_eval_bits- Runtime binary operationslfe_translate- Compile-time binary translation
Special Considerations
Validation: Ensures specs are valid (e.g., float must be 32 or 64 bits).
Error Reporting: Returns descriptive errors for invalid specs.
lfe_types.erl - Type System Support
Purpose: Parse and convert type specifications for use with Dialyzer.
Location: src/lfe_types.erl
Size: 444 LOC, 16KB
Module Classification: Library support, type system
Public API
to_type_def(TypeDef, Line) -> ErlangTypeAST
to_func_spec(Spec, Line) -> ErlangSpecAST
Convert LFE type definitions to Erlang type AST. Located at lfe_types.erl:52-87.
Type Syntax
LFE Type Definitions:
(deftype int () integer)
(deftype list (a) (list a))
(deftype pair (a b) (tuple a b))
(deftype tree (a)
(union (tuple 'leaf a)
(tuple 'node (tree a) (tree a))))
Function Specs:
(defun add
"Add two numbers"
{(spec [[integer integer] integer])}
([x y] (+ x y)))
; Multiple arities
(defun process
{(spec [[atom] ok]
[[atom list] {ok value}])}
([cmd] ...)
([cmd args] ...))
Type Constructors
Built-in Types:
any, none, atom, integer, float, number, boolean,
binary, bitstring, list, tuple, map, pid, port, reference,
function, ...
Constructors:
(list TYPE) ; List of TYPE
(tuple TYPE ...) ; Tuple with types
(map KEY-TYPE VAL-TYPE) ; Map
(union TYPE ...) ; Sum type
(fun ARGS RESULT) ; Function type
(record NAME FIELDS) ; Record type
Translation to Erlang
Type Definition Translation (to_type_def/2 at lines 134-256):
; LFE
(deftype pair (a b) (tuple a b))
; Erlang AST
-type pair(A, B) :: {A, B}.
Spec Translation (to_func_spec/2 at lines 298-387):
; LFE
{(spec [[integer integer] integer])}
; Erlang AST
-spec func(integer(), integer()) -> integer().
Dialyzer Integration
Type specs are:
- Converted to Erlang AST
- Stored in module attributes
- Included in debug_info
- Read by Dialyzer for type checking
Zero Runtime Cost: Types are compile-time only.
Dependencies
LFE modules:
lfe_internal- Type validation
Erlang compiler:
erl_parse- Erlang AST
Used By
lfe_lint- Type checking during lintinglfe_codegen- Generating type attributes- Dialyzer - Type analysis
Special Considerations
Optional: Types are optional in LFE (like Erlang).
Gradual Typing: Can add types incrementally to codebase.
Dialyzer Only: Types are for static analysis, not runtime checking.
lfe_ms.erl - Match Specification DSL
Purpose: Provide a DSL for creating Erlang match specifications used in ETS, Mnesia, and tracing.
Location: src/lfe_ms.erl
Size: 473 LOC, 16KB
Module Classification: Library, DSL
Public API
ets_transform(Spec) -> MatchSpec
trace_transform(Spec) -> MatchSpec
Transform LFE match spec DSL to Erlang match spec. Located at lfe_ms.erl:48-65.
Match Specification DSL
Erlang Match Specs (complex):
% Match ETS tuples where first element > 10, return second element
[{{'$1', '$2', '_'}, [{'>', '$1', 10}], ['$2']}]
LFE DSL (readable):
(ets-ms
([(tuple a b _)]
(when (> a 10))
b))
Components:
- Head Pattern: Match against input
- Guards: Filter matches
- Body: Transform result
ETS Match Specs
Examples:
; Select all
(ets-ms ([x] x))
; Select with guard
(ets-ms
([x] (when (> x 10)) x))
; Transform result
(ets-ms
([(tuple k v)]
(tuple v k))) ; Swap key/value
; Multiple clauses
(ets-ms
([(tuple 'foo v)] v)
([(tuple 'bar v)] (* v 2))
([_] 'undefined))
Trace Match Specs
Tracing Function Calls:
(trace-ms
([[arg1 arg2]] ; Match arguments
(when (> arg1 100)) ; Guard
(return_trace))) ; Action
Actions:
(return_trace)- Trace return value(message MSG)- Send message(exception_trace)- Trace exceptions
Implementation
Transformation (transform/2 at lines 123-278):
Converts DSL to Erlang match spec format:
- Parse head pattern →
MatchHead - Parse guards →
[ConditionList] - Parse body →
[Body] - Return:
[{MatchHead, [ConditionList], [Body]}]
Variable Mapping (assign_vars/1 at lines 302-345):
Maps LFE variables to match spec variables ($1, $2, ...).
Guard Conversion (convert_guard/1 at lines 378-421):
Converts LFE guards to match spec guard expressions.
Dependencies
LFE modules:
lfe_macro- Used by callers for macro expansion
Erlang stdlib:
lists,orddict
Used By
- User code via
ets-msandtrace-msmacros - Advanced ETS/Mnesia queries
- Tracing and debugging
Special Considerations
Compile-Time: Match specs generated at compile time (zero runtime overhead).
Type Safety: No runtime type checking; invalid specs cause ETS/trace errors.
Power: Match specs are very powerful but complex; DSL makes them accessible.
lfe_qlc.erl - Query List Comprehension
Purpose: Transform LFE QLC syntax to Erlang's qlc module for database queries.
Location: src/lfe_qlc.erl
Size: 51 LOC, 1.9KB
Module Classification: Library, QLC integration
Public API
transform(QlcForm) -> QlcExpression
Transform LFE QLC to Erlang qlc calls. Located at lfe_qlc.erl:39-42.
QLC Syntax
Purpose: Query databases (Mnesia, ETS, DETS) with comprehension-like syntax.
Example:
(qlc (lc ((<- p (: mnesia table person))
(> (person-age p) 18))
(person-name p)))
Translates to:
qlc:q([person_name(P) || P <- mnesia:table(person),
person_age(P) > 18]).
Transformation
Simple Delegation (transform/1 at lines 45-51):
Actually delegates to standard LFE list comprehension transformation, then wraps in qlc:q/1.
transform([qlc|ListComp]) ->
['call', 'qlc', 'q', transform_lc(ListComp)].
Dependencies
Erlang stdlib:
qlcmodule (for runtime execution)
Used By
- User code querying Mnesia/ETS/DETS
- Database applications
Special Considerations
Thin Wrapper: Minimal module, mostly delegates to standard comprehension.
Runtime Dependency: Requires qlc application to be loaded.
Optimization: QLC can optimize queries across multiple tables (joins, filtering).
lfe_gen.erl - Dynamic Code Generation
Purpose: Runtime code generation and dynamic function creation.
Location: src/lfe_gen.erl
Size: 121 LOC, 3.9KB
Module Classification: Library, metaprogramming
Public API
new_module(ModuleName) -> ModuleState
add_exports(Exports, ModuleState) -> ModuleState
add_function(Name, Arity, Lambda, ModuleState) -> ModuleState
finish_module(ModuleState) -> {ok, Module, Binary, Warnings}
Programmatically build modules. Located at lfe_gen.erl:42-78.
Module Building
Process:
- Create new module
- Add exports
- Add function definitions
- Finish and compile
Example:
% Build a simple module
M1 = lfe_gen:new_module(mymod),
M2 = lfe_gen:add_exports([{hello, 1}], M1),
M3 = lfe_gen:add_function(hello, 1,
[lambda, [name],
[list, "Hello", name]], M2),
{ok, mymod, Bin, []} = lfe_gen:finish_module(M3),
% Load it
code:load_binary(mymod, "mymod.beam", Bin),
% Use it
mymod:hello("World"). % → "Hello World"
Implementation
Module State (internal record):
-record(module, {
name, % Module name
exports=[], % Export list
functions=[] % Function definitions
}).
Finish Module (finish_module/1 at lines 98-121):
- Generate
define-moduleform - Generate
define-functionforms - Call
lfe_comp:forms/2to compile - Return binary
Dependencies
LFE modules:
lfe_comp- Compilationlfe_env- Environment
Used By
- Metaprogramming code
- Code generators
- DSL implementations
Special Considerations
Runtime Generation: Modules can be created at runtime.
Performance: Compilation is slow; use sparingly.
Dynamic Loading: Generated modules can be loaded/unloaded dynamically.
lfe_struct.erl - Struct Runtime Support
Purpose: Runtime support functions for struct operations (map-based records).
Location: src/lfe_struct.erl
Size: 33 LOC, 988B
Module Classification: Library, runtime support
This is the smallest module in the codebase.
Public API
new(Module) -> Struct
new(Module, Fields) -> Struct
is(Struct, Module) -> boolean()
fetch(Struct, Module, Field) -> Value
Struct operations. Located at lfe_struct.erl:25-33.
Struct Format
Structs are maps with __struct__ key:
#{__struct__ => ModuleName,
field1 => Value1,
field2 => Value2}
Operations
Create:
Struct = lfe_struct:new(person, [{name, "Alice"}, {age, 30}])
% → #{__struct__ => person, name => "Alice", age => 30}
Type Check:
lfe_struct:is(Struct, person) % → true
lfe_struct:is(Struct, other) % → false
Field Access:
lfe_struct:fetch(Struct, person, name) % → "Alice"
Dependencies
maps(Erlang stdlib)
Used By
lfe_macro_struct- Macro expansionlfe_eval- Struct evaluation- User code - Struct operations
Special Considerations
Elixir-Inspired: Borrows from Elixir's struct concept.
Map-Based: More flexible than records (tuples).
Type Tagged: __struct__ key identifies struct type.
Support Utilities
These foundational modules provide common functionality used throughout the codebase. They constitute 5.8% of the codebase but are used extensively.
lfe.erl - Public API Facade
Purpose: Provide a simple, stable public API for common LFE operations.
Location: src/lfe.erl
Size: 143 LOC, 4.9KB
Module Classification: API facade, entry point
Public API
Compilation:
compile(File) -> Result
compile(File, Options) -> Result
Compile LFE file. Delegates to lfe_comp:file/2.
Evaluation:
eval(Sexpr) -> Value
eval(Sexpr, Env) -> Value
Evaluate expression. Delegates to lfe_eval:expr/1,2.
Version:
version() -> string()
Return LFE version.
Facade Pattern
Purpose: Stable interface insulating users from internal module changes.
Example:
% User code (stable)
lfe:eval('(+ 1 2)').
% Instead of (unstable)
lfe_eval:expr('(+ 1 2)', lfe_env:new()).
Dependencies
LFE modules:
lfe_comp- Compilationlfe_eval- Evaluation
Used By
- User code
- External tools
- Documentation examples
Special Considerations
Stability: This API rarely changes (backward compatibility).
Convenience: Provides sensible defaults for common operations.
Minimal: Only most common operations exposed.
lfe_lib.erl - Utility Functions
Purpose: General-purpose utility functions used across multiple modules.
Location: src/lfe_lib.erl
Size: 124 LOC, 4.5KB
Module Classification: Utilities, shared functions
Public API
Type Predicates:
is_symb(Value) -> boolean()
is_symb_list(Value) -> boolean()
is_posint_list(Value) -> boolean()
is_proper_list(Value) -> boolean()
is_doc_string(Value) -> boolean()
Type checking predicates. Lines 32-54.
Form Processing:
proc_forms(Fun, Forms, State) -> {Forms, State}
proc_forms(Fun, Forms, Line, State) -> {Forms, State}
Process nested progn forms. Lines 56-84.
Name Parsing:
split_name(Name) -> [Module] | [Module, Function] | [Module, Function, Arity]
Parse qualified names like 'lists:map/2'. Lines 105-123.
Form Processing
Purpose: Flatten nested (progn ...) forms while applying a function to each form.
Example:
Forms = [{[progn,
[progn, Form1, Form2],
Form3], 1}],
Fun = fun(F, Line, State) ->
{[process(F)], State}
end,
{ProcessedForms, FinalState} = lfe_lib:proc_forms(Fun, Forms, State0)
Use Case: Compiler passes need to process all forms at any nesting level.
Dependencies
lists,string(Erlang stdlib)
Used By
lfe_comp- Form processinglfe_macro- Form processinglfe_lint- Form processing- Other compiler modules
Special Considerations
Shared Utility: One of the most widely used utility modules.
Simple Functions: Small, focused, well-tested functions.
lfe_internal.erl - Type and Form Validation
Purpose: Centralized knowledge of core forms, BIFs, guards, and types.
Location: src/lfe_internal.erl
Size: 360 LOC, 14KB
Module Classification: Utilities, validation
Public API
Form Classification:
is_core_form(Name) -> boolean()
is_core_func(Name, Arity) -> boolean()
is_guard_func(Name, Arity) -> boolean()
is_bif(Name, Arity) -> boolean()
is_erl_bif(Name, Arity) -> boolean()
is_lfe_bif(Name, Arity) -> boolean()
is_type(Name, Arity) -> boolean()
Check if name/arity is a known form/function/type. Lines 32-89.
Core Forms
Special Forms (is_core_form/1 at lines 123-178):
is_core_form(quote) -> true;
is_core_form(cons) -> true;
is_core_form(car) -> true;
is_core_form(cdr) -> true;
is_core_form(list) -> true;
is_core_form(tuple) -> true;
is_core_form(binary) -> true;
is_core_form(map) -> true;
is_core_form(lambda) -> true;
is_core_form('match-lambda') -> true;
is_core_form(let) -> true;
is_core_form('let-function') -> true;
is_core_form('letrec-function') -> true;
is_core_form('let-macro') -> true;
is_core_form(progn) -> true;
is_core_form(if) -> true;
is_core_form('case') -> true;
is_core_form(receive) -> true;
is_core_form(after) -> true;
is_core_form(call) -> true;
is_core_form(try) -> true;
is_core_form('catch') -> true;
is_core_form(funcall) -> true;
is_core_form(_) -> false.
Guard Functions
Allowed in Guards (is_guard_func/2 at lines 201-267):
Type tests, comparisons, arithmetic, boolean operators, bitwise operators, etc.
is_guard_func(is_atom, 1) -> true;
is_guard_func(is_list, 1) -> true;
is_guard_func('==', 2) -> true;
is_guard_func('=:=', 2) -> true;
is_guard_func('+', 2) -> true;
is_guard_func('and', 2) -> true;
is_guard_func('or', 2) -> true;
...
Not Allowed: User-defined functions, most BIFs.
BIF Lists
Erlang BIFs (is_erl_bif/2 at lines 289-367):
Comprehensive list of Erlang's built-in functions.
LFE BIFs (is_lfe_bif/2 at lines 369-399):
LFE-specific functions in lfe module.
Type Names
Built-in Types (is_type/2 at lines 421-467):
is_type(any, 0) -> true;
is_type(none, 0) -> true;
is_type(atom, 0) -> true;
is_type(integer, 0) -> true;
is_type(float, 0) -> true;
is_type(list, 0) -> true;
is_type(list, 1) -> true; % list(T)
is_type(tuple, 0) -> true;
is_type(map, 0) -> true;
...
Dependencies
- None (self-contained)
Used By
lfe_eval- Validationlfe_translate- Validationlfe_lint- Validationlfe_macro- Form checking
Special Considerations
Central Authority: Single source of truth for "what's valid in LFE".
Frequently Updated: When adding language features, this module must be updated.
Performance: Functions are called frequently; must be fast (usually constant-time lookups).
lfe_env.erl - Environment Management
Purpose: Manage lexical environments for variable, function, macro, and record bindings.
Location: src/lfe_env.erl
Size: 252 LOC, 8.4KB
Module Classification: Infrastructure, state management
This module is heavily used throughout the codebase (imported by lfe_macro, used by lfe_eval, lfe_lint, lfe_comp).
Public API
Environment Creation:
new() -> Env
add_env(Env1, Env2) -> Env
Create and merge environments. Lines 111-124.
Variables:
add_vbinding(Name, Value, Env) -> Env
add_vbindings([{Name, Value}], Env) -> Env
is_vbound(Name, Env) -> boolean()
get_vbinding(Name, Env) -> {yes, Value} | no
fetch_vbinding(Name, Env) -> Value
del_vbinding(Name, Env) -> Env
Variable bindings. Lines 169-189.
Functions:
add_fbinding(Name, Arity, Value, Env) -> Env
add_fbindings([{Name, Arity, Value}], Env) -> Env
is_fbound(Name, Arity, Env) -> boolean()
get_fbinding(Name, Arity, Env) -> {yes, Value} | {yes, Mod, Name} | no
add_ibinding(Mod, RemoteName, Arity, LocalName, Env) -> Env
Function bindings (including imports). Lines 193-233.
Macros:
add_mbinding(Name, Macro, Env) -> Env
add_mbindings([{Name, Macro}], Env) -> Env
is_mbound(Name, Env) -> boolean()
get_mbinding(Name, Env) -> {yes, Macro} | no
Macro bindings. Lines 237-255.
Records:
add_record(Name, Fields, Env) -> Env
get_record(Name, Env) -> {yes, Fields} | no
Record definitions. Lines 259-266.
Environment Structure
Record (lines 62-63):
-record(env, {
vars=null, % Variable bindings (map/orddict)
funs=null, % Function/macro bindings (map/orddict)
recs=null % Record definitions (map/orddict)
}).
Conditional Implementation (lines 37-60):
Uses maps if available, otherwise orddict:
-ifdef(HAS_MAPS).
-define(NEW(), #{}).
-define(GET(K, D), maps:get(K, D)).
-define(PUT(K, V, D), maps:put(K, V, D)).
...
-else.
-define(NEW(), orddict:new()).
-define(GET(K, D), orddict:fetch(K, D)).
-define(PUT(K, V, D), orddict:store(K, V, D)).
...
-endif.
Why? Backward compatibility with older Erlang versions.
Function/Macro Shadowing
Key Design (lines 103-110):
Functions and macros share the same namespace, with shadowing rules:
- Defining a macro shadows all functions with that name
- Defining a function shadows a macro with that name
- Functions with different arities coexist
Storage:
Funs = #{
foo => {macro, MacroDef},
bar => {function, [{1, Fun1}, {2, Fun2}]}
}
Dependencies
lists(for fold operations)- Conditional:
mapsororddict
Used By
- lfe_macro - IMPORTS this module (only module to do so!)
lfe_eval- Heavy use (39 calls)lfe_lint- Heavy uselfe_comp- Moderate use- All modules doing evaluation or compilation
Special Considerations
Performance Critical: Environment operations are hot paths.
Thread Safety: Environments are immutable (functional updates).
Scoping: Supports lexical scoping via environment chaining.
lfe_error.erl - Error Formatting
Purpose: Format exceptions and errors for user-friendly display.
Location: src/lfe_error.erl
Size: 171 LOC, 5.7KB
Module Classification: Utilities, error handling
Public API
format_exception(Class, Reason, StackTrace) -> Chars
format_exception(Class, Reason, StackTrace, SkipFun, FormatFun, Indentation) -> Chars
Format exception with stack trace. Located at lfe_error.erl:39-52.
Parameters:
Class-error,throw, orexitReason- Exception reasonStackTrace- List of stack framesSkipFun- Function to filter stack framesFormatFun- Function to format termsIndentation- Initial indentation level
Exception Formatting
Output Format:
exception error: badarg
in (lfe_eval:eval_expr/2 line 345)
in (lfe_shell:eval_form/2 line 502)
Stack Trace Filtering (filter_stack/2 at lines 87-112):
Allows skipping internal frames:
SkipFun = fun(Module, _Function, _Arity) ->
Module == lfe_eval orelse Module == lfe_shell
end
Term Formatting (format_term/3 at lines 134-156):
Custom formatting for exception data:
FormatFun = fun(Term, Indent) ->
lfe_io:prettyprint1(Term, 15, Indent, 80)
end
Exception Classes
Error (runtime errors):
exception error: badarg
Throw (user throws):
exception throw: my_exception
Exit (process exits):
exception exit: normal
Stack Frame Format
Erlang Stack Frame:
{Module, Function, Arity, [{file, File}, {line, Line}]}
Formatted:
in (Module:Function/Arity line Line)
in (Module:Function/Arity File:Line)
Dependencies
LFE modules:
lfe_io- Term formatting
Erlang stdlib:
lists,string
Used By
lfe_shell- Exception reportinglfe_init- Script error reportinglfescript- Script errors
Special Considerations
User-Friendly: Formats for human readability, not machine parsing.
Filtering: Can hide internal implementation details from users.
Customizable: Format function allows LFE-style or Erlang-style output.
Documentation Modules
These modules implement EEP-48 documentation support, enabling LFE to participate in Erlang's documentation ecosystem.
lfe_docs.erl - Documentation Generation
Purpose: Generate and extract EEP-48 compliant documentation from LFE modules.
Location: src/lfe_docs.erl
Size: 280 LOC, 9.6KB
Module Classification: Documentation, code generation
This module was covered in detail in PHASE4_ANALYSIS.md. Key points:
- Implements EEP-48 documentation format
- Generates "Docs" BEAM chunk
- Stores module, function, and macro documentation
- Compatible with Erlang documentation tools
lfe_shell_docs.erl - Documentation Rendering
Purpose: Render LFE documentation for display in the shell.
Location: src/lfe_shell_docs.erl
Size: 92 LOC, 3.3KB
Module Classification: Documentation, display
This module was covered in detail in PHASE4_ANALYSIS.md. Key points:
- Renders EEP-48 docs for terminal display
- Color-coded output
- Supports module, function, and macro docs
- Integrates with
lfe_shellfor(doc ...)command
Language Features Matrix
This section provides a comprehensive, systematic catalog of all language features in LFE. It serves as a reference for understanding what constructs are available, their syntax, semantics, and usage patterns.
Overview
LFE is a Lisp-2 with:
- Separate namespaces for functions and variables
- S-expression syntax (homoiconic)
- Erlang semantics (immutable data, process-oriented, pattern matching)
- Full Erlang interoperability (zero-overhead FFI)
- Powerful macro system (compile-time code transformation)
- Multiple paradigm support (functional, concurrent, actor-based)
Language Characteristics:
| Aspect | Description |
|---|---|
| Type System | Dynamically typed with optional Dialyzer specs |
| Evaluation | Eager/strict evaluation (call-by-value) |
| Scoping | Lexical scoping with environment-based bindings |
| Closures | Full closure support with lambda lifting |
| Tail Calls | Tail call optimization (via Erlang/BEAM) |
| Concurrency | Actor model (Erlang processes) |
| Persistence | All data structures immutable |
| Memory Model | Garbage collected, per-process heaps |
Core Forms (23)
quote cons car cdr list tuple binary map
lambda match-lambda
let let* letrec let-function letrec-function let-macro
progn if case receive try catch
call funcall function
Built-in Macros (50+)
defmodule defun defmacro defrecord defstruct
cond when unless do
let* flet fletrec flet*
lc bc qlc
caar cadr cdar cddr ... (c*r family)
list* != === !==
Data Types (13)
atom integer float string binary boolean
list tuple map record struct
function pid port reference
Operators (30+)
Arithmetic: + - * / div rem abs
Comparison: == /= =:= =/= < > =< >=
Boolean: not and or andalso orelse xor
Bitwise: band bor bxor bnot bsl bsr
LFE vs Common Lisp
| Feature | LFE | Common Lisp | Notes |
|---|---|---|---|
| Namespaces | Lisp-2 | Lisp-2 | Separate function/variable namespaces |
| Evaluation | Eager | Eager | Call-by-value |
| Typing | Dynamic | Dynamic | Runtime type checking |
| Macros | Unhygienic | Unhygienic | Both support quasiquotation |
| CLOS | No | Yes | LFE uses Erlang behaviors instead |
| Tail calls | Yes (BEAM) | Yes | Proper TCO |
| Concurrency | Actors (processes) | Threads | Different models |
| Mutability | Immutable | Mutable | Erlang semantics |
LFE vs Scheme
| Feature | LFE | Scheme | Notes |
|---|---|---|---|
| Namespaces | Lisp-2 | Lisp-1 | LFE has separate namespaces |
| Evaluation | Eager | Eager | Both call-by-value |
| Macros | Unhygienic | Hygienic (syntax-rules) | LFE has unhygienic macros |
| Continuations | No | Yes (call/cc) | BEAM doesn't support |
| Tail calls | Yes | Yes | Both have proper TCO |
| Numbers | Erlang types | Scheme numbers | Different numeric tower |
LFE vs Clojure
| Feature | LFE | Clojure | Notes |
|---|---|---|---|
| Platform | BEAM/Erlang | JVM | Different VMs |
| Namespaces | Lisp-2 | Lisp-1 | Different conventions |
| Data structures | Erlang types | Persistent collections | Both immutable |
| Concurrency | Actors | Agents/Atoms/STM | Different models |
| Macros | Traditional | Hygienic-ish | Clojure has syntax-quote |
| Typing | Dynamic | Dynamic | Both with optional specs |
| Performance | BEAM-level | JVM-level | Different characteristics |
Core Special Forms
Special forms are the fundamental building blocks of LFE. They have special evaluation rules and cannot be shadowed or redefined.
Data Construction
| Form | Syntax | Description | Example |
|---|---|---|---|
quote | (quote expr) or 'expr | Return expr unevaluated | '(a b c) → (a b c) |
cons | (cons head tail) | Construct cons cell | (cons 1 '(2 3)) → (1 2 3) |
car | (car list) | Get first element | (car '(a b c)) → a |
cdr | (cdr list) | Get tail | (cdr '(a b c)) → (b c) |
list | (list e1 e2 ...) | Construct list | (list 1 2 3) → (1 2 3) |
tuple | (tuple e1 e2 ...) | Construct tuple | (tuple 'ok 42) → {ok, 42} |
binary | (binary seg1 seg2 ...) | Construct binary | (binary (42 (size 8))) → <<42>> |
map | (map k1 v1 k2 v2 ...) | Construct map | (map 'a 1 'b 2) → #{a => 1, b => 2} |
Notes:
cons,car,cdrare Lisp traditional names- Also available as
hd,tl(Erlang style) - Lists are linked lists (Erlang lists)
- Tuples are fixed-size arrays (Erlang tuples)
- Maps are hash maps (Erlang maps, OTP 17+)
Functions and Closures
| Form | Syntax | Description |
|---|---|---|
lambda | (lambda (args) body) | Anonymous function |
match-lambda | (match-lambda (pattern body) ...) | Pattern-matching function |
let | (let ((var val) ...) body) | Parallel bindings |
let* | (let* ((var val) ...) body) | Sequential bindings |
letrec | (letrec ((var val) ...) body) | Recursive bindings |
let-function | (let-function ((name lambda) ...) body) | Local functions |
letrec-function | (letrec-function ((name lambda) ...) body) | Recursive local functions |
let-macro | (let-macro ((name macro) ...) body) | Local macros |
Lambda Examples:
;; Simple lambda
(lambda (x) (* x x))
;; Multi-argument
(lambda (x y) (+ x y))
;; Zero arguments
(lambda () 42)
;; Variable-arity (not directly supported - use match-lambda)
Match-Lambda Examples:
;; Pattern matching on arguments
(match-lambda
([0] 1) ; Base case
([n] (when (> n 0)) ; Guard
(* n (factorial (- n 1))))) ; Recursive case
;; Multiple patterns
(match-lambda
([(cons h t)] (list 'cons h t)) ; List pattern
([x] (when (is_atom x)) 'atom) ; Guard pattern
([_] 'other)) ; Catch-all
Let Forms Comparison:
;; let - parallel binding (x, y unbound during evaluation)
(let ((x 1)
(y 2))
(+ x y)) ; → 3
;; let* - sequential binding (x bound during y evaluation)
(let* ((x 1)
(y (+ x 1)))
(+ x y)) ; → 3
;; letrec - recursive binding (both bound during evaluation)
(letrec ((even? (lambda (n)
(if (== n 0) 'true (odd? (- n 1)))))
(odd? (lambda (n)
(if (== n 0) 'false (even? (- n 1))))))
(even? 4)) ; → true
Function Binding Examples:
;; let-function - local function definitions
(let-function ((square (lambda (x) (* x x)))
(cube (lambda (x) (* x x x))))
(+ (square 2) (cube 3))) ; → 31
;; letrec-function - mutually recursive functions
(letrec-function ((even? (lambda (n)
(if (== n 0) 'true (odd? (- n 1)))))
(odd? (lambda (n)
(if (== n 0) 'false (even? (- n 1))))))
(even? 10)) ; → true
Control Flow
| Form | Syntax | Description |
|---|---|---|
progn | (progn expr ...) | Sequence expressions, return last |
if | (if test then else) | Conditional (3-argument only) |
case | (case expr (pattern body) ...) | Pattern matching |
receive | (receive (pattern body) ... (after timeout body)) | Message receiving |
try | (try expr (case ...) (catch ...) (after ...)) | Exception handling |
catch | (catch expr) | Catch exceptions |
If Form:
;; Must have exactly 3 arguments
(if (> x 10)
'big
'small)
;; For multiple branches, use cond macro
(cond
((< x 0) 'negative)
((> x 0) 'positive)
('true 'zero))
Case Form:
;; Pattern matching
(case (tuple 'ok 42)
((tuple 'ok n) n) ; Match and bind
((tuple 'error _) 0) ; Match, ignore
(_ 'unknown)) ; Catch-all
;; With guards
(case x
(n (when (> n 10)) 'big)
(n (when (> n 0)) 'small)
(_ 'other))
;; Nested patterns
(case (list 1 (tuple 'ok 42) 3)
((list _ (tuple 'ok value) _)
value)) ; → 42
Receive Form:
;; Simple receive
(receive
((tuple 'msg data)
(io:format "Got: ~p~n" (list data))))
;; With timeout
(receive
((tuple 'stop) 'stopped)
((tuple 'msg data) data)
(after 5000
'timeout))
;; Pattern matching in receive
(receive
((tuple from (tuple 'request id data))
(! from (tuple 'response id (process data))))
(after 1000
'timeout))
Try Form:
;; Full try form
(try
(risky-operation)
(case result
((tuple 'ok value) value)
((tuple 'error _) 'failed))
(catch
((tuple 'error reason stack)
(log-error reason stack)
'error)
((tuple 'throw value stack)
(handle-throw value)
'thrown))
(after
(cleanup-resources)))
;; Simplified catch
(catch (risky-operation)) ; Returns {'EXIT', Reason} on error
Function Application
| Form | Syntax | Description |
|---|---|---|
call | (call module function arg ...) | Remote call |
funcall | (funcall func arg ...) | Apply function |
: | (: module function arg ...) | Remote call (syntax sugar) |
function | (function name arity) | Function reference |
function | (function module name arity) | Remote function reference |
Call Forms:
;; Direct call
(lists:map (lambda (x) (* x 2)) '(1 2 3))
;; call form (dynamic module/function)
(call 'lists 'map (lambda (x) (* x 2)) '(1 2 3))
;; : syntax (compile-time binding)
(: lists map (lambda (x) (* x 2)) '(1 2 3))
;; funcall (higher-order)
(funcall (lambda (x) (* x 2)) 5) ; → 10
;; Function references
(function square 1) ; Local function
#'square/1 ; Syntax sugar
(function lists reverse 1) ; Remote function
#'lists:reverse/1 ; Syntax sugar
Dynamic Dispatch:
;; Module/function computed at runtime
(let ((mod 'lists)
(func 'reverse))
(call mod func '(1 2 3))) ; → (3 2 1)
;; Function value
(let ((f (function lists reverse 1)))
(funcall f '(1 2 3))) ; → (3 2 1)
Built-in Functions
LFE provides built-in functions in the lfe module and inherits all Erlang BIFs.
List Operations
| Function | Signature | Description | Example |
|---|---|---|---|
list | (list e1 ...) | Construct list | (list 1 2 3) |
list* | (list* e1 ... tail) | List with explicit tail | (list* 1 2 '(3 4)) → (1 2 3 4) |
append | (++ l1 l2) | Concatenate lists | (++ '(1 2) '(3 4)) → (1 2 3 4) |
length | (length list) | List length | (length '(a b c)) → 3 |
reverse | (lists:reverse list) | Reverse list | (lists:reverse '(1 2 3)) → (3 2 1) |
member | (lists:member elem list) | Membership test | (lists:member 2 '(1 2 3)) → true |
nth | (lists:nth n list) | Get nth element (1-indexed) | (lists:nth 2 '(a b c)) → b |
Note: Most list functions are in the lists module (Erlang stdlib).
Arithmetic and Comparison
| Function | Syntax | Description |
|---|---|---|
+ | (+ a b ...) | Addition |
- | (- a b ...) | Subtraction |
* | (* a b ...) | Multiplication |
/ | (/ a b) | Division (float result) |
div | (div a b) | Integer division |
rem | (rem a b) | Remainder |
abs | (abs n) | Absolute value |
== | (== a b) | Equal (type coercion) |
/= | (/= a b) | Not equal (type coercion) |
=:= | (=:= a b) | Exactly equal |
=/= | (=/= a b) | Exactly not equal |
< | (< a b) | Less than |
> | (> a b) | Greater than |
=< | (=< a b) | Less than or equal |
>= | (>= a b) | Greater than or equal |
Comparison Semantics:
;; Type coercion
(== 1 1.0) ; → true
(=:= 1 1.0) ; → false
;; Ordering across types
(< 1 'atom) ; → true (numbers < atoms)
(< 'atom "string") ; → true (atoms < references < funs < ports < pids < tuples < maps < lists < binaries)
Arithmetic Examples:
(+ 1 2 3 4) ; → 10
(- 10 5 2) ; → 3
(* 2 3 4) ; → 24
(/ 10 3) ; → 3.3333...
(div 10 3) ; → 3
(rem 10 3) ; → 1
;; Mixed integer/float
(+ 1 2.5) ; → 3.5
(* 2 3.0) ; → 6.0
Boolean and Logical
| Function | Syntax | Description |
|---|---|---|
not | (not expr) | Logical NOT |
and | (and e1 e2 ...) | Logical AND (all evaluated) |
or | (or e1 e2 ...) | Logical OR (all evaluated) |
andalso | (andalso e1 e2 ...) | Short-circuit AND |
orelse | (orelse e1 e2 ...) | Short-circuit OR |
xor | (xor e1 e2) | Logical XOR |
Short-Circuit Evaluation:
;; andalso stops at first false
(andalso (< x 10) (expensive-check x))
;; orelse stops at first true
(orelse (is-cached x) (compute-expensive x))
;; Regular and/or evaluate all arguments
(and (< x 10) (expensive-check x)) ; Always calls expensive-check
Type Predicates
| Predicate | Tests For |
|---|---|
is_atom/1 | Atom |
is_binary/1 | Binary |
is_bitstring/1 | Bitstring |
is_boolean/1 | Boolean (true/false) |
is_float/1 | Float |
is_function/1 | Function |
is_function/2 | Function of specific arity |
is_integer/1 | Integer |
is_list/1 | List |
is_map/1 | Map |
is_number/1 | Number (int or float) |
is_pid/1 | Process identifier |
is_port/1 | Port |
is_reference/1 | Reference |
is_tuple/1 | Tuple |
Usage in Guards:
(defun process (x)
(cond
((is_atom x) (process-atom x))
((is_list x) (process-list x))
((is_tuple x) (process-tuple x))
('true 'unknown)))
Tuple Operations
| Function | Syntax | Description |
|---|---|---|
tuple | (tuple e1 ...) | Create tuple |
tuple_size | (tuple_size t) | Tuple size |
element | (element n t) | Get element (1-indexed) |
setelement | (setelement n t v) | Set element (returns new tuple) |
Examples:
(let ((t (tuple 'ok 42 "hello")))
(tuple_size t)) ; → 3
(element 2 (tuple 'ok 42)) ; → 42
(setelement 2 (tuple 'ok 0) 42) ; → {ok, 42}
Map Operations
| Function | Syntax | Description |
|---|---|---|
map | (map k1 v1 ...) | Create map |
map-size | (map-size m) | Map size |
map-get | (map-get k m) or (maps:get k m) | Get value |
map-set | (map-set m k v ...) | Set keys (returns new map) |
map-update | (map-update m k v ...) | Update existing keys |
maps:put | (maps:put k v m) | Add/update single key |
maps:remove | (maps:remove k m) | Remove key |
Map Examples:
;; Create
(let ((m (map 'a 1 'b 2)))
(map-size m)) ; → 2
;; Access
(map-get 'a (map 'a 1 'b 2)) ; → 1
(maps:get 'c (map 'a 1) 'default) ; → default
;; Update
(map-set (map 'a 1) 'b 2 'c 3) ; → #{a => 1, b => 2, c => 3}
;; Pattern matching
(case m
((map 'a v) v) ; Match if key 'a exists, bind value
(_ 'not-found'))
Binary Operations
| Function | Syntax | Description |
|---|---|---|
binary | (binary seg ...) | Create binary |
byte_size | (byte_size b) | Binary size in bytes |
bit_size | (bit_size b) | Binary size in bits |
binary_to_list | (binary_to_list b) | Convert to list |
list_to_binary | (list_to_binary l) | Convert from list |
Binary Syntax:
;; Simple binary
(binary (42)) ; → <<42>>
;; Sized segments
(binary (42 (size 16))) ; → <<0,42>> (16-bit)
;; Multiple segments
(binary (1) (2) (3)) ; → <<1,2,3>>
;; Type specifiers
(binary (3.14 (float 32))) ; → 32-bit float
;; String
(binary ((str "hello"))) ; → <<"hello">>
;; Concatenation
(let ((b1 (binary (1) (2)))
(b2 (binary (3) (4))))
(binary (b1 (binary))
(b2 (binary)))) ; → <<1,2,3,4>>
Process Operations
| Function | Syntax | Description |
|---|---|---|
self | (self) | Current process PID |
spawn | (spawn fun) | Spawn process |
spawn_link | (spawn_link fun) | Spawn linked process |
! | (! pid msg) | Send message |
register | (register name pid) | Register process name |
whereis | (whereis name) | Look up registered process |
link | (link pid) | Link to process |
unlink | (unlink pid) | Unlink from process |
exit | (exit reason) | Exit current process |
exit | (exit pid reason) | Exit other process |
Process Examples:
;; Spawn a process
(let ((pid (spawn (lambda ()
(receive
((tuple 'msg data)
(io:format "Got: ~p~n" (list data))))))))
;; Send it a message
(! pid (tuple 'msg "hello"))
pid)
;; Registered processes
(register 'my-server (self))
(! 'my-server (tuple 'request 42))
;; Linked processes
(let ((pid (spawn_link (lambda () (work)))))
;; If pid exits, we exit too
(monitor pid))
Macros
LFE has 50+ built-in macros providing syntactic conveniences and control structures.
Definition Macros
| Macro | Syntax | Expands To |
|---|---|---|
defmodule | (defmodule name ...) | (define-module name ...) |
defun | (defun name args body) | (define-function name ...) |
defmacro | (defmacro name args body) | (define-macro name ...) |
defrecord | (defrecord name fields) | (define-record name ...) |
defstruct | (defstruct fields) | (define-struct ...) |
Module Definition:
(defmodule mymod
(export (add 2) (sub 2))
(export-macro when-positive)
(import (from lists map filter)))
;; Function definition
(defun add (x y)
"Add two numbers."
(+ x y))
;; Multi-clause function
(defun factorial
([0] 1)
([n] (when (> n 0))
(* n (factorial (- n 1)))))
;; Macro definition
(defmacro when-positive (x body)
`(if (> ,x 0) ,body 'undefined))
Control Flow Macros
| Macro | Description | Example |
|---|---|---|
cond | Multi-branch conditional | (cond (test1 result1) ...) |
when | Conditional execution | (when test body) |
unless | Inverted conditional | (unless test body) |
do | Iteration macro | (do ((var init update)) test body) |
Cond:
(cond
((< x 0) 'negative)
((> x 0) 'positive)
('true 'zero))
;; With multiple forms per branch
(cond
((is_atom x)
(io:format "Atom: ~p~n" (list x))
(process-atom x))
((is_list x)
(io:format "List: ~p~n" (list x))
(process-list x)))
When/Unless:
(when (> x 10)
(io:format "Big number: ~p~n" (list x))
(process-big x))
(unless (is-empty list)
(process list))
Do Loop:
;; Count from 1 to 10
(do ((i 1 (+ i 1)))
((> i 10) 'done)
(io:format "~p~n" (list i)))
;; Multiple variables
(do ((i 0 (+ i 1))
(sum 0 (+ sum i)))
((> i 10) sum)
(io:format "i=~p sum=~p~n" (list i sum)))
Binding Macros
| Macro | Description |
|---|---|
let* | Sequential let (each binding sees previous) |
flet | Local function definitions (like let-function) |
fletrec | Recursive local functions (like letrec-function) |
flet* | Sequential function definitions |
Examples:
;; let* - each binding sees previous
(let* ((x 1)
(y (+ x 1))
(z (+ y 1)))
(list x y z)) ; → (1 2 3)
;; flet - local functions
(flet ((double (x) (* x 2))
(triple (x) (* x 3)))
(+ (double 5) (triple 3))) ; → 19
;; fletrec - mutually recursive
(fletrec ((even? (n)
(if (== n 0) 'true (odd? (- n 1))))
(odd? (n)
(if (== n 0) 'false (even? (- n 1)))))
(even? 42)) ; → true
List Comprehensions
| Macro | Description |
|---|---|
lc | List comprehension |
bc | Binary comprehension |
qlc | Query list comprehension (for databases) |
List Comprehension:
;; Basic
(lc ((<- x '(1 2 3 4 5)))
(* x 2))
; → (2 4 6 8 10)
;; With filter
(lc ((<- x '(1 2 3 4 5 6))
(> x 3))
x)
; → (4 5 6)
;; Multiple generators
(lc ((<- x '(1 2 3))
(<- y '(a b)))
(tuple x y))
; → ((1 a) (1 b) (2 a) (2 b) (3 a) (3 b))
;; Pattern matching in generator
(lc ((<- (tuple 'ok value) (list (tuple 'ok 1) (tuple 'error 2) (tuple 'ok 3))))
value)
; → (1 3)
Binary Comprehension:
;; Create binary from list
(bc ((<- x '(65 66 67)))
(x (size 8)))
; → <<"ABC">>
;; Transform binary
(let ((bin <<"hello">>))
(bc ((<= (c (size 8)) bin))
((- c 32) (size 8))))
; → <<"HELLO">>
Function Shortcuts
| Macro | Description | Example |
|---|---|---|
fun | Lambda shorthand | #'square/1 → (function square 1) |
Syntax Variations:
;; Function reference
#'func/2 ; Local function
#'module:func/2 ; Remote function
;; In higher-order functions
(lists:map #'square/1 '(1 2 3))
(lists:filter #'even?/1 '(1 2 3 4))
C-style Accessors
LFE provides the traditional Lisp c*r family of list accessors:
(caar x) ; (car (car x))
(cadr x) ; (car (cdr x))
(cdar x) ; (cdr (car x))
(cddr x) ; (cdr (cdr x))
(caaar x) ; (car (car (car x)))
...
; Up to 4 levels: caaaaar, etc.
Operator Aliases
| Alias | Equivalent | Description |
|---|---|---|
!= | /= | Not equal (with coercion) |
=== | =:= | Exactly equal |
!== | =/= | Exactly not equal |
Pattern Matching
Pattern matching is pervasive in LFE, used in function definitions, case expressions, let bindings, and receive clauses.
Pattern Types
| Pattern Type | Syntax | Description | Example |
|---|---|---|---|
| Literal | 42, 'atom, "string" | Match exact value | (case x (42 'found)) |
| Variable | x, name | Bind to any value | (case x (n n)) |
| Don't-care | _ | Match anything, don't bind | (case x (_ 'matched)) |
| Cons | (cons h t) | Match list head/tail | (case l ((cons h t) h)) |
| List | (list a b c) | Match list of specific length | (case x ((list a b) a)) |
| Tuple | (tuple 'ok value) | Match tuple structure | (case x ((tuple 'ok v) v)) |
| Binary | (binary (x (size 8))) | Match binary segments | See below |
| Map | (map 'key value) | Match map with key | (case m ((map 'a v) v)) |
| Alias | (= pattern1 pattern2) | Match both patterns | (= (cons h t) full-list) |
Pattern Examples
List Patterns:
;; Empty list
(case x
('() 'empty))
;; Cons pattern
(case x
((cons head tail) head))
;; List pattern (fixed length)
(case x
((list a b c) (tuple a b c)))
;; List pattern with tail
(case x
((list first second . rest)
(tuple first second rest)))
Tuple Patterns:
;; Fixed structure
(case result
((tuple 'ok value) value)
((tuple 'error reason) reason))
;; Nested tuples
(case x
((tuple 'person name (tuple 'address city state))
(tuple city state)))
;; Size matching
(case x
((tuple _ _) 'pair)
((tuple _ _ _) 'triple))
Binary Patterns:
;; Fixed-size header
(case packet
((binary (type (size 8))
(length (size 16))
(payload (binary)))
(tuple type length payload)))
;; String prefix
(case bin
((binary <<"GET ">> (rest (binary)))
(handle-get rest))
((binary <<"POST ">> (rest (binary)))
(handle-post rest)))
;; Bit-level matching
(case bits
((binary (flag (size 1))
(value (size 7)))
(tuple flag value)))
Map Patterns:
;; Match specific keys
(case person
((map 'name n 'age a)
(io:format "~s is ~p years old~n" (list n a))))
;; Match subset of keys
(case config
((map 'port p)
p)
(_ 8080)) ; Default
;; Nested maps
(case data
((map 'user (map 'name n))
n))
Alias Patterns:
;; Bind to both parts and whole
(case list
((= (cons h t) full)
(tuple h t full)))
;; Multiple aliases
(case x
((= (tuple a b) (= t1 (= t2 _)))
(tuple a b t1 t2)))
Guards in Patterns
Guards provide additional constraints on patterns:
;; Simple guard
(case x
(n (when (> n 10)) 'big)
(n (when (> n 0)) 'small)
(_ 'other))
;; Multiple conditions
(case x
(n (when (> n 0) (< n 100))
'in-range))
;; Guard functions
(case x
(x (when (is_atom x)) 'atom)
(x (when (is_list x)) 'list)
(_ 'other))
;; Complex guards
(case (tuple x y)
((tuple a b) (when (> a 0) (> b 0) (== (+ a b) 10))
'valid))
Allowed in Guards:
- Type tests:
is_atom/1,is_list/1, etc. - Comparisons:
==,<,>, etc. - Arithmetic:
+,-,*,div,rem, etc. - Boolean:
and,or,not,andalso,orelse - BIFs:
abs/1,element/2,tuple_size/1, etc.
Not Allowed in Guards:
- User-defined functions
- Send/receive operations
- Most BIFs (only guard-safe ones allowed)
Data Types
Primitive Types
| Type | Syntax | Example | Notes |
|---|---|---|---|
| Atom | 'atom or unquoted | 'hello, ok, foo-bar | Interned symbols |
| Integer | Decimal, hex, binary | 42, #x2A, #b101010 | Arbitrary precision |
| Float | Decimal point or exponent | 3.14, 1.5e10 | IEEE 754 double |
| String | Double quotes | "hello" | Actually a list of integers |
| Binary | #"..." | #"bytes" | Raw byte string |
| Boolean | 'true, 'false | 'true | Just atoms |
Atom Syntax:
;; Simple atoms (no quotes needed)
ok
error
foo
foo-bar
foo_bar
;; Quoted atoms (for special characters)
'hello world'
'|complex!@#$|'
;; Booleans are atoms
'true
'false
Number Syntax:
;; Integers
42
-17
0
;; Different bases
#b1010 ; Binary: 10
#o755 ; Octal: 493
#x1A2F ; Hex: 6703
#16rFF ; Base 16: 255
#2r1010 ; Base 2: 10
;; Floats
3.14
-0.5
1.5e10
6.022e23
String vs Binary:
;; String (list of integers)
"hello"
; → (104 101 108 108 111)
;; Binary (raw bytes)
#"hello"
; → <<"hello">>
;; Binary syntax
(binary ((str "hello")))
; → <<"hello">>
Compound Types
| Type | Syntax | Example | Characteristics |
|---|---|---|---|
| List | '(...) or (list ...) | '(1 2 3) | Linked list, O(n) access |
| Improper List | (cons a b) | (cons 1 2) → (1 . 2) | Cons cell |
| Tuple | #(...) or (tuple ...) | #(ok 42) | Fixed array, O(1) access |
| Map | #M(...) or (map ...) | #M(a 1 b 2) | Hash map, O(log n) access |
| Record | (make-rec ...) | See records | Named tuple (macro) |
| Struct | (struct mod ...) | See structs | Map with struct key |
Lists:
;; Empty list
'()
;; Simple list
'(1 2 3)
(list 1 2 3)
;; Nested lists
'((1 2) (3 4) (5 6))
;; Improper list (dotted pair)
(cons 1 2) ; → (1 . 2)
(cons 1 (cons 2 3)) ; → (1 2 . 3)
;; List with tail
(list* 1 2 '(3 4)) ; → (1 2 3 4)
Tuples:
;; Literal syntax
#(a b c)
;; Constructor
(tuple 'a 'b 'c)
;; Tagged tuples (common pattern)
#(ok 42)
#(error "not found")
;; Nested
#(person "Alice" #(address "NYC" "NY"))
Maps:
;; Literal syntax
#M(a 1 b 2)
;; Constructor
(map 'a 1 'b 2)
;; Empty map
#M()
(map)
;; Nested maps
#M(user #M(name "Alice" age 30)
status 'active)
;; Keys can be any term
(map 1 'a 2 'b (tuple 'x 'y) 'c)
Records (macros generate):
;; Define
(defrecord person
name
age
(city "Unknown"))
;; Create
(make-person name "Alice" age 30)
; → #(person "Alice" 30 "Unknown")
;; Access
(person-name p) ; Get name
(set-person-age p 31) ; Set age (returns new record)
;; Pattern match
(let (((match-person name n age a) p))
(tuple n a))
;; Update
(update-person p age 31 city "NYC")
Structs (map-based):
;; Define
(defmodule person
(defstruct [name age city]))
;; Create
(person:__struct__ '(name "Alice" age 30))
; → #M(__struct__ person name "Alice" age 30 city undefined)
;; Access (via maps)
(map-get 'name person)
Function Types
| Type | Description |
|---|---|
| Function | First-class function value |
| Closure | Function with captured environment |
| Module Function | Reference to module function |
Function Values:
;; Lambda creates function value
(let ((f (lambda (x) (* x 2))))
(funcall f 5)) ; → 10
;; Function reference
(let ((f (function lists reverse 1)))
(funcall f '(1 2 3))) ; → (3 2 1)
;; Closure (captures x)
(let ((x 10))
(lambda (y) (+ x y)))
Process Types
| Type | Description |
|---|---|
| PID | Process identifier |
| Port | Port identifier (for external programs) |
| Reference | Unique reference |
Process Identifiers:
;; Current process
(self) ; → #Pid<0.123.0>
;; Spawn returns PID
(let ((pid (spawn (lambda () (work)))))
(! pid 'message)
pid)
;; Named processes
(register 'my-server (self))
(whereis 'my-server) ; → PID
Advanced Features
Metaprogramming
Quasiquotation (backquote):
;; Backquote
`(a b c) ; → (a b c)
;; Unquote
(let ((x 42))
`(a ,x c)) ; → (a 42 c)
;; Unquote-splicing
(let ((l '(b c d)))
`(a ,@l e)) ; → (a b c d e)
;; Nested quasiquote
(let ((x 10))
`(let ((y ,x))
(+ y ,(* x 2))))
; → (let ((y 10)) (+ y 20))
Macro Definition:
;; Simple macro
(defmacro when-positive (x body)
`(if (> ,x 0) ,body 'undefined))
;; Pattern-based macro
(defmacro my-cond args
`(cond ,@args))
;; Recursive macro
(defmacro my-let
([() body] body)
([((cons (list var val) rest)) body]
`(let ((,var ,val))
(my-let ,rest ,body))))
Types and Specs
Type Definitions:
;; Simple type alias
(deftype byte () (integer 0 255))
;; Parameterized type
(deftype list (a) (list a))
;; Union type
(deftype number () (union integer float))
;; Record type
(deftype person ()
(tuple 'person
string ; name
integer)) ; age
Function Specs:
;; Simple spec
(defun add
{(spec [[integer integer] integer])}
([x y] (+ x y)))
;; Multiple clauses
(defun process
{(spec [[atom] ok]
[[atom any] {ok any}])}
([cmd] ...)
([cmd arg] ...))
;; With type variables
(defun id
{(spec [[a] a])}
([x] x))
Documentation
Module Documentation:
(defmodule mymod
(doc "This module does amazing things."))
Function Documentation:
(defun add (x y)
"Add two numbers together.
Returns the sum of x and y."
(+ x y))
Access Documentation:
;; In shell
(doc 'mymod) ; Module docs
(doc 'mymod 'add) ; Function docs
(doc 'mymod 'add 2) ; Specific arity
Syntactic Sugar and Shorthands
Quote Forms
| Form | Equivalent |
|---|---|
'expr | (quote expr) |
`expr | (backquote expr) |
,expr | (comma expr) |
,@expr | (comma-at expr) |
Hash Forms
| Form | Equivalent |
|---|---|
#(...) | Tuple literal |
#B(...) | Binary literal |
#M(...) | Map literal |
#"..." | Binary string |
#'f/2 | (function f 2) |
#'m:f/2 | (function m f 2) |
#.expr | Eval at read time |
Module Call Syntax
| Form | Equivalent |
|---|---|
(: mod func args) | (call 'mod 'func args) |
(mod:func args) | (call 'mod 'func args) |
Erlang Integration
LFE's defining characteristic is its transparent, zero-overhead integration with Erlang. This section documents how LFE achieves seamless interoperability while maintaining its Lisp identity.
Core Principle: LFE compiles to identical BEAM bytecode as Erlang.
Key Implications:
- No Foreign Function Interface (FFI) layer
- No marshaling or data conversion
- No performance penalty
- Complete access to Erlang ecosystem
- LFE and Erlang code are indistinguishable at runtime
Design Decision: Rather than implementing a Lisp on top of Erlang, LFE is a Lisp that is Erlang with different syntax.
Module System Integration
Calling Erlang from LFE
Calling LFE from Erlang
OTP Behaviors
Process Model Integration
Type System Integration
Records and Structs
Build System Integration
Debug Info and Tool Integration
Integration Patterns
Performance Characteristics
Integration Summary
Compatibility Layers
Common Lisp Compatibility
Clojure Compatibility
Scheme Compatibility
Compatibility Layer Comparison
Design Patterns
Performance Considerations
Compatibility Summary
Tooling
lfec - The Compiler
lfe - The REPL
lfescript - Script Runner
Testing Tools
Build Tool Integration
Editor Support
Debugging Tools
Documentation Tools
Profiling and Performance
Tooling Summary
Data Structure Catalog
Runtime Data Structures
Compiler Data Structures
Abstract Syntax Trees
Environment Structures
Documentation Structures
Special Purpose Structures
Memory Layout Considerations
Data Structure Performance Summary
Design Recommendations
Cross-Reference with Source
Key Takeaways
Component Relationship Graphs
High-Level Architecture Layers
Compiler Pipeline Dependencies
Core Module Dependencies
Module Categories by Coupling
Compilation Order
Core vs Periphery
Dependency Metrics
Refactoring Opportunities
Architecture Quality Assessment
Dependency Impact Analysis
Dependency Visualization Summary
Testing & Quality
Test Suite Organization
Test Coverage Analysis
Testing Strategies
Test Execution
Example Programs
Quality Metrics
Known Issues & Technical Debt
Testing Best Practices
Quality Assurance Process
Testing Infrastructure Quality
Comparison with Similar Projects
Future Testing Enhancements
Design Patterns & Idioms
Architectural Patterns
Functional Programming Idioms
Erlang-Specific Idioms
Code Organization Patterns
Performance Patterns
Compiler Construction Patterns
Testing Patterns
Anti-Patterns to Avoid
Pattern Summary
Key Takeaways
Performance Considerations
Compilation Performance
Runtime Performance
Memory Usage
Performance Hotspots
Optimization Opportunities
Benchmarking
Performance Best Practices
Performance Comparison
Future Performance Work
Key Takeaways
Future Directions
High Priority Enhancements
Medium Priority Enhancements
Low Priority / Opportunistic
Ecosystem Enhancements
Tooling Ecosystem
Compatibility & Standards
Implementation Priorities
Community & Governance
Risks & Mitigation
Key Takeaways
Resources
Glossary
Complete Module Reference
File Organization
Key Data Structures Reference
Common Patterns Quick Reference
Build & Tooling Commands
Performance Tuning Checklist
Testing Checklist
Code Review Checklist
Analysis Methodology
Additional Resources
Part VIII - Conclusion
You have completed a remarkable journey. From the first tentative steps at the REPL to the sophisticated architecture patterns of OTP, from understanding how atoms and lists form the building blocks of computation to wielding macros that reshape the language itself, you have traversed the full spectrum of LFE programming. More than that, you have absorbed a way of thinking that synthesizes the mathematical elegance of Lisp with the industrial pragmatism of Erlang - a perspective that will serve you well far beyond any specific programming task.
Lisp pioneered many ideas in computer science, including tree data structures, automatic storage management, dynamic typing, conditionals, higher-order functions, recursion, the self-hosting compiler, and the read–eval–print loop. LFE inherits this innovative legacy while adding the fault-tolerance, distribution, and concurrency features that make Erlang legendary in systems programming. By mastering LFE, you have gained fluency in both traditions, positioning yourself at the intersection of two of the most influential paradigms in computing history.
The progression through this manual reflects the natural evolution of how programmers think about and build systems. Part I showed you how to transform data into computation through evaluation. Part II revealed how LFE's data structures become the vocabulary for expressing any conceivable program. Part III demonstrated how those same structures spring to life as processes, functions, and distributed systems. Part IV equipped you with advanced techniques for building robust, maintainable code. Part V introduced you to OTP's battle-tested patterns for creating systems that never stop running. Part VI gave you the tools to be productive from day one. Part VII provided the wisdom to write code that others can understand, extend, and celebrate.
But this manual is not just about LFE - it's about a philosophy of programming that has profound implications for how we think about computation itself. The property allows code to be treated as data and vice versa, enabling powerful metaprogramming techniques, and this homoiconicity represents something deeper than a mere technical feature. It embodies the idea that the boundaries between different aspects of computing - between data and programs, between specification and implementation, between design and execution - are more fluid than we typically imagine.
This fluidity has practical consequences that extend far beyond LFE programming. When you understand how to manipulate code as data, you develop an intuition for abstraction that applies to system design, API development, and architectural decision-making. When you internalize the actor model's approach to fault tolerance, you begin to see how similar principles apply to organizational design, distributed teams, and resilient business processes. When you master the discipline of functional programming, you develop habits of thought that lead to clearer reasoning about complex problems in any domain.
The tools and techniques you've learned - pattern matching, tail recursion, supervision trees, hot code swapping, macro-based DSLs - represent solutions to fundamental problems in computing that appear in many different contexts. The principles underlying these solutions - immutability, isolation, composition, declarative specification - are universally applicable patterns for managing complexity. By learning to think in LFE, you've developed a toolkit for reasoning about systems that will serve you throughout your career, regardless of which languages or platforms you encounter.
Perhaps most importantly, you've experienced what it means to work with a language that treats programmers as creative partners rather than adversaries to be constrained. LFE doesn't try to prevent you from making mistakes through restrictive type systems or elaborate safety mechanisms. Instead, it provides powerful tools and trusts you to use them wisely. This trust comes with responsibility - to write clear code, to test thoroughly, to document your intentions, to consider the needs of future maintainers. But it also comes with freedom - to explore unconventional solutions, to push the boundaries of what's possible, to create new abstractions when existing ones prove inadequate.
The combination of Lisp's expressiveness with Erlang's reliability creates unique opportunities for innovation. You can prototype new ideas with the rapid feedback of the REPL, then scale them to production systems that handle millions of users without missing a beat. You can embed domain-specific languages directly in your application code, creating interfaces so natural that domain experts can contribute directly to implementation. You can build systems that evolve continuously, adapting to changing requirements without downtime or data loss.
As you continue your journey with LFE, remember that mastery is not a destination but a continuous process of discovery. The language will continue to evolve, new libraries will emerge, and novel applications will push the boundaries of what's possible. Your role is not just to consume these innovations but to contribute to them. Whether through code contributions, documentation improvements, community building, or simply sharing your experiences with others, you are now part of the ongoing story of LFE development.
The future of computing faces unprecedented challenges: systems of extraordinary complexity, demands for reliability that exceed traditional engineering approaches, and requirements for adaptability that strain conventional programming models. LFE, with its fusion of mathematical rigor and systems pragmatism, represents one promising approach to meeting these challenges. By mastering LFE, you've equipped yourself not just to participate in this future, but to help shape it.
Your journey with LFE has just begun. The real adventure starts now, as you apply these concepts to problems that matter to you, discover new ways to combine the tools you've learned, and push the boundaries of what's possible when code and data dance together in perfect harmony. Welcome to the future of programming. It's going to be an extraordinary ride!
End of transmission. LFE REPL ready for your next adventure.
Feedback and Documentation Bugs
If you would like to provide feedback about this guide, we would welcome the chance to improve the experience for everyone. Please create a ticket in the Github issue tracker. Be sure to give a full description so that we can best help you!







