RSS Amplifier

Abdul Rahman Sibahi · May 24, 2025

Chapter 6: Conditions

0
Sign in to vote or save

This page cannot be shown here. You can still read it on the original site — the toolbar below keeps your place in the directory.

This is chapter 6 of implementing Writing a C Compiler in Zig. So, without further ado, time to get on with if statements and conditional expressions. Lexer New keywords! if and else , and two new tokens ? and : . Apparently the word query is an acceptable single word name for a question mark so I am going with that in the lexer. The new keywords are simple, enough, but it requires updating the…

This is chapter 6 of implementing Writing a C Compiler</a> in Zig. So, without further ado, time to get on with if</code> statements and conditional expressions.</p>


Lexer</h2>

New keywords! if</code> and else</code>, and two new tokens ?</code> and :</code>. Apparently the word query</code> is an acceptable single word name for a question mark so I am going with that in the lexer.</p>

The new keywords are simple, enough, but it requires updating the keyword map.</p>

pub const</span> keywords</span> =</span> std.StaticStringMap(Tag).initComptime(.{</span></span>
    .{ </span>"int"</span>, .type_int },</span></span>
    .{ </span>"return"</span>, .keyword_return },</span></span>
    .{ </span>"void"</span>, .keyword_void },</span></span>
</span>
    // new lines :</span></span>
    .{ </span>"if"</span>, .keyword_if },</span></span>
    .{ </span>"else"</span>, .keyword_else },</span></span>
});</span></span></code></pre>

The two new tokens are trivial to add and require no new states in the lexer's state machine. So onto AST and parsing.</p>

AST</h2>

One new statement and one new expression today. Compound statements survive another day without being implemented.</p>

pub const</span> Stmt</span> =</span> union</span>(</span>enum</span>) {</span></span>
    // snip --</span></span>
    @"if":</span> struct</span> { cond:</span> *</span>Expr, then:</span> *</span>Stmt, @"else":</span> ?*</span>Stmt },</span></span>
};</span></span></code></pre>
pub const</span> Expr</span> =</span> union</span>(</span>enum</span>) {</span></span>
    // snip --</span></span>
    ternary:</span> struct</span> { </span>*</span>Expr,</span> *</span>Expr,</span> *</span>Expr },</span></span>
};</span></span></code></pre>

And that is pretty much it for the AST. No revision of previous major design decisions.</p>

Parser</h2>

So how do you parse an expression like c ? a : b</code>?. The rule for it is that it is right associative (not unlike the assignment operator =</code>), but</em> the expression in the middle is treated as it is part of the expression itself or between two parenthesis. In other words, think of ? a :</code> as an binary operator between c</code> and b</code> all by itself, only for parsing purposes. So first update the binop_precedence</code> function</p>

pub fn</span> binop_precedence(self:</span> @This</span>()) </span>?</span>struct</span> { </span>u8</span>,</span> u8</span> } {</span></span>
    return switch</span> (self) {</span></span>
        .query</span> =></span> .{ </span>3</span>,</span> 0</span> },</span></span>
        // the rest</span></span>
    };</span></span>
}</span></span></code></pre>

Then update parse_expr</code> to deal with it. This is slightly trickier than usual. The middle expression should be parsed before the right hand side is. Therefore this silly dance, before parsing rhs</code>.</p>

const</span> then_ptr:</span> ?*</span>ast.Expr</span> =</span> if</span> (current.tag</span> ==</span> .query) t: {</span></span>
    const</span> then</span> =</span> try</span> parse_expr(arena, tokens,</span> 0</span>);</span></span>
    try</span> expect(.colon, tokens);</span></span>
</span>
    break</span> :t</span> try</span> utils.create(ast.Expr, arena, then);</span></span>
} </span>else</span> null</span>;</span></span></code></pre>

Then, in the big switch board, this tiny line is added.</p>

