This chapter ends with two working compilers on your machine and a native executable that one of them produced. Nothing here is a sketch: every command below is run exactly as written.

What you need

A 64-bit Linux machine on x86-64 or ARM64 — or Docker on any host, which gives you one.

If you have Docker, take that road. It is one command, it works identically on both architectures, and it leaves nothing on your machine when you are done:

git clone https://gitlab.com/mica-lang/mica-container.git
cd mica-container
docker run -it --rm -v "$PWD:/work" -w /work micalang/mica

Everything from here on runs inside that shell. If you would rather install natively, Installing Mica is two packages and a .deb; come back here afterwards and clone the same repository.

Step 1 — the backend

The two compilers in this series produce native code by handing their work to Dragon, the backend that compiles the Mica language. Dragon is not in the image by default, and its absence is deliberate:

mica-install-sdk

That command prints the licence terms, waits for you, and then installs two files: a static archive /usr/lib/libmicadragon-<arch>.a and a C header /usr/include/mica_dragon.h.

Read the terms before you accept them. The Mica compiler and the Dragon SDK ship under different licences, and this is the one place in the Mica ecosystem where that matters:

Mica compilerDragon SDK
Licencefree for every purpose, commercial work includedPolyForm Noncommercial 1.0.0
Free foranythingresearch, teaching, personal study, hobby projects; educational, charitable, public-research and government institutions
Commercial useincludedby separate written agreementinfo@mica-dev.com

If you are working through this series as a student, a teacher, or on your own time, you are inside the free grant. If you are at a company and this is work, talk to us first — that is what the address is for.

Step 2 — build the C twin

cd sdk/pl0/c
make

That compiles seven translation units and links them against the archive:

cc -std=c23 -Wall -Wextra -I /usr/include -o ../build/c/pl0 \
   diagnostics.c scanner.c symbols.c parser.c pcode.c spectra.c pl0.c \
   /usr/lib/libmicadragon-arm64.a -lpthread

There is no build system to learn: the Makefile runs one cc command. If you skipped step 1, make says so in one line rather than burying you in errors about a missing header:

pl0: the Dragon SDK is not installed - run 'mica-install-sdk'
     (needs /usr/include/mica_dragon.h and /usr/lib/libmicadragon-arm64.a)

Step 3 — run a program

make run

runs all six checked-in programs. To run one by hand:

../build/c/pl0 run ../testdata/square.pl0
1
1
2
4
3
9
4
16
5
25
6
36
7
49
8
64
9
81
10

That is the compiler compiling square.pl0 and then executing it on Wirth’s stack machine, which lives inside the same binary. Remember from chapter 1 that PL/0 has no output statement — every one of those numbers is a store.

Step 4 — look at the code it generated

../build/c/pl0 list ../testdata/square.pl0
    0   jmp   0     8
    1   jmp   0     2
    2   int   0     3
    3   lod   1     3
    4   lod   1     3
    5   opr   0     4
    6   sto   1     4
    7   opr   0     0
    8   int   0     5
    9   lit   0     1
   10   sto   0     3
   11   lod   0     3
   12   lit   0    10
   13   opr   0    10
   14   jpc   0    21
   15   cal   0     2
   16   lod   0     3
   17   lit   0     1
   18   opr   0     2
   19   sto   0     3
   20   jmp   0    11
   21   opr   0     0

Twenty-two instructions, and that is the entire square program compiled. You will be able to read every line of this by the end of chapter 8 — but you can already guess a lot. Line 5 is opr 0 4, the multiply, sitting between two lods that push arg twice. Line 20 jumps back to 11, which is the while loop’s test.

Print it and keep it. It is the most useful single page in this series.

Step 5 — native code

Now the other back end:

../build/c/pl0 build square ../testdata/square.pl0
validate: 0 findings
build: ok

Three files appeared:

FileWhat it is
square.ilthe program in Spectra, the intermediate language Dragon compiles
square.snative assembly for your machine
square.rt.sa small runtime unit every Mica-family link joins

