RSS Amplifier

Abdul Rahman Sibahi · Jun 1, 2025

Chapter 9: Functions

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.

After eight grueling (not really) chapters of Writing a C Compiler , time to implement more assembly instructions. Functions! Linkage! Commas! Lexer, AST, and Parser Lexer just has a comma now. I thought about adding the comma operator but that didn't seem worth the trouble. The AST has two new additions. Function call expressions and function declarations (which are rebranded and improved…

After eight</a> grueling (not really) chapters of Writing a C Compiler</a>, time to implement more assembly instructions. Functions! Linkage! Commas!</p>


Lexer, AST, and Parser</h2>

Lexer just has a comma now. I thought about adding the comma operator but that didn't seem worth the trouble.</p>

The AST has two new additions. Function call expressions and function declarations (which are rebranded and improved function definitions)! Other changes include how the structures themselves are defined. A program is now a list</em> of function declarations, instead of just one. How about that?</p>

These are the new AST nodes. I am not sure these compile or not yet, which I will find out when I am done with the parser. The use of SegmentedList</code> is discussed a couple of chapters ago as a more Arena-friendly collection type.</p>

pub const</span> Prgm</span> =</span> struct</span> {</span></span>
    funcs: std.SegmentedList(FuncDecl,</span> 0</span>),</span></span>
};</span></span>
</span>
pub const</span> Block</span> =</span> struct</span> {</span></span>
    body: std.SegmentedList(BlockItem,</span> 0</span>),</span></span>
};</span></span>
</span>
pub const</span> BlockItem</span> =</span> union</span>(</span>enum</span>) {</span></span>
    D: Decl,</span></span>
    S: Stmt,</span></span>
};</span></span>
</span>
pub const</span> Decl</span> =</span> union</span>(</span>enum</span>) {</span></span>
    F: FuncDecl,</span></span>
    V: VarDecl,</span></span>
};</span></span>
</span>
pub const</span> FuncDecl</span> =</span> struct</span> {</span></span>
    name: []</span>const</span> u8</span>,</span></span>
    params: std.SegmentedList(Identifier,</span> 0</span>),</span></span>
    block:</span> ?</span>Block,</span></span>
};</span></span>
</span>
pub const</span> VarDecl</span> =</span> struct</span> {</span></span>
    name: Identifier,</span></span>
    init:</span> ?*</span>Expr,</span></span>
};</span></span>
</span>
pub const</span> Expr</span> =</span> union</span>(</span>enum</span>) {</span></span>
    // snip --</span></span>
    func_call:</span> struct</span> { Identifier, std.SegmentedList(Expr,</span> 0</span>) },</span></span>
};</span></span>
</span>
// This was implemented last chapter fixing the Segmentation Fault!</span></span>
pub const</span> Identifier</span> =</span> union</span>(</span>enum</span>) {</span></span>
    name: []</span>const</span> u8</span>,</span></span>
    idx: utils.StringInterner.Idx,</span></span>
};</span></span></code></pre>

It is going to be annoying fixing all the type errors throughout. Nonetheless, the parsing grammar for these new node types are going to change significantly. Here is. This is the new, tentative, parse_prgm</code>. I am not sure this is entirely correct yet.</p>

pub fn</span> parse_prgm(</span></span>
    arena: std.mem.Allocator,</span></span>
    tokens:</span> *</span>lexer.Tokenizer,</span></span>
) Error</span>!</span>ast.Prgm {</span></span>
    var</span> funcs: std.SegmentedList(ast.FuncDecl,</span> 0</span>) </span>=</span> .{};</span></span>
</span>
    while</span> (tokens.next()) </span>|</span>next_token</span>|</span> {</span></span>
        tokens.put_back(next_token);</span></span>
        const</span> func_decl</span> =</span> try</span> parse_func_decl(arena, tokens);</span></span>
        try</span> funcs.append(arena, func_decl);</span></span>
    }</span></span>
</span>
    return</span> .{ .funcs</span> =</span> funcs };</span></span>
}</span></span></code></pre>

parse_func_decl</code> is the same as the old parse_func_def</code>, but with optional parameters and an optional body. Ok maybe not the same, it is a behemoth. And all this is going to get significantly more complex when adding different types than int</code>.</p>

fn</span> parse_func_decl(</span></span>
    arena: std.mem.Allocator,</span></span>
    tokens:</span> *</span>lexer.Tokenizer,</span></span>
) Error</span>!</span>ast.FuncDecl {</span></span>
    // same old</span></span>
    try</span> expect(.type_int, tokens);</span></span>
    const</span> name</span> =</span> try</span> expect(.identifier, tokens);</span></span>
</span>
    // new stuff !!</span></span>
    var</span> params: std.SegmentedList(ast.Identifier,</span> 0</span>) </span>=</span> .{};</span></span>
    { </span>// params</span></span>
        try</span> expect(.l_paren, tokens);</span></span>
        const</span> next_token</span> =</span> tokens.next() </span>orelse</span></span>
            return</span> error</span>.SyntaxError;</span></span>
        // labelled switch to loop over multiple parameters.</span></span>
        params:</span> switch</span> (next_token.tag) {</span></span>
            // old bahaviour is this:</span></span>
            .keyword_void</span> =></span> try</span> expect(.r_paren, tokens),</span></span>
</span>
            // new optional parameters</span></span>
            .type_int</span> =></span> {</span></span>
                const</span> ident</span> =</span> try</span> expect(.identifier, tokens);</span></span>
                try</span> params.append(arena, .{ .name</span> =</span> ident });</span></span>
                const</span> next_next</span> =</span> tokens.next() </span>orelse</span></span>
                    return</span> error</span>.SyntaxError;</span></span>
</span>
                // loops back here. this would be significantly more annoying</span></span>
                // to write without labeled switch</span></span>
                switch</span> (next_next.tag) {</span></span>
                    .comma</span> =></span> {</span></span>
                        try</span> expect(.type_int, tokens);</span></span>
                        continue</span> :params .type_int;</span></span>
                    },</span></span>
                    .r_paren</span> =></span> {},</span></span>
</span>
                    // could just return here but where is the fun then?</span></span>
                    else</span> =></span> continue</span> :params .invalid,</span></span>
                }</span></span>
            },</span></span>
            else</span> =></span> return</span> error</span>.SyntaxError,</span></span>
        }</span></span>
    }</span></span>
    const</span> block</span> =</span> block: {</span></span>
        const</span> peeked</span> =</span> tokens.next() </span>orelse</span></span>
            return</span> error</span>.SyntaxError;</span></span>
</span>
        switch</span> (peeked.tag) {</span></span>
            .semicolon</span> =></span> break</span> :block</span> null</span>,</span></span>
            .l_brace</span> =></span> {</span></span>
                tokens.put_back(peeked);</span></span>
                break</span> :block</span> try</span> parse_block(arena, tokens);</span></span>
            },</span></span>
            else</span> =></span> return</span> error</span>.SyntaxError,</span></span>
        }</span></span>
    };</span></span>
</span>
    return</span> .{ .name</span> =</span> name, .params</span> =</span> params, .block</span> =</span> block };</span></span>
}</span></span></code></pre>

There is one big item left, which is BlockItem</code>, which can be a function declaration as well as a variable declaration, and it is not possible to know which is which from the token type_int</code> as done before.</p>

The most straightforward way to implement it in the current code base is as follows: look for an type_int</code> token, verify there is an identifier token afterwards, without</em> recording it, then see what the third token is. If it is a semicolon</code> or an equals</code>, it is a variable; if it is a parenthesis, it is a function; otherwise it is illegal. Then the parser is rewinded to the int</code> and the correct function is called. In later chapters, with global variables, it will be apparent that the only place which only accepts one type of declarations is the for</code> loop statement, but I will cross that bridge when I get to it.</p>

fn</span> parse_decl(</span></span>
    arena: std.mem.Allocator,</span></span>
    tokens:</span> *</span>lexer.Tokenizer,</span></span>
) Error</span>!</span>ast.Decl {</span></span>
    const</span> int_token</span> =</span> tokens.next() </span>orelse</span></span>
        return</span> error</span>.NotEnoughJunk;</span></span>
    if</span> (int_token.tag</span> !=</span> .type_int) </span>return</span> error</span>.SyntaxError;</span></span>
    _</span> =</span> try</span> expect(.identifier, tokens);</span></span>
</span>
    const</span> new_token</span> =</span> tokens.next() </span>orelse</span></span>
        return</span> error</span>.NotEnoughJunk;</span></span>
</span>
    tokens.put_back(int_token);</span></span>
    switch</span> (new_token.tag) {</span></span>
        .semicolon, .equals</span> =></span> return</span> .{ .V</span> =</span> try</span> parse_var_decl(arena, tokens) },</span></span>
        .l_paren</span> =></span> return</span> .{ .F</span> =</span> try</span> parse_func_decl(arena, tokens) },</span></span>
        else</span> =></span> return</span> error</span>.SyntaxError,</span></span>
    }</span></span>
}</span></span></code></pre>

Parsing function calls is a new challenge. Previously, any identifier is immediately assumed to be a variable. But now, if the identifier is followed by parenthesis, it could be a function call with an arbitrary number of parameters. The new identifier</code> case exhibits a serious case of rightward drift.</p>