.greater_equals</span> =></span> .{ .binop_ge</span> =</span> bin_op },</span></span>
.query</span> =></span> .{ .ternary</span> =</span> .{ lhs_ptr, then_ptr.</span>?</span>, rhs_ptr } },</span> // <-</span></span></code></pre>

And voila. Ternary expressions are parsed now. This creates a very tiny bit of waste creating the boiler-plate reducing bin_op</code> (which is shared between all the other expressions). But all is fair in the name of easier to write code.</p>

For parsing the if</code> statement, I hit upon a previous design decision. In adding declarations last chapter, I decided to forgo a separate parse_stmt</code> in exchange for a simpler, at the time, single parse_block_item</code>, and discriminating then between declarations and statements. Now, since the then</code> argument of the if</code> statement can only be a statement and not a declaration, this requires a divorce, and a separate parse_stmt</code>. Declarations are not ready to leave the house yet and therefore will remain in parse_block_item</code>. This is the trimmed parse_block_item</code>, followed by parse_stmt</code> building its own house.</p>

fn</span> parse_block_item(</span></span>
    arena: std.mem.Allocator,</span></span>
    tokens:</span> *</span>lexer.Tokenizer,</span></span>
) Error</span>!</span>ast.BlockItem {</span></span>
    const</span> current</span> =</span> tokens.next() </span>orelse</span></span>
        return</span> error</span>.NotEnoughJunk;</span></span>
</span>
    switch</span> (current.tag) {</span></span>
        .type_int</span> =></span> {</span></span>
            const</span> name</span> =</span> try</span> expect(.identifier, tokens);</span></span>
            const</span> new_token</span> =</span> tokens.next() </span>orelse</span></span>
                return</span> error</span>.NotEnoughJunk;</span></span>
</span>
            const</span> init:</span> ?*</span>ast.Expr</span> =</span> switch</span> (new_token.tag) {</span></span>
                .equals</span> =></span> ret: {</span></span>
                    const</span> expr</span> =</span> try</span> parse_expr(arena, tokens,</span> 0</span>);</span></span>
                    const</span> expr_ptr</span> =</span> try</span> utils.create(ast.Expr, arena, expr);</span></span>
</span>
                    try</span> expect(.semicolon, tokens);</span></span>
                    break</span> :ret expr_ptr;</span></span>
                },</span></span>
                .semicolon</span> =></span> null</span>,</span></span>
                else</span> =></span> return</span> error</span>.SyntaxError,</span></span>
            };</span></span>
</span>
            return</span> .decl(.{ .name</span> =</span> name, .init</span> =</span> init });</span></span>
        },</span></span>
        else</span> =></span> {</span></span>
            tokens.put_back(current);</span></span>
            const</span> stmt</span> =</span> try</span> parse_stmt(arena, tokens);</span></span>
</span>
            return</span> .stmt(stmt);</span></span>
        },</span></span>
    }</span></span>
}</span></span>
</span>
fn</span> parse_stmt(</span></span>
    arena: std.mem.Allocator,</span></span>
    tokens:</span> *</span>lexer.Tokenizer,</span></span>
) Error</span>!</span>ast.Stmt {</span></span>
    const</span> current</span> =</span> tokens.next() </span>orelse</span></span>
        return</span> error</span>.NotEnoughJunk;</span></span>
    switch</span> (current.tag) {</span></span>
        .semicolon</span> =></span> return</span> .</span>null</span>,</span></span>
        .keyword_return</span> =></span> {</span></span>
            const</span> expr</span> =</span> try</span> parse_expr(arena, tokens,</span> 0</span>);</span></span>
            const</span> expr_ptr</span> =</span> try</span> utils.create(ast.Expr, arena, expr);</span></span>
            try</span> expect(.semicolon, tokens);</span></span>
            return</span> .{ .@"return"</span> =</span> expr_ptr };</span></span>
        },</span></span>
        // if statement goes here</span></span>
        else</span> =></span> {</span></span>
            tokens.put_back(current);</span></span>
            const</span> expr</span> =</span> try</span> parse_expr(arena, tokens,</span> 0</span>);</span></span>
            const</span> expr_ptr</span> =</span> try</span> utils.create(ast.Expr, arena, expr);</span></span>
            try</span> expect(.semicolon, tokens);</span></span>
            return</span> .{ .expr</span> =</span> expr_ptr };</span></span>
        },</span></span>
    }</span></span>
}</span></span></code></pre>

