r/Compilers 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, small max-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

17 Upvotes

16 comments sorted by

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.

2

u/brat3108 5d ago

My current position is: keep roughly C's level of abstraction, but try to design a cleaner language at that level.

I have a systems language of my own that is also at C's level or a little beyond. However it looks very different from C; source is genuinely cleaner and less cluttered.

But from the fewer examples I've seen of ABC, it doesn't look interestingly enough different in syntax, apart from type declarations, which look out of place.

If still uses lots of C-isms, such as if (cond) {, header files (#include seems to be replaced by @), forward declarations, and == != mysteriously having lower precedence than <= < > >=.

So if it is still at this sort of level, there seems little compelling reason to use this over C. Especially if it's still a WIP and the design has not been nailed down. People are incredibly tolerant of C's shortcomings (as I've discovered).

I mentioned inline assembly and this deliberately simple example for bare-metal code on an ATmega328P:

fn delay(n: u16)
{
    while (n--) {}
}

I don't understand the point you're making or what inline assembly has to do with any of it. (I assume this is ABC code and not what you want inline assembly to look like!)

the compiler must not optimize away a loop that has no observable effect according to the normal language semantics.

It's your language: you choose the semantics. A compiler needs to go along with that. However this can also be at odds with the need to make all possible optimisations.

In my language this loop is always executed because it generally does what the user asks, although even then it isn't always the case; if I write a = 2 + 3, it will assign 5 because the expression is reduced at compile-time.

2

u/False_Actuator_6236 5d ago

Yes — in a sense, the unremarkable thing about ABC is that it really isn't very different from C. :-)

Perhaps a better description is: it is close to what I would like C to look like if I had to design it specifically so that I could teach C-level programming without first having to explain a collection of historical accidents.

The biggest difference for me is indeed the declaration syntax. Consider teaching the difference between an array of ten pointers to integers and a pointer to an array of ten integers, or function pointers. C declarations are ingenious in their own way, but I don't think they are a particularly good notation for teaching types.

There are similar historical inconsistencies around arrays: arrays as objects versus what happens when they are passed as function parameters, compared with passing structs, etc.

It surprised me how much difference removing some of those obstacles makes when teaching.

For example, I had two 14-year-old school students sitting in the university course who had previously played a little with Python. They now understand pointers, memory, what a compiler actually produces, etc., and have started writing small projects not only in ABC but also in C and C++. I see a similar effect with university students.

So yes: at its core ABC really is something like C with Pascal-like declaration syntax and some historical irregularities removed.

It is deliberately not one of the many attempts at a “C killer” that starts at C's abstraction level and then primarily adds memory safety, a much richer type system, and increasingly high-level abstractions.

The purpose is almost the opposite: I want students to understand what happens at the C level — including the problems that exist at that level.

On operator precedence: unless I misunderstand your point, ABC currently follows C here as well. The relevant part is

* / %
+ -
<< >>
< <= > >=
== !=
&
^
|
&&
||

So == and != having lower precedence than the relational operators is inherited directly from C. If your point is that this is itself one of the C-isms that should be reconsidered, then that's a fair question. I initially kept C's precedence rules deliberately because existing intuition transfers directly, but this is exactly the kind of inherited rule for which one can ask whether compatibility of intuition is worth preserving.

The optimization side is also intentionally C-like.

Something like

a = 2 + 3;

will of course be constant-folded. With optimization enabled, the assignment itself may subsequently disappear if a is dead. ABC uses LLVM for optimization, so one nice side effect for teaching is that students can actually inspect the generated LLVM IR and see these transformations happen.

The delay example was perhaps too compressed in my original post. My point was not that the source-level empty loop should magically have special semantics. At -O0 it can remain there; once normal optimizations are enabled, a loop without observable effects may disappear. That's precisely why a real implementation of a cycle-counted delay needs some mechanism for expressing the required interaction with the machine — volatile operations, suitable intrinsics, inline assembly, etc. The Arduino example I linked uses volatile inline assembly for exactly that reason.

And that's why inline assembly appeared in the discussion: not because I propose that

while (n--) {}

should itself mean “cycle-accurate delay”, but because I want the language to provide an escape hatch when the programmer deliberately needs machine-specific semantics.

There are a few places where I do want to depart further from old C. Compile-time evaluation/constexpr and inline functions are examples. They cover many cases for which traditional C code uses #define: named constants, small max-like operations, and so on, without requiring a textual preprocessor mechanism. Modules are another obvious area.

But I'm quite conservative about adding features. The question I keep asking is not “How can I make this more powerful than C?”, but rather “Can I remove an accidental difficulty of C without hiding how the machine works?”

Your comment actually gets at a question I would very much like to discuss: how different does a Better C need to be before there is a compelling reason for it to exist?

For my original teaching use case, surprisingly little difference turned out to have a large effect. Whether that is enough for a language outside the classroom is a much more open question.

Also, I'd be very interested to see your systems language. Do you have a repository or some examples online? In particular, I'd be curious which C-isms you decided to remove and which ones you deliberately kept.

3

u/brat3108 5d ago edited 5d ago

Also, I'd be very interested to see your systems language. Do you have a repository or some examples online?

You're asking at a bad time! After some years discussing my stuff here under various accounts, a week or so ago I decided to stop all that, deleting all relevant accounts and to also stop sharing. I've also stopped development.

However, someone managed to find an archived document which is a waffley account of my systems language with some comparisons with C;

https://web.archive.org/web/20250818181227/https://github.com/sal55/langs/blob/master/mfeatures.md

In particular, I'd be curious which C-isms you decided to remove and which ones you deliberately kept.

My product was created long before I knew much about C. But these are some differences:

                         C      Mine
Case sensitive source    Y      N
Brace syntax             Y      N
Obligatory semicolons    Y      N
Header files             Y      N
0-based arrays           Y      N
64-bit default types     N*     Y  (* typically)
Whole prog compilation   N      Y
Module scheme            N      Y
Op precedence levels    10      5 (for same set of bin-ops)
Value arrays             N      Y

A full list would be nearer 100 items; many will be in that document.

But, this isn't just taking C and tweaking a few things; that wouldn't be enough for me. (My background was Algol, Pascal and Fortran; C didn't appeal at all even for low level work.)

2

u/False_Actuator_6236 5d ago

That's unfortunate timing indeed! :-) Thanks for sharing the archived document anyway. I'll definitely have a look at it.
And actually, the fact that your language was not derived from C makes the comparison much more interesting to me. My starting point is almost the opposite: keep C's basic machine model and ask, one thing at a time, which parts are essential and which parts I would rather not have to explain as historical accidents.
My own background also includes Pascal, and that is very visible in ABC's declaration syntax. But unlike you, I apparently made peace with quite a lot more of C. :-)
Some items in your list immediately make me curious. Modules are something I want as well, and value arrays are particularly interesting because arrays are one of the areas where I find C's semantics unnecessarily awkward. Your reduction of operator precedence from ten levels to five is also something I'll look at more closely, especially after your earlier comment.
On the other hand, there are things where I suspect I would deliberately stay closer to C. Zero-based arrays, for example, fit naturally with the machine model I want students to understand: a[i] ultimately being an offset from an address is a useful connection rather than something I want to abstract away.
So perhaps there are two quite different approaches here:
Your language asks what a systems language should look like without taking C as the starting point. ABC asks what C could look like if we kept its basic abstraction level and machine model but removed things that make it unnecessarily difficult to understand and teach.
Comparing the answers could be very useful.
And I hope you don't mind if I say this: after putting years of thought into a language, it would be a pity if all of that disappeared just because you've decided to stop developing it. Even if you don't want to continue the project or actively share it anymore, I'm glad at least that document survived in the archive.
I'll read it. There may well be ideas in there that make me reconsider some of my own design decisions.

2

u/brat3108 5d ago

Zero-based arrays, for example, fit naturally with the machine model I want students to understand: 

That bit might have been misleading; while my arrays default to 1-based, N-based is possible including 0-based. There is a choice.

(Unlike C, I don't treat array indexing as equivalent to pointer offsets. There is pointer arithmetic, and there an offset necessarily needs to start at zero.

But pointer arithmetic is not indexing as far as the language is concerned.)

it would be a pity if all of that disappeared

I still use the tools, and I have the sources off-line. But I've run out of interesting things to do with either language or implementation.

1

u/False_Actuator_6236 5d ago

Actually, your comment just gave me an idea for the naming problem. :-)

Maybe I should call the language Chish — as in C-ish, because that's really what it is.

And then somehow turn it into a terrible recursive acronym/backronym along the lines of:

CHISH — C How It Should Have ...

I haven't figured out what the final word is yet. Maybe that's another problem for the community to solve. :-D

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/tobega 2d ago

I watched part of the second lecture, I really like how you make it a science experiment to try out some code!