.identifier</span> =></span> {</span></span>
    const</span> next_token</span> =</span> tokens.next() </span>orelse</span></span>
        return</span> error</span>.NotEnoughJunk;</span></span>
    const</span> name</span> =</span> tokens.buffer[current.loc.start..current.loc.end];</span></span>
</span>
    switch</span> (next_token.tag) {</span></span>
        .l_paren</span> =></span> return</span> .{ .func_call</span> =</span> .{</span></span>
            .{ .name</span> =</span> name },</span></span>
            try</span> parse_args(arena, tokens),</span></span>
        } },</span></span>
        else</span> =></span> {</span></span>
            tokens.put_back(next_token);</span></span>
            return</span> .{ .@"var"</span> =</span> .{ .name</span> =</span> name } };</span></span>
        },</span></span>
    }</span></span>
},</span></span>
</span>
// elsewhere:</span></span>
fn</span> parse_args(</span></span>
    arena: std.mem.Allocator,</span></span>
    tokens:</span> *</span>lexer.Tokenizer,</span></span>
) Error</span>!</span>std.SegmentedList(ast.Expr,</span> 0</span>) {</span></span>
    // assumes l_paren already consumed</span></span>
    var</span> ret: std.SegmentedList(ast.Expr,</span> 0</span>) </span>=</span> .{};</span></span>
</span>
    const</span> current</span> =</span> tokens.next() </span>orelse</span></span>
        return</span> error</span>.NotEnoughJunk;</span></span>
</span>
    args:</span> switch</span> (current.tag) {</span></span>
        .r_paren</span> =></span> return</span> ret,</span></span>
        .comma</span> =></span> {</span></span>
            const</span> expr</span> =</span> try</span> parse_expr(arena, tokens,</span> 0</span>);</span></span>
            try</span> ret.append(arena, expr);</span></span>
</span>
            const</span> n_token</span> =</span> tokens.next() </span>orelse</span></span>
                return</span> error</span>.NotEnoughJunk;</span></span>
            continue</span> :args n_token.tag;</span></span>
        },</span></span>
        else</span> =></span> { </span>// only actually relevant for the first argument.</span></span>
            tokens.put_back(current);</span></span>
            continue</span> :args .comma;</span></span>
        },</span></span>
    }</span></span>
}</span></span></code></pre>

Semantic Analysis</h2>

This is a bit more involved than usual this chapter. In addition to variable resolution, the compiler needs to do type checking! Do all declarations of functions (which can</em> repeat), have the same number of parameters?</p>

Starting with the easier stuff, the identifier resolution pass should handle the new function syntax. Start with function calls. Here is the added part in resolve_expr</code>.</p>

.func_call</span> => |*</span>f</span>|</span> {</span></span>
    if</span> (bp.variable_map.get(f.@"0".name)) </span>|</span>entry</span>|</span> {</span></span>
        f.@"0"</span> =</span> .{ .idx</span> =</span> entry.name };</span></span>
        var</span> iter</span> =</span> f.@"1".iterator(</span>0</span>);</span></span>
        while</span> (iter.next()) </span>|</span>item</span>|</span></span>
            try</span> resolve_expr(bp, item);</span></span>
    } </span>else return</span> error</span>.Undeclaredfunction;</span></span>
},</span></span></code></pre>

Resolving function declarations had me move the creation of the main variable map back into resolve_prgm</code> instead, and create a new inner map for every function. Also, I need to update the Entry</code> type of the variable map. This is the new Entry</code>.</p>

const</span> Entry</span> =</span> struct</span> {</span></span>
    name: utils.StringInterner.Idx,</span></span>
    scope:</span> enum</span> { local, parent } </span>=</span> .local,</span></span>
    linkage:</span> enum</span> { none, external } </span>=</span> .none,</span> // <-- new</span></span>
};</span></span></code></pre>

Then this is resolve_func_decl</code>, and the inner parameters resolution, which is really just a cheap copy of resolve_var_decl</code> itself rebranded resolve_decl</code>.</p>

fn</span> resolve_func_decl(</span></span>
    bp: Boilerplate,</span></span>
    func_decl:</span> *</span>ast.FuncDecl,</span></span>
) Error</span>!void</span> {</span></span>
    if</span> (bp.variable_map.get(func_decl.name)) </span>|</span>prev</span>|</span></span>
        if</span> (prev.scope</span> ==</span> .local</span> and</span> prev.linkage</span> !=</span> .external)</span></span>
            return</span> error</span>.DuplicateFunctionDecl;</span></span>
</span>
    try</span> bp.variable_map.put(bp.gpa, func_decl.name, .{</span></span>
        .name</span> =</span> try</span> bp.strings.get_or_put(bp.gpa, func_decl.name),</span></span>
        .scope</span> =</span> .local,</span></span>
        .linkage</span> =</span> .external,</span></span>
    });</span></span>
</span>
    var</span> variable_map</span> =</span> try</span> bp.variable_map.clone(bp.gpa);</span></span>
    defer</span> variable_map.deinit(bp.gpa);</span></span>
</span>
    var</span> iter</span> =</span> variable_map.valueIterator();</span></span>
    while</span> (iter.next()) </span>|</span>value</span>|</span></span>
        value.</span>* =</span> .{</span></span>
            .name</span> =</span> value.name,</span></span>
            .scope</span> =</span> .parent,</span></span>
            .linkage</span> =</span> value.linkage,</span></span>
        };</span></span>
</span>
    const</span> inner_bp</span> =</span> bp.into_ineer(</span>&</span>variable_map);</span></span>
</span>
    var</span> iter</span> =</span> func_decl.params.iterator(</span>0</span>);</span></span>
    while</span> (iter.next()) </span>|</span>param</span>|</span> {</span></span>
        // a cheap imitation of `resolve_var_decl`</span></span>
        // should pribably be in its own function.</span></span>
        if</span> (inner_bp.variable_map.get(param.name)) </span>|</span>entry</span>|</span></span>
            if</span> (entry.scope</span> ==</span> .local)</span></span>
                return</span> error</span>.DuplicateVariableDecl;</span></span>
</span>
        const</span> unique_name</span> =</span> try</span> inner_bp.make_temporary(param.name);</span></span>
        try</span> inner_bp.variable_map.put(inner_bp.gpa, param.name, .{ .name</span> =</span> unique_name });</span></span>
</span>
        param.</span>* =</span> .{ .idx</span> =</span> unique_name };</span></span>
    }</span></span>
</span>
    if</span> (func_decl.block) </span>|*</span>block</span>|</span></span>
        try</span> resolve_block(inner_bp,</span> null</span>, block);</span></span>
}</span></span></code></pre>

One more thing, there is need to check that block level function declarations have no body. I am going to do that in resolve_block</code>.</p>

fn</span> resolve_block(</span></span>
    bp: Boilerplate,</span></span>
    current_label:</span> ?</span>utils.StringInterner.Idx,</span></span>
    block:</span> *</span>ast.Block,</span></span>
) Error</span>!void</span> {</span></span>
    var</span> iter</span> =</span> block.body.iterator(</span>0</span>);</span></span>
    while</span> (iter.next()) </span>|</span>item</span>|</span> switch</span> (item.</span>*</span>) {</span></span>
        .S</span> => |*</span>s</span>|</span> try</span> resolve_stmt(bp, current_label, s),</span></span>
        .D</span> => |*</span>d</span>|</span> switch</span> (d.</span>*</span>) {</span></span>
            .F</span> => |*</span>f</span>|</span> if</span> (f.block) </span>|</span>_</span>|</span></span>
                return</span> error</span>.IllegalFuncDefinition</span></span>
            else</span></span>
                try</span> resolve_func_decl(bp, f),</span></span>
            .V</span> => |*</span>v</span>|</span> try</span> resolve_var_decl(bp, v),</span></span>
        },</span></span>
    };</span></span>
}</span></span></code></pre>

Before delving into type checking, the book suggests to run the test suite, but expect a number of failures. Thankfully, I can check the individual folders separately using the eye test, without running the test suite in the official manner, I can tell that the compiler is passing all the right files it should at this stage, even the ones in invalid_types</code>.</p>

Type checking</h2>

The Book has the type checking done in its own pass. At first, I tried just stuffing the type checking logic right into the same identifier resolution pass. But the actual problem was that I needed a separate data structure anyway, since function declarations have to match even when they are in different scopes. So I am still doing it in the same run, but with a separate data structure.</p>

The type checking consists mostly of the following: not using the same identifier for both int</code>s and functions, making sure functions always have the same number of parameters in all declarations, and make sure a function is not defined (with a body) twice.</p>

A new data structure would be needed to stuff this info, global throughout the whole file.1</a></sup> A hashmap taking the unique identifiers as keys and their types as values. The type is either an integer or a function with defined arity (number of parameters). I also need to track whether a function has been defined or not.</p>

const</span> TypeMap</span> =</span> std.AutoHashMapUnmanaged(</span></span>
    u32</span>,</span></span>
    Type,</span></span>
);</span></span>
</span>
const</span> Type</span> =</span> union</span>(</span>enum</span>) {</span></span>
    int,</span></span>
    func:</span> struct</span> {</span></span>
        arity:</span> usize</span>,</span></span>
        defined:</span> bool</span>,</span></span>
    },</span></span>
};</span></span></code></pre>