The if</code> statement parsing is slightly more involved than usual. The condition is always an expression that is parenthesized, and the then</code> statement always exists. The else</code> statement's existence depends on the existence of the keyword else</code>, so I have to peek</em>. But because of the way I implemented peeking, by "putting back" the consumed token, it makes for some weird looking code, making more than usual use of block expressions.</p>

.keyword_if</span> =></span> {</span></span>
    try</span> expect(.l_paren, tokens);</span></span>
    const</span> cond</span> =</span> try</span> parse_expr(arena, tokens,</span> 0</span>);</span></span>
    const</span> cond_ptr</span> =</span> try</span> utils.create(ast.Expr, arena, cond);</span></span>
    try</span> expect(.r_paren, tokens);</span></span>
</span>
    const</span> then</span> =</span> try</span> parse_stmt(arena, tokens);</span></span>
    const</span> then_ptr</span> =</span> try</span> utils.create(ast.Stmt, arena, then);</span></span>
</span>
    const</span> peek</span> =</span> tokens.next() </span>orelse</span></span>
        return</span> error</span>.NotEnoughJunk;</span></span>
</span>
    // weird looking code starts here.</span></span>
    const</span> else_ptr:</span> ?*</span>ast.Stmt</span> =</span> if</span> (peek.tag</span> ==</span> .keyword_else) s: {</span></span>
        const</span> e</span> =</span> try</span> parse_stmt(arena, tokens);</span></span>
        break</span> :s</span> try</span> utils.create(ast.Stmt, arena, e);</span></span>
    } </span>else</span> n: {</span></span>
        tokens.put_back(peek);</span></span>
        break</span> :n</span> null</span>;</span></span>
    };</span></span>
</span>
    return</span> .{ .@"if"</span> =</span> .{</span></span>
        .cond</span> =</span> cond_ptr,</span></span>
        .then</span> =</span> then_ptr,</span></span>
        .@"else"</span> =</span> else_ptr,</span></span>
    } };</span></span>
},</span></span></code></pre>

Now, before eating the pudding, the semantic analysis phase requires some tiny updates. There are no changes other than handling the new statements and expressions, and adding those is 11 lines total.</p>

// resolve_stmt</span></span>
.@"if"</span> => |</span>i</span>|</span> {</span></span>
    try</span> resolve_expr(bp, i.cond);</span></span>
    try</span> resolve_stmt(bp, i.then);</span></span>
    if</span> (i.@"else") </span>|</span>e</span>|</span></span>
        try</span> resolve_stmt(bp, e);</span></span>
},</span></span>
</span>
// resolve_expr</span></span>
.ternary</span> => |</span>t</span>|</span> {</span></span>
    try</span> resolve_expr(bp, t.@"0");</span></span>
    try</span> resolve_expr(bp, t.@"1");</span></span>
    try</span> resolve_expr(bp, t.@"2");</span></span>
},</span></span></code></pre>

And that is it, really. Back to proving the pudding. Testing it on this lovely and non-confusing C file:</p>

int</span> main(</span>void</span>) {</span></span>
    int</span> a </span>=</span> 0</span>;</span></span>
    if</span> (</span>!</span>a)</span></span>
        if</span> (</span>3</span> /</span> 4</span>)</span></span>
            a </span>=</span> 3</span>;</span></span>
        else</span></span>
            a </span>=</span> 8</span> /</span> 2</span>;</span></span>
</span>
    return</span> a;</span></span>
}</span></span></code></pre>

Gives me this result, which judging by the formatting is probably parsed correctly.</p>