Look at the first one — it is the interesting one:

@main:
    prologue @main, 0
    t1.1:int64 = copyLiteral 1:int64 #1
    storeVariable t1.1:int64, v1.1:int64 #2
    t1.2:string = copyLiteral "%lld\n":string #3
    t1.3:int64 = loadVariable v1.1:int64 #4
    call @wprintf, 0:int32, @a1.1
         argument t1.2:string
         argument t1.3:int64
@l1.3:
    t1.4:int64 = loadVariable v1.1:int64 #5
    t1.5:int64 = copyLiteral 10:int64 #6
    t1.6:bool = less t1.4:int64, t1.5:int64 #7
    jumpNotEqual t1.6:bool, @l1.5
    call @f1.1, 0:int32
    ...

That is arg := 1 followed by the store’s print, then the while test. Compare it with the p-code listing above: same program, two very different representations. Chapters 8 and 9 are about exactly that difference.

Link and run it. The two .s files join the Dragon runtime — the floor every emitted program links, installed by the mica-dragon-rt package — and nothing else: the program’s one import, wprintf, is libc’s own.

gcc -no-pie -o square square.s square.rt.s /usr/lib/libmicadragon-rt-arm64.a
./square
1
1
2
4
3
9
...

The same numbers. You have now taken a program written in a small language through a real compiler pipeline — scan, parse, intermediate code, optimization, register allocation, instruction selection — and run the machine code that came out. That the output matches the stack machine’s byte for byte is not a coincidence; it is the property chapter 10 is about.

Step 6 — the Mica twin

The same compiler, in the other language:

cd ../mica
make run

The build command is one line, and it names no backend at all:

mica --compile --link --optimize debug --platform linux,arm64,utf-32 --assembly intel \
     --source diagnostics.mica,scanner.mica,symbols.mica,pcode.mica,spectra.mica,parser.mica,pl0.mica \
     --build ../build/Pl0

There is no -I, no archive on the command line, and no configuration file. A Mica program reaches the engine by importing it:

imp
    ContextNew, ContextFree, RegistryNew : dragon;
    UnitNew, UnitDump, Validate, Build : dragon;

The compiler carries that surface itself, so the program compiles and is diagnosed before any licence question arises — only the link asks for engine code, and the driver finds the archive on its own.

One difference you will notice immediately: the Mica twin takes its command on standard input rather than as arguments.

echo "run ../testdata/square.pl0" | ../build/Pl0/Pl0

That is not a design statement about command lines; the Mica standard library does not publish process arguments yet, so the twin asks for its command the way it can. It is the one place where the two compilers differ for a reason that has nothing to do with compilers.

Step 7 — see them agree

Run the same program through both:

../build/c/pl0 run ../testdata/staticlinks.pl0
echo "run ../testdata/staticlinks.pl0" | ../build/Pl0/Pl0
20
0
40
41

Identical, and that is the smallest demonstration of the idea this series keeps coming back to: two independent implementations that agree are evidence; one implementation that looks right is not.

Where things are

sdk/pl0/
├── c/            the C23 twin — seven .c/.h pairs and a Makefile
├── mica/         the Mica twin — seven .mica units, a Makefile and a mica.project
└── testdata/     six PL/0 programs, shared by both

Both twins carry a diagnostics command that runs their own error table through their own parser — worth trying now, and worth understanding after chapter 6:

../build/c/pl0 diagnostics | tail -3
code 9 a program without its period: ok
recovery: 5 diagnostics
diagnostics: 25 of 25

If something did not work

SymptomCause
mica-install-sdk: command not foundyou are not in the container image; install the SDK natively instead
the Dragon SDK is not installedstep 1 was skipped, or you are on an architecture the SDK is not published for
libmicadragon-rt…: No such file at the link in step 5the runtime package installs it — check with ls /usr/lib/libmicadragon-rt-*.a
the Mica twin hangsit is waiting for its command on standard input; pipe one in, as step 6 shows

Next: 3 — The shape of a compiler, where the seven parts get named and put in order.