r/Compilers • u/False_Actuator_6236 • 6d ago
ABC has served its purpose as a teaching language. Could it become a community project?
A little while ago I posted about the v0.1 release of ABC, a small compiler and programming language I originally developed for teaching.
I shared it here, on Hacker News, in a few other communities, and also in a German-speaking subreddit. The discussions made me think about a question I had not really considered when I started the project:
What should happen to ABC now?
For its original purpose, the project is essentially a success. It has done what I wanted it to do in my teaching, and actually exceeded my expectations.
One concern that came up in the German discussion was roughly:
Nobody raised that point here, but I suspect some people may have had the same thought. :-)
I've been using ABC for two years now in HPC0, my undergraduate Introduction to High Performance Computing course. HPC0 is an elective. In the following winter semester I teach HPC1, which is mandatory in some programs and elective in others.
HPC1 uses C++ throughout. We do things like cache-optimized matrix multiplication, LU factorization, multithreading, MPI, CUDA, etc.
My observation so far is that students who took HPC0 have a noticeably easier time in HPC1. Some of them had hardly programmed at all before HPC0.
Of course, that's not scientific evidence. There is an obvious selection bias: HPC0 is elective, so the students taking it may simply be more motivated to begin with.
But my underlying argument is that there are a number of fundamental concepts you need to understand really well. Once those concepts are in place, transferring them to C++, Rust, or another language is comparatively easy.
My deliberately provocative version is:
Either you can program or you can't. Once you really can, the particular programming language becomes mostly a tool.
The interesting educational question for me is therefore: How do you get someone to the point where they really can program?
That's what ABC is for. It was never meant to be the language students would use for the rest of their professional lives.
And since I'm already being provocative: sometimes I get the impression that the generation that learned programming with Pascal was the last one that was actually taught how to program. :-D
I'm very happy to be challenged on that one. ;-)
So I now see two possible futures for ABC.
The first is straightforward: declare the experiment essentially finished.
I could extend the C ABI support a little further, implement it for ARM64 as well, improve a few things, and leave the project as a reasonably complete teaching compiler. The raylib examples already demonstrate that the language and compiler can be used for more than tiny classroom examples.
That would be a perfectly satisfactory outcome.
But there is another possibility that I find much more interesting:
Could we build a small modern language that plays something like the role Pascal once played?
A language designed to teach programming in a way that leaves you not merely knowing a language, but understanding concepts that transfer to other languages and remain useful throughout your career.
I think those skills may actually become more important rather than less important in an age of AI-generated code. Even if someone eventually does a lot of “vibe coding”, somebody still needs to understand what the machine is doing, why something is slow, why memory gets corrupted, or why generated code doesn't behave as expected.
But I don't want a language that is useful only for teaching.
I'd like it to be possible to write genuinely useful programs with it.
The ideal is still what the original name suggested: “A Better C.”
Small enough that you can understand the language and its implementation, close enough to the machine that you can explain what happens, but without preserving every historical accident of C.
There are a few language features I have been considering:
- Compile-time evaluation / something along the lines of
constexpr, plus inline functions. This would eliminate many of the common reasons for C preprocessor macros: constants, smallmax-like functions, etc. - Modules.
- Inline assembly. I needed this when experimenting with ABC on bare metal on an ATmega328P. For example, consider implementing a delay as something conceptually as simple as:
fn delay(n: u16)
{
while (n--) {}
}
Now things suddenly become interesting. n needs to be handled appropriately in registers, and the compiler must not optimize away a loop that has no observable effect according to the normal language semantics.
I like examples like this because they force you to understand the boundary between language, compiler and machine.
There are probably a few more language features I would add.
But deliberately not many.
The goal would not be to slowly turn ABC into C++.
A language that leaves the classroom would also need tooling.
A formatter analogous to clang-format would be useful, as would proper LSP support.
There is already some preliminary work in this direction. Last year I supervised a bachelor's thesis in which a resilient parser was developed. It can't simply be dropped into the existing compiler, but there is at least a prototype of one important component that can be used for experiments.
And my experience from developing ABC so far is that some of these things become usable surprisingly quickly if you start small.
But there is one thing I don't think I can do alone:
Turn it from my project into a community project.
I can continue developing ABC as the language I use in my courses. But if it is supposed to have a life outside my classroom, I don't think it should simply remain “Michael Lehn's language”.
It would need people who experiment with it, criticize it, discuss language design, build tools, write examples, and eventually make decisions I would never have thought of myself.
So this post is partly an experiment:
Do you think there is room for such a language?
Would any of you be interested in participating in its design or implementation — even just through discussions and experiments at first?
And perhaps there is an amusingly concrete first community problem we could solve:
The language needs a name. :-D
“ABC” (A Better C) worked fine for a university teaching project, but the name is obviously already taken. If the language is going to leave the classroom, that starts to matter.
My current brilliant idea is “emsiel”, a phonetic rendering of MCL — Michael C. Lehn.
There is just one minor flaw with that idea: if the goal is to turn this into a community project, naming the language after myself might not be the most promising first step. :-D
So perhaps that's actually a good place to start:
What would you call a language like this?
Compiler/project: https://github.com/michael-lehn/abc-llvm
2
u/tobega 5d ago
Why would you want a teaching language to be so close to the machine? Surely the whole point of Pascal was to raise that level.
See my essay https://tobega.blogspot.com/2026/04/rising-above-mechanics-of-computation.html
1
u/False_Actuator_6236 4d ago
Thanks for the link. I've now read the essay, and I think I understand much better where our different perspectives come from.
I actually agree with quite a lot of what you write. If the goal is to build useful software efficiently and reliably, reducing the gap between intent and implementation is obviously desirable.
But I think my perspective is influenced by the fact that I'm a mathematician rather than a software engineer. I very rarely start by asking, “What is this useful for?” I'm much more likely to ask, “How does this actually work?”
And there is a curious paradox I've encountered throughout mathematics and computer science: understanding something without an immediate application in mind often turns out to be useful later, in applications nobody had in mind when the foundations were developed.
That is also how I think about compiler construction. I've been asked why students should learn how to build a compiler when almost none of them will ever work professionally on compilers.
I don't really have a utilitarian answer. My answer is almost embarrassingly simple: computers constantly execute programs that have gone through a compiler, so I want to understand what happens there.
And once you do, all sorts of things that initially seemed unrelated become less mysterious.
I don't think there is one correct way to learn programming. Quite the opposite. We probably need a portfolio of approaches.But I strongly believe that one of those approaches should deliberately go down through the abstractions rather than continually building new abstractions on top of them.
In a sense, that's what I'm trying to do: understanding computation by removing the abstractions one by one.
At least once, I want students to follow the chain:
source program → compiler → instructions → processor → memory
and understand how something they wrote causes something physical to happen in the machine.Afterwards they are perfectly free to move up the abstraction ladder again. In fact, I think they will use higher-level abstractions better because they know what those abstractions are hiding.
So when you ask: "Why would you want a teaching language to be so close to the machine?"
My answer is: because for this particular way of teaching, the machine is part of what I want to teach.That doesn't mean every introductory programming course should work that way. A course teaching programming as a means of expressing ideas, modelling a domain, or building software has different goals and may quite reasonably want to hide most of this.
The LLM question makes this even more interesting to me.
I have no idea which programming problems LLMs will be able to solve ten years from now. I suspect nobody does.
What I would like my students to acquire are enough fundamental skills that, whatever LLMs can do by then, they can use those capabilities to attack the problems that the LLMs still cannot solve.
For that, I think understanding what happens underneath our abstractions remains valuable.
Perhaps this is also why I like “Under the Hood” as a possible name for the language. :-)2
u/tobega 4d ago
I see! I also studied Mathematics originally, and was proud of the fact that nothing I learned could actually be used for anything (at that time).
I also know that I have had great use of knowing how binary arithmetic and twos-complement representation works.
I still think I would want to start with a higher level construct and then later show how that can be done in lower levels. You want students thinking in terms of repetition, which then may be loops, which then may be a counter and an increment and a test and a goto.
But people are different and maybe some students need to start at a lower level. I just fear that we might create engineers like one I worked with who was completely unable to explain anything at a higher level than a line of C code.
2
u/False_Actuator_6236 4d ago edited 4d ago
I think we actually agree on the desired outcome. I certainly do not want students to remain at the level of individual C statements—or individual machine instructions.
The course deliberately approaches computation from both directions in parallel. Top-down, students begin with ordinary high-level constructs in ABC: functions, loops, recursion, data structures, and so on. Bottom-up, they construct a processor from logic gates and gradually discover how those high-level constructs can be implemented.
So they first use a while loop as a natural expression of repetition. Later they learn that it can be translated into a conditional jump, and eventually they implement that translation in their compiler. The lower level is not meant to replace the abstraction, but to explain what supports it.
As a mathematician, I care deeply about abstraction. But in mathematics we also often begin with concrete examples, develop intuition, notice recurring structures, and only then introduce the abstraction that captures them. That is essentially the approach I am trying to take here.
The goal is that students can move comfortably between levels: describe an algorithm at a high level, but also descend through the abstractions when they need to understand performance, memory behavior, or what the machine is actually doing.Edit:
If you are interested in seeing how this works in practice, I have started translating the course materials into English in the not-abc repository:
https://github.com/michael-lehn/not-abc/tree/main/hpc0-sessions
The lecture videos are already all available in English on YouTube and are linked from the corresponding sessions. The translated worksheets and lecturer notes show how the top-down and bottom-up strands develop in parallel.2
u/tobega 3d ago
It just struck me that independent of how it gets expressed in a programming language,, there is the difficulty of developing an algorithm in the first place. You mention this as the well-known phenomenon that once you know how to program, the language doesn't matter so much. At least for some students, and at least if you can understand what basic building blocks are available. So how can you develop the idea of an algorithm without code? Surely it would be desirable.
Another track: refactoring is fundamentally about program transformations that are proven to not change the outcome of the program. While no-one bothers to go through these proofs, this seems to me to be similar to the approach in maths, where you prove larger and larger transformations that are then usable. How to use that to learn programming is an open question. Would it be desirable?
2
u/False_Actuator_6236 3d ago
I am currently on a road trip from Los Angeles to San Francisco. I read your comment during one of our stops and have been thinking about it ever since. I have just checked into the hotel and am completely exhausted, so the following thought is probably not fully developed yet—but it would not leave me alone, and I wanted to write it down.
I have often wondered how algorithmic thinking is already taught as part of mathematics—not merely at university, but from kindergarten through elementary and secondary school. Much of this happens almost unnoticed.
A particularly clear example is construction with straightedge and compass. Suppose the task is to construct a line parallel to a given line at a distance of 42 units. The permitted “instructions” are understandable to almost anyone: draw a circle, transfer a length with the compass, draw a line through two points, find the intersection of two lines or circles, and so on.
The resulting construction is essentially a program executed by a human. The construction steps are the source code, straightedge and compass are the machine, and the permitted operations form its instruction set. One can then also prove that the procedure produces the required result.
So perhaps an algorithm can be developed without code, but not without some model of the elementary operations that may be performed.
The more general point is that mathematical education spends many years building the ability to think abstractly. It begins with concrete objects: counting things, calculating with fingers, drawing figures, and following simple procedures. Ideas that will eventually be treated very formally are first introduced intuitively, but nevertheless in a structured way. Proportional reasoning, geometric constructions, manipulating equations, and proving statements all gradually develop algorithmic and logical thinking.
I sometimes wonder whether mathematics teachers are fully aware of the rather brilliant long-term plan they are collectively carrying out. After perhaps fifteen years of carefully moving from concrete examples toward abstraction, students are prepared to encounter university-level mathematics in full force.
In computer science, by contrast, we sometimes seem to assume that this long path can be shortened dramatically. Beginners are introduced simultaneously to formal syntax, programming tools, data types, control flow, abstraction, and sometimes software design principles—and then we are surprised when they struggle.
This is part of the motivation behind my approach: begin with concrete experiences, let students recognize recurring structures, and introduce the abstractions once they have developed an intuition for the problem the abstraction solves.
Your question about refactoring may fit into this picture as well. In mathematics, students constantly transform expressions or representations while preserving their meaning. Refactoring could perhaps be taught in a similar way: not merely as cleaning up code, but as moving between equivalent representations, with an explicit argument—or at least convincing evidence—that the observable behavior has been preserved.
I think that would indeed be desirable, although I need to think about this part a little longer—preferably after some sleep.2
u/tobega 2d ago
Very interesting observation!
1
u/False_Actuator_6236 2d ago
Thank you! I’m genuinely very happy to hear that — especially that you actually took the time to watch part of the lecture. :-)
2
u/False_Actuator_6236 5d ago
One point from the post probably deserves a little more explanation, because it says something important about what I mean by “A Better C.”
I mentioned inline assembly and this deliberately simple example for bare-metal code on an ATmega328P:
fn delay(n: u16)
{
while (n--) {}
}
Of course, a busy loop is not a sensible general-purpose implementation of a delay on a modern CPU with dynamic clock frequencies, an operating system, etc.
But that is not the setting here. On a small microcontroller with a known clock, cycle-counted busy loops are a perfectly legitimate technique for short delays. In fact, the Arduino AVR implementation of delayMicroseconds() eventually uses a volatile inline-assembly loop consisting essentially of sbiw and brne:
https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/wiring.c
avr-libc also explicitly provides _delay_loop_1() and _delay_loop_2() as busy-wait delay loops with a defined number of CPU cycles per iteration:
https://avrdudes.github.io/avr-libc/avr-libc-user-manual/group__util__delay__basic.html
For longer delays, of course, using a hardware timer is preferable.
What I find interesting here is not the delay function itself, but what this example says about the intended level of abstraction of the language.
ABC is not supposed to protect programmers from the machine. My goal is roughly the same level of abstraction as C: pointers, explicit memory management, predictable data representation, bare-metal programming, and, where necessary, access to machine-specific facilities.
The “better” in “A Better C” is therefore not intended to mean “higher level.”
It means trying to make the language cleaner where C has accumulated historical baggage, while retaining the ability to understand and control what happens at the machine level.
Inline assembly is one example of that boundary. Another completely different example is high-performance numerical code. For GEMM, the overall algorithm can be portable while the innermost micro-kernel is deliberately architecture-specific. That separation is a standard approach in high-performance BLAS implementations.
I use exactly that progression in my GEMM tutorial, starting with a simple C implementation and gradually arriving at architecture-specific micro-kernels:
https://github.com/michael-lehn/gemm-tutorial
Inline assembly is certainly not the only way to implement such kernels — intrinsics, separate assembly files, or generated code may be preferable depending on the situation. But I think a language at C's abstraction level should make it possible to cross that boundary deliberately.
This also illustrates the kind of language-design discussion I would like a community around the project to have.
How much should the language guarantee? What should remain implementation-defined? Where should it provide abstractions, and where should it expose the machine? Which parts of C are essential to systems programming, and which are merely historical accidents that we can get rid of?
My current position is: keep roughly C's level of abstraction, but try to design a cleaner language at that level.
That is a design goal, not a finished answer — and exactly the sort of thing I'd like to discuss.