PROGRAM</span></span>
	FUNCTION main</span></span>
		int a <- 0;</span></span>
		IF (! a)</span></span>
			IF (/ 3 4)</span></span>
				a <- 3;</span></span>
			ELSE</span></span>
				a <- (/ 8 2);</span></span>
		RETURN a</span></span></code></pre>

All parsing and semantic analysis are succeeding and failing where they should. That's good news.</p>


The Eye Test</h2>

Since I implemented all this nice pretty parsing, I would like to inspect on all the valid test files of the chapter. At least, visually confirm that there is nothing out of the ordinary. Same as I have been doing for C files in each chapter, but instead of copying a couple of interesting ones and running my app on them, I can do a batch comparison between each C file and the visual result of parsing it or validation or whatever.</p>

The first thought was to do it with a shell script. But shell is voodoo to me.1</a></sup> build.zig</code> is right there, so maybe I can use it to that effect.</p>

The new command would need two arguments: the stage, which is then passed directly to the executable, and the target folder. I cannot just pass the target chapter because not all chapters have the same structure.</p>

What followed was one of the most aggravating and annoying parts of this series.</p>

The first hurdle was navigating Zig's standard library's file system API. The docs are not very clear and the different APIs are organized in weird places, and how does any of that tie back in the build system?</p>

There is a walk</code> function.2</a></sup> Excellent, but it needs a std.fs.Dir</code> object. All I have is a std.Build.LazyPath</code>. How do I make one into the other?</p>

Turns out LazyPath</code> has a getPath3</code> method (getPath</code> and getPath2</code> are deprecated) that gives you a std.Build.Cache.Path</code> object. And that</em> in its turn has an openDir</code> method that gives you a std.fs.Dir</code> that then you can walk</code>. Try figuring that out only from the docs.</p>

The document traversal afterwards was simple enough. Basic Zig code.</p>

while</span> (</span>try</span> walker.next()) </span>|</span>entry</span>|</span> if</span> (entry.kind</span> ==</span> .file</span> and</span></span>
    std.mem.endsWith(</span>u8</span>, entry.basename,</span> ".c"</span>)) </span>// not to do .s files</span></span>
{</span></span>
    // insert entry handling here</span></span>
};</span></span></code></pre>

The second hurdle, and if you actually know what I know now this will all seem very stupid, is a bit more involved. At first I passed the arguments as I did for the other previous commands. So like this</p>

zig</span> build eye</span> --</span> path/to/test/folder</span> --parse</span></span></code></pre>

And I desugared the arguments accordingly.</p>

const</span> args: []</span>const</span> []</span>const</span> u8 =</span> b.args</span> orelse</span></span>
    &</span>.{ </span>"./c_files/"</span>,</span> "--lex"</span> };</span> // least harmful default;</span></span></code></pre>

So this way, if I don't pass anything, these would pass and the tree would be walked properly. Now zig build eye</code> worked fine.</p>

The problem is, if I run another</em> command than eye</code>, (say .. run</code>), the directory walk would run anyway and then the build script would fail because the first argument is not a valid directory or whatever.</p>

I did know that, btw.</p>

Looking for help again someone suggested that the solution is to create a custom step, and even provided helpful code to do so. Here was a nice Gist I found on Google</a>. This seems perfect. So I do this:</p>