Then adding a pointer to it to Boilerplate</code>, and adjusting resolve_prgm</code> as follows.</p>

pub fn</span> resolve_prgm(</span></span>
    gpa: std.mem.Allocator,</span></span>
    strings:</span> *</span>utils.StringInterner,</span></span>
    prgm:</span> *</span>ast.Prgm,</span></span>
) Error</span>!void</span> {</span></span>
    var</span> variable_map: VariableMap</span> =</span> .empty;</span></span>
    defer</span> variable_map.deinit(gpa);</span></span>
</span>
    var</span> type_map: TypeMap</span> =</span> .empty;</span></span>
    defer</span> type_map.deinit(gpa);</span></span>
</span>
    const</span> bp: Boilerplate</span> =</span> .{</span></span>
        .gpa</span> =</span> gpa,</span></span>
        .strings</span> =</span> strings,</span></span>
        .variable_map</span> = &</span>variable_map,</span></span>
        .type_map</span> = &</span>type_map,</span></span>
    };</span></span>
</span>
    var</span> iter</span> =</span> prgm.funcs.iterator(</span>0</span>);</span></span>
    while</span> (iter.next()) </span>|</span>item</span>|</span></span>
        try</span> resolve_func_decl(bp, item);</span></span>
}</span></span></code></pre>

What follows next is lots of annoying boilerplate. I must make sure every time I add something to a variable_map</code>, I am adding its new name (if any) to type_map</code>. Hairier than usual logic, but should try to straighten it out before stuffing it in a Boilerplate</code> method.</p>

For example, in resolve_finc_decl</code>, adding a name to variable_map</code> and type_map</code> is done as follows:</p>

{</span></span>
    const</span> nname</span> =</span> try</span> bp.strings.get_or_put(bp.gpa, func_decl.name);</span></span>
    try</span> bp.variable_map.put(bp.gpa, func_decl.name, .{</span></span>
        .name</span> =</span> nname,</span></span>
        .scope</span> =</span> .local,</span></span>
        .linkage</span> =</span> .external,</span></span>
    });</span></span>
    const</span> gop</span> =</span> try</span> bp.type_map.getOrPut(bp.gpa, nname.real_idx);</span></span>
    if</span> (gop.found_existing) {</span></span>
        if</span> (gop.value_ptr.</span>* !=</span> .func</span> or</span></span>
            gop.value_ptr.func.arity</span> !=</span> func_decl.params.count())</span></span>
        {</span></span>
            return</span> error</span>.TypeError;</span></span>
        } </span>else if</span> (gop.value_ptr.func.defined</span> and</span></span>
            func_decl.block</span> !=</span> null</span>)</span></span>
        {</span></span>
            return</span> error</span>.DuplicateFunctionDef;</span></span>
        }</span></span>
    } </span>else</span> gop.value_ptr.</span>* =</span> .{ .func</span> =</span> .{</span></span>
        .arity</span> =</span> func_decl.params.count(),</span></span>
        .defined</span> =</span> func_decl.block</span> !=</span> null</span>,</span></span>
    } };</span></span>
}</span></span></code></pre>

A similar thing to do in resolve_var_decl</code>, and inside the small tidbit in resolve_func_decl</code> that resolves parameters. This is how it looks like.</p>

{</span></span>
    const</span> gop</span> =</span> try</span> bp.type_map.getOrPut(bp.gpa, unique_name.real_idx);</span></span>
    if</span> (gop.found_existing) {</span></span>
        if</span> (gop.value_ptr.</span>* !=</span> .int) {</span></span>
            return</span> error</span>.TypeError;</span></span>
        }</span></span>
    } </span>else</span> gop.value_ptr.</span>* =</span> .int;</span></span>
}</span></span></code></pre>

There does not seem enough shared logic right now to try and DRY these. Maybe later. What is left is checking these things in function calls and variable declarations. They are both very funny looking.</p>

.@"var"</span> => |</span>name</span>|</span> {</span></span>
    if</span> (bp.variable_map.get(name.name)) </span>|</span>un</span>|</span> {</span></span>
        if</span> (bp.type_map.get(un.name.real_idx).</span>? ==</span> .int) </span>// unwrap optional</span></span>
            expr.</span>* =</span> .{ .@"var"</span> =</span> .{ .idx</span> =</span> un.name } }</span></span>
        else</span></span>
            return</span> error</span>.TypeError;</span></span>
    } </span>else return</span> error</span>.UndeclaredVariable;</span></span>
},</span></span>
.func_call</span> => |*</span>f</span>|</span> {</span></span>
    if</span> (bp.variable_map.get(f.@"0".name)) </span>|</span>entry</span>|</span> {</span></span>
        const</span> t</span> =</span> bp.type_map.get(entry.name.real_idx).</span>?</span>;</span></span>
        if</span> (t</span> ==</span> .func</span> and</span></span>
            t.func.arity</span> ==</span> f.@"1".count())</span></span>
        {</span></span>
            f.@"0"</span> =</span> .{ .idx</span> =</span> entry.name };</span></span>
            var</span> iter</span> =</span> f.@"1".iterator(</span>0</span>);</span></span>
            while</span> (iter.next()) </span>|</span>item</span>|</span></span>
                try</span> resolve_expr(bp, item);</span></span>
        } </span>else return</span> error</span>.TypeError;</span></span>
    } </span>else return</span> error</span>.UndeclaredFunction;</span></span>
},</span></span></code></pre>

I think</em> this should be it. I checked for functions being defined twice/ I checked that functions should be functions and types should be types. I checked all functions with the same name should have the same arity. zig build</code> returns no errors. What is left?</p>

The proof of the pudding is in the test suite. Time to run the test suite. (which now applies --latest-only</code> by default as not to take too</em> long.)</p>

%</span> paella ❱ zig build submit</span> -- --chapter 9 --stage</span> validate</span></span>
----------------------------------------------------------------------</span></span>
Ran</span> 61</span> tests in 157.089s</span></span>
</span>
OK</span></span></code></pre>

Phew</em>. Mind you this does not mean the logic is correct. It just means it is failing the ones it should fail and passing the ones it should pass. The full test suite passes as well, which is cool.</p>

fn</span> resolve_var_decl(</span></span>
    comptime</span> T:</span> enum</span> { param, @"var" },</span> // anonymous types yay</span></span>
    bp: Boilerplate,</span></span>
    item:</span> switch</span> (T) { </span>// can simply be `if` but this is more readable i think</span></span>
        .@"var"</span> => *</span>ast.VarDecl,</span></span>
        .param</span> => *</span>ast.Identifier,</span></span>
    },</span></span>
) Error</span>!void</span> {</span></span>
    const</span> identifier</span> =</span> switch</span> (T) { </span>// pulling out the common logic</span></span>
        .@"var"</span> => &</span>item.name,</span></span>
        .param</span> =></span> item,</span></span>
    };</span></span>
    if</span> (bp.variable_map.get(identifier.name)) </span>|</span>entry</span>|</span> if</span> (entry.scope</span> ==</span> .local)</span></span>
        return</span> error</span>.DuplicateDecl;</span></span>
</span>
    const</span> unique_name</span> =</span> try</span> bp.make_temporary(identifier.name);</span></span>
    try</span> bp.variable_map.put(bp.gpa, identifier.name, .{ .name</span> =</span> unique_name });</span></span>
</span>
    { </span>// TYPE CHECKING</span></span>
        const</span> gop</span> =</span> try</span> bp.type_map.getOrPut(bp.gpa, unique_name.real_idx);</span></span>
        if</span> (gop.found_existing) {</span></span>
            if</span> (gop.value_ptr.</span>* !=</span> .int)</span></span>
                return</span> error</span>.TypeError;</span></span>
        } </span>else</span> gop.value_ptr.</span>* =</span> .int;</span></span>
    }</span></span>
</span>
    identifier.</span>* =</span> .{ .idx</span> =</span> unique_name };</span></span>
</span>
    if</span> (T</span> ==</span> .@"var") </span>// logic unique to declarations</span></span>
        if</span> (item.init) </span>|</span>expr</span>|</span></span>
            try</span> resolve_expr(bp, expr);</span></span>
}</span></span>
</span>
// called like this, inside `resolve_func_decl`</span></span>
while</span> (params.next()) </span>|</span>param</span>|</span></span>
    try</span> resolve_var_decl(.param, inner_bp, param);</span></span></code></pre>

I ran the test suite again (and the eye tests) after this and everything seems to work out.</p>


Internal Representation</h2>

After a long drought, finally time to update the IR syntax tree and generation. Which means new assembly stuff later. Couldn't the Book stick to adding more control flow constructs for ever? Maybe function calls can be implemented by just jumping around. Is that a thing?</p>

The syntax tree updates mirror the AST's.</p>