const</span> std</span> =</span> @import</span>(</span>"std"</span>);</span></span>
pub fn</span> build(b:</span> *</span>std.Build) </span>!void</span> {</span></span>
</span>
    // stuff unchanged</span></span>
</span>
    { </span>// `zig build eye` command</span></span>
        const</span> eye_step</span> =</span> b.step(</span>"eye"</span>,</span> "eye test all the files in a given directory"</span>);</span></span>
</span>
        var</span> closure</span> =</span> b.allocator.create(Closure) </span>catch unreachable</span>;</span></span>
        closure.</span>* =</span> .{ .exe</span> =</span> exe, .step</span> =</span> std.Build.Step.init(.{</span></span>
            .id</span> =</span> .custom,</span></span>
            .name</span> =</span> "inner_eye"</span>,</span></span>
            .makeFn</span> =</span> make_eye_step,</span></span>
            .owner</span> =</span> b,</span></span>
        }) };</span></span>
</span>
        eye_step.dependOn(</span>&</span>closure.step);</span></span>
    }</span></span>
}</span></span>
</span>
const</span> Closure</span> =</span> struct</span> {</span></span>
    exe:</span> *</span>std.Build.Step.Compile,</span></span>
    step: std.Build.Step,</span></span>
};</span></span>
</span>
fn</span> make_eye_step(step:</span> *</span>std.Build.Step, _: std.Build.Step.MakeOptions) </span>!void</span> {</span></span>
    const</span> b</span> =</span> step.owner;</span></span>
    const</span> closure:</span> *</span>const</span> Closure</span> =</span> @fieldParentPtr</span>(</span>"step"</span>, step);</span></span>
    const</span> exe</span> =</span> closure.exe;</span></span>
</span>
    const</span> args: []</span>const</span> []</span>const</span> u8 =</span> b.args</span> orelse</span></span>
        &</span>.{ </span>"./c_files/"</span>,</span> "--lex"</span> };</span> // least harmful default;</span></span>
</span>
    // directory api boilerplate</span></span>
    const</span> lazy</span> =</span> b.path(args[</span>0</span>]);</span></span>
    const</span> path</span> =</span> lazy.getPath3(b,</span> null</span>);</span></span>
</span>
    const</span> dir</span> =</span> try</span> path.openDir(</span>""</span>, .{ .access_sub_paths</span> =</span> false</span> });</span></span>
    var</span> walker</span> =</span> try</span> dir.walk(b.allocator);</span></span>
    defer</span> walker.deinit();</span></span>
</span>
    var</span> prev_run_cmd:</span> ?*</span>std.Build.Step.Run</span> =</span> null</span>;</span></span>
</span>
    while</span> (</span>try</span> walker.next()) </span>|</span>entry</span>|</span> {</span></span>
        if</span> (entry.kind</span> ==</span> .file</span> and</span> std.mem.endsWith(</span>u8</span>, entry.basename,</span> ".c"</span>)) {</span></span>
            const</span> file</span> =</span> entry.path;</span></span>
</span>
            const</span> bat</span> =</span> b.addSystemCommand(</span>&</span>.{ </span>"bat"</span>, file });</span></span>
            bat.setCwd(lazy);</span> // I am proud of myself for finding this</span></span>
            bat.stdio</span> =</span> .inherit;</span></span>
</span>
            // each invokation of `bat` depends on the previous run step so they're sequential</span></span>
            if</span> (prev_run_cmd) </span>|</span>c</span>|</span></span>
                bat.step.dependOn(</span>&</span>c.step);</span></span>
</span>
            const</span> run_cmd</span> =</span> b.addRunArtifact(exe);</span></span>
            run_cmd.setCwd(lazy);</span></span>
            run_cmd.addArg(file);</span></span>
            run_cmd.addArgs(args[</span>1</span>..]);</span></span>
            run_cmd.stdio</span> =</span> .inherit;</span></span>
</span>
            // exactly what the default for `zig build run` does.</span></span>
            run_cmd.step.dependOn(b.getInstallStep());</span></span>
</span>
            // keeping the squence</span></span>
            run_cmd.step.dependOn(</span>&</span>bat.step);</span></span>
            prev_run_cmd</span> =</span> run_cmd;</span></span>
        }</span></span>
    }</span></span>
</span>
    // making sure the OG step depends on the tail of the chain.</span></span>
    step.dependOn(</span>&</span>prev_run_cmd.</span>?</span>.step);</span></span>
}</span></span></code></pre>

The way I see it, I am doing all the right things. All the steps depend on the proper steps, and best of all, the other zig build</code> commands like run</code> and test</code> and what have you, all work fine.</p>

Except this does not work. And I could not figure out.</p>

Running this would dutifully go through the loop properly, as I was able to verify by inserting a few prints</code>, but the commands do not run. And asking for help on the Discord only led me into StackOverflow-esque answers that I am not interested in repeating.</p>

Asking for help on Ziggit</a> actually gave me enough info to understand the real problem. This reply in particular</a> answered the dilemma for me.</p>

Do you know where the two phases of the build system are mentioned in the Official Build System Documentation and Intro</a>? Because I cannot find it. It is mentioned in passing in the documentation of some methods like getPath3</code> and run</code>, but with no proper explanation.</p>

Anyway, it clicked, and I was able to use the user provided options to insert the needed details.</p>

{ </span>// `zig build eye` command</span></span>
    const</span> eye_step</span> =</span> b.step(</span>"eye"</span>,</span> "Eye test all the files in a given directory"</span>);</span></span>
</span>
    if</span> (b.option(std.Build.LazyPath,</span> "folder"</span>,</span> "Path to eye"</span>)) </span>|</span>lazy</span>|</span> {</span></span>
        // same logic as before, more or less</span></span>
        try</span> walk_tree(b, exe, eye_step, lazy);</span></span>
    } </span>else</span> {</span></span>
        // *this* step, and only this one would fail</span></span>
        // if the `folder` option is not set, because it</span></span>
        // depends on `fail` only if the path does not exist</span></span>
        const</span> fail</span> =</span> b.addFail(</span>"folder needed for eye"</span>);</span></span>
        eye_step.dependOn(</span>&</span>fail.step);</span></span>
    }</span></span>
}</span></span></code></pre>

b.option</code> creates a new argument for the user of the build system. This way, if the argument is present, the directory tree is walked and the proper dependencies are added to the eye</code> command. If it is not, then the eye</code> command will fail.</p>

Other commands are unchanged and unaffected. The only difference from my first envisioning it is the way it is called, which I am not happy with, but eh. Pick your battles.</p>

zig</span> build eye</span> -Dfolder=</span>"./c_files/"</span> -- --parse</span></span></code></pre>

And it works exactly like I wanted. It calls bat</code> on every file in a given directory followed by paella</code> at the given stage, allowing for a quick eye test between the two. I'd paste the whole output here but I will settle for one file.</p>

File:</span> multiple_if.c</span></span>
1</span> int main</span>(void)</span> {</span></span>
2</span>     int a =</span> 0</span>;</span></span>
3</span>     int b =</span> 0</span>;</span></span>
4</span></span>
5</span>     if</span> (a)</span></span>
6</span>         a =</span> 2</span>;</span></span>
7</span>     else</span></span>
8</span>         a =</span> 3</span>;</span></span>
9</span></span>
10</span>     if</span> (b)</span></span>
11</span>         b =</span> 4</span>;</span></span>
12</span>     else</span></span>
13</span>         b =</span> 5</span>;</span></span>
14</span></span>
15</span>     return a + b</span>;</span></span>
16</span> }</span></span>
PROGRAM</span></span>
	FUNCTION</span> main</span></span>
		int</span> a</span> <</span>-</span> 0</span>;</span></span>
		int</span> b</span> <</span>-</span> 0</span>;</span></span>
		IF</span> a</span></span>
			a</span> <</span>-</span> 2</span>;</span></span>
		ELSE</span></span>
			a</span> <</span>-</span> 3</span>;</span></span>
		IF</span> b</span></span>
			b</span> <</span>-</span> 4</span>;</span></span>
		ELSE</span></span>
			b</span> <</span>-</span> 5</span>;</span></span>
		RETURN (+</span> a b</span>)</span></span>
================================</span></span></code></pre>

Automatic Formatting</h2>

While mucking around in the documentation I discovered a nice addFmt</code> method that is there to check for proper formatting in CI, but can just format the code for you. Adding these two lines pretty much anywhere in the file formatted everything every time I zig build</code>.</p>

const</span> fmt_step</span> =</span> b.addFmt(.{ .paths</span> = &</span>.{</span>"./"</span>} });</span></span>
exe.step.dependOn(</span>&</span>fmt_step.step);</span></span></code></pre>

Internal Representation</h2>

Now is the time to get on with new IR generation. Similarly to lat chapter, no new instructions need be appended. So after the IR generation stage, it is done. Short chapter all around.</p>

Generating the IR for if</code> statements is straightforward, with a slight complication around the optional else</code> statement.</p>

.@"if"</span> => |</span>c</span>|</span> {</span></span>
    const</span> cond</span> =</span> try</span> expr_emit_ir(bp, c.cond);</span></span>
    const</span> else_label</span> =</span> try</span> bp.make_temporary(</span>"else"</span>);</span></span>
    try</span> bp.append(.{ .jump_z</span> =</span> .{ .cond</span> =</span> cond, .target</span> =</span> else_label } });</span></span>
    try</span> stmt_emit_ir(bp, c.then);</span></span>
    if</span> (c.@"else") </span>|</span>@"else"</span>|</span> {</span></span>
        const</span> end_label</span> =</span> try</span> bp.make_temporary(</span>"end"</span>);</span></span>
        try</span> bp.append(.{ .jump</span> =</span> end_label });</span></span>
        try</span> bp.append(.{ .label</span> =</span> else_label });</span></span>
        try</span> stmt_emit_ir(bp, @"else");</span></span>
        try</span> bp.append(.{ .label</span> =</span> end_label });</span></span>
    } </span>else try</span> bp.append(.{ .label</span> =</span> else_label });</span></span>
},</span></span></code></pre>

For conditional expressions, it is much of the same, except expr_emit_ir</code> is called instead and its value is returned, and there is no optional else</code> clause.</p>

.ternary</span> => |</span>t</span>|</span> {</span></span>
    const</span> else_label</span> =</span> try</span> bp.make_temporary(</span>"else"</span>);</span></span>
    const</span> end_label</span> =</span> try</span> bp.make_temporary(</span>"end"</span>);</span></span>
    const</span> dst_name</span> =</span> try</span> bp.make_temporary(</span>"ter"</span>);</span></span>
    const</span> dst: ir.Value</span> =</span> .{ .variable</span> =</span> dst_name };</span></span>
</span>
    const</span> cond</span> =</span> try</span> expr_emit_ir(bp, t.@"0");</span></span>
    try</span> bp.append(.{ .jump_z</span> =</span> .{ .cond</span> =</span> cond, .target</span> =</span> else_label } });</span></span>
    const</span> then</span> =</span> try</span> expr_emit_ir(bp, t.@"1");</span></span>
    try</span> bp.append(.{ .copy</span> =</span> .init(then, dst) });</span></span>
</span>
    try</span> bp.append(.{ .jump</span> =</span> end_label });</span></span>
    try</span> bp.append(.{ .label</span> =</span> else_label });</span></span>
</span>
    const</span> else_</span> =</span> try</span> expr_emit_ir(bp, t.@"2");</span></span>
    try</span> bp.append(.{ .copy</span> =</span> .init(else_, dst) });</span></span>
</span>
    try</span> bp.append(.{ .label</span> =</span> end_label });</span></span>
</span>
    return</span> dst;</span></span>
},</span></span></code></pre>

And that's it. I do not even have to run the intermediate codepaths as no changes to instructions or assembly generation this chapter. There is a small extra credit of implementing goto</code>, but I am skipping extra credit this time.</p>


Lessons Learned</h2>

Not to repeat ranting at Zig's documentation, but I definitely now understand Zig's build system at a deeper level than before.</p>


  1. If all else fails I can get some LLM to generate it for me. It would work fine. ↩</a></p> </li>

  2. Turns out it is recursive and I actually wanted iterate()</code>. But that is on me, it is clearly written. ↩</a></p> </li> </ol> </section>

Read on ar-ms.me

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.