const</span> Identifier</span> =</span> utils.StringInterner.Idx;</span> // type alias</span></span>
</span>
pub const</span> Prgm</span> =</span> struct</span> {</span></span>
    funcs: std.ArrayListUnmanaged(FuncDef),</span> // <--</span></span>
};</span></span>
</span>
pub const</span> FuncDef</span> =</span> struct</span> {</span></span>
    name: Identifier,</span></span>
    params: std.ArrayListUnmanaged(Identifier),</span> // <--</span></span>
    instrs: std.ArrayListUnmanaged(Instr),</span></span>
};</span></span>
</span>
pub const</span> Instr</span> =</span> union</span>(</span>enum</span>) {</span></span>
    func_call:</span> struct</span> {</span></span>
        name: Identifier,</span></span>
        args: std.ArrayListUnmanaged(Value),</span></span>
        dst: Value,</span></span>
    },</span></span>
</span>
    // rest of the owl</span></span>
};</span></span></code></pre>

And the rest is pretty much the same, sans a few updates to the pretty printers and deinitializers. Note that there are no function declarations, as they are discarded in this stage after being done with the type checking.</p>

prgm_emit_ir</code> is simply a small update from the one-function version to the many-functions version, skipping over declarations without bodies</p>

pub fn</span> prgm_emit_ir(</span></span>
    alloc: std.mem.Allocator,</span></span>
    strings:</span> *</span>utils.StringInterner,</span></span>
    prgm:</span> *</span>const</span> ast.Prgm,</span></span>
) Error</span>!</span>ir.Prgm {</span></span>
    var</span> funcs: std.ArrayListUnmanaged(ir.FuncDef) </span>=</span> try</span> .initCapacity(</span></span>
        alloc,</span></span>
        prgm.funcs.len,</span></span>
    );</span></span>
</span>
    var</span> iter</span> =</span> prgm.funcs.constIterator(</span>0</span>);</span></span>
    while</span> (iter.next()) </span>|</span>f</span>|</span> if</span> (f.block) </span>|</span>_</span>|</span> { </span>// skip empty declarations</span></span>
        const</span> fir</span> =</span> try</span> func_def_emit_ir(alloc, strings, f);</span></span>
        try</span> funcs.append(alloc, fir);</span></span>
    };</span></span>
</span>
    return</span> .{ .funcs</span> =</span> funcs };</span></span>
}</span></span></code></pre>

func_def_emit_ir</code> is also almost exactly the same, except that the identifiers are moved over.</p>

fn</span> func_def_emit_ir(</span></span>
    alloc: std.mem.Allocator,</span></span>
    strings:</span> *</span>utils.StringInterner,</span></span>
    func_def:</span> *</span>const</span> ast.FuncDecl,</span></span>
) Error</span>!</span>ir.FuncDef {</span></span>
    const</span> name</span> =</span> try</span> strings.get_or_put(alloc, func_def.name);</span></span>
</span>
    var</span> params: std.ArrayListUnmanaged(utils.StringInterner.Idx) </span>=</span> try</span> .initCapacity(</span></span>
        alloc,</span></span>
        func_def.params.count(),</span></span>
    );</span></span>
    var</span> iter</span> =</span> func_def.params.constIterator(</span>0</span>);</span></span>
</span>
    while</span> (iter.next()) </span>|</span>param</span>|</span></span>
        try</span> params.append(alloc, param.idx);</span></span>
</span>
    var</span> instrs: std.ArrayListUnmanaged(ir.Instr) </span>=</span> .empty;</span></span>
    const</span> bp: Boilerplate</span> =</span> .{</span></span>
        .alloc</span> =</span> alloc,</span></span>
        .strings</span> =</span> strings,</span></span>
        .instrs</span> = &</span>instrs,</span></span>
    };</span></span>
</span>
    try</span> block_emit_ir(bp,</span> &</span>func_def.block.</span>?</span>);</span></span>
    try</span> instrs.append(alloc, .{ .ret</span> =</span> .{ .constant</span> =</span> 0</span> } });</span></span>
</span>
    return</span> .{ .name</span> =</span> name, .params</span> =</span> params, .instrs</span> =</span> instrs };</span></span>
}</span></span></code></pre>

Function call expressions are new, even though they follow the same pattern as all other expressions.</p>

.func_call</span> => |</span>f</span>|</span> {</span></span>
    var</span> args: std.ArrayListUnmanaged(ir.Value) </span>=</span> try</span> .initCapacity(</span></span>
        bp.alloc,</span></span>
        f.@"1".count(),</span></span>
    );</span></span>
</span>
    const</span> dst</span> =</span> .{ .variable</span> =</span> try</span> bp.make_temporary(</span>"fn"</span>) };</span></span>
</span>
    var</span> iter</span> =</span> f.@"1".constIterator(</span>0</span>);</span></span>
    while</span> (iter.next()) </span>|</span>e</span>|</span> {</span></span>
        const</span> v</span> =</span> try</span> expr_emit_ir(bp, e);</span></span>
        try</span> args.append(bp.alloc, v);</span></span>
    }</span></span>
</span>
    try</span> bp.append(.{ .func_call</span> =</span> .{</span></span>
        .name</span> =</span> f.@"0".idx,</span></span>
        .args</span> =</span> args,</span></span>
        .dst</span> =</span> dst,</span></span>
    } });</span></span>
</span>
    return</span> dst;</span></span>
},</span></span></code></pre>

The last thing left to do here is to make sure that function declaration as block items are skipped as well.</p>

fn</span> block_emit_ir(</span></span>
    bp: Boilerplate,</span></span>
    block:</span> *</span>const</span> ast.Block,</span></span>
) Error</span>!void</span> {</span></span>
    var</span> iter</span> =</span> block.body.constIterator(</span>0</span>);</span></span>
    while</span> (iter.next()) </span>|</span>item</span>|</span> switch</span> (item.</span>*</span>) {</span></span>
        .S</span> => |*</span>s</span>|</span> try</span> stmt_emit_ir(bp, s),</span></span>
        .D</span> => |</span>d</span>|</span> if</span> (d</span> ==</span> .V) </span>try</span> var_decl_emit_ir(bp,</span> &</span>d.V),</span> // <-</span></span>
    };</span></span>
}</span></span></code></pre>

Small Detour Back to Type Checking</h3>

I am honestly not quite sure about some decisions up to now. For example, if function declarations are discarded, why are their parameters given unique identities at all? Maybe I should go back and simply check the count (and ideally the types, but there are no types in here). This would change the end of resolve_func_decl</code> to the following, instead of the if statement only encasing the last line. Since the count is checked earlier</p>

if</span> (func_decl.block) </span>|*</span>block</span>|</span> {</span></span>
    var</span> params</span> =</span> func_decl.params.iterator(</span>0</span>);</span></span>
    while</span> (params.next()) </span>|</span>param</span>|</span></span>
        try</span> resolve_var_decl(.param, inner_bp, param);</span></span>
</span>
    try</span> resolve_block(inner_bp,</span> null</span>, block);</span></span>
}</span></span></code></pre>

This passes all validations tests, except one.</p>

/* Duplicate parameter names are illegal in function declarations</span></span>
   as well as definitions */</span></span>
int</span> foo(</span>int</span> a,</span> int</span> a);</span></span>
</span>
int</span> main(</span>void</span>) {</span></span>
    return</span> foo(</span>1</span>,</span> 2</span>);</span></span>
}</span></span>
</span>
int</span> foo(</span>int</span> a,</span> int</span> b) {</span></span>
    return</span> a </span>+</span> b;</span></span>
}</span></span></code></pre>

Ah well. Amusingly, at least according to the LLM I asked, int foo(int, int)</code> is</em> a legal declaration. Anyway, this was a failed detour.</p>


Assembly Generation</h2>

This is the first serious brush in the Book with the System V ABI, the most common binary interface for Unixes. As there are currently no types more complex than int</code>, all the compiler needs to worry about is stuffing parameters in the right registers. (And for function bodies, retrieving them from the right registers). The IR has not concerned itself with this, relying on the surrounding passes to make sense out of it.</p>

In my Rust implementation, I used clever iterator combinators to write the logic for this section. I am kind of curious to see how it will turn out in Zig.</p>

The first step, as usual, is to update the assembly syntax tree. A program needs to be redefined as a list of functions, and there are new instructions and new registers. Some necessary refactorings should be timed now as well. This is assembly.Prgm</code>.</p>

pub const</span> Prgm</span> =</span> struct</span> {</span></span>
    funcs: std.ArrayListUnmanaged(FuncDef),</span></span>
</span>
    pub fn</span> fixup(</span></span>
        self:</span> *</span>@This</span>(),</span></span>
        alloc: std.mem.Allocator,</span></span>
    ) </span>!void</span> {</span></span>
        for</span> (self.funcs.items) </span>|*</span>func</span>|</span> {</span></span>
            // functions called here changed from taking *Prgm to *FuncDef</span></span>
            // ideally this should live under FuncDef, but this is fine for now</span></span>
            const</span> depth</span> =</span> try</span> pass_pseudo.replace_pseudos(alloc, func);</span></span>
            try</span> func.instrs.insert(alloc,</span> 0</span>, .{</span></span>
                .allocate_stack</span> =</span> @abs</span>(depth),</span></span>
            });</span></span>
            try</span> pass_fixup.fixup_instrs(alloc, func);</span></span>
        }</span></span>
    }</span></span></code></pre>

The new instructions are simple additions. However, I am using two type aliases here: Depth</code> for an unsigned version of stack depth, and Identifier</code> for utils.StringInterner.Idx</code>.</p>

allocate_stack: Depth,</span> // old</span></span>
dealloc_stack: Depth,</span></span>
</span>
push: Operand,</span></span>
call: Identifier,</span></span></code></pre>

And aside from the new Registers, which I will not bore you with, this is pretty much it.3</a></sup> Implementing the codegen is the next step. parameters in function definitions need to be taken from their correct register and stack positions; and function calls need to stuff them there. prgm_to_asm</code>, the main entry point, is straightforward.</p>

pub fn</span> prgm_to_asm(</span></span>
    alloc: std.mem.Allocator,</span></span>
    prgm: ir.Prgm,</span></span>
) </span>!</span>assembly.Prgm {</span></span>
    var</span> funcs: std.ArrayListUnmanaged(assembly.FuncDef) </span>=</span> try</span> .initCapacity(</span></span>
        alloc,</span></span>
        prgm.funcs.items.len,</span></span>
    );</span></span>
</span>
    for</span> (prgm.funcs.items) </span>|</span>func</span>|</span></span>
        try</span> funcs.append(</span></span>
            alloc,</span></span>
            try</span> func_def_to_asm(alloc, func),</span></span>
        );</span></span>
</span>
    return</span> .{ .funcs</span> =</span> funcs };</span></span>
}</span></span></code></pre>

For functions, it is slightly different. The first six parameters should be passed in these registers in this specific order: DI</code>, SI</code>, DX</code>, CX</code>, R8</code>, and R9</code>. Remaining arguments are pushed into the stack in reverse order</em>. So normally, I will just assign a constant for them registers.</p>

const</span> REGISTERS: [</span>6</span>]assembly.Operand.Register</span> =</span></span>
    .{ .DI, .SI, .DX, .CX, .R8, .R9 };</span></span></code></pre>

In func_def_to_asm</code> itself, a zipped for</code> loop is perhaps the most straightforward way of doing this. The first section is simple enough. And since the remaining parameters are pushed into the stack at function call in reverse order, they are in the correct order here. Starting from a 16 stack</code> offset, they are moved one by one. Luckily, this same loop can be used!</p>

for</span> (func_def.params.items,</span> 0</span>..) </span>|</span>param, idx</span>|</span></span>
    if</span> (idx</span> <</span> REGISTERS.len)</span></span>
        try</span> instrs.append(alloc, .{ .mov</span> =</span> .init(</span></span>
            .{ .reg</span> =</span> REGISTERS[idx] },</span></span>
            .{ .pseudo</span> =</span> param },</span></span>
        ) })</span></span>
    else</span> {</span></span>
        const</span> offset</span> =</span> (idx</span> -</span> REGISTERS.len</span> +</span> 2</span>) </span>*</span> 8</span>;</span> // 16, 24, etc</span></span>
        try</span> instrs.append(alloc, .{ .mov</span> =</span> .init(</span></span>
            .{ .stack</span> =</span> @intCast</span>(offset) },</span></span>
            .{ .pseudo</span> =</span> param },</span></span>
        ) });</span></span>
    };</span></span></code></pre>

Implementing function calls is a lot more involved. First, the stack needs to be aligned properly at 16, as per the System V ABI. Copying the passed in expressions to the input registers is easy, but reversing the rest of them requires some shenanigans. First is to have a stack allocated array with the maximum possible size because it is unknown at the outset what the size of the returned slice could be.</p>

// the return is of unknown size.</span></span>
// maximum possible size is parameter count * 2 + 4</span></span>
var</span> ret: std.ArrayListUnmanaged(assembly.Instr) </span>=</span></span>
    try</span> .initCapacity(alloc, c.args.items.len</span> *</span> 2</span> +</span> 4</span>);</span></span></code></pre>

Then calculate the amount of arguments in the stack and the needed padding to 16. Zig has a saturating subtraction operator -|</code> that is perfect for this.</p>

const</span> depth</span> =</span> c.args.items.len</span> -|</span> REGISTERS.len;</span></span>
const</span> padding: assembly.Instr.Depth</span> =</span></span>
    if</span> (depth</span> %</span> 2</span> ==</span> 0</span>) </span>8</span> else</span> 0</span>;</span></span>
if</span> (padding</span> ></span> 0</span>)</span></span>
    try</span> ret.append(alloc, .{ .allocate_stack</span> =</span> padding });</span> // 1</span></span></code></pre>

The numbering in the comments is me keeping track of instructions that do not depend on parameters count. After that comes putting the values in their respective registers, and the stack.4</a></sup></p>

for</span> (c.args.items,</span> 0</span>..) </span>|</span>arg, idx</span>|</span> {</span></span>
    if</span> (idx</span> >=</span> REGISTERS.len) </span>break</span>;</span></span>
</span>
    try</span> ret.append(alloc, .{ .mov</span> =</span> .init(</span></span>
        .{ .reg</span> =</span> REGISTERS[idx] },</span></span>
        value_to_asm(arg),</span></span>
    ) });</span></span>
}</span></span>
for</span> (</span>0</span>..depth) </span>|</span>idx</span>|</span> {</span></span>
    const</span> v_ir</span> =</span> c.args.items[c.args.items.len</span> -</span> 1</span> -</span> idx];</span></span>
    const</span> v_asm</span> =</span> value_to_asm(v_ir);</span></span>
    switch</span> (v_asm) {</span></span>
        .imm, .reg</span> =></span> try</span> ret.append(alloc, .{ .push</span> =</span> v_asm }),</span></span>
        else</span> =></span> try</span> ret.appendSlice(alloc,</span> &</span>.{</span></span>
            .{ .mov</span> =</span> .init(v_asm, .{ .reg</span> =</span> .AX }) },</span></span>
            .{ .push</span> =</span> .{ .reg</span> =</span> .AX } },</span></span>
        }),</span></span>
    }</span></span>
}</span></span></code></pre>

I think this should work. 0..depth</code> will be .. nothing, and the loop will not run. If it is larger, it starts from the last item (at index len - , 1</code>), then subtracts the current index of the loop. And finally, dealing with the return value, and returning the slice.</p>

// emit call instruction</span></span>
try</span> ret.append(alloc, .{ .call</span> =</span> c.name });</span> // 2</span></span>
</span>
const</span> bytes_to_remove</span> =</span> 8</span> *</span> depth</span> +</span> padding;</span></span>
if</span> (bytes_to_remove</span> !=</span> 0</span>)</span></span>
    try</span> ret.append(alloc, .{ .dealloc_stack</span> =</span> bytes_to_remove });</span> // 3</span></span>
</span>
try</span> ret.append(alloc, .{ .mov</span> =</span> .init(</span></span>
    .{ .reg</span> =</span> .AX },</span></span>
    value_to_asm(c.dst),</span></span>
) });</span> // 4</span></span>
</span>
return</span> ret.items;</span></span></code></pre>

Doing the eye test for codegen, I quickly noticed something stupid. I am moving the register's value to an immutable. Oops.</p>

int</span> add(</span>int</span> x,</span> int</span> y);</span></span>
</span>
int</span> main(</span>void</span>) {</span></span>
    return</span> add(</span>1</span>,</span> 2</span>);</span></span>
}</span></span>
// PROGRAM</span></span>
// 	FUNCTION main</span></span>
// 		allocate	4</span></span>
// 		allocate	8</span></span>
// 		mov	DI -> imm 1</span></span>
// 		mov	SI -> imm 2</span></span>
// 		call	.Ladd</span></span>
// 		deallocate	8</span></span>
// 		mov	AX -> stack -4</span></span>
// 		mov	stack -4 -> AX</span></span>
// 		ret</span></span>
// 		mov	imm 0 -> AX</span></span>
// 		ret</span></span></code></pre>

Fixing that by switching two lines, the eye test passes fine. The test suite does not hit any errors (and, as a reminder, does not test beyond success and failure).</p>

After that comes the annoying fixup pass adjustments. The depth returned by replace_pseudos</code> is adjusted to save the depth of each function in a field in the function definition. And also, when allocating, rounding that up to the nearest multiple of 16.</p>

There is a neat trick to round up a number, n</code>, to the nearest multiple of 16. Basically (n + 15) & ~15</code>. Unfortunately, Zig makes this a pain to write because the integer type inference is not clever enough to understand ~15</code>.</p>

There in the standard library is a helpful function to do that math: std.mem.alignForward</code>. Kindly pointed to me by the Zig Discord. So I added this bit at the end of replace_pseudos</code>.</p>

func_def.depth</span> =</span> @intCast</span>(pseudo_map.count() </span>*</span> 4</span>);</span></span>
</span>
const</span> aligned</span> =</span> std.mem.alignForward(assembly.Instr.Depth, func_def.depth,</span> 16</span>);</span></span>
try</span> func_def.instrs.insert(alloc,</span> 0</span>, .{ .allocate_stack</span> =</span> aligned });</span></span></code></pre>

So, time to actually emit the assembly.</p>

Code Emission</h2>

The changes here are pretty small. The new instructions are one thing, but there are new registers and new register sizes for push</code>, which takes 8-width registers. The logic for printing registers got pretty large so I put it in its own function.</p>

fn</span> emit_register(</span></span>
    reg: Operand.Register,</span></span>
    width:</span> usize</span>,</span></span>
    writer:</span> anytype</span>,</span></span>
) </span>!void</span> {</span></span>
    p:</span> switch</span> (width) {</span></span>
        1</span> =></span> switch</span> (reg) {</span></span>
            .AX</span> =></span> try</span> writer.print(</span>"%al"</span>, .{}),</span></span>
            .DX</span> =></span> try</span> writer.print(</span>"%dl"</span>, .{}),</span></span>
            .CX</span> =></span> try</span> writer.print(</span>"%cl"</span>, .{}),</span></span>
            .DI</span> =></span> try</span> writer.print(</span>"%dil"</span>, .{}),</span></span>
            .SI</span> =></span> try</span> writer.print(</span>"%sil"</span>, .{}),</span></span>
            .R8</span> =></span> try</span> writer.print(</span>"%r8b"</span>, .{}),</span></span>
            .R9</span> =></span> try</span> writer.print(</span>"%r9b"</span>, .{}),</span></span>
            .R10</span> =></span> try</span> writer.print(</span>"%r10b"</span>, .{}),</span></span>
            .R11</span> =></span> try</span> writer.print(</span>"%r11b"</span>, .{}),</span></span>
        },</span></span>
        4</span> =></span> switch</span> (reg) {</span></span>
            .AX</span> =></span> try</span> writer.print(</span>"%eax"</span>, .{}),</span></span>
            .DX</span> =></span> try</span> writer.print(</span>"%edx"</span>, .{}),</span></span>
            .CX</span> =></span> try</span> writer.print(</span>"%ecx"</span>, .{}),</span></span>
            .DI</span> =></span> try</span> writer.print(</span>"%edi"</span>, .{}),</span></span>
            .SI</span> =></span> try</span> writer.print(</span>"%esi"</span>, .{}),</span></span>
            .R8</span> =></span> try</span> writer.print(</span>"%r8d"</span>, .{}),</span></span>
            .R9</span> =></span> try</span> writer.print(</span>"%r9d"</span>, .{}),</span></span>
            .R10</span> =></span> try</span> writer.print(</span>"%r10d"</span>, .{}),</span></span>
            .R11</span> =></span> try</span> writer.print(</span>"%r11d"</span>, .{}),</span></span>
        },</span></span>
        8</span> =></span> switch</span> (reg) {</span></span>
            .AX</span> =></span> try</span> writer.print(</span>"%rax"</span>, .{}),</span></span>
            .DX</span> =></span> try</span> writer.print(</span>"%rdx"</span>, .{}),</span></span>
            .CX</span> =></span> try</span> writer.print(</span>"%rcx"</span>, .{}),</span></span>
            .DI</span> =></span> try</span> writer.print(</span>"%rdi"</span>, .{}),</span></span>
            .SI</span> =></span> try</span> writer.print(</span>"%rsi"</span>, .{}),</span></span>
            .R8</span> =></span> try</span> writer.print(</span>"%r8"</span>, .{}),</span></span>
            .R9</span> =></span> try</span> writer.print(</span>"%r9"</span>, .{}),</span></span>
            .R10</span> =></span> try</span> writer.print(</span>"%r10"</span>, .{}),</span></span>
            .R11</span> =></span> try</span> writer.print(</span>"%r11"</span>, .{}),</span></span>
        },</span></span>
        else</span> =></span> continue</span> :p</span> 4</span>,</span> // default case</span></span>
    }</span></span>
}</span></span></code></pre>

And then in the format function just do the following. There is a bit of redundancy regarding the 4 as a default case. You are not paranoid if they are</em> out to get you!</p>

.reg</span> => |</span>r</span>|</span> try</span> emit_register(r, options.width</span> orelse</span> 4</span>, writer),</span></span></code></pre>

Running the test suite, I get into 16 failures. Oh come on.</p>

Most of them seem to be UnrecognizedFlag</code>, which is the error I have in the compiler driver for flags that are unrecognized. That is what I get for not reading the text and jumping straight to the tables. There is a tidbit there about recognizing a -c</code> flag, which is the gcc</code> flag for creating object files instead of executables.</p>

This is a small change to the argument parser. That is the new one:</p>

</span>
pub fn</span> parse_args() </span>!</span>Args {</span></span>
    var</span> args</span> =</span> std.process.args();</span></span>
    _</span> =</span> args.skip();</span></span>
</span>
    var</span> path:</span> ?</span>[:</span>0</span>]</span>const</span> u8 =</span> null</span>;</span></span>
    var</span> mode: Mode</span> =</span> .compile;</span></span>
    var</span> c_flag</span> =</span> false</span>;</span> // new</span></span>
</span>
    while</span> (args.next()) </span>|</span>arg</span>|</span> {</span></span>
        if</span> (arg[</span>0</span>] </span>==</span> '-'</span>) {</span></span>
            if</span> (arg[</span>1</span>] </span>==</span> 'c'</span>) </span>// new</span></span>
                c_flag</span> =</span> true</span></span>
            else</span></span>
                mode</span> =</span> std.meta.stringToEnum(Mode, arg[</span>2</span>..]) </span>orelse</span></span>
                    return</span> error</span>.UnrecognizedFlag;</span></span>
        } </span>else if</span> (path</span> ==</span> null</span>)</span></span>
            path</span> =</span> arg</span></span>
        else</span></span>
            return</span> error</span>.PathDuplicated;</span></span>
    }</span></span>
</span>
    return</span> .{</span></span>
        .path</span> =</span> path</span> orelse return</span> error</span>.PathNotFound,</span></span>
        .mode</span> =</span> mode,</span></span>
        .c_flag</span> =</span> c_flag,</span> // new</span></span>
    };</span></span>
}</span></span></code></pre>

And much below that, when calling the assembler, the -c</code> flag is passed if c_flag</code> is true. And changing the extension to .o</code>. So it becomes the following (changing the extension itself is done elsewhere in a bit of rather annoying code). This is probably the nicest way of doing it.</p>

{ </span>// assembler</span></span>
    var</span> child</span> =</span> std.process.Child.init(</span></span>
        if</span> (args.c_flag)</span></span>
            &</span>.{ </span>"gcc"</span>,</span> "-c"</span>, asm_out,</span> "-o"</span>, obj }</span></span>
        else</span></span>
            &</span>.{ </span>"gcc"</span>, asm_out,</span> "-o"</span>, exe },</span></span>
        gpa,</span></span>
    );</span></span>
</span>
    const</span> term</span> =</span> try</span> child.spawnAndWait();</span></span>
    if</span> (</span>!</span>std.meta.eql(term, .{ .Exited</span> =</span> 0</span> }))</span></span>
        return</span> error</span>.AssemblerFail;</span></span>
</span>
    try</span> std.fs.cwd().deleteFile(asm_out);</span> // cleanup</span></span>
}</span></span></code></pre>

Now running the test suite for real.</p>


Debugging</h2>

On the plus side, all the programs are compiling and running. On the negative side, I got a lot of bad return codes. There are logic mistakes. Also 3 errors relating to unicode and timeouts? I do not get it. The test suite's output is generally jumbled up and I find it hard to process. Having said that, the easiest way to attack the problem is to look at the individual files and compiles them myself.</p>

UnicodeDecodeError</h3>

This is one of the weird errors, Nora Sandler's comments and all. The program should call the system's putchar</code>, which prints a character to standard out.</p>

#ifdef</span> SUPPRESS_WARNINGS</span></span>
#pragma</span> GCC diagnostic ignored</span> "-Wunused-parameter"</span></span>
#endif</span></span>
int</span> putchar(</span>int</span> c);</span></span>
</span>
/* Make sure we can correctly manage calling conventions from the callee side</span></span>
 * (by accessing parameters, including parameters on the stack) and the caller side</span></span>
 * (by calling a standard library function) in the same function</span></span>
 */</span></span>
int</span> foo(</span>int</span> a,</span> int</span> b,</span> int</span> c,</span> int</span> d,</span> int</span> e,</span> int</span> f,</span> int</span> g,</span> int</span> h) {</span></span>
    putchar(h);</span></span>
    return</span> a </span>+</span> g;</span></span>
}</span></span>
</span>
int</span> main(</span>void</span>) {</span></span>
    return</span> foo(</span>1</span>,</span> 2</span>,</span> 3</span>,</span> 4</span>,</span> 5</span>,</span> 6</span>,</span> 7</span>,</span> 65</span>);</span></span>
}</span></span></code></pre>

This is the output for each stage.</p>

PARSING</span></span>
	FUNCTION putchar c</span></span>
	FUNCTION foo a b c d e f g h</span></span>
		(putchar h);</span></span>
		RETURN (+ a g)</span></span>
	FUNCTION main</span></span>
		RETURN (foo 1 2 3 4 5 6 7 65)</span></span>
================================</span></span>
VALIDATION</span></span>
	FUNCTION putchar c.0</span></span>
	FUNCTION foo a.1 b.2 c.3 d.4 e.5 f.6 g.7 h.8</span></span>
		(putchar h.8);</span></span>
		RETURN (+ a.1 g.7)</span></span>
	FUNCTION main</span></span>
		RETURN (foo 1 2 3 4 5 6 7 65)</span></span>
================================</span></span>
IR</span></span>
	FUNCTION foo</span></span>
		fn.9 <- putchar(h.8)</span></span>
		add.10 <- a.1 + g.7</span></span>
		ret add.10</span></span>
		ret 0</span></span>
	FUNCTION main</span></span>
		fn.11 <- foo(1, 2, 3, 4, 5, 6, 7, 65)</span></span>
		ret fn.11</span></span>
		ret 0</span></span>
================================</span></span>
CODEGEN</span></span>
	FUNCTION foo</span></span>
		allocate	48</span></span>
		mov	DI -> stack -4</span></span>
		mov	SI -> stack -8</span></span>
		mov	DX -> stack -12</span></span>
		mov	CX -> stack -16</span></span>
		mov	R8 -> stack -20</span></span>
		mov	R9 -> stack -24</span></span>
		mov	stack 16 -> R10</span></span>
		mov	R10 -> stack -28</span></span>
		mov	stack 24 -> R10</span></span>
		mov	R10 -> stack -32</span></span>
		allocate	8</span></span>
		mov	stack -32 -> DI</span></span>
		call	.Lputchar</span></span>
		deallocate	8</span></span>
		mov	AX -> stack -36</span></span>
		mov	stack -4 -> R10</span></span>
		mov	R10 -> stack -40</span></span>
		mov	stack -28 -> R10</span></span>
		add	R10 -> stack -40</span></span>
		mov	stack -40 -> AX</span></span>
		ret</span></span>
		mov	imm 0 -> AX</span></span>
		ret</span></span>
	FUNCTION main</span></span>
		allocate	16</span></span>
		allocate	8</span></span>
		mov	imm 1 -> DI</span></span>
		mov	imm 2 -> SI</span></span>
		mov	imm 3 -> DX</span></span>
		mov	imm 4 -> CX</span></span>
		mov	imm 5 -> R8</span></span>
		mov	imm 6 -> R9</span></span>
		push	imm 65</span></span>
		push	imm 7</span></span>
		call	.Lfoo</span></span>
		deallocate	24</span></span>
		mov	AX -> stack -4</span></span>
		mov	stack -4 -> AX</span></span>
		ret</span></span>
		mov	imm 0 -> AX</span></span>
		ret</span></span></code></pre>

Hold on, before going into assembly, I spot a weird thing here. Why is the call to foo</code> is preceded by .L</code>. This is probably a formatting error which does not matter. But more importantly, the numbers for deallocations seem weird. Why is it deallocating 24? Let's review the logic in instr_to_asm</code> in .func_call</code>.</p>

const</span> bytes_to_remove</span> =</span> 8</span> *</span> depth</span> +</span> padding;</span></span>
if</span> (bytes_to_remove</span> !=</span> 0</span>)</span></span>
    try</span> ret.append(alloc, .{ .dealloc_stack</span> =</span> bytes_to_remove });</span> // 3</span></span></code></pre>

Eh, this makes sense. Counting the arguments passed on the stack there, there are 2. So why is there padding?</p>

const</span> padding: assembly.Instr.Depth</span> =</span></span>
    if</span> (depth</span> %</span> 2</span> ==</span> 0</span>) </span>8</span> else</span> 0</span>;</span> // <--- here</span></span>
if</span> (padding</span> !=</span> 0</span>)</span></span>
    try</span> ret.append(alloc, .{ .allocate_stack</span> =</span> padding });</span> // 1</span></span></code></pre>

Oh I am assigning the padding in reverse. If the number of arguments is even, there should be no padding. Silly me.</p>

This does not actually deal with the error I got for this file, which is this:</p>

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x90 in position 0: invalid start byte</span></span></code></pre>

This might be the valued printed by putchar</code> failing to read. However, only way to know is to run the test suit again.</p>

%</span> paella ❱ zig build submit</span> --release=safe  -- --chapter 9</span></span>
</span>
# many moths later</span></span>
----------------------------------------------------------------------</span></span>
Ran</span> 61</span> tests in 60.822s</span></span>
</span>
FAILED (failures=12)</span></span></code></pre>

Hey at least no weird UTF-8 errors. That's 7 less failures than last time (which was 16 failures and 3 errors).</p>

AssertionError</h3>

Note: Please do not rad this section. It is embarrassing.</p> </blockquote>

This is a more readable error than the last one.</p>

AssertionError:</span> Incorrect behavior in chapter_9/valid/stack_arguments/test_for_memory_leaks</span></span>
*</span> Bad return code: expected 1 and got 0</span></span></code></pre>

I can work with this. This is the C file in question. These are a lot of arguments.</p>

/* Make sure stack arguments are deallocated correctly after returning from a function call; also test passing variables as stack arguments */</span></span>
</span>
#ifdef</span> SUPPRESS_WARNINGS</span></span>
#pragma</span> GCC diagnostic ignored</span> "-Wunused-parameter"</span></span>
#endif</span></span>
</span>
int</span> lots_of_args(</span>int</span> a,</span> int</span> b,</span> int</span> c,</span> int</span> d,</span> int</span> e,</span> int</span> f,</span> int</span> g,</span> int</span> h,</span> int</span> i,</span> int</span> j,</span> int</span> k,</span> int</span> l,</span> int</span> m,</span> int</span> n,</span> int</span> o) {</span></span>
    return</span> l </span>+</span> o;</span></span>
}</span></span>
</span>
int</span> main(</span>void</span>) {</span></span>
    int</span> ret </span>=</span> 0</span>;</span></span>
    for</span> (</span>int</span> i </span>=</span> 0</span>; i </span><</span> 10000000</span>; i </span>=</span> i </span>+</span> 1</span>) {</span></span>
        ret </span>=</span> lots_of_args(</span>1</span>,</span> 2</span>,</span> 3</span>,</span> 4</span>,</span> 5</span>,</span> 6</span>,</span> 7</span>,</span> 8</span>,</span> 9</span>,</span> 10</span>,</span> 11</span>, ret,</span> 13</span>,</span> 14</span>,</span> 15</span>);</span></span>
    }</span></span>
    return</span> ret </span>==</span> 150000000</span>;</span></span>
}</span></span></code></pre>

I will skip the whole parsing to IR thing and simply give you here the last codegen, with added comments. It is huge</em>.</p>

PROGRAM</span></span>
	FUNCTION lots_of_args</span></span>
		allocate	64</span></span>
		mov	DI -> stack -4		; a</span></span>
		mov	SI -> stack -8		; b</span></span>
		mov	DX -> stack -12		; c</span></span>
		mov	CX -> stack -16		; d</span></span>
		mov	R8 -> stack -20		; e</span></span>
		mov	R9 -> stack -24		; f</span></span>
		mov	stack 16 -> R10</span></span>
		mov	R10 -> stack -28	; g</span></span>
		mov	stack 24 -> R10</span></span>
		mov	R10 -> stack -32	; h</span></span>
		mov	stack 32 -> R10</span></span>
		mov	R10 -> stack -36	; i</span></span>
		mov	stack 40 -> R10</span></span>
		mov	R10 -> stack -40	; j</span></span>
		mov	stack 48 -> R10</span></span>
		mov	R10 -> stack -44	; k</span></span>
		mov	stack 56 -> R10</span></span>
		mov	R10 -> stack -48	; l</span></span>
		mov	stack 64 -> R10</span></span>
		mov	R10 -> stack -52	; m</span></span>
		mov	stack 72 -> R10</span></span>
		mov	R10 -> stack -56	; n</span></span>
		mov	stack 80 -> R10</span></span>
		mov	R10 -> stack -60	; o</span></span>
		mov	stack -48 -> R10</span></span>
		mov	R10 -> stack -64	; l</span></span>
		mov	stack -60 -> R10	; o</span></span>
		add	R10 -> stack -64	; l + o</span></span>
		mov	stack -64 -> AX</span></span>
		ret</span></span>
		mov	imm 0 -> AX</span></span>
		ret</span></span>
	FUNCTION main</span></span>
		allocate	32</span></span>
		mov	imm 0 -> stack -4	; ret</span></span>
		mov	imm 0 -> stack -8	; i</span></span>
		=> .Lst_for.16</span></span>
		cmp	imm 10000000 -> stack -8</span></span>
		mov	imm 0 -> stack -12</span></span>
		setl	stack -12</span></span>
		cmp	imm 0 -> stack -12</span></span>
		jmpe	.Lbr_for.16</span></span>
		allocate	8	; odd number of arguments</span></span>
		; register arguments</span></span>
		mov	imm 1 -> DI	; a</span></span>
		mov	imm 2 -> SI	; b</span></span>
		mov	imm 3 -> DX	; c</span></span>
		mov	imm 4 -> CX	; d</span></span>
		mov	imm 5 -> R8	; e</span></span>
		mov	imm 6 -> R9	; f</span></span>
		; stack arguments</span></span>
		push	imm 15		; o</span></span>
		push	imm 14		; n</span></span>
		push	imm 13		; m</span></span>
		mov	stack -4 -> AX 	; ret into l</span></span>
		push	AX		; l</span></span>
		push	imm 11		; k</span></span>
		push	imm 10		; j</span></span>
		push	imm 9		; i</span></span>
		push	imm 8		; h</span></span>
		push	imm 7		; g</span></span>
		call	lots_of_args</span></span>
		deallocate	80	; why is this 80? 9 pushes + padding</span></span>
		mov	AX -> stack -16	; l + o here</span></span>
		mov	stack -16 -> R10</span></span>
		mov	R10 -> stack -4	; assign to ret</span></span>
		=> .Lcn_for.16</span></span>
		mov	stack -8 -> R10</span></span>
		mov	R10 -> stack -20</span></span>
		add	imm 1 -> stack -20</span></span>
		mov	stack -20 -> R10</span></span>
		mov	R10 -> stack -8</span></span>
		jmp	.Lst_for.16</span></span>
		=> .Lbr_for.16</span></span>
		cmp	imm 150000000 -> stack -4</span></span>
		mov	imm 0 -> stack -24</span></span>
		sete	stack -24</span></span>
		mov	stack -24 -> AX</span></span>
		ret</span></span>
		mov	imm 0 -> AX</span></span>
		ret</span></span></code></pre>

I am actually at a loss. All this seems to make sense. The generated assembly is more of the same. Let us see another failure:</p>

AssertionError:</span> Incorrect behavior in chapter_9/valid/arguments_in_registers/fibonacci</span></span>
*</span> Bad return code: expected 8 and got -11</span></span></code></pre>

How would you even get</em> -11?</p>

int</span> fib(</span>int</span> n) {</span></span>
    if</span> (n </span>==</span> 0</span> ||</span> n </span>==</span> 1</span>) {</span></span>
        return</span> n;</span></span>
    }</span> else</span> {</span></span>
        return</span> fib(n </span>-</span> 1</span>)</span> +</span> fib(n </span>-</span> 2</span>);</span></span>
    }</span></span>
}</span></span>
</span>
int</span> main(</span>void</span>) {</span></span>
    int</span> n </span>=</span> 6</span>;</span></span>
    return</span> fib(n);</span></span>
}</span></span></code></pre>

This is the assembly this time.</p>

	.globl _fib</span></span>
_fib:</span></span>
	pushq   %rbp		;; function prologue</span></span>
	movq    %rsp, %rbp</span></span>
	subq    $48, %rsp	;; allocate 48</span></span>
	movl    %edi, -4(%rsp)	;; n</span></span>
	cmpl    $0, -4(%rsp)	;; does n equal 0?</span></span>
	movl    $0, -8(%rsp)</span></span>
	sete      -8(%rsp)</span></span>
	cmpl    $0, -8(%rsp)	;; is n == 0 true ?</span></span>
	jne     .Ltrue_or.2</span></span>
	cmpl    $1, -4(%rsp)	;; does n equal 1?</span></span>
	movl    $0, -12(%rsp)</span></span>
	sete      -12(%rsp)</span></span>
	cmpl    $0, -12(%rsp)	;; is n == 1 true?</span></span>
	jne     .Ltrue_or.2</span></span>
	movl    $0, -16(%rsp)</span></span>
	jmp    .Lend_or.3</span></span>
.Ltrue_or.2:</span></span>
	movl    $1, -16(%rsp)</span></span>
.Lend_or.3:</span></span>
	cmpl    $0, -16(%rsp)	;; is the previous expression false?</span></span>
	je      .Lelse.7</span></span>
	movl    -4(%rsp), %eax	;; if so just return n</span></span>
	movq    %rbp, %rsp</span></span>
	popq    %rbp</span></span>
	ret</span></span>
	jmp    .Lend.8</span></span>
.Lelse.7:</span></span>
	movl    -4(%rsp), %r10d		;; if not ..</span></span>
	movl    %r10d, -20(%rsp)	;; move n here</span></span>
	subl    $1, -20(%rsp)		;; n - 1</span></span>
	movl    -20(%rsp), %edi		;; move n-1 to edi</span></span>
	call    _fib			;; call fib</span></span>
	movl    %eax, -24(%rsp)		;; stash result of fib(n-1)</span></span>
	movl    -4(%rsp), %r10d</span></span>
	movl    %r10d, -28(%rsp)	;; move n here</span></span>
	subl    $2, -28(%rsp)		;; n - 2</span></span>
	movl    -28(%rsp), %edi		;; move n-1 to edi</span></span>
	call    _fib			;; call fib</span></span>
	movl    %eax, -32(%rsp)		;; stash result of fib(n-2)</span></span>
	movl    -24(%rsp), %r10d	;; stashed result of fib(n-1)</span></span>
	movl    %r10d, -36(%rsp)</span></span>
	movl    -32(%rsp), %r10d	;; stashed result of fib(n-2)</span></span>
	addl    %r10d, -36(%rsp)	;; add two results together</span></span>
	movl    -36(%rsp), %eax		;; move result to proper place</span></span>
	movq    %rbp, %rsp</span></span>
	popq    %rbp</span></span>
	ret				;; the end</span></span>
.Lend.8:</span></span>
	movl    $0, %eax	;; this bit here is the useless return 0</span></span>
	movq    %rbp, %rsp	;; added at the end of every function</span></span>
	popq    %rbp</span></span>
	ret</span></span>
	.globl _main</span></span>
_main:</span></span>
	pushq   %rbp</span></span>
	movq    %rsp, %rbp</span></span>
	subq    $16, %rsp</span></span>
	movl    $6, -4(%rsp)</span></span>
	movl    -4(%rsp), %edi</span></span>
	call    _fib</span></span>
	movl    %eax, -8(%rsp)</span></span>
	movl    -8(%rsp), %eax</span></span>
	movq    %rbp, %rsp</span></span>
	popq    %rbp</span></span>
	ret</span></span>
	movl    $0, %eax</span></span>
	movq    %rbp, %rsp</span></span>
	popq    %rbp</span></span>
	ret</span></span></code></pre>

The logic seems fine. I am not debugging the logic for if</code>s and for</code>s as those test cases pass fine already. This sample here has no stack arguments yet it is still failing. This definitely means there is a memory corruption somewhere, but where</em>? I am sure the smart ones of you spotted it. I am not that smart.5</a></sup></p>

I figured maybe the mistake is from using the subshell. So I tried doing the test suite the manual way I did it previously. But nope, same result. The good news is that my subshell spell works perfectly.</p>

On a whim, which is most likely wrong, I thought of changing the sign of the stack offset for function bodies. (So they start from -16 downwards rather than 16 upwards). And that would not affect the failure in the fibonacci</code> example anyway. Surprisingly, that actually affected absolutely nothing: same number of failing cases. I am starting to think this is a parking_lot</code> bug.</p>

I moved to compare the codegen stage with my previous rust program, and it is identical</em>. I moved on to comparing the assembly generated. Even using diff</code> with colors</em> to compare the two assemblies.</p>

Then there I saw it. The logic is fine, but there is a typo: something I wrote many chapters ago.</p>

Behold: stack offsets: -8(%rsp)</code>. It should be from %rbp</code>.</p>

Now all the tests pass. Wonder how any of them did in the first place.</p>


Lessons Learned</h2>
  1. Spell checking is important.</li>
  2. diff --color</code> is a useful tool.</li>
  3. Unrelated to paella</code> itself, but I made some improvements to the build script, and learned more about jj</code> and git</code>. I think I will add git</code> tags to commits that finish each chapter, which would make browsing them easier.</li>
  4. This week I installed llm</code></a>, and had it set up to talk to the free version of Github Copilot. Really convenient and useful, and I used to spell check the articles. I will try some local models next but I do not expect they will do much with my Macbook Air M2.</li>
  5. Can I have typed integer literals or slightly greedier type inference in Zig please? It is absurd that ~15</code> just does not work</em>.</li> </ol>

    One chapter left for Part 1. Unsure what to do next.</p>


    1. Type errors between different files is very pointedly a not-my-problem. It is a linker error, while this is a compiler. ↩</a></p> </li>

    2. When will Rust have enums as proper const generics? Please, Rust. ↩</a></p> </li>

    3. To be honest, and for all chapters, I am skipping most of the reading and jumping through to the tables at the end of the section to implement the functions. I assume that is what the tables are for. ↩</a></p> </li>

    4. The Book's pseudocode gives different instructions to add if the IR value is converted to an immediate value, register, or a pseudo value. The weird part here is that, well until chapter 15 (where I stopped last time), the IR value is never converted to a register at this stage. This could be something in preparation for the optimizations in Part 3. ↩</a></p> </li>

    5. Gemini LLM told me this is fine. Though to be honest, Github Copilot pointed it out, but I missed it among the five useless things it also mentioned. ↩</a></p> </li> </ol> </section>

I took the time to refactor out the common logic between function parameters and variable declarations. Zig's comptime</code> allows some tricks that Rust would torture me for,2</a></sup> forcing me to use a trait or some weird amalgamation of const generics. It turned out most of the logic is for identifiers, except for initializers in variable declarations. The new function follows.</p>

Read on ar-ms.me

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.