Commit a32d3a85d2

Andrew Kelley <andrew@ziglang.org>
2020-05-10 08:05:54
rework self-hosted compiler for incremental builds
* introduce std.ArrayListUnmanaged for when you have the allocator stored elsewhere * move std.heap.ArenaAllocator implementation to its own file. extract the main state into std.heap.ArenaAllocator.State, which can be stored as an alternative to storing the entire ArenaAllocator, saving 24 bytes per ArenaAllocator on 64 bit targets. * std.LinkedList.Node pointer field now defaults to being null initialized. * Rework self-hosted compiler Package API * Delete almost all the bitrotted self-hosted compiler code. The only bit rotted code left is in main.zig and compilation.zig * Add call instruction to ZIR * self-hosted compiler ir API and link API are reworked to support a long-running compiler that incrementally updates declarations * Introduce the concept of scopes to ZIR semantic analysis * ZIR text format supports referencing named decls that are declared later in the file * Figure out how memory management works for the long-running compiler and incremental compilation. The main roots are top level declarations. There is a table of decls. The key is a cryptographic hash of the fully qualified decl name. Each decl has an arena allocator where all of the memory related to that decl is stored. Each code block has its own arena allocator for the lifetime of the block. Values that want to survive when going out of scope in a block must get copied into the outer block. Finally, values must get copied into the Decl arena to be long-lived. * Delete the unused MemoryCell struct. Instead, comptime pointers are based on references to Decl structs. * Figure out how caching works. Each Decl will store a set of other Decls which must be recompiled when it changes. This branch is still work-in-progress; this commit breaks the build.
1 parent ae080b5
lib/std/heap/arena_allocator.zig
@@ -0,0 +1,102 @@
+const std = @import("../std.zig");
+const assert = std.debug.assert;
+const mem = std.mem;
+const Allocator = std.mem.Allocator;
+
+/// This allocator takes an existing allocator, wraps it, and provides an interface
+/// where you can allocate without freeing, and then free it all together.
+pub const ArenaAllocator = struct {
+    allocator: Allocator,
+
+    child_allocator: *Allocator,
+    state: State,
+
+    /// Inner state of ArenaAllocator. Can be stored rather than the entire ArenaAllocator
+    /// as a memory-saving optimization.
+    pub const State = struct {
+        buffer_list: std.SinglyLinkedList([]u8) = @as(std.SinglyLinkedList([]u8), .{}),
+        end_index: usize = 0,
+
+        pub fn promote(self: State, child_allocator: *Allocator) ArenaAllocator {
+            return .{
+                .allocator = Allocator{
+                    .reallocFn = realloc,
+                    .shrinkFn = shrink,
+                },
+                .child_allocator = child_allocator,
+                .state = self,
+            };
+        }
+    };
+
+    const BufNode = std.SinglyLinkedList([]u8).Node;
+
+    pub fn init(child_allocator: *Allocator) ArenaAllocator {
+        return (State{}).promote(child_allocator);
+    }
+
+    pub fn deinit(self: ArenaAllocator) void {
+        var it = self.state.buffer_list.first;
+        while (it) |node| {
+            // this has to occur before the free because the free frees node
+            const next_it = node.next;
+            self.child_allocator.free(node.data);
+            it = next_it;
+        }
+    }
+
+    fn createNode(self: *ArenaAllocator, prev_len: usize, minimum_size: usize) !*BufNode {
+        const actual_min_size = minimum_size + @sizeOf(BufNode);
+        var len = prev_len;
+        while (true) {
+            len += len / 2;
+            len += mem.page_size - @rem(len, mem.page_size);
+            if (len >= actual_min_size) break;
+        }
+        const buf = try self.child_allocator.alignedAlloc(u8, @alignOf(BufNode), len);
+        const buf_node_slice = mem.bytesAsSlice(BufNode, buf[0..@sizeOf(BufNode)]);
+        const buf_node = &buf_node_slice[0];
+        buf_node.* = BufNode{
+            .data = buf,
+            .next = null,
+        };
+        self.state.buffer_list.prepend(buf_node);
+        self.state.end_index = 0;
+        return buf_node;
+    }
+
+    fn alloc(allocator: *Allocator, n: usize, alignment: u29) ![]u8 {
+        const self = @fieldParentPtr(ArenaAllocator, "allocator", allocator);
+
+        var cur_node = if (self.state.buffer_list.first) |first_node| first_node else try self.createNode(0, n + alignment);
+        while (true) {
+            const cur_buf = cur_node.data[@sizeOf(BufNode)..];
+            const addr = @ptrToInt(cur_buf.ptr) + self.state.end_index;
+            const adjusted_addr = mem.alignForward(addr, alignment);
+            const adjusted_index = self.state.end_index + (adjusted_addr - addr);
+            const new_end_index = adjusted_index + n;
+            if (new_end_index > cur_buf.len) {
+                cur_node = try self.createNode(cur_buf.len, n + alignment);
+                continue;
+            }
+            const result = cur_buf[adjusted_index..new_end_index];
+            self.state.end_index = new_end_index;
+            return result;
+        }
+    }
+
+    fn realloc(allocator: *Allocator, old_mem: []u8, old_align: u29, new_size: usize, new_align: u29) ![]u8 {
+        if (new_size <= old_mem.len and new_align <= new_size) {
+            // We can't do anything with the memory, so tell the client to keep it.
+            return error.OutOfMemory;
+        } else {
+            const result = try alloc(allocator, new_size, new_align);
+            @memcpy(result.ptr, old_mem.ptr, std.math.min(old_mem.len, result.len));
+            return result;
+        }
+    }
+
+    fn shrink(allocator: *Allocator, old_mem: []u8, old_align: u29, new_size: usize, new_align: u29) []u8 {
+        return old_mem[0..new_size];
+    }
+};
lib/std/array_list.zig
@@ -8,13 +8,13 @@ const Allocator = mem.Allocator;
 /// A contiguous, growable list of items in memory.
 /// This is a wrapper around an array of T values. Initialize with `init`.
 pub fn ArrayList(comptime T: type) type {
-    return AlignedArrayList(T, null);
+    return ArrayListAligned(T, null);
 }
 
-pub fn AlignedArrayList(comptime T: type, comptime alignment: ?u29) type {
+pub fn ArrayListAligned(comptime T: type, comptime alignment: ?u29) type {
     if (alignment) |a| {
         if (a == @alignOf(T)) {
-            return AlignedArrayList(T, null);
+            return ArrayListAligned(T, null);
         }
     }
     return struct {
@@ -76,6 +76,10 @@ pub fn AlignedArrayList(comptime T: type, comptime alignment: ?u29) type {
             };
         }
 
+        pub fn toUnmanaged(self: Self) ArrayListAlignedUnmanaged(T, alignment) {
+            return .{ .items = self.items, .capacity = self.capacity };
+        }
+
         /// The caller owns the returned memory. ArrayList becomes empty.
         pub fn toOwnedSlice(self: *Self) Slice {
             const allocator = self.allocator;
@@ -84,8 +88,8 @@ pub fn AlignedArrayList(comptime T: type, comptime alignment: ?u29) type {
             return result;
         }
 
-        /// Insert `item` at index `n`. Moves `list[n .. list.len]`
-        /// to make room.
+        /// Insert `item` at index `n` by moving `list[n .. list.len]` to make room.
+        /// This operation is O(N).
         pub fn insert(self: *Self, n: usize, item: T) !void {
             try self.ensureCapacity(self.items.len + 1);
             self.items.len += 1;
@@ -94,8 +98,7 @@ pub fn AlignedArrayList(comptime T: type, comptime alignment: ?u29) type {
             self.items[n] = item;
         }
 
-        /// Insert slice `items` at index `i`. Moves
-        /// `list[i .. list.len]` to make room.
+        /// Insert slice `items` at index `i` by moving `list[i .. list.len]` to make room.
         /// This operation is O(N).
         pub fn insertSlice(self: *Self, i: usize, items: SliceConst) !void {
             try self.ensureCapacity(self.items.len + items.len);
@@ -259,6 +262,232 @@ pub fn AlignedArrayList(comptime T: type, comptime alignment: ?u29) type {
     };
 }
 
+/// Bring-your-own allocator with every function call.
+/// Initialize directly and deinitialize with `deinit` or use `toOwnedSlice`.
+pub fn init() Self {
+    return .{
+        .items = &[_]T{},
+        .capacity = 0,
+    };
+}
+
+pub fn ArrayListUnmanaged(comptime T: type) type {
+    return ArrayListAlignedUnmanaged(T, null);
+}
+
+pub fn ArrayListAlignedUnmanaged(comptime T: type, comptime alignment: ?u29) type {
+    if (alignment) |a| {
+        if (a == @alignOf(T)) {
+            return ArrayListAlignedUnmanaged(T, null);
+        }
+    }
+    return struct {
+        const Self = @This();
+
+        /// Content of the ArrayList.
+        items: Slice = &[_]T{},
+        capacity: usize = 0,
+
+        pub const Slice = if (alignment) |a| ([]align(a) T) else []T;
+        pub const SliceConst = if (alignment) |a| ([]align(a) const T) else []const T;
+
+        /// Initialize with capacity to hold at least num elements.
+        /// Deinitialize with `deinit` or use `toOwnedSlice`.
+        pub fn initCapacity(allocator: *Allocator, num: usize) !Self {
+            var self = Self.init(allocator);
+            try self.ensureCapacity(allocator, num);
+            return self;
+        }
+
+        /// Release all allocated memory.
+        pub fn deinit(self: *Self, allocator: *Allocator) void {
+            allocator.free(self.allocatedSlice());
+            self.* = undefined;
+        }
+
+        pub fn toManaged(self: *Self, allocator: *Allocator) ArrayListAligned(T, alignment) {
+            return .{ .items = self.items, .capacity = self.capacity, .allocator = allocator };
+        }
+
+        /// The caller owns the returned memory. ArrayList becomes empty.
+        pub fn toOwnedSlice(self: *Self, allocator: *Allocator) Slice {
+            const result = allocator.shrink(self.allocatedSlice(), self.items.len);
+            self.* = init(allocator);
+            return result;
+        }
+
+        /// Insert `item` at index `n`. Moves `list[n .. list.len]`
+        /// to make room.
+        pub fn insert(self: *Self, allocator: *Allocator, n: usize, item: T) !void {
+            try self.ensureCapacity(allocator, self.items.len + 1);
+            self.items.len += 1;
+
+            mem.copyBackwards(T, self.items[n + 1 .. self.items.len], self.items[n .. self.items.len - 1]);
+            self.items[n] = item;
+        }
+
+        /// Insert slice `items` at index `i`. Moves
+        /// `list[i .. list.len]` to make room.
+        /// This operation is O(N).
+        pub fn insertSlice(self: *Self, allocator: *Allocator, i: usize, items: SliceConst) !void {
+            try self.ensureCapacity(allocator, self.items.len + items.len);
+            self.items.len += items.len;
+
+            mem.copyBackwards(T, self.items[i + items.len .. self.items.len], self.items[i .. self.items.len - items.len]);
+            mem.copy(T, self.items[i .. i + items.len], items);
+        }
+
+        /// Extend the list by 1 element. Allocates more memory as necessary.
+        pub fn append(self: *Self, allocator: *Allocator, item: T) !void {
+            const new_item_ptr = try self.addOne(allocator);
+            new_item_ptr.* = item;
+        }
+
+        /// Extend the list by 1 element, but asserting `self.capacity`
+        /// is sufficient to hold an additional item.
+        pub fn appendAssumeCapacity(self: *Self, item: T) void {
+            const new_item_ptr = self.addOneAssumeCapacity();
+            new_item_ptr.* = item;
+        }
+
+        /// Remove the element at index `i` from the list and return its value.
+        /// Asserts the array has at least one item.
+        /// This operation is O(N).
+        pub fn orderedRemove(self: *Self, i: usize) T {
+            const newlen = self.items.len - 1;
+            if (newlen == i) return self.pop();
+
+            const old_item = self.items[i];
+            for (self.items[i..newlen]) |*b, j| b.* = self.items[i + 1 + j];
+            self.items[newlen] = undefined;
+            self.items.len = newlen;
+            return old_item;
+        }
+
+        /// Removes the element at the specified index and returns it.
+        /// The empty slot is filled from the end of the list.
+        /// This operation is O(1).
+        pub fn swapRemove(self: *Self, i: usize) T {
+            if (self.items.len - 1 == i) return self.pop();
+
+            const old_item = self.items[i];
+            self.items[i] = self.pop();
+            return old_item;
+        }
+
+        /// Append the slice of items to the list. Allocates more
+        /// memory as necessary.
+        pub fn appendSlice(self: *Self, allocator: *Allocator, items: SliceConst) !void {
+            const oldlen = self.items.len;
+            const newlen = self.items.len + items.len;
+
+            try self.ensureCapacity(allocator, newlen);
+            self.items.len = newlen;
+            mem.copy(T, self.items[oldlen..], items);
+        }
+
+        /// Same as `append` except it returns the number of bytes written, which is always the same
+        /// as `m.len`. The purpose of this function existing is to match `std.io.OutStream` API.
+        /// This function may be called only when `T` is `u8`.
+        fn appendWrite(self: *Self, allocator: *Allocator, m: []const u8) !usize {
+            try self.appendSlice(allocator, m);
+            return m.len;
+        }
+
+        /// Append a value to the list `n` times.
+        /// Allocates more memory as necessary.
+        pub fn appendNTimes(self: *Self, allocator: *Allocator, value: T, n: usize) !void {
+            const old_len = self.items.len;
+            try self.resize(self.items.len + n);
+            mem.set(T, self.items[old_len..self.items.len], value);
+        }
+
+        /// Adjust the list's length to `new_len`.
+        /// Does not initialize added items if any.
+        pub fn resize(self: *Self, allocator: *Allocator, new_len: usize) !void {
+            try self.ensureCapacity(allocator, new_len);
+            self.items.len = new_len;
+        }
+
+        /// Reduce allocated capacity to `new_len`.
+        /// Invalidates element pointers.
+        pub fn shrink(self: *Self, allocator: *Allocator, new_len: usize) void {
+            assert(new_len <= self.items.len);
+
+            self.items = allocator.realloc(self.allocatedSlice(), new_len) catch |e| switch (e) {
+                error.OutOfMemory => { // no problem, capacity is still correct then.
+                    self.items.len = new_len;
+                    return;
+                },
+            };
+            self.capacity = new_len;
+        }
+
+        pub fn ensureCapacity(self: *Self, allocator: *Allocator, new_capacity: usize) !void {
+            var better_capacity = self.capacity;
+            if (better_capacity >= new_capacity) return;
+
+            while (true) {
+                better_capacity += better_capacity / 2 + 8;
+                if (better_capacity >= new_capacity) break;
+            }
+
+            const new_memory = try allocator.realloc(self.allocatedSlice(), better_capacity);
+            self.items.ptr = new_memory.ptr;
+            self.capacity = new_memory.len;
+        }
+
+        /// Increases the array's length to match the full capacity that is already allocated.
+        /// The new elements have `undefined` values.
+        /// This operation does not invalidate any element pointers.
+        pub fn expandToCapacity(self: *Self) void {
+            self.items.len = self.capacity;
+        }
+
+        /// Increase length by 1, returning pointer to the new item.
+        /// The returned pointer becomes invalid when the list is resized.
+        pub fn addOne(self: *Self, allocator: *Allocator) !*T {
+            const newlen = self.items.len + 1;
+            try self.ensureCapacity(allocator, newlen);
+            return self.addOneAssumeCapacity();
+        }
+
+        /// Increase length by 1, returning pointer to the new item.
+        /// Asserts that there is already space for the new item without allocating more.
+        /// The returned pointer becomes invalid when the list is resized.
+        /// This operation does not invalidate any element pointers.
+        pub fn addOneAssumeCapacity(self: *Self) *T {
+            assert(self.items.len < self.capacity);
+
+            self.items.len += 1;
+            return &self.items[self.items.len - 1];
+        }
+
+        /// Remove and return the last element from the list.
+        /// Asserts the list has at least one item.
+        /// This operation does not invalidate any element pointers.
+        pub fn pop(self: *Self) T {
+            const val = self.items[self.items.len - 1];
+            self.items.len -= 1;
+            return val;
+        }
+
+        /// Remove and return the last element from the list.
+        /// If the list is empty, returns `null`.
+        /// This operation does not invalidate any element pointers.
+        pub fn popOrNull(self: *Self) ?T {
+            if (self.items.len == 0) return null;
+            return self.pop();
+        }
+
+        /// For a nicer API, `items.len` is the length, not the capacity.
+        /// This requires "unsafe" slicing.
+        fn allocatedSlice(self: Self) Slice {
+            return self.items.ptr[0..self.capacity];
+        }
+    };
+}
+
 test "std.ArrayList.init" {
     var list = ArrayList(i32).init(testing.allocator);
     defer list.deinit();
lib/std/heap.zig
@@ -11,6 +11,7 @@ const maxInt = std.math.maxInt;
 
 pub const LoggingAllocator = @import("heap/logging_allocator.zig").LoggingAllocator;
 pub const loggingAllocator = @import("heap/logging_allocator.zig").loggingAllocator;
+pub const ArenaAllocator = @import("heap/arena_allocator.zig").ArenaAllocator;
 
 const Allocator = mem.Allocator;
 
@@ -510,95 +511,6 @@ pub const HeapAllocator = switch (builtin.os.tag) {
     else => @compileError("Unsupported OS"),
 };
 
-/// This allocator takes an existing allocator, wraps it, and provides an interface
-/// where you can allocate without freeing, and then free it all together.
-pub const ArenaAllocator = struct {
-    allocator: Allocator,
-
-    child_allocator: *Allocator,
-    buffer_list: std.SinglyLinkedList([]u8),
-    end_index: usize,
-
-    const BufNode = std.SinglyLinkedList([]u8).Node;
-
-    pub fn init(child_allocator: *Allocator) ArenaAllocator {
-        return ArenaAllocator{
-            .allocator = Allocator{
-                .reallocFn = realloc,
-                .shrinkFn = shrink,
-            },
-            .child_allocator = child_allocator,
-            .buffer_list = std.SinglyLinkedList([]u8).init(),
-            .end_index = 0,
-        };
-    }
-
-    pub fn deinit(self: ArenaAllocator) void {
-        var it = self.buffer_list.first;
-        while (it) |node| {
-            // this has to occur before the free because the free frees node
-            const next_it = node.next;
-            self.child_allocator.free(node.data);
-            it = next_it;
-        }
-    }
-
-    fn createNode(self: *ArenaAllocator, prev_len: usize, minimum_size: usize) !*BufNode {
-        const actual_min_size = minimum_size + @sizeOf(BufNode);
-        var len = prev_len;
-        while (true) {
-            len += len / 2;
-            len += mem.page_size - @rem(len, mem.page_size);
-            if (len >= actual_min_size) break;
-        }
-        const buf = try self.child_allocator.alignedAlloc(u8, @alignOf(BufNode), len);
-        const buf_node_slice = mem.bytesAsSlice(BufNode, buf[0..@sizeOf(BufNode)]);
-        const buf_node = &buf_node_slice[0];
-        buf_node.* = BufNode{
-            .data = buf,
-            .next = null,
-        };
-        self.buffer_list.prepend(buf_node);
-        self.end_index = 0;
-        return buf_node;
-    }
-
-    fn alloc(allocator: *Allocator, n: usize, alignment: u29) ![]u8 {
-        const self = @fieldParentPtr(ArenaAllocator, "allocator", allocator);
-
-        var cur_node = if (self.buffer_list.first) |first_node| first_node else try self.createNode(0, n + alignment);
-        while (true) {
-            const cur_buf = cur_node.data[@sizeOf(BufNode)..];
-            const addr = @ptrToInt(cur_buf.ptr) + self.end_index;
-            const adjusted_addr = mem.alignForward(addr, alignment);
-            const adjusted_index = self.end_index + (adjusted_addr - addr);
-            const new_end_index = adjusted_index + n;
-            if (new_end_index > cur_buf.len) {
-                cur_node = try self.createNode(cur_buf.len, n + alignment);
-                continue;
-            }
-            const result = cur_buf[adjusted_index..new_end_index];
-            self.end_index = new_end_index;
-            return result;
-        }
-    }
-
-    fn realloc(allocator: *Allocator, old_mem: []u8, old_align: u29, new_size: usize, new_align: u29) ![]u8 {
-        if (new_size <= old_mem.len and new_align <= new_size) {
-            // We can't do anything with the memory, so tell the client to keep it.
-            return error.OutOfMemory;
-        } else {
-            const result = try alloc(allocator, new_size, new_align);
-            @memcpy(result.ptr, old_mem.ptr, std.math.min(old_mem.len, result.len));
-            return result;
-        }
-    }
-
-    fn shrink(allocator: *Allocator, old_mem: []u8, old_align: u29, new_size: usize, new_align: u29) []u8 {
-        return old_mem[0..new_size];
-    }
-};
-
 pub const FixedBufferAllocator = struct {
     allocator: Allocator,
     end_index: usize,
lib/std/linked_list.zig
@@ -49,7 +49,7 @@ pub fn SinglyLinkedList(comptime T: type) type {
             }
         };
 
-        first: ?*Node,
+        first: ?*Node = null,
 
         /// Initialize a linked list.
         ///
lib/std/std.zig
@@ -1,6 +1,8 @@
-pub const AlignedArrayList = @import("array_list.zig").AlignedArrayList;
 pub const ArrayList = @import("array_list.zig").ArrayList;
+pub const ArrayListAligned = @import("array_list.zig").ArrayListAligned;
+pub const ArrayListAlignedUnmanaged = @import("array_list.zig").ArrayListAlignedUnmanaged;
 pub const ArrayListSentineled = @import("array_list_sentineled.zig").ArrayListSentineled;
+pub const ArrayListUnmanaged = @import("array_list.zig").ArrayListUnmanaged;
 pub const AutoHashMap = @import("hash_map.zig").AutoHashMap;
 pub const BloomFilter = @import("bloom_filter.zig").BloomFilter;
 pub const BufMap = @import("buf_map.zig").BufMap;
src-self-hosted/ir/text.zig
@@ -16,10 +16,16 @@ pub const Inst = struct {
     tag: Tag,
     /// Byte offset into the source.
     src: usize,
+    name: []const u8,
 
     /// These names are used directly as the instruction names in the text format.
     pub const Tag = enum {
         breakpoint,
+        call,
+        /// Represents a reference to a global decl by name.
+        /// Canonicalized ZIR will not have any of these. The
+        /// syntax `@foo` is equivalent to `declref("foo")`.
+        declref,
         str,
         int,
         ptrtoint,
@@ -46,6 +52,8 @@ pub const Inst = struct {
     pub fn TagToType(tag: Tag) type {
         return switch (tag) {
             .breakpoint => Breakpoint,
+            .call => Call,
+            .declref => DeclRef,
             .str => Str,
             .int => Int,
             .ptrtoint => PtrToInt,
@@ -85,6 +93,29 @@ pub const Inst = struct {
         kw_args: struct {},
     };
 
+    pub const Call = struct {
+        pub const base_tag = Tag.call;
+        base: Inst,
+
+        positionals: struct {
+            func: *Inst,
+            args: []*Inst,
+        },
+        kw_args: struct {
+            modifier: std.builtin.CallOptions.Modifier = .auto,
+        },
+    };
+
+    pub const DeclRef = struct {
+        pub const base_tag = Tag.declref;
+        base: Inst,
+
+        positionals: struct {
+            name: *Inst,
+        },
+        kw_args: struct {},
+    };
+
     pub const Str = struct {
         pub const base_tag = Tag.str;
         base: Inst,
@@ -212,55 +243,55 @@ pub const Inst = struct {
         kw_args: struct {},
 
         pub const BuiltinType = enum {
-            @"isize",
-            @"usize",
-            @"c_short",
-            @"c_ushort",
-            @"c_int",
-            @"c_uint",
-            @"c_long",
-            @"c_ulong",
-            @"c_longlong",
-            @"c_ulonglong",
-            @"c_longdouble",
-            @"c_void",
-            @"f16",
-            @"f32",
-            @"f64",
-            @"f128",
-            @"bool",
-            @"void",
-            @"noreturn",
-            @"type",
-            @"anyerror",
-            @"comptime_int",
-            @"comptime_float",
+            isize,
+            usize,
+            c_short,
+            c_ushort,
+            c_int,
+            c_uint,
+            c_long,
+            c_ulong,
+            c_longlong,
+            c_ulonglong,
+            c_longdouble,
+            c_void,
+            f16,
+            f32,
+            f64,
+            f128,
+            bool,
+            void,
+            noreturn,
+            type,
+            anyerror,
+            comptime_int,
+            comptime_float,
 
             fn toType(self: BuiltinType) Type {
                 return switch (self) {
-                    .@"isize" => Type.initTag(.@"isize"),
-                    .@"usize" => Type.initTag(.@"usize"),
-                    .@"c_short" => Type.initTag(.@"c_short"),
-                    .@"c_ushort" => Type.initTag(.@"c_ushort"),
-                    .@"c_int" => Type.initTag(.@"c_int"),
-                    .@"c_uint" => Type.initTag(.@"c_uint"),
-                    .@"c_long" => Type.initTag(.@"c_long"),
-                    .@"c_ulong" => Type.initTag(.@"c_ulong"),
-                    .@"c_longlong" => Type.initTag(.@"c_longlong"),
-                    .@"c_ulonglong" => Type.initTag(.@"c_ulonglong"),
-                    .@"c_longdouble" => Type.initTag(.@"c_longdouble"),
-                    .@"c_void" => Type.initTag(.@"c_void"),
-                    .@"f16" => Type.initTag(.@"f16"),
-                    .@"f32" => Type.initTag(.@"f32"),
-                    .@"f64" => Type.initTag(.@"f64"),
-                    .@"f128" => Type.initTag(.@"f128"),
-                    .@"bool" => Type.initTag(.@"bool"),
-                    .@"void" => Type.initTag(.@"void"),
-                    .@"noreturn" => Type.initTag(.@"noreturn"),
-                    .@"type" => Type.initTag(.@"type"),
-                    .@"anyerror" => Type.initTag(.@"anyerror"),
-                    .@"comptime_int" => Type.initTag(.@"comptime_int"),
-                    .@"comptime_float" => Type.initTag(.@"comptime_float"),
+                    .isize => Type.initTag(.isize),
+                    .usize => Type.initTag(.usize),
+                    .c_short => Type.initTag(.c_short),
+                    .c_ushort => Type.initTag(.c_ushort),
+                    .c_int => Type.initTag(.c_int),
+                    .c_uint => Type.initTag(.c_uint),
+                    .c_long => Type.initTag(.c_long),
+                    .c_ulong => Type.initTag(.c_ulong),
+                    .c_longlong => Type.initTag(.c_longlong),
+                    .c_ulonglong => Type.initTag(.c_ulonglong),
+                    .c_longdouble => Type.initTag(.c_longdouble),
+                    .c_void => Type.initTag(.c_void),
+                    .f16 => Type.initTag(.f16),
+                    .f32 => Type.initTag(.f32),
+                    .f64 => Type.initTag(.f64),
+                    .f128 => Type.initTag(.f128),
+                    .bool => Type.initTag(.bool),
+                    .void => Type.initTag(.void),
+                    .noreturn => Type.initTag(.noreturn),
+                    .type => Type.initTag(.type),
+                    .anyerror => Type.initTag(.anyerror),
+                    .comptime_int => Type.initTag(.comptime_int),
+                    .comptime_float => Type.initTag(.comptime_float),
                 };
             }
         };
@@ -376,7 +407,7 @@ pub const ErrorMsg = struct {
 pub const Module = struct {
     decls: []*Inst,
     errors: []ErrorMsg,
-    arena: std.heap.ArenaAllocator,
+    arena: std.heap.ArenaAllocator.State,
 
     pub const Body = struct {
         instructions: []*Inst,
@@ -385,7 +416,7 @@ pub const Module = struct {
     pub fn deinit(self: *Module, allocator: *Allocator) void {
         allocator.free(self.decls);
         allocator.free(self.errors);
-        self.arena.deinit();
+        self.arena.promote(allocator).deinit();
         self.* = undefined;
     }
 
@@ -431,6 +462,7 @@ pub const Module = struct {
         // TODO I tried implementing this with an inline for loop and hit a compiler bug
         switch (decl.tag) {
             .breakpoint => return self.writeInstToStreamGeneric(stream, .breakpoint, decl, inst_table),
+            .call => return self.writeInstToStreamGeneric(stream, .call, decl, inst_table),
             .str => return self.writeInstToStreamGeneric(stream, .str, decl, inst_table),
             .int => return self.writeInstToStreamGeneric(stream, .int, decl, inst_table),
             .ptrtoint => return self.writeInstToStreamGeneric(stream, .ptrtoint, decl, inst_table),
@@ -543,9 +575,9 @@ pub fn parse(allocator: *Allocator, source: [:0]const u8) Allocator.Error!Module
         .arena = std.heap.ArenaAllocator.init(allocator),
         .i = 0,
         .source = source,
-        .decls = std.ArrayList(*Inst).init(allocator),
-        .errors = std.ArrayList(ErrorMsg).init(allocator),
         .global_name_map = &global_name_map,
+        .errors = .{},
+        .decls = .{},
     };
     errdefer parser.arena.deinit();
 
@@ -555,10 +587,11 @@ pub fn parse(allocator: *Allocator, source: [:0]const u8) Allocator.Error!Module
         },
         else => |e| return e,
     };
+
     return Module{
-        .decls = parser.decls.toOwnedSlice(),
-        .errors = parser.errors.toOwnedSlice(),
-        .arena = parser.arena,
+        .decls = parser.decls.toOwnedSlice(allocator),
+        .errors = parser.errors.toOwnedSlice(allocator),
+        .arena = parser.arena.state,
     };
 }
 
@@ -567,8 +600,8 @@ const Parser = struct {
     arena: std.heap.ArenaAllocator,
     i: usize,
     source: [:0]const u8,
-    errors: std.ArrayList(ErrorMsg),
-    decls: std.ArrayList(*Inst),
+    errors: std.ArrayListUnmanaged(ErrorMsg),
+    decls: std.ArrayListUnmanaged(*Inst),
     global_name_map: *std.StringHashMap(usize),
 
     const Body = struct {
@@ -893,8 +926,25 @@ const Parser = struct {
         const ident = self.source[name_start..self.i];
         const kv = map.get(ident) orelse {
             const bad_name = self.source[name_start - 1 .. self.i];
-            self.i = name_start - 1;
-            return self.fail("unrecognized identifier: {}", .{bad_name});
+            const src = name_start - 1;
+            if (local_ref) {
+                self.i = src;
+                return self.fail("unrecognized identifier: {}", .{bad_name});
+            } else {
+                const name = try self.arena.allocator.create(Inst.Str);
+                name.* = .{
+                    .base = .{ .src = src, .tag = Inst.Str.base_tag },
+                    .positionals = .{ .bytes = ident },
+                    .kw_args = .{},
+                };
+                const declref = try self.arena.allocator.create(Inst.DeclRef);
+                declref.* = .{
+                    .base = .{ .src = src, .tag = Inst.DeclRef.base_tag },
+                    .positionals = .{ .name = &name.base },
+                    .kw_args = .{},
+                };
+                return &declref.base;
+            }
         };
         if (local_ref) {
             return body_ctx.?.instructions.items[kv.value];
@@ -1065,6 +1115,24 @@ const EmitZIR = struct {
         for (body.instructions) |inst| {
             const new_inst = switch (inst.tag) {
                 .breakpoint => try self.emitTrivial(inst.src, Inst.Breakpoint),
+                .call => blk: {
+                    const old_inst = inst.cast(ir.Inst.Call).?;
+                    const new_inst = try self.arena.allocator.create(Inst.Call);
+
+                    const args = try self.arena.allocator.alloc(*Inst, old_inst.args.args.len);
+                    for (args) |*elem, i| {
+                        elem.* = try self.resolveInst(inst_table, old_inst.args.args[i]);
+                    }
+                    new_inst.* = .{
+                        .base = .{ .src = inst.src, .tag = Inst.Call.base_tag },
+                        .positionals = .{
+                            .func = try self.resolveInst(inst_table, old_inst.args.func),
+                            .args = args,
+                        },
+                        .kw_args = .{},
+                    };
+                    break :blk &new_inst.base;
+                },
                 .unreach => try self.emitTrivial(inst.src, Inst.Unreachable),
                 .ret => try self.emitTrivial(inst.src, Inst.Return),
                 .constant => unreachable, // excluded from function bodies
src-self-hosted/c.zig
@@ -1,7 +0,0 @@
-pub usingnamespace @cImport({
-    @cDefine("__STDC_CONSTANT_MACROS", "");
-    @cDefine("__STDC_LIMIT_MACROS", "");
-    @cInclude("inttypes.h");
-    @cInclude("config.h");
-    @cInclude("zig_llvm.h");
-});
src-self-hosted/codegen.zig
@@ -6,38 +6,24 @@ const Type = @import("type.zig").Type;
 const Value = @import("value.zig").Value;
 const Target = std.Target;
 
-pub const ErrorMsg = struct {
-    byte_offset: usize,
-    msg: []const u8,
-};
-
-pub const Symbol = struct {
-    errors: []ErrorMsg,
-
-    pub fn deinit(self: *Symbol, allocator: *mem.Allocator) void {
-        for (self.errors) |err| {
-            allocator.free(err.msg);
-        }
-        allocator.free(self.errors);
-        self.* = undefined;
-    }
-};
-
-pub fn generateSymbol(typed_value: ir.TypedValue, module: ir.Module, code: *std.ArrayList(u8)) !Symbol {
+pub fn generateSymbol(
+    typed_value: ir.TypedValue,
+    module: ir.Module,
+    code: *std.ArrayList(u8),
+    errors: *std.ArrayList(ir.ErrorMsg),
+) !void {
     switch (typed_value.ty.zigTypeTag()) {
         .Fn => {
-            const index = typed_value.val.cast(Value.Payload.Function).?.index;
-            const module_fn = module.fns[index];
+            const module_fn = typed_value.val.cast(Value.Payload.Function).?.func;
 
             var function = Function{
                 .module = &module,
-                .mod_fn = &module_fn,
+                .mod_fn = module_fn,
                 .code = code,
                 .inst_table = std.AutoHashMap(*ir.Inst, Function.MCValue).init(code.allocator),
-                .errors = std.ArrayList(ErrorMsg).init(code.allocator),
+                .errors = errors,
             };
             defer function.inst_table.deinit();
-            defer function.errors.deinit();
 
             for (module_fn.body.instructions) |inst| {
                 const new_inst = function.genFuncInst(inst) catch |err| switch (err) {
@@ -52,7 +38,7 @@ pub fn generateSymbol(typed_value: ir.TypedValue, module: ir.Module, code: *std.
 
             return Symbol{ .errors = function.errors.toOwnedSlice() };
         },
-        else => @panic("TODO implement generateSymbol for non-function types"),
+        else => @panic("TODO implement generateSymbol for non-function decls"),
     }
 }
 
@@ -61,7 +47,7 @@ const Function = struct {
     mod_fn: *const ir.Module.Fn,
     code: *std.ArrayList(u8),
     inst_table: std.AutoHashMap(*ir.Inst, MCValue),
-    errors: std.ArrayList(ErrorMsg),
+    errors: *std.ArrayList(ir.ErrorMsg),
 
     const MCValue = union(enum) {
         none,
@@ -78,6 +64,7 @@ const Function = struct {
     fn genFuncInst(self: *Function, inst: *ir.Inst) !MCValue {
         switch (inst.tag) {
             .breakpoint => return self.genBreakpoint(inst.src),
+            .call => return self.genCall(inst.cast(ir.Inst.Call).?),
             .unreach => return MCValue{ .unreach = {} },
             .constant => unreachable, // excluded from function bodies
             .assembly => return self.genAsm(inst.cast(ir.Inst.Assembly).?),
@@ -101,6 +88,13 @@ const Function = struct {
         return .unreach;
     }
 
+    fn genCall(self: *Function, inst: *ir.Inst.Call) !MCValue {
+        switch (self.module.target.cpu.arch) {
+            else => return self.fail(inst.base.src, "TODO implement call for {}", .{self.module.target.cpu.arch}),
+        }
+        return .unreach;
+    }
+
     fn genRet(self: *Function, inst: *ir.Inst.Ret) !MCValue {
         switch (self.module.target.cpu.arch) {
             .i386, .x86_64 => {
@@ -140,6 +134,7 @@ const Function = struct {
     fn genRelativeFwdJump(self: *Function, src: usize, amount: u32) !void {
         switch (self.module.target.cpu.arch) {
             .i386, .x86_64 => {
+                // TODO x86 treats the operands as signed
                 if (amount <= std.math.maxInt(u8)) {
                     try self.code.resize(self.code.items.len + 2);
                     self.code.items[self.code.items.len - 2] = 0xeb;
@@ -433,14 +428,11 @@ const Function = struct {
 
     fn fail(self: *Function, src: usize, comptime format: []const u8, args: var) error{ CodegenFail, OutOfMemory } {
         @setCold(true);
-        const msg = try std.fmt.allocPrint(self.errors.allocator, format, args);
-        {
-            errdefer self.errors.allocator.free(msg);
-            (try self.errors.addOne()).* = .{
-                .byte_offset = src,
-                .msg = msg,
-            };
-        }
+        try self.errors.ensureCapacity(self.errors.items.len + 1);
+        self.errors.appendAssumeCapacity(.{
+            .byte_offset = src,
+            .msg = try std.fmt.allocPrint(self.errors.allocator, format, args),
+        });
         return error.CodegenFail;
     }
 };
src-self-hosted/compilation.zig
@@ -19,7 +19,6 @@ const AtomicOrder = builtin.AtomicOrder;
 const Scope = @import("scope.zig").Scope;
 const Decl = @import("decl.zig").Decl;
 const ir = @import("ir.zig");
-const Visib = @import("visib.zig").Visib;
 const Value = @import("value.zig").Value;
 const Type = Value.Type;
 const Span = errmsg.Span;
@@ -30,7 +29,11 @@ const link = @import("link.zig").link;
 const LibCInstallation = @import("libc_installation.zig").LibCInstallation;
 const CInt = @import("c_int.zig").CInt;
 const fs = std.fs;
-const util = @import("util.zig");
+
+pub const Visib = enum {
+    Private,
+    Pub,
+};
 
 const max_src_size = 2 * 1024 * 1024 * 1024; // 2 GiB
 
@@ -45,7 +48,7 @@ pub const ZigCompiler = struct {
 
     native_libc: event.Future(LibCInstallation),
 
-    var lazy_init_targets = std.once(util.initializeAllTargets);
+    var lazy_init_targets = std.once(initializeAllTargets);
 
     pub fn init(allocator: *Allocator) !ZigCompiler {
         lazy_init_targets.call();
@@ -119,6 +122,8 @@ pub const LlvmHandle = struct {
 };
 
 pub const Compilation = struct {
+    pub const FnLinkSet = std.TailQueue(?*Value.Fn);
+
     zig_compiler: *ZigCompiler,
     name: ArrayListSentineled(u8, 0),
     llvm_triple: ArrayListSentineled(u8, 0),
@@ -152,8 +157,6 @@ pub const Compilation = struct {
     /// it uses an optional pointer so that tombstone removals are possible
     fn_link_set: event.Locked(FnLinkSet) = event.Locked(FnLinkSet).init(FnLinkSet.init()),
 
-    pub const FnLinkSet = std.TailQueue(?*Value.Fn);
-
     link_libs_list: ArrayList(*LinkLib),
     libc_link_lib: ?*LinkLib = null,
 
@@ -361,8 +364,7 @@ pub const Compilation = struct {
             return comp;
         } else if (await frame) |_| unreachable else |err| return err;
     }
-
-    async fn createAsync(
+    fn createAsync(
         out_comp: *?*Compilation,
         zig_compiler: *ZigCompiler,
         name: []const u8,
@@ -372,7 +374,7 @@ pub const Compilation = struct {
         build_mode: builtin.Mode,
         is_static: bool,
         zig_lib_dir: []const u8,
-    ) !void {
+    ) callconv(.Async) !void {
         const allocator = zig_compiler.allocator;
 
         // TODO merge this line with stage2.zig crossTargetToTarget
@@ -442,8 +444,8 @@ pub const Compilation = struct {
         }
 
         comp.name = try ArrayListSentineled(u8, 0).init(comp.arena(), name);
-        comp.llvm_triple = try util.getLLVMTriple(comp.arena(), target);
-        comp.llvm_target = try util.llvmTargetFromTriple(comp.llvm_triple);
+        comp.llvm_triple = try getLLVMTriple(comp.arena(), target);
+        comp.llvm_target = try llvmTargetFromTriple(comp.llvm_triple);
         comp.zig_std_dir = try fs.path.join(comp.arena(), &[_][]const u8{ zig_lib_dir, "std" });
 
         const opt_level = switch (build_mode) {
@@ -726,8 +728,7 @@ pub const Compilation = struct {
     fn start(self: *Compilation) void {
         self.main_loop_future.resolve();
     }
-
-    async fn mainLoop(self: *Compilation) void {
+    fn mainLoop(self: *Compilation) callconv(.Async) void {
         // wait until start() is called
         _ = self.main_loop_future.get();
 
@@ -790,8 +791,7 @@ pub const Compilation = struct {
             build_result = group.wait();
         }
     }
-
-    async fn rebuildFile(self: *Compilation, root_scope: *Scope.Root) BuildError!void {
+    fn rebuildFile(self: *Compilation, root_scope: *Scope.Root) callconv(.Async) BuildError!void {
         const tree_scope = blk: {
             const source_code = fs.cwd().readFileAlloc(
                 self.gpa(),
@@ -964,15 +964,14 @@ pub const Compilation = struct {
             try link(self);
         }
     }
-
     /// caller takes ownership of resulting Code
-    async fn genAndAnalyzeCode(
+    fn genAndAnalyzeCode(
         comp: *Compilation,
         tree_scope: *Scope.AstTree,
         scope: *Scope,
         node: *ast.Node,
         expected_type: ?*Type,
-    ) !*ir.Code {
+    ) callconv(.Async) !*ir.Code {
         const unanalyzed_code = try ir.gen(
             comp,
             node,
@@ -1000,13 +999,12 @@ pub const Compilation = struct {
 
         return analyzed_code;
     }
-
-    async fn addCompTimeBlock(
+    fn addCompTimeBlock(
         comp: *Compilation,
         tree_scope: *Scope.AstTree,
         scope: *Scope,
         comptime_node: *ast.Node.Comptime,
-    ) BuildError!void {
+    ) callconv(.Async) BuildError!void {
         const void_type = Type.Void.get(comp);
         defer void_type.base.base.deref(comp);
 
@@ -1024,12 +1022,11 @@ pub const Compilation = struct {
         };
         analyzed_code.destroy(comp.gpa());
     }
-
-    async fn addTopLevelDecl(
+    fn addTopLevelDecl(
         self: *Compilation,
         decl: *Decl,
         locked_table: *Decl.Table,
-    ) BuildError!void {
+    ) callconv(.Async) BuildError!void {
         const is_export = decl.isExported(decl.tree_scope.tree);
 
         if (is_export) {
@@ -1065,11 +1062,10 @@ pub const Compilation = struct {
 
         try self.prelink_group.call(addCompileErrorAsync, .{ self, msg });
     }
-
-    async fn addCompileErrorAsync(
+    fn addCompileErrorAsync(
         self: *Compilation,
         msg: *Msg,
-    ) BuildError!void {
+    ) callconv(.Async) BuildError!void {
         errdefer msg.destroy();
 
         const compile_errors = self.compile_errors.acquire();
@@ -1077,8 +1073,7 @@ pub const Compilation = struct {
 
         try compile_errors.value.append(msg);
     }
-
-    async fn verifyUniqueSymbol(self: *Compilation, decl: *Decl) BuildError!void {
+    fn verifyUniqueSymbol(self: *Compilation, decl: *Decl) callconv(.Async) BuildError!void {
         const exported_symbol_names = self.exported_symbol_names.acquire();
         defer exported_symbol_names.release();
 
@@ -1129,8 +1124,7 @@ pub const Compilation = struct {
         }
         return link_lib;
     }
-
-    async fn startFindingNativeLibC(self: *Compilation) void {
+    fn startFindingNativeLibC(self: *Compilation) callconv(.Async) void {
         event.Loop.startCpuBoundOperation();
         // we don't care if it fails, we're just trying to kick off the future resolution
         _ = self.zig_compiler.getNativeLibC() catch return;
@@ -1234,7 +1228,7 @@ pub const Compilation = struct {
     }
 
     /// This declaration has been blessed as going into the final code generation.
-    pub async fn resolveDecl(comp: *Compilation, decl: *Decl) BuildError!void {
+    pub fn resolveDecl(comp: *Compilation, decl: *Decl) callconv(.Async) BuildError!void {
         if (decl.resolution.start()) |ptr| return ptr.*;
 
         decl.resolution.data = try generateDecl(comp, decl);
@@ -1335,8 +1329,7 @@ fn generateDeclFn(comp: *Compilation, fn_decl: *Decl.Fn) !void {
     try comp.prelink_group.call(codegen.renderToLlvm, .{ comp, fn_val, analyzed_code });
     try comp.prelink_group.call(addFnToLinkSet, .{ comp, fn_val });
 }
-
-async fn addFnToLinkSet(comp: *Compilation, fn_val: *Value.Fn) Compilation.BuildError!void {
+fn addFnToLinkSet(comp: *Compilation, fn_val: *Value.Fn) callconv(.Async) Compilation.BuildError!void {
     fn_val.base.ref();
     defer fn_val.base.deref(comp);
 
@@ -1432,3 +1425,33 @@ fn generateDeclFnProto(comp: *Compilation, fn_decl: *Decl.Fn) !void {
     fn_decl.value = .{ .FnProto = fn_proto_val };
     symbol_name_consumed = true;
 }
+
+pub fn llvmTargetFromTriple(triple: [:0]const u8) !*llvm.Target {
+    var result: *llvm.Target = undefined;
+    var err_msg: [*:0]u8 = undefined;
+    if (llvm.GetTargetFromTriple(triple, &result, &err_msg) != 0) {
+        std.debug.warn("triple: {s} error: {s}\n", .{ triple, err_msg });
+        return error.UnsupportedTarget;
+    }
+    return result;
+}
+
+pub fn initializeAllTargets() void {
+    llvm.InitializeAllTargets();
+    llvm.InitializeAllTargetInfos();
+    llvm.InitializeAllTargetMCs();
+    llvm.InitializeAllAsmPrinters();
+    llvm.InitializeAllAsmParsers();
+}
+
+pub fn getLLVMTriple(allocator: *std.mem.Allocator, target: std.Target) ![:0]u8 {
+    var result = try std.ArrayListSentineled(u8, 0).initSize(allocator, 0);
+    defer result.deinit();
+
+    try result.outStream().print(
+        "{}-unknown-{}-{}",
+        .{ @tagName(target.cpu.arch), @tagName(target.os.tag), @tagName(target.abi) },
+    );
+
+    return result.toOwnedSlice();
+}
src-self-hosted/decl.zig
@@ -1,102 +0,0 @@
-const std = @import("std");
-const Allocator = mem.Allocator;
-const mem = std.mem;
-const ast = std.zig.ast;
-const Visib = @import("visib.zig").Visib;
-const event = std.event;
-const Value = @import("value.zig").Value;
-const Token = std.zig.Token;
-const errmsg = @import("errmsg.zig");
-const Scope = @import("scope.zig").Scope;
-const Compilation = @import("compilation.zig").Compilation;
-
-pub const Decl = struct {
-    id: Id,
-    name: []const u8,
-    visib: Visib,
-    resolution: event.Future(Compilation.BuildError!void),
-    parent_scope: *Scope,
-
-    // TODO when we destroy the decl, deref the tree scope
-    tree_scope: *Scope.AstTree,
-
-    pub const Table = std.StringHashMap(*Decl);
-
-    pub fn cast(base: *Decl, comptime T: type) ?*T {
-        if (base.id != @field(Id, @typeName(T))) return null;
-        return @fieldParentPtr(T, "base", base);
-    }
-
-    pub fn isExported(base: *const Decl, tree: *ast.Tree) bool {
-        switch (base.id) {
-            .Fn => {
-                const fn_decl = @fieldParentPtr(Fn, "base", base);
-                return fn_decl.isExported(tree);
-            },
-            else => return false,
-        }
-    }
-
-    pub fn getSpan(base: *const Decl) errmsg.Span {
-        switch (base.id) {
-            .Fn => {
-                const fn_decl = @fieldParentPtr(Fn, "base", base);
-                const fn_proto = fn_decl.fn_proto;
-                const start = fn_proto.fn_token;
-                const end = fn_proto.name_token orelse start;
-                return errmsg.Span{
-                    .first = start,
-                    .last = end + 1,
-                };
-            },
-            else => @panic("TODO"),
-        }
-    }
-
-    pub fn findRootScope(base: *const Decl) *Scope.Root {
-        return base.parent_scope.findRoot();
-    }
-
-    pub const Id = enum {
-        Var,
-        Fn,
-        CompTime,
-    };
-
-    pub const Var = struct {
-        base: Decl,
-    };
-
-    pub const Fn = struct {
-        base: Decl,
-        value: union(enum) {
-            Unresolved,
-            Fn: *Value.Fn,
-            FnProto: *Value.FnProto,
-        },
-        fn_proto: *ast.Node.FnProto,
-
-        pub fn externLibName(self: Fn, tree: *ast.Tree) ?[]const u8 {
-            return if (self.fn_proto.extern_export_inline_token) |tok_index| x: {
-                const token = tree.tokens.at(tok_index);
-                break :x switch (token.id) {
-                    .Extern => tree.tokenSlicePtr(token),
-                    else => null,
-                };
-            } else null;
-        }
-
-        pub fn isExported(self: Fn, tree: *ast.Tree) bool {
-            if (self.fn_proto.extern_export_inline_token) |tok_index| {
-                const token = tree.tokens.at(tok_index);
-                return token.id == .Keyword_export;
-            } else {
-                return false;
-            }
-        }
-    };
-
-    pub const CompTime = struct {
-        base: Decl,
-    };
-};
src-self-hosted/ir.zig
@@ -1,12 +1,16 @@
 const std = @import("std");
 const mem = std.mem;
 const Allocator = std.mem.Allocator;
+const ArrayListUnmanaged = std.ArrayListUnmanaged;
+const LinkedList = std.TailQueue;
 const Value = @import("value.zig").Value;
 const Type = @import("type.zig").Type;
 const assert = std.debug.assert;
 const BigIntConst = std.math.big.int.Const;
 const BigIntMutable = std.math.big.int.Mutable;
 const Target = std.Target;
+const Package = @import("Package.zig");
+const link = @import("link.zig");
 
 pub const text = @import("ir/text.zig");
 
@@ -25,6 +29,7 @@ pub const Inst = struct {
         assembly,
         bitcast,
         breakpoint,
+        call,
         cmp,
         condbr,
         constant,
@@ -84,6 +89,15 @@ pub const Inst = struct {
         args: void,
     };
 
+    pub const Call = struct {
+        pub const base_tag = Tag.call;
+        base: Inst,
+        args: struct {
+            func: *Inst,
+            args: []const *Inst,
+        },
+    };
+
     pub const Cmp = struct {
         pub const base_tag = Tag.cmp;
 
@@ -158,170 +172,416 @@ pub const TypedValue = struct {
     val: Value,
 };
 
+fn swapRemoveElem(allocator: *Allocator, comptime T: type, item: T, list: *ArrayListUnmanaged(T)) void {
+    var i: usize = 0;
+    while (i < list.items.len) {
+        if (list.items[i] == item) {
+            list.swapRemove(allocator, i);
+            continue;
+        }
+        i += 1;
+    }
+}
+
 pub const Module = struct {
-    exports: []Export,
-    errors: []ErrorMsg,
-    arena: std.heap.ArenaAllocator,
-    fns: []Fn,
-    target: Target,
-    link_mode: std.builtin.LinkMode,
-    output_mode: std.builtin.OutputMode,
-    object_format: std.Target.ObjectFormat,
+    /// General-purpose allocator.
+    allocator: *Allocator,
+    /// Module owns this resource.
+    root_pkg: *Package,
+    /// Module owns this resource.
+    root_scope: *Scope.ZIRModule,
+    /// Pointer to externally managed resource.
+    bin_file: *link.ElfFile,
+    failed_decls: ArrayListUnmanaged(*Decl) = .{},
+    failed_fns: ArrayListUnmanaged(*Fn) = .{},
+    failed_files: ArrayListUnmanaged(*Scope.ZIRModule) = .{},
+    decl_table: std.AutoHashMap(Decl.Hash, *Decl),
     optimize_mode: std.builtin.Mode,
-
-    pub const Export = struct {
-        name: []const u8,
-        typed_value: TypedValue,
+    link_error_flags: link.ElfFile.ErrorFlags = .{},
+
+    pub const Decl = struct {
+        /// Contains the memory for `typed_value` and this `Decl` itself.
+        /// If the Decl is a function, also contains that memory.
+        /// If the decl has any export nodes, also contains that memory.
+        /// TODO look into using a more memory efficient arena that will cost less bytes per decl.
+        /// This one has a minimum allocation of 4096 bytes.
+        arena: std.heap.ArenaAllocator.State,
+        /// This name is relative to the containing namespace of the decl. It uses a null-termination
+        /// to save bytes, since there can be a lot of decls in a compilation. The null byte is not allowed
+        /// in symbol names, because executable file formats use null-terminated strings for symbol names.
+        name: [*:0]const u8,
+        /// It's rare for a decl to be exported, and it's even rarer for a decl to be mapped to more
+        /// than one export, so we use a linked list to save memory.
+        export_node: ?*LinkedList(std.builtin.ExportOptions).Node = null,
+        /// Byte offset into the source file that contains this declaration.
+        /// This is the base offset that src offsets within this Decl are relative to.
         src: usize,
+        /// Represents the "shallow" analysis status. For example, for decls that are functions,
+        /// the function type is analyzed with this set to `in_progress`, however, the semantic
+        /// analysis of the function body is performed with this value set to `success`. Functions
+        /// have their own analysis status field.
+        analysis: union(enum) {
+            in_progress,
+            failure: ErrorMsg,
+            success: TypedValue,
+        },
+        /// The direct container of the Decl. This field will need to get more fleshed out when
+        /// self-hosted supports proper struct types and Zig AST => ZIR.
+        scope: *Scope.ZIRModule,
+
+        pub fn destroy(self: *Decl, allocator: *Allocator) void {
+            var arena = self.arena.promote(allocator);
+            arena.deinit();
+        }
+
+        pub const Hash = [16]u8;
+
+        /// Must generate unique bytes with no collisions with other decls.
+        /// The point of hashing here is only to limit the number of bytes of
+        /// the unique identifier to a fixed size (16 bytes).
+        pub fn fullyQualifiedNameHash(self: Decl) Hash {
+            // Right now we only have ZIRModule as the source. So this is simply the
+            // relative name of the decl.
+            var out: Hash = undefined;
+            std.crypto.Blake3.hash(mem.spanZ(u8, self.name), &out);
+            return out;
+        }
     };
 
+    /// Memory is managed by the arena of the owning Decl.
     pub const Fn = struct {
-        analysis_status: enum { in_progress, failure, success },
-        body: Body,
         fn_type: Type,
+        analysis: union(enum) {
+            in_progress: *Analysis,
+            failure: ErrorMsg,
+            success: Body,
+        },
+        /// The direct container of the Fn. This field will need to get more fleshed out when
+        /// self-hosted supports proper struct types and Zig AST => ZIR.
+        scope: *Scope.ZIRModule,
+
+        /// This memory managed by the general purpose allocator.
+        pub const Analysis = struct {
+            inner_block: Scope.Block,
+            /// null value means a semantic analysis error happened.
+            inst_table: std.AutoHashMap(*text.Inst, ?*Inst),
+        };
+    };
+
+    pub const Scope = struct {
+        tag: Tag,
+
+        pub fn cast(base: *Scope, comptime T: type) ?*T {
+            if (base.tag != T.base_tag)
+                return null;
+
+            return @fieldParentPtr(T, "base", base);
+        }
+
+        pub const Tag = enum {
+            zir_module,
+            block,
+            decl,
+        };
+
+        pub const ZIRModule = struct {
+            pub const base_tag: Tag = .zir_module;
+            base: Scope = Scope{ .tag = base_tag },
+            /// Relative to the owning package's root_src_dir.
+            /// Reference to external memory, not owned by ZIRModule.
+            sub_file_path: []const u8,
+            contents: union(enum) {
+                unloaded,
+                parse_failure: ParseFailure,
+                success: Contents,
+            },
+            pub const ParseFailure = struct {
+                source: [:0]const u8,
+                errors: []ErrorMsg,
+
+                pub fn deinit(self: *ParseFailure, allocator: *Allocator) void {
+                    allocator.free(self.errors);
+                    allocator.free(source);
+                }
+            };
+            pub const Contents = struct {
+                source: [:0]const u8,
+                module: *text.Module,
+            };
+
+            pub fn deinit(self: *ZIRModule, allocator: *Allocator) void {
+                switch (self.contents) {
+                    .unloaded => {},
+                    .parse_failure => |pf| pd.deinit(allocator),
+                    .success => |contents| {
+                        allocator.free(contents.source);
+                        contents.src_zir_module.deinit(allocator);
+                    },
+                }
+                self.* = undefined;
+            }
+
+            pub fn loadContents(self: *ZIRModule, allocator: *Allocator) !*Contents {
+                if (self.contents) |contents| return contents;
+
+                const max_size = std.math.maxInt(u32);
+                const source = try self.root_pkg_dir.readFileAllocOptions(allocator, self.root_src_path, max_size, 1, 0);
+                errdefer allocator.free(source);
+
+                var errors = std.ArrayList(ErrorMsg).init(allocator);
+                defer errors.deinit();
+
+                var src_zir_module = try text.parse(allocator, source, &errors);
+                errdefer src_zir_module.deinit(allocator);
+
+                switch (self.contents) {
+                    .parse_failure => |pf| pf.deinit(allocator),
+                    .unloaded => {},
+                    .success => unreachable,
+                }
+
+                if (errors.items.len != 0) {
+                    self.contents = .{ .parse_failure = errors.toOwnedSlice() };
+                    return error.ParseFailure;
+                }
+                self.contents = .{
+                    .success = .{
+                        .source = source,
+                        .module = src_zir_module,
+                    },
+                };
+                return &self.contents.success;
+            }
+        };
+
+        /// This is a temporary structure, references to it are valid only
+        /// during semantic analysis of the block.
+        pub const Block = struct {
+            pub const base_tag: Tag = .block;
+            base: Scope = Scope{ .tag = base_tag },
+            func: *Fn,
+            instructions: ArrayListUnmanaged(*Inst),
+        };
+
+        /// This is a temporary structure, references to it are valid only
+        /// during semantic analysis of the decl.
+        pub const DeclAnalysis = struct {
+            pub const base_tag: Tag = .decl;
+            base: Scope = Scope{ .tag = base_tag },
+            decl: *Decl,
+        };
     };
 
     pub const Body = struct {
         instructions: []*Inst,
     };
 
-    pub fn deinit(self: *Module, allocator: *Allocator) void {
-        allocator.free(self.exports);
+    pub const AllErrors = struct {
+        arena: std.heap.ArenaAllocator.State,
+        list: []const Message,
+
+        pub const Message = struct {
+            src_path: []const u8,
+            line: usize,
+            column: usize,
+            byte_offset: usize,
+            msg: []const u8,
+        };
+
+        pub fn deinit(self: *AllErrors, allocator: *Allocator) void {
+            self.arena.promote(allocator).deinit();
+        }
+
+        fn add(
+            arena: *std.heap.ArenaAllocator,
+            errors: *std.ArrayList(Message),
+            sub_file_path: []const u8,
+            source: []const u8,
+            simple_err_msg: ErrorMsg,
+        ) !void {
+            const loc = std.zig.findLineColumn(source, simple_err_msg.byte_offset);
+            try errors.append(.{
+                .src_path = try mem.dupe(u8, &arena.allocator, sub_file_path),
+                .msg = try mem.dupe(u8, &arena.allocator, simple_err_msg.msg),
+                .byte_offset = simple_err_msg.byte_offset,
+                .line = loc.line,
+                .column = loc.column,
+            });
+        }
+    };
+
+    pub fn deinit(self: *Module) void {
+        const allocator = self.allocator;
         allocator.free(self.errors);
-        for (self.fns) |f| {
-            allocator.free(f.body.instructions);
+        {
+            var it = self.decl_table.iterator();
+            while (it.next()) |kv| {
+                kv.value.destroy(allocator);
+            }
+            self.decl_table.deinit();
         }
-        allocator.free(self.fns);
-        self.arena.deinit();
+        self.root_pkg.destroy();
+        self.root_scope.deinit();
         self.* = undefined;
     }
-};
 
-pub const ErrorMsg = struct {
-    byte_offset: usize,
-    msg: []const u8,
-};
+    pub fn target(self: Module) std.Target {
+        return self.bin_file.options.target;
+    }
 
-pub const AnalyzeOptions = struct {
-    target: Target,
-    output_mode: std.builtin.OutputMode,
-    link_mode: std.builtin.LinkMode,
-    object_format: ?std.Target.ObjectFormat = null,
-    optimize_mode: std.builtin.Mode,
-};
+    /// Detect changes to source files, perform semantic analysis, and update the output files.
+    pub fn update(self: *Module) !void {
+        // TODO Use the cache hash file system to detect which source files changed.
+        // Here we simulate a full cache miss.
+        // Analyze the root source file now.
+        self.analyzeRoot(self.root_scope) catch |err| switch (err) {
+            error.AnalysisFail => {
+                assert(self.totalErrorCount() != 0);
+            },
+            else => |e| return e,
+        };
 
-pub fn analyze(allocator: *Allocator, old_module: text.Module, options: AnalyzeOptions) !Module {
-    var ctx = Analyze{
-        .allocator = allocator,
-        .arena = std.heap.ArenaAllocator.init(allocator),
-        .old_module = &old_module,
-        .errors = std.ArrayList(ErrorMsg).init(allocator),
-        .decl_table = std.AutoHashMap(*text.Inst, Analyze.NewDecl).init(allocator),
-        .exports = std.ArrayList(Module.Export).init(allocator),
-        .fns = std.ArrayList(Module.Fn).init(allocator),
-        .target = options.target,
-        .optimize_mode = options.optimize_mode,
-        .link_mode = options.link_mode,
-        .output_mode = options.output_mode,
-    };
-    defer ctx.errors.deinit();
-    defer ctx.decl_table.deinit();
-    defer ctx.exports.deinit();
-    defer ctx.fns.deinit();
-    errdefer ctx.arena.deinit();
-
-    ctx.analyzeRoot() catch |err| switch (err) {
-        error.AnalysisFail => {
-            assert(ctx.errors.items.len != 0);
-        },
-        else => |e| return e,
-    };
-    return Module{
-        .exports = ctx.exports.toOwnedSlice(),
-        .errors = ctx.errors.toOwnedSlice(),
-        .fns = ctx.fns.toOwnedSlice(),
-        .arena = ctx.arena,
-        .target = ctx.target,
-        .link_mode = ctx.link_mode,
-        .output_mode = ctx.output_mode,
-        .object_format = options.object_format orelse ctx.target.getObjectFormat(),
-        .optimize_mode = ctx.optimize_mode,
-    };
-}
+        try self.bin_file.flush();
+        self.link_error_flags = self.bin_file.error_flags;
+    }
 
-const Analyze = struct {
-    allocator: *Allocator,
-    arena: std.heap.ArenaAllocator,
-    old_module: *const text.Module,
-    errors: std.ArrayList(ErrorMsg),
-    decl_table: std.AutoHashMap(*text.Inst, NewDecl),
-    exports: std.ArrayList(Module.Export),
-    fns: std.ArrayList(Module.Fn),
-    target: Target,
-    link_mode: std.builtin.LinkMode,
-    optimize_mode: std.builtin.Mode,
-    output_mode: std.builtin.OutputMode,
+    pub fn totalErrorCount(self: *Module) usize {
+        return self.failed_decls.items.len +
+            self.failed_fns.items.len +
+            self.failed_decls.items.len +
+            @boolToInt(self.link_error_flags.no_entry_point_found);
+    }
 
-    const NewDecl = struct {
-        /// null means a semantic analysis error happened
-        ptr: ?*Inst,
-    };
+    pub fn getAllErrorsAlloc(self: *Module) !AllErrors {
+        var arena = std.heap.ArenaAllocator.init(self.allocator);
+        errdefer arena.deinit();
 
-    const NewInst = struct {
-        /// null means a semantic analysis error happened
-        ptr: ?*Inst,
-    };
+        var errors = std.ArrayList(AllErrors.Message).init(self.allocator);
+        defer errors.deinit();
 
-    const Fn = struct {
-        /// Index into Module fns array
-        fn_index: usize,
-        inner_block: Block,
-        inst_table: std.AutoHashMap(*text.Inst, NewInst),
-    };
+        for (self.failed_files.items) |scope| {
+            const source = scope.parse_failure.source;
+            for (scope.parse_failure.errors) |parse_error| {
+                AllErrors.add(&arena, &errors, scope.sub_file_path, source, parse_error);
+            }
+        }
 
-    const Block = struct {
-        func: *Fn,
-        instructions: std.ArrayList(*Inst),
-    };
+        for (self.failed_fns.items) |func| {
+            const source = func.scope.success.source;
+            for (func.analysis.failure) |err_msg| {
+                AllErrors.add(&arena, &errors, func.scope.sub_file_path, source, err_msg);
+            }
+        }
+
+        for (self.failed_decls.items) |decl| {
+            const source = decl.scope.success.source;
+            for (decl.analysis.failure) |err_msg| {
+                AllErrors.add(&arena, &errors, decl.scope.sub_file_path, source, err_msg);
+            }
+        }
+
+        if (self.link_error_flags.no_entry_point_found) {
+            try errors.append(.{
+                .src_path = self.module.root_src_path,
+                .line = 0,
+                .column = 0,
+                .byte_offset = 0,
+                .msg = try std.fmt.allocPrint(&arena.allocator, "no entry point found", .{}),
+            });
+        }
+
+        assert(errors.items.len == self.totalErrorCount());
+
+        return AllErrors{
+            .arena = arena.state,
+            .list = try mem.dupe(&arena.allocator, AllErrors.Message, errors.items),
+        };
+    }
 
     const InnerError = error{ OutOfMemory, AnalysisFail };
 
-    fn analyzeRoot(self: *Analyze) !void {
-        for (self.old_module.decls) |decl| {
+    fn analyzeRoot(self: *Module, root_scope: *Scope.ZIRModule) !void {
+        // TODO use the cache to identify, from the modified source files, the decls which have
+        // changed based on the span of memory that represents the decl in the re-parsed source file.
+        // Use the cached dependency graph to recursively determine the set of decls which need
+        // regeneration.
+        // Here we simulate adding a source file which was previously not part of the compilation,
+        // which means scanning the decls looking for exports.
+        // TODO also identify decls that need to be deleted.
+        const contents = blk: {
+            // Clear parse errors.
+            swapRemoveElem(self.allocator, *Scope.ZIRModule, root_scope, self.failed_files);
+            try self.failed_files.ensureCapacity(self.allocator, self.failed_files.items.len + 1);
+            break :blk root_scope.loadContents(self.allocator) catch |err| switch (err) {
+                error.ParseFailure => {
+                    self.failed_files.appendAssumeCapacity(root_scope);
+                    return error.AnalysisFail;
+                },
+                else => |e| return e,
+            };
+        };
+        for (contents.module.decls) |decl| {
             if (decl.cast(text.Inst.Export)) |export_inst| {
-                try analyzeExport(self, null, export_inst);
+                try analyzeExport(self, &root_scope.base, export_inst);
             }
         }
     }
 
-    fn resolveInst(self: *Analyze, opt_block: ?*Block, old_inst: *text.Inst) InnerError!*Inst {
-        if (opt_block) |block| {
+    fn resolveDecl(self: *Module, scope: *Scope, old_inst: *text.Inst) InnerError!*Decl {
+        const hash = old_inst.fullyQualifiedNameHash();
+        if (self.decl_table.get(hash)) |kv| {
+            return kv.value;
+        } else {
+            const new_decl = blk: {
+                var decl_arena = std.heap.ArenaAllocator.init(self.allocator);
+                errdefer decl_arena.deinit();
+                const new_decl = try decl_arena.allocator.create(Decl);
+                const name = try mem.dupeZ(&decl_arena.allocator, u8, old_inst.name);
+                new_decl.* = .{
+                    .arena = decl_arena.state,
+                    .name = name,
+                    .src = old_inst.src,
+                    .analysis = .in_progress,
+                    .scope = scope.findZIRModule(),
+                };
+                try self.decl_table.putNoClobber(hash, new_decl);
+                break :blk new_decl;
+            };
+
+            var decl_scope: Scope.DeclAnalysis = .{ .decl = new_decl };
+            const typed_value = self.analyzeInstConst(&decl_scope.base, old_inst) catch |err| switch (err) {
+                error.AnalysisFail => return error.AnalysisFail,
+                else => |e| return e,
+            };
+            new_decl.analysis = .{ .success = typed_value };
+            if (try self.bin_file.updateDecl(self.*, typed_value, new_decl.export_node, hash)) |err_msg| {
+                new_decl.analysis = .{ .success = typed_value };
+            } else |err| {
+                return err;
+            }
+            return new_decl;
+        }
+    }
+
+    fn resolveInst(self: *Module, scope: *Scope, old_inst: *text.Inst) InnerError!*Inst {
+        if (scope.cast(Scope.Block)) |block| {
             if (block.func.inst_table.get(old_inst)) |kv| {
                 return kv.value.ptr orelse return error.AnalysisFail;
             }
         }
 
-        if (self.decl_table.get(old_inst)) |kv| {
-            return kv.value.ptr orelse return error.AnalysisFail;
-        } else {
-            const new_inst = self.analyzeInst(null, old_inst) catch |err| switch (err) {
-                error.AnalysisFail => {
-                    try self.decl_table.putNoClobber(old_inst, .{ .ptr = null });
-                    return error.AnalysisFail;
-                },
-                else => |e| return e,
-            };
-            try self.decl_table.putNoClobber(old_inst, .{ .ptr = new_inst });
-            return new_inst;
-        }
+        const decl = try self.resolveDecl(scope, old_inst);
+        const decl_ref = try self.analyzeDeclRef(scope, old_inst.src, decl);
+        return self.analyzeDeref(scope, old_inst.src, decl_ref);
     }
 
-    fn requireRuntimeBlock(self: *Analyze, block: ?*Block, src: usize) !*Block {
-        return block orelse return self.fail(src, "instruction illegal outside function body", .{});
+    fn requireRuntimeBlock(self: *Module, scope: *Scope, src: usize) !*Scope.Block {
+        return scope.cast(Scope.Block) orelse
+            return self.fail(scope, src, "instruction illegal outside function body", .{});
     }
 
-    fn resolveInstConst(self: *Analyze, block: ?*Block, old_inst: *text.Inst) InnerError!TypedValue {
-        const new_inst = try self.resolveInst(block, old_inst);
+    fn resolveInstConst(self: *Module, scope: *Scope, old_inst: *text.Inst) InnerError!TypedValue {
+        const new_inst = try self.resolveInst(scope, old_inst);
         const val = try self.resolveConstValue(new_inst);
         return TypedValue{
             .ty = new_inst.ty,
@@ -329,60 +589,67 @@ const Analyze = struct {
         };
     }
 
-    fn resolveConstValue(self: *Analyze, base: *Inst) !Value {
+    fn resolveConstValue(self: *Module, scope: *Scope, base: *Inst) !Value {
         return (try self.resolveDefinedValue(base)) orelse
-            return self.fail(base.src, "unable to resolve comptime value", .{});
+            return self.fail(scope, base.src, "unable to resolve comptime value", .{});
     }
 
-    fn resolveDefinedValue(self: *Analyze, base: *Inst) !?Value {
+    fn resolveDefinedValue(self: *Module, scope: *Scope, base: *Inst) !?Value {
         if (base.value()) |val| {
             if (val.isUndef()) {
-                return self.fail(base.src, "use of undefined value here causes undefined behavior", .{});
+                return self.fail(scope, base.src, "use of undefined value here causes undefined behavior", .{});
             }
             return val;
         }
         return null;
     }
 
-    fn resolveConstString(self: *Analyze, block: ?*Block, old_inst: *text.Inst) ![]u8 {
-        const new_inst = try self.resolveInst(block, old_inst);
+    fn resolveConstString(self: *Module, scope: *Scope, old_inst: *text.Inst) ![]u8 {
+        const new_inst = try self.resolveInst(scope, old_inst);
         const wanted_type = Type.initTag(.const_slice_u8);
-        const coerced_inst = try self.coerce(block, wanted_type, new_inst);
+        const coerced_inst = try self.coerce(scope, wanted_type, new_inst);
         const val = try self.resolveConstValue(coerced_inst);
         return val.toAllocatedBytes(&self.arena.allocator);
     }
 
-    fn resolveType(self: *Analyze, block: ?*Block, old_inst: *text.Inst) !Type {
-        const new_inst = try self.resolveInst(block, old_inst);
+    fn resolveType(self: *Module, scope: *Scope, old_inst: *text.Inst) !Type {
+        const new_inst = try self.resolveInst(scope, old_inst);
         const wanted_type = Type.initTag(.@"type");
-        const coerced_inst = try self.coerce(block, wanted_type, new_inst);
+        const coerced_inst = try self.coerce(scope, wanted_type, new_inst);
         const val = try self.resolveConstValue(coerced_inst);
         return val.toType();
     }
 
-    fn analyzeExport(self: *Analyze, block: ?*Block, export_inst: *text.Inst.Export) !void {
-        const symbol_name = try self.resolveConstString(block, export_inst.positionals.symbol_name);
-        const typed_value = try self.resolveInstConst(block, export_inst.positionals.value);
-
-        switch (typed_value.ty.zigTypeTag()) {
-            .Fn => {},
-            else => return self.fail(
-                export_inst.positionals.value.src,
-                "unable to export type '{}'",
-                .{typed_value.ty},
-            ),
+    fn analyzeExport(self: *Module, scope: *Scope, export_inst: *text.Inst.Export) !void {
+        const symbol_name = try self.resolveConstString(scope, export_inst.positionals.symbol_name);
+        const decl = try self.resolveDecl(scope, export_inst.positionals.value);
+
+        switch (decl.analysis) {
+            .in_progress => unreachable,
+            .failure => return error.AnalysisFail,
+            .success => |typed_value| switch (typed_value.ty.zigTypeTag()) {
+                .Fn => {},
+                else => return self.fail(
+                    scope,
+                    export_inst.positionals.value.src,
+                    "unable to export type '{}'",
+                    .{typed_value.ty},
+                ),
+            },
         }
-        try self.exports.append(.{
-            .name = symbol_name,
-            .typed_value = typed_value,
-            .src = export_inst.base.src,
-        });
+        const Node = LinkedList(std.builtin.ExportOptions).Node;
+        export_node = try decl.arena.promote(self.allocator).allocator.create(Node);
+        export_node.* = .{ .data = .{ .name = symbol_name } };
+        decl.export_node = export_node;
+
+        // TODO Avoid double update in the case of exporting a decl that we just created.
+        self.bin_file.updateDeclExports();
     }
 
     /// TODO should not need the cast on the last parameter at the callsites
     fn addNewInstArgs(
-        self: *Analyze,
-        block: *Block,
+        self: *Module,
+        block: *Scope.Block,
         src: usize,
         ty: Type,
         comptime T: type,
@@ -393,7 +660,7 @@ const Analyze = struct {
         return &inst.base;
     }
 
-    fn addNewInst(self: *Analyze, block: *Block, src: usize, ty: Type, comptime T: type) !*T {
+    fn addNewInst(self: *Module, block: *Scope.Block, src: usize, ty: Type, comptime T: type) !*T {
         const inst = try self.arena.allocator.create(T);
         inst.* = .{
             .base = .{
@@ -403,11 +670,11 @@ const Analyze = struct {
             },
             .args = undefined,
         };
-        try block.instructions.append(&inst.base);
+        try block.instructions.append(self.allocator, &inst.base);
         return inst;
     }
 
-    fn constInst(self: *Analyze, src: usize, typed_value: TypedValue) !*Inst {
+    fn constInst(self: *Module, src: usize, typed_value: TypedValue) !*Inst {
         const const_inst = try self.arena.allocator.create(Inst.Constant);
         const_inst.* = .{
             .base = .{
@@ -420,7 +687,7 @@ const Analyze = struct {
         return &const_inst.base;
     }
 
-    fn constStr(self: *Analyze, src: usize, str: []const u8) !*Inst {
+    fn constStr(self: *Module, src: usize, str: []const u8) !*Inst {
         const array_payload = try self.arena.allocator.create(Type.Payload.Array_u8_Sentinel0);
         array_payload.* = .{ .len = str.len };
 
@@ -436,35 +703,35 @@ const Analyze = struct {
         });
     }
 
-    fn constType(self: *Analyze, src: usize, ty: Type) !*Inst {
+    fn constType(self: *Module, src: usize, ty: Type) !*Inst {
         return self.constInst(src, .{
             .ty = Type.initTag(.type),
             .val = try ty.toValue(&self.arena.allocator),
         });
     }
 
-    fn constVoid(self: *Analyze, src: usize) !*Inst {
+    fn constVoid(self: *Module, src: usize) !*Inst {
         return self.constInst(src, .{
             .ty = Type.initTag(.void),
             .val = Value.initTag(.the_one_possible_value),
         });
     }
 
-    fn constUndef(self: *Analyze, src: usize, ty: Type) !*Inst {
+    fn constUndef(self: *Module, src: usize, ty: Type) !*Inst {
         return self.constInst(src, .{
             .ty = ty,
             .val = Value.initTag(.undef),
         });
     }
 
-    fn constBool(self: *Analyze, src: usize, v: bool) !*Inst {
+    fn constBool(self: *Module, src: usize, v: bool) !*Inst {
         return self.constInst(src, .{
             .ty = Type.initTag(.bool),
             .val = ([2]Value{ Value.initTag(.bool_false), Value.initTag(.bool_true) })[@boolToInt(v)],
         });
     }
 
-    fn constIntUnsigned(self: *Analyze, src: usize, ty: Type, int: u64) !*Inst {
+    fn constIntUnsigned(self: *Module, src: usize, ty: Type, int: u64) !*Inst {
         const int_payload = try self.arena.allocator.create(Value.Payload.Int_u64);
         int_payload.* = .{ .int = int };
 
@@ -474,7 +741,7 @@ const Analyze = struct {
         });
     }
 
-    fn constIntSigned(self: *Analyze, src: usize, ty: Type, int: i64) !*Inst {
+    fn constIntSigned(self: *Module, src: usize, ty: Type, int: i64) !*Inst {
         const int_payload = try self.arena.allocator.create(Value.Payload.Int_i64);
         int_payload.* = .{ .int = int };
 
@@ -484,7 +751,7 @@ const Analyze = struct {
         });
     }
 
-    fn constIntBig(self: *Analyze, src: usize, ty: Type, big_int: BigIntConst) !*Inst {
+    fn constIntBig(self: *Module, src: usize, ty: Type, big_int: BigIntConst) !*Inst {
         const val_payload = if (big_int.positive) blk: {
             if (big_int.to(u64)) |x| {
                 return self.constIntUnsigned(src, ty, x);
@@ -513,9 +780,18 @@ const Analyze = struct {
         });
     }
 
-    fn analyzeInst(self: *Analyze, block: ?*Block, old_inst: *text.Inst) InnerError!*Inst {
+    fn analyzeInstConst(self: *Module, scope: *Scope, old_inst: *text.Inst) InnerError!TypedValue {
+        const new_inst = try self.analyzeInst(scope, old_inst);
+        return TypedValue{
+            .ty = new_inst.ty,
+            .val = try self.resolveConstValue(scope, new_inst),
+        };
+    }
+
+    fn analyzeInst(self: *Module, scope: *Scope, old_inst: *text.Inst) InnerError!*Inst {
         switch (old_inst.tag) {
-            .breakpoint => return self.analyzeInstBreakpoint(block, old_inst.cast(text.Inst.Breakpoint).?),
+            .breakpoint => return self.analyzeInstBreakpoint(scope, old_inst.cast(text.Inst.Breakpoint).?),
+            .call => return self.analyzeInstCall(scope, old_inst.cast(text.Inst.Call).?),
             .str => {
                 // We can use this reference because Inst.Const's Value is arena-allocated.
                 // The value would get copied to a MemoryCell before the `text.Inst.Str` lifetime ends.
@@ -526,53 +802,118 @@ const Analyze = struct {
                 const big_int = old_inst.cast(text.Inst.Int).?.positionals.int;
                 return self.constIntBig(old_inst.src, Type.initTag(.comptime_int), big_int);
             },
-            .ptrtoint => return self.analyzeInstPtrToInt(block, old_inst.cast(text.Inst.PtrToInt).?),
-            .fieldptr => return self.analyzeInstFieldPtr(block, old_inst.cast(text.Inst.FieldPtr).?),
-            .deref => return self.analyzeInstDeref(block, old_inst.cast(text.Inst.Deref).?),
-            .as => return self.analyzeInstAs(block, old_inst.cast(text.Inst.As).?),
-            .@"asm" => return self.analyzeInstAsm(block, old_inst.cast(text.Inst.Asm).?),
-            .@"unreachable" => return self.analyzeInstUnreachable(block, old_inst.cast(text.Inst.Unreachable).?),
-            .@"return" => return self.analyzeInstRet(block, old_inst.cast(text.Inst.Return).?),
-            .@"fn" => return self.analyzeInstFn(block, old_inst.cast(text.Inst.Fn).?),
+            .ptrtoint => return self.analyzeInstPtrToInt(scope, old_inst.cast(text.Inst.PtrToInt).?),
+            .fieldptr => return self.analyzeInstFieldPtr(scope, old_inst.cast(text.Inst.FieldPtr).?),
+            .deref => return self.analyzeInstDeref(scope, old_inst.cast(text.Inst.Deref).?),
+            .as => return self.analyzeInstAs(scope, old_inst.cast(text.Inst.As).?),
+            .@"asm" => return self.analyzeInstAsm(scope, old_inst.cast(text.Inst.Asm).?),
+            .@"unreachable" => return self.analyzeInstUnreachable(scope, old_inst.cast(text.Inst.Unreachable).?),
+            .@"return" => return self.analyzeInstRet(scope, old_inst.cast(text.Inst.Return).?),
+            // TODO postpone function analysis until later
+            .@"fn" => return self.analyzeInstFn(scope, old_inst.cast(text.Inst.Fn).?),
             .@"export" => {
-                try self.analyzeExport(block, old_inst.cast(text.Inst.Export).?);
+                try self.analyzeExport(scope, old_inst.cast(text.Inst.Export).?);
                 return self.constVoid(old_inst.src);
             },
             .primitive => return self.analyzeInstPrimitive(old_inst.cast(text.Inst.Primitive).?),
-            .fntype => return self.analyzeInstFnType(block, old_inst.cast(text.Inst.FnType).?),
-            .intcast => return self.analyzeInstIntCast(block, old_inst.cast(text.Inst.IntCast).?),
-            .bitcast => return self.analyzeInstBitCast(block, old_inst.cast(text.Inst.BitCast).?),
-            .elemptr => return self.analyzeInstElemPtr(block, old_inst.cast(text.Inst.ElemPtr).?),
-            .add => return self.analyzeInstAdd(block, old_inst.cast(text.Inst.Add).?),
-            .cmp => return self.analyzeInstCmp(block, old_inst.cast(text.Inst.Cmp).?),
-            .condbr => return self.analyzeInstCondBr(block, old_inst.cast(text.Inst.CondBr).?),
-            .isnull => return self.analyzeInstIsNull(block, old_inst.cast(text.Inst.IsNull).?),
-            .isnonnull => return self.analyzeInstIsNonNull(block, old_inst.cast(text.Inst.IsNonNull).?),
+            .fntype => return self.analyzeInstFnType(scope, old_inst.cast(text.Inst.FnType).?),
+            .intcast => return self.analyzeInstIntCast(scope, old_inst.cast(text.Inst.IntCast).?),
+            .bitcast => return self.analyzeInstBitCast(scope, old_inst.cast(text.Inst.BitCast).?),
+            .elemptr => return self.analyzeInstElemPtr(scope, old_inst.cast(text.Inst.ElemPtr).?),
+            .add => return self.analyzeInstAdd(scope, old_inst.cast(text.Inst.Add).?),
+            .cmp => return self.analyzeInstCmp(scope, old_inst.cast(text.Inst.Cmp).?),
+            .condbr => return self.analyzeInstCondBr(scope, old_inst.cast(text.Inst.CondBr).?),
+            .isnull => return self.analyzeInstIsNull(scope, old_inst.cast(text.Inst.IsNull).?),
+            .isnonnull => return self.analyzeInstIsNonNull(scope, old_inst.cast(text.Inst.IsNonNull).?),
         }
     }
 
-    fn analyzeInstBreakpoint(self: *Analyze, block: ?*Block, inst: *text.Inst.Breakpoint) InnerError!*Inst {
-        const b = try self.requireRuntimeBlock(block, inst.base.src);
+    fn analyzeInstBreakpoint(self: *Module, scope: *Scope, inst: *text.Inst.Breakpoint) InnerError!*Inst {
+        const b = try self.requireRuntimeBlock(scope, inst.base.src);
         return self.addNewInstArgs(b, inst.base.src, Type.initTag(.void), Inst.Breakpoint, Inst.Args(Inst.Breakpoint){});
     }
 
-    fn analyzeInstFn(self: *Analyze, block: ?*Block, fn_inst: *text.Inst.Fn) InnerError!*Inst {
-        const fn_type = try self.resolveType(block, fn_inst.positionals.fn_type);
+    fn analyzeInstCall(self: *Module, scope: *Scope, inst: *text.Inst.Call) InnerError!*Inst {
+        const func = try self.resolveInst(scope, inst.positionals.func);
+        if (func.ty.zigTypeTag() != .Fn)
+            return self.fail(scope, inst.positionals.func.src, "type '{}' not a function", .{func.ty});
+
+        const cc = func.ty.fnCallingConvention();
+        if (cc == .Naked) {
+            // TODO add error note: declared here
+            return self.fail(
+                scope,
+                inst.positionals.func.src,
+                "unable to call function with naked calling convention",
+                .{},
+            );
+        }
+        const call_params_len = inst.positionals.args.len;
+        const fn_params_len = func.ty.fnParamLen();
+        if (func.ty.fnIsVarArgs()) {
+            if (call_params_len < fn_params_len) {
+                // TODO add error note: declared here
+                return self.fail(
+                    scope,
+                    inst.positionals.func.src,
+                    "expected at least {} arguments, found {}",
+                    .{ fn_params_len, call_params_len },
+                );
+            }
+            return self.fail(scope, inst.base.src, "TODO implement support for calling var args functions", .{});
+        } else if (fn_params_len != call_params_len) {
+            // TODO add error note: declared here
+            return self.fail(
+                scope,
+                inst.positionals.func.src,
+                "expected {} arguments, found {}",
+                .{ fn_params_len, call_params_len },
+            );
+        }
+
+        if (inst.kw_args.modifier == .compile_time) {
+            return self.fail(scope, inst.base.src, "TODO implement comptime function calls", .{});
+        }
+        if (inst.kw_args.modifier != .auto) {
+            return self.fail(scope, inst.base.src, "TODO implement call with modifier {}", .{inst.kw_args.modifier});
+        }
+
+        // TODO handle function calls of generic functions
+
+        const fn_param_types = try self.allocator.alloc(Type, fn_params_len);
+        defer self.allocator.free(fn_param_types);
+        func.ty.fnParamTypes(fn_param_types);
+
+        const casted_args = try self.arena.allocator.alloc(*Inst, fn_params_len);
+        for (inst.positionals.args) |src_arg, i| {
+            const uncasted_arg = try self.resolveInst(scope, src_arg);
+            casted_args[i] = try self.coerce(scope, fn_param_types[i], uncasted_arg);
+        }
+
+        const b = try self.requireRuntimeBlock(scope, inst.base.src);
+        return self.addNewInstArgs(b, inst.base.src, Type.initTag(.void), Inst.Call, Inst.Args(Inst.Call){
+            .func = func,
+            .args = casted_args,
+        });
+    }
+
+    fn analyzeInstFn(self: *Module, scope: *Scope, fn_inst: *text.Inst.Fn) InnerError!*Inst {
+        const fn_type = try self.resolveType(scope, fn_inst.positionals.fn_type);
 
         var new_func: Fn = .{
             .fn_index = self.fns.items.len,
             .inner_block = .{
                 .func = undefined,
-                .instructions = std.ArrayList(*Inst).init(self.allocator),
+                .instructions = .{},
             },
-            .inst_table = std.AutoHashMap(*text.Inst, NewInst).init(self.allocator),
+            .inst_table = std.AutoHashMap(*text.Inst, ?*Inst).init(self.allocator),
         };
         new_func.inner_block.func = &new_func;
         defer new_func.inner_block.instructions.deinit();
         defer new_func.inst_table.deinit();
         // Don't hang on to a reference to this when analyzing body instructions, since the memory
         // could become invalid.
-        (try self.fns.addOne()).* = .{
+        (try self.fns.addOne(self.allocator)).* = .{
             .analysis_status = .in_progress,
             .fn_type = fn_type,
             .body = undefined,
@@ -593,8 +934,15 @@ const Analyze = struct {
         });
     }
 
-    fn analyzeInstFnType(self: *Analyze, block: ?*Block, fntype: *text.Inst.FnType) InnerError!*Inst {
-        const return_type = try self.resolveType(block, fntype.positionals.return_type);
+    fn analyzeInstFnType(self: *Module, scope: *Scope, fntype: *text.Inst.FnType) InnerError!*Inst {
+        const return_type = try self.resolveType(scope, fntype.positionals.return_type);
+
+        if (return_type.zigTypeTag() == .NoReturn and
+            fntype.positionals.param_types.len == 0 and
+            fntype.kw_args.cc == .Unspecified)
+        {
+            return self.constType(fntype.base.src, Type.initTag(.fn_noreturn_no_args));
+        }
 
         if (return_type.zigTypeTag() == .NoReturn and
             fntype.positionals.param_types.len == 0 and
@@ -610,37 +958,37 @@ const Analyze = struct {
             return self.constType(fntype.base.src, Type.initTag(.fn_ccc_void_no_args));
         }
 
-        return self.fail(fntype.base.src, "TODO implement fntype instruction more", .{});
+        return self.fail(scope, fntype.base.src, "TODO implement fntype instruction more", .{});
     }
 
-    fn analyzeInstPrimitive(self: *Analyze, primitive: *text.Inst.Primitive) InnerError!*Inst {
+    fn analyzeInstPrimitive(self: *Module, primitive: *text.Inst.Primitive) InnerError!*Inst {
         return self.constType(primitive.base.src, primitive.positionals.tag.toType());
     }
 
-    fn analyzeInstAs(self: *Analyze, block: ?*Block, as: *text.Inst.As) InnerError!*Inst {
-        const dest_type = try self.resolveType(block, as.positionals.dest_type);
-        const new_inst = try self.resolveInst(block, as.positionals.value);
-        return self.coerce(block, dest_type, new_inst);
+    fn analyzeInstAs(self: *Module, scope: *Scope, as: *text.Inst.As) InnerError!*Inst {
+        const dest_type = try self.resolveType(scope, as.positionals.dest_type);
+        const new_inst = try self.resolveInst(scope, as.positionals.value);
+        return self.coerce(scope, dest_type, new_inst);
     }
 
-    fn analyzeInstPtrToInt(self: *Analyze, block: ?*Block, ptrtoint: *text.Inst.PtrToInt) InnerError!*Inst {
-        const ptr = try self.resolveInst(block, ptrtoint.positionals.ptr);
+    fn analyzeInstPtrToInt(self: *Module, scope: *Scope, ptrtoint: *text.Inst.PtrToInt) InnerError!*Inst {
+        const ptr = try self.resolveInst(scope, ptrtoint.positionals.ptr);
         if (ptr.ty.zigTypeTag() != .Pointer) {
-            return self.fail(ptrtoint.positionals.ptr.src, "expected pointer, found '{}'", .{ptr.ty});
+            return self.fail(scope, ptrtoint.positionals.ptr.src, "expected pointer, found '{}'", .{ptr.ty});
         }
         // TODO handle known-pointer-address
-        const b = try self.requireRuntimeBlock(block, ptrtoint.base.src);
+        const b = try self.requireRuntimeBlock(scope, ptrtoint.base.src);
         const ty = Type.initTag(.usize);
         return self.addNewInstArgs(b, ptrtoint.base.src, ty, Inst.PtrToInt, Inst.Args(Inst.PtrToInt){ .ptr = ptr });
     }
 
-    fn analyzeInstFieldPtr(self: *Analyze, block: ?*Block, fieldptr: *text.Inst.FieldPtr) InnerError!*Inst {
-        const object_ptr = try self.resolveInst(block, fieldptr.positionals.object_ptr);
-        const field_name = try self.resolveConstString(block, fieldptr.positionals.field_name);
+    fn analyzeInstFieldPtr(self: *Module, scope: *Scope, fieldptr: *text.Inst.FieldPtr) InnerError!*Inst {
+        const object_ptr = try self.resolveInst(scope, fieldptr.positionals.object_ptr);
+        const field_name = try self.resolveConstString(scope, fieldptr.positionals.field_name);
 
         const elem_ty = switch (object_ptr.ty.zigTypeTag()) {
             .Pointer => object_ptr.ty.elemType(),
-            else => return self.fail(fieldptr.positionals.object_ptr.src, "expected pointer, found '{}'", .{object_ptr.ty}),
+            else => return self.fail(scope, fieldptr.positionals.object_ptr.src, "expected pointer, found '{}'", .{object_ptr.ty}),
         };
         switch (elem_ty.zigTypeTag()) {
             .Array => {
@@ -657,24 +1005,26 @@ const Analyze = struct {
                     });
                 } else {
                     return self.fail(
+                        scope,
                         fieldptr.positionals.field_name.src,
                         "no member named '{}' in '{}'",
                         .{ field_name, elem_ty },
                     );
                 }
             },
-            else => return self.fail(fieldptr.base.src, "type '{}' does not support field access", .{elem_ty}),
+            else => return self.fail(scope, fieldptr.base.src, "type '{}' does not support field access", .{elem_ty}),
         }
     }
 
-    fn analyzeInstIntCast(self: *Analyze, block: ?*Block, intcast: *text.Inst.IntCast) InnerError!*Inst {
-        const dest_type = try self.resolveType(block, intcast.positionals.dest_type);
-        const new_inst = try self.resolveInst(block, intcast.positionals.value);
+    fn analyzeInstIntCast(self: *Module, scope: *Scope, intcast: *text.Inst.IntCast) InnerError!*Inst {
+        const dest_type = try self.resolveType(scope, intcast.positionals.dest_type);
+        const new_inst = try self.resolveInst(scope, intcast.positionals.value);
 
         const dest_is_comptime_int = switch (dest_type.zigTypeTag()) {
             .ComptimeInt => true,
             .Int => false,
             else => return self.fail(
+                scope,
                 intcast.positionals.dest_type.src,
                 "expected integer type, found '{}'",
                 .{
@@ -686,6 +1036,7 @@ const Analyze = struct {
         switch (new_inst.ty.zigTypeTag()) {
             .ComptimeInt, .Int => {},
             else => return self.fail(
+                scope,
                 intcast.positionals.value.src,
                 "expected integer type, found '{}'",
                 .{new_inst.ty},
@@ -693,22 +1044,22 @@ const Analyze = struct {
         }
 
         if (dest_is_comptime_int or new_inst.value() != null) {
-            return self.coerce(block, dest_type, new_inst);
+            return self.coerce(scope, dest_type, new_inst);
         }
 
-        return self.fail(intcast.base.src, "TODO implement analyze widen or shorten int", .{});
+        return self.fail(scope, intcast.base.src, "TODO implement analyze widen or shorten int", .{});
     }
 
-    fn analyzeInstBitCast(self: *Analyze, block: ?*Block, inst: *text.Inst.BitCast) InnerError!*Inst {
-        const dest_type = try self.resolveType(block, inst.positionals.dest_type);
-        const operand = try self.resolveInst(block, inst.positionals.operand);
-        return self.bitcast(block, dest_type, operand);
+    fn analyzeInstBitCast(self: *Module, scope: *Scope, inst: *text.Inst.BitCast) InnerError!*Inst {
+        const dest_type = try self.resolveType(scope, inst.positionals.dest_type);
+        const operand = try self.resolveInst(scope, inst.positionals.operand);
+        return self.bitcast(scope, dest_type, operand);
     }
 
-    fn analyzeInstElemPtr(self: *Analyze, block: ?*Block, inst: *text.Inst.ElemPtr) InnerError!*Inst {
-        const array_ptr = try self.resolveInst(block, inst.positionals.array_ptr);
-        const uncasted_index = try self.resolveInst(block, inst.positionals.index);
-        const elem_index = try self.coerce(block, Type.initTag(.usize), uncasted_index);
+    fn analyzeInstElemPtr(self: *Module, scope: *Scope, inst: *text.Inst.ElemPtr) InnerError!*Inst {
+        const array_ptr = try self.resolveInst(scope, inst.positionals.array_ptr);
+        const uncasted_index = try self.resolveInst(scope, inst.positionals.index);
+        const elem_index = try self.coerce(scope, Type.initTag(.usize), uncasted_index);
 
         if (array_ptr.ty.isSinglePointer() and array_ptr.ty.elemType().zigTypeTag() == .Array) {
             if (array_ptr.value()) |array_ptr_val| {
@@ -717,28 +1068,25 @@ const Analyze = struct {
                     const index_u64 = index_val.toUnsignedInt();
                     // @intCast here because it would have been impossible to construct a value that
                     // required a larger index.
-                    const elem_val = try array_ptr_val.elemValueAt(&self.arena.allocator, @intCast(usize, index_u64));
-
-                    const ref_payload = try self.arena.allocator.create(Value.Payload.RefVal);
-                    ref_payload.* = .{ .val = elem_val };
+                    const elem_ptr = try array_ptr_val.elemPtr(&self.arena.allocator, @intCast(usize, index_u64));
 
                     const type_payload = try self.arena.allocator.create(Type.Payload.SingleConstPointer);
                     type_payload.* = .{ .pointee_type = array_ptr.ty.elemType().elemType() };
 
                     return self.constInst(inst.base.src, .{
                         .ty = Type.initPayload(&type_payload.base),
-                        .val = Value.initPayload(&ref_payload.base),
+                        .val = elem_ptr,
                     });
                 }
             }
         }
 
-        return self.fail(inst.base.src, "TODO implement more analyze elemptr", .{});
+        return self.fail(scope, inst.base.src, "TODO implement more analyze elemptr", .{});
     }
 
-    fn analyzeInstAdd(self: *Analyze, block: ?*Block, inst: *text.Inst.Add) InnerError!*Inst {
-        const lhs = try self.resolveInst(block, inst.positionals.lhs);
-        const rhs = try self.resolveInst(block, inst.positionals.rhs);
+    fn analyzeInstAdd(self: *Module, scope: *Scope, inst: *text.Inst.Add) InnerError!*Inst {
+        const lhs = try self.resolveInst(scope, inst.positionals.lhs);
+        const rhs = try self.resolveInst(scope, inst.positionals.rhs);
 
         if (lhs.ty.zigTypeTag() == .Int and rhs.ty.zigTypeTag() == .Int) {
             if (lhs.value()) |lhs_val| {
@@ -758,7 +1106,7 @@ const Analyze = struct {
                     const result_limbs = result_bigint.limbs[0..result_bigint.len];
 
                     if (!lhs.ty.eql(rhs.ty)) {
-                        return self.fail(inst.base.src, "TODO implement peer type resolution", .{});
+                        return self.fail(scope, inst.base.src, "TODO implement peer type resolution", .{});
                     }
 
                     const val_payload = if (result_bigint.positive) blk: {
@@ -779,14 +1127,14 @@ const Analyze = struct {
             }
         }
 
-        return self.fail(inst.base.src, "TODO implement more analyze add", .{});
+        return self.fail(scope, inst.base.src, "TODO implement more analyze add", .{});
     }
 
-    fn analyzeInstDeref(self: *Analyze, block: ?*Block, deref: *text.Inst.Deref) InnerError!*Inst {
-        const ptr = try self.resolveInst(block, deref.positionals.ptr);
+    fn analyzeInstDeref(self: *Module, scope: *Scope, deref: *text.Inst.Deref) InnerError!*Inst {
+        const ptr = try self.resolveInst(scope, deref.positionals.ptr);
         const elem_ty = switch (ptr.ty.zigTypeTag()) {
             .Pointer => ptr.ty.elemType(),
-            else => return self.fail(deref.positionals.ptr.src, "expected pointer, found '{}'", .{ptr.ty}),
+            else => return self.fail(scope, deref.positionals.ptr.src, "expected pointer, found '{}'", .{ptr.ty}),
         };
         if (ptr.value()) |val| {
             return self.constInst(deref.base.src, .{
@@ -795,30 +1143,30 @@ const Analyze = struct {
             });
         }
 
-        return self.fail(deref.base.src, "TODO implement runtime deref", .{});
+        return self.fail(scope, deref.base.src, "TODO implement runtime deref", .{});
     }
 
-    fn analyzeInstAsm(self: *Analyze, block: ?*Block, assembly: *text.Inst.Asm) InnerError!*Inst {
-        const return_type = try self.resolveType(block, assembly.positionals.return_type);
-        const asm_source = try self.resolveConstString(block, assembly.positionals.asm_source);
-        const output = if (assembly.kw_args.output) |o| try self.resolveConstString(block, o) else null;
+    fn analyzeInstAsm(self: *Module, scope: *Scope, assembly: *text.Inst.Asm) InnerError!*Inst {
+        const return_type = try self.resolveType(scope, assembly.positionals.return_type);
+        const asm_source = try self.resolveConstString(scope, assembly.positionals.asm_source);
+        const output = if (assembly.kw_args.output) |o| try self.resolveConstString(scope, o) else null;
 
         const inputs = try self.arena.allocator.alloc([]const u8, assembly.kw_args.inputs.len);
         const clobbers = try self.arena.allocator.alloc([]const u8, assembly.kw_args.clobbers.len);
         const args = try self.arena.allocator.alloc(*Inst, assembly.kw_args.args.len);
 
         for (inputs) |*elem, i| {
-            elem.* = try self.resolveConstString(block, assembly.kw_args.inputs[i]);
+            elem.* = try self.resolveConstString(scope, assembly.kw_args.inputs[i]);
         }
         for (clobbers) |*elem, i| {
-            elem.* = try self.resolveConstString(block, assembly.kw_args.clobbers[i]);
+            elem.* = try self.resolveConstString(scope, assembly.kw_args.clobbers[i]);
         }
         for (args) |*elem, i| {
-            const arg = try self.resolveInst(block, assembly.kw_args.args[i]);
-            elem.* = try self.coerce(block, Type.initTag(.usize), arg);
+            const arg = try self.resolveInst(scope, assembly.kw_args.args[i]);
+            elem.* = try self.coerce(scope, Type.initTag(.usize), arg);
         }
 
-        const b = try self.requireRuntimeBlock(block, assembly.base.src);
+        const b = try self.requireRuntimeBlock(scope, assembly.base.src);
         return self.addNewInstArgs(b, assembly.base.src, return_type, Inst.Assembly, Inst.Args(Inst.Assembly){
             .asm_source = asm_source,
             .is_volatile = assembly.kw_args.@"volatile",
@@ -829,9 +1177,9 @@ const Analyze = struct {
         });
     }
 
-    fn analyzeInstCmp(self: *Analyze, block: ?*Block, inst: *text.Inst.Cmp) InnerError!*Inst {
-        const lhs = try self.resolveInst(block, inst.positionals.lhs);
-        const rhs = try self.resolveInst(block, inst.positionals.rhs);
+    fn analyzeInstCmp(self: *Module, scope: *Scope, inst: *text.Inst.Cmp) InnerError!*Inst {
+        const lhs = try self.resolveInst(scope, inst.positionals.lhs);
+        const rhs = try self.resolveInst(scope, inst.positionals.rhs);
         const op = inst.positionals.op;
 
         const is_equality_cmp = switch (op) {
@@ -853,7 +1201,7 @@ const Analyze = struct {
                 const is_null = opt_val.isNull();
                 return self.constBool(inst.base.src, if (op == .eq) is_null else !is_null);
             }
-            const b = try self.requireRuntimeBlock(block, inst.base.src);
+            const b = try self.requireRuntimeBlock(scope, inst.base.src);
             switch (op) {
                 .eq => return self.addNewInstArgs(
                     b,
@@ -874,64 +1222,64 @@ const Analyze = struct {
         } else if (is_equality_cmp and
             ((lhs_ty_tag == .Null and rhs.ty.isCPtr()) or (rhs_ty_tag == .Null and lhs.ty.isCPtr())))
         {
-            return self.fail(inst.base.src, "TODO implement C pointer cmp", .{});
+            return self.fail(scope, inst.base.src, "TODO implement C pointer cmp", .{});
         } else if (lhs_ty_tag == .Null or rhs_ty_tag == .Null) {
             const non_null_type = if (lhs_ty_tag == .Null) rhs.ty else lhs.ty;
-            return self.fail(inst.base.src, "comparison of '{}' with null", .{non_null_type});
+            return self.fail(scope, inst.base.src, "comparison of '{}' with null", .{non_null_type});
         } else if (is_equality_cmp and
             ((lhs_ty_tag == .EnumLiteral and rhs_ty_tag == .Union) or
             (rhs_ty_tag == .EnumLiteral and lhs_ty_tag == .Union)))
         {
-            return self.fail(inst.base.src, "TODO implement equality comparison between a union's tag value and an enum literal", .{});
+            return self.fail(scope, inst.base.src, "TODO implement equality comparison between a union's tag value and an enum literal", .{});
         } else if (lhs_ty_tag == .ErrorSet and rhs_ty_tag == .ErrorSet) {
             if (!is_equality_cmp) {
-                return self.fail(inst.base.src, "{} operator not allowed for errors", .{@tagName(op)});
+                return self.fail(scope, inst.base.src, "{} operator not allowed for errors", .{@tagName(op)});
             }
-            return self.fail(inst.base.src, "TODO implement equality comparison between errors", .{});
+            return self.fail(scope, inst.base.src, "TODO implement equality comparison between errors", .{});
         } else if (lhs.ty.isNumeric() and rhs.ty.isNumeric()) {
             // This operation allows any combination of integer and float types, regardless of the
             // signed-ness, comptime-ness, and bit-width. So peer type resolution is incorrect for
             // numeric types.
-            return self.cmpNumeric(block, inst.base.src, lhs, rhs, op);
+            return self.cmpNumeric(scope, inst.base.src, lhs, rhs, op);
         }
-        return self.fail(inst.base.src, "TODO implement more cmp analysis", .{});
+        return self.fail(scope, inst.base.src, "TODO implement more cmp analysis", .{});
     }
 
-    fn analyzeInstIsNull(self: *Analyze, block: ?*Block, inst: *text.Inst.IsNull) InnerError!*Inst {
-        const operand = try self.resolveInst(block, inst.positionals.operand);
-        return self.analyzeIsNull(block, inst.base.src, operand, true);
+    fn analyzeInstIsNull(self: *Module, scope: *Scope, inst: *text.Inst.IsNull) InnerError!*Inst {
+        const operand = try self.resolveInst(scope, inst.positionals.operand);
+        return self.analyzeIsNull(scope, inst.base.src, operand, true);
     }
 
-    fn analyzeInstIsNonNull(self: *Analyze, block: ?*Block, inst: *text.Inst.IsNonNull) InnerError!*Inst {
-        const operand = try self.resolveInst(block, inst.positionals.operand);
-        return self.analyzeIsNull(block, inst.base.src, operand, false);
+    fn analyzeInstIsNonNull(self: *Module, scope: *Scope, inst: *text.Inst.IsNonNull) InnerError!*Inst {
+        const operand = try self.resolveInst(scope, inst.positionals.operand);
+        return self.analyzeIsNull(scope, inst.base.src, operand, false);
     }
 
-    fn analyzeInstCondBr(self: *Analyze, block: ?*Block, inst: *text.Inst.CondBr) InnerError!*Inst {
-        const uncasted_cond = try self.resolveInst(block, inst.positionals.condition);
-        const cond = try self.coerce(block, Type.initTag(.bool), uncasted_cond);
+    fn analyzeInstCondBr(self: *Module, scope: *Scope, inst: *text.Inst.CondBr) InnerError!*Inst {
+        const uncasted_cond = try self.resolveInst(scope, inst.positionals.condition);
+        const cond = try self.coerce(scope, Type.initTag(.bool), uncasted_cond);
 
         if (try self.resolveDefinedValue(cond)) |cond_val| {
             const body = if (cond_val.toBool()) &inst.positionals.true_body else &inst.positionals.false_body;
-            try self.analyzeBody(block, body.*);
+            try self.analyzeBody(scope, body.*);
             return self.constVoid(inst.base.src);
         }
 
-        const parent_block = try self.requireRuntimeBlock(block, inst.base.src);
+        const parent_block = try self.requireRuntimeBlock(scope, inst.base.src);
 
-        var true_block: Block = .{
+        var true_block: Scope.Block = .{
             .func = parent_block.func,
-            .instructions = std.ArrayList(*Inst).init(self.allocator),
+            .instructions = .{},
         };
         defer true_block.instructions.deinit();
-        try self.analyzeBody(&true_block, inst.positionals.true_body);
+        try self.analyzeBody(&true_block.base, inst.positionals.true_body);
 
-        var false_block: Block = .{
+        var false_block: Scope.Block = .{
             .func = parent_block.func,
-            .instructions = std.ArrayList(*Inst).init(self.allocator),
+            .instructions = .{},
         };
         defer false_block.instructions.deinit();
-        try self.analyzeBody(&false_block, inst.positionals.false_body);
+        try self.analyzeBody(&false_block.base, inst.positionals.false_body);
 
         // Copy the instruction pointers to the arena memory
         const true_instructions = try self.arena.allocator.alloc(*Inst, true_block.instructions.items.len);
@@ -947,7 +1295,7 @@ const Analyze = struct {
         });
     }
 
-    fn wantSafety(self: *Analyze, block: ?*Block) bool {
+    fn wantSafety(self: *Module, scope: *Scope) bool {
         return switch (self.optimize_mode) {
             .Debug => true,
             .ReleaseSafe => true,
@@ -956,47 +1304,47 @@ const Analyze = struct {
         };
     }
 
-    fn analyzeInstUnreachable(self: *Analyze, block: ?*Block, unreach: *text.Inst.Unreachable) InnerError!*Inst {
-        const b = try self.requireRuntimeBlock(block, unreach.base.src);
-        if (self.wantSafety(block)) {
+    fn analyzeInstUnreachable(self: *Module, scope: *Scope, unreach: *text.Inst.Unreachable) InnerError!*Inst {
+        const b = try self.requireRuntimeBlock(scope, unreach.base.src);
+        if (self.wantSafety(scope)) {
             // TODO Once we have a panic function to call, call it here instead of this.
             _ = try self.addNewInstArgs(b, unreach.base.src, Type.initTag(.void), Inst.Breakpoint, {});
         }
         return self.addNewInstArgs(b, unreach.base.src, Type.initTag(.noreturn), Inst.Unreach, {});
     }
 
-    fn analyzeInstRet(self: *Analyze, block: ?*Block, inst: *text.Inst.Return) InnerError!*Inst {
-        const b = try self.requireRuntimeBlock(block, inst.base.src);
+    fn analyzeInstRet(self: *Module, scope: *Scope, inst: *text.Inst.Return) InnerError!*Inst {
+        const b = try self.requireRuntimeBlock(scope, inst.base.src);
         return self.addNewInstArgs(b, inst.base.src, Type.initTag(.noreturn), Inst.Ret, {});
     }
 
-    fn analyzeBody(self: *Analyze, block: ?*Block, body: text.Module.Body) !void {
+    fn analyzeBody(self: *Module, scope: *Scope, body: text.Module.Body) !void {
         for (body.instructions) |src_inst| {
-            const new_inst = self.analyzeInst(block, src_inst) catch |err| {
-                if (block) |b| {
+            const new_inst = self.analyzeInst(scope, src_inst) catch |err| {
+                if (scope.cast(Scope.Block)) |b| {
                     self.fns.items[b.func.fn_index].analysis_status = .failure;
                     try b.func.inst_table.putNoClobber(src_inst, .{ .ptr = null });
                 }
                 return err;
             };
-            if (block) |b| try b.func.inst_table.putNoClobber(src_inst, .{ .ptr = new_inst });
+            if (scope.cast(Scope.Block)) |b| try b.func.inst_table.putNoClobber(src_inst, .{ .ptr = new_inst });
         }
     }
 
     fn analyzeIsNull(
-        self: *Analyze,
-        block: ?*Block,
+        self: *Module,
+        scope: *Scope,
         src: usize,
         operand: *Inst,
         invert_logic: bool,
     ) InnerError!*Inst {
-        return self.fail(src, "TODO implement analysis of isnull and isnotnull", .{});
+        return self.fail(scope, src, "TODO implement analysis of isnull and isnotnull", .{});
     }
 
     /// Asserts that lhs and rhs types are both numeric.
     fn cmpNumeric(
-        self: *Analyze,
-        block: ?*Block,
+        self: *Module,
+        scope: *Scope,
         src: usize,
         lhs: *Inst,
         rhs: *Inst,
@@ -1010,14 +1358,14 @@ const Analyze = struct {
 
         if (lhs_ty_tag == .Vector and rhs_ty_tag == .Vector) {
             if (lhs.ty.arrayLen() != rhs.ty.arrayLen()) {
-                return self.fail(src, "vector length mismatch: {} and {}", .{
+                return self.fail(scope, src, "vector length mismatch: {} and {}", .{
                     lhs.ty.arrayLen(),
                     rhs.ty.arrayLen(),
                 });
             }
-            return self.fail(src, "TODO implement support for vectors in cmpNumeric", .{});
+            return self.fail(scope, src, "TODO implement support for vectors in cmpNumeric", .{});
         } else if (lhs_ty_tag == .Vector or rhs_ty_tag == .Vector) {
-            return self.fail(src, "mixed scalar and vector operands to comparison operator: '{}' and '{}'", .{
+            return self.fail(scope, src, "mixed scalar and vector operands to comparison operator: '{}' and '{}'", .{
                 lhs.ty,
                 rhs.ty,
             });
@@ -1036,7 +1384,7 @@ const Analyze = struct {
         // of this function if we don't need to.
 
         // It must be a runtime comparison.
-        const b = try self.requireRuntimeBlock(block, src);
+        const b = try self.requireRuntimeBlock(scope, src);
         // For floats, emit a float comparison instruction.
         const lhs_is_float = switch (lhs_ty_tag) {
             .Float, .ComptimeFloat => true,
@@ -1054,14 +1402,14 @@ const Analyze = struct {
                 } else if (rhs_ty_tag == .ComptimeFloat) {
                     break :x lhs.ty;
                 }
-                if (lhs.ty.floatBits(self.target) >= rhs.ty.floatBits(self.target)) {
+                if (lhs.ty.floatBits(self.target()) >= rhs.ty.floatBits(self.target())) {
                     break :x lhs.ty;
                 } else {
                     break :x rhs.ty;
                 }
             };
-            const casted_lhs = try self.coerce(block, dest_type, lhs);
-            const casted_rhs = try self.coerce(block, dest_type, rhs);
+            const casted_lhs = try self.coerce(scope, dest_type, lhs);
+            const casted_rhs = try self.coerce(scope, dest_type, rhs);
             return self.addNewInstArgs(b, src, dest_type, Inst.Cmp, Inst.Args(Inst.Cmp){
                 .lhs = casted_lhs,
                 .rhs = casted_rhs,
@@ -1117,7 +1465,7 @@ const Analyze = struct {
         } else if (lhs_is_float) {
             dest_float_type = lhs.ty;
         } else {
-            const int_info = lhs.ty.intInfo(self.target);
+            const int_info = lhs.ty.intInfo(self.target());
             lhs_bits = int_info.bits + @boolToInt(!int_info.signed and dest_int_is_signed);
         }
 
@@ -1152,19 +1500,19 @@ const Analyze = struct {
         } else if (rhs_is_float) {
             dest_float_type = rhs.ty;
         } else {
-            const int_info = rhs.ty.intInfo(self.target);
+            const int_info = rhs.ty.intInfo(self.target());
             rhs_bits = int_info.bits + @boolToInt(!int_info.signed and dest_int_is_signed);
         }
 
         const dest_type = if (dest_float_type) |ft| ft else blk: {
             const max_bits = std.math.max(lhs_bits, rhs_bits);
             const casted_bits = std.math.cast(u16, max_bits) catch |err| switch (err) {
-                error.Overflow => return self.fail(src, "{} exceeds maximum integer bit count", .{max_bits}),
+                error.Overflow => return self.fail(scope, src, "{} exceeds maximum integer bit count", .{max_bits}),
             };
             break :blk try self.makeIntType(dest_int_is_signed, casted_bits);
         };
-        const casted_lhs = try self.coerce(block, dest_type, lhs);
-        const casted_rhs = try self.coerce(block, dest_type, lhs);
+        const casted_lhs = try self.coerce(scope, dest_type, lhs);
+        const casted_rhs = try self.coerce(scope, dest_type, lhs);
 
         return self.addNewInstArgs(b, src, dest_type, Inst.Cmp, Inst.Args(Inst.Cmp){
             .lhs = casted_lhs,
@@ -1173,7 +1521,7 @@ const Analyze = struct {
         });
     }
 
-    fn makeIntType(self: *Analyze, signed: bool, bits: u16) !Type {
+    fn makeIntType(self: *Module, signed: bool, bits: u16) !Type {
         if (signed) {
             const int_payload = try self.arena.allocator.create(Type.Payload.IntSigned);
             int_payload.* = .{ .bits = bits };
@@ -1185,14 +1533,14 @@ const Analyze = struct {
         }
     }
 
-    fn coerce(self: *Analyze, block: ?*Block, dest_type: Type, inst: *Inst) !*Inst {
+    fn coerce(self: *Module, scope: *Scope, dest_type: Type, inst: *Inst) !*Inst {
         // If the types are the same, we can return the operand.
         if (dest_type.eql(inst.ty))
             return inst;
 
         const in_memory_result = coerceInMemoryAllowed(dest_type, inst.ty);
         if (in_memory_result == .ok) {
-            return self.bitcast(block, dest_type, inst);
+            return self.bitcast(scope, dest_type, inst);
         }
 
         // *[N]T to []T
@@ -1212,55 +1560,61 @@ const Analyze = struct {
         if (inst.ty.zigTypeTag() == .ComptimeInt and dest_type.zigTypeTag() == .Int) {
             // The representation is already correct; we only need to make sure it fits in the destination type.
             const val = inst.value().?; // comptime_int always has comptime known value
-            if (!val.intFitsInType(dest_type, self.target)) {
-                return self.fail(inst.src, "type {} cannot represent integer value {}", .{ inst.ty, val });
+            if (!val.intFitsInType(dest_type, self.target())) {
+                return self.fail(scope, inst.src, "type {} cannot represent integer value {}", .{ inst.ty, val });
             }
             return self.constInst(inst.src, .{ .ty = dest_type, .val = val });
         }
 
         // integer widening
         if (inst.ty.zigTypeTag() == .Int and dest_type.zigTypeTag() == .Int) {
-            const src_info = inst.ty.intInfo(self.target);
-            const dst_info = dest_type.intInfo(self.target);
+            const src_info = inst.ty.intInfo(self.target());
+            const dst_info = dest_type.intInfo(self.target());
             if (src_info.signed == dst_info.signed and dst_info.bits >= src_info.bits) {
                 if (inst.value()) |val| {
                     return self.constInst(inst.src, .{ .ty = dest_type, .val = val });
                 } else {
-                    return self.fail(inst.src, "TODO implement runtime integer widening", .{});
+                    return self.fail(scope, inst.src, "TODO implement runtime integer widening", .{});
                 }
             } else {
-                return self.fail(inst.src, "TODO implement more int widening {} to {}", .{ inst.ty, dest_type });
+                return self.fail(scope, inst.src, "TODO implement more int widening {} to {}", .{ inst.ty, dest_type });
             }
         }
 
-        return self.fail(inst.src, "TODO implement type coercion from {} to {}", .{ inst.ty, dest_type });
+        return self.fail(scope, inst.src, "TODO implement type coercion from {} to {}", .{ inst.ty, dest_type });
     }
 
-    fn bitcast(self: *Analyze, block: ?*Block, dest_type: Type, inst: *Inst) !*Inst {
+    fn bitcast(self: *Module, scope: *Scope, dest_type: Type, inst: *Inst) !*Inst {
         if (inst.value()) |val| {
             // Keep the comptime Value representation; take the new type.
             return self.constInst(inst.src, .{ .ty = dest_type, .val = val });
         }
         // TODO validate the type size and other compile errors
-        const b = try self.requireRuntimeBlock(block, inst.src);
+        const b = try self.requireRuntimeBlock(scope, inst.src);
         return self.addNewInstArgs(b, inst.src, dest_type, Inst.BitCast, Inst.Args(Inst.BitCast){ .operand = inst });
     }
 
-    fn coerceArrayPtrToSlice(self: *Analyze, dest_type: Type, inst: *Inst) !*Inst {
+    fn coerceArrayPtrToSlice(self: *Module, dest_type: Type, inst: *Inst) !*Inst {
         if (inst.value()) |val| {
             // The comptime Value representation is compatible with both types.
             return self.constInst(inst.src, .{ .ty = dest_type, .val = val });
         }
-        return self.fail(inst.src, "TODO implement coerceArrayPtrToSlice runtime instruction", .{});
+        return self.fail(scope, inst.src, "TODO implement coerceArrayPtrToSlice runtime instruction", .{});
     }
 
-    fn fail(self: *Analyze, src: usize, comptime format: []const u8, args: var) InnerError {
+    fn fail(self: *Module, scope: *Scope, src: usize, comptime format: []const u8, args: var) InnerError {
         @setCold(true);
-        const msg = try std.fmt.allocPrint(&self.arena.allocator, format, args);
-        (try self.errors.addOne()).* = .{
+        const err_msg = ErrorMsg{
             .byte_offset = src,
-            .msg = msg,
+            .msg = try std.fmt.allocPrint(self.allocator, format, args),
         };
+        if (scope.cast(Scope.Block)) |block| {
+            block.func.analysis = .{ .failure = err_msg };
+        } else if (scope.cast(Scope.Decl)) |scope_decl| {
+            scope_decl.decl.analysis = .{ .failure = err_msg };
+        } else {
+            unreachable;
+        }
         return error.AnalysisFail;
     }
 
@@ -1279,6 +1633,11 @@ const Analyze = struct {
     }
 };
 
+pub const ErrorMsg = struct {
+    byte_offset: usize,
+    msg: []const u8,
+};
+
 pub fn main() anyerror!void {
     var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
     defer arena.deinit();
@@ -1288,63 +1647,68 @@ pub fn main() anyerror!void {
     defer std.process.argsFree(allocator, args);
 
     const src_path = args[1];
+    const bin_path = args[2];
     const debug_error_trace = true;
-
-    const source = try std.fs.cwd().readFileAllocOptions(allocator, src_path, std.math.maxInt(u32), 1, 0);
-    defer allocator.free(source);
-
-    var zir_module = try text.parse(allocator, source);
-    defer zir_module.deinit(allocator);
-
-    if (zir_module.errors.len != 0) {
-        for (zir_module.errors) |err_msg| {
-            const loc = std.zig.findLineColumn(source, err_msg.byte_offset);
-            std.debug.warn("{}:{}:{}: error: {}\n", .{ src_path, loc.line + 1, loc.column + 1, err_msg.msg });
-        }
-        if (debug_error_trace) return error.ParseFailure;
-        std.process.exit(1);
-    }
+    const output_zir = true;
 
     const native_info = try std.zig.system.NativeTargetInfo.detect(allocator, .{});
 
-    var analyzed_module = try analyze(allocator, zir_module, .{
+    var bin_file = try link.openBinFilePath(allocator, std.fs.cwd(), bin_path, .{
         .target = native_info.target,
-        .output_mode = .Obj,
+        .output_mode = .Exe,
         .link_mode = .Static,
-        .optimize_mode = .Debug,
+        .object_format = options.object_format orelse native_info.target.getObjectFormat(),
     });
-    defer analyzed_module.deinit(allocator);
+    defer bin_file.deinit(allocator);
+
+    var module = blk: {
+        const root_pkg = try Package.create(allocator, std.fs.cwd(), ".", src_path);
+        errdefer root_pkg.destroy();
+
+        const root_scope = try allocator.create(Module.Scope.ZIRModule);
+        errdefer allocator.destroy(root_scope);
+        root_scope.* = .{
+            .sub_file_path = root_pkg.root_src_path,
+            .contents = .unloaded,
+        };
 
-    if (analyzed_module.errors.len != 0) {
-        for (analyzed_module.errors) |err_msg| {
-            const loc = std.zig.findLineColumn(source, err_msg.byte_offset);
-            std.debug.warn("{}:{}:{}: error: {}\n", .{ src_path, loc.line + 1, loc.column + 1, err_msg.msg });
+        break :blk Module{
+            .allocator = allocator,
+            .root_pkg = root_pkg,
+            .root_scope = root_scope,
+            .bin_file = &bin_file,
+            .optimize_mode = .Debug,
+            .decl_table = std.AutoHashMap(Decl.Hash, *Decl).init(allocator),
+        };
+    };
+    defer module.deinit();
+
+    try module.update();
+
+    const errors = try module.getAllErrorsAlloc();
+    defer errors.deinit();
+
+    if (errors.list.len != 0) {
+        for (errors.list) |full_err_msg| {
+            std.debug.warn("{}:{}:{}: error: {}\n", .{
+                full_err_msg.src_path,
+                full_err_msg.line + 1,
+                full_err_msg.column + 1,
+                full_err_msg.msg,
+            });
         }
         if (debug_error_trace) return error.AnalysisFail;
         std.process.exit(1);
     }
 
-    const output_zir = true;
     if (output_zir) {
-        var new_zir_module = try text.emit_zir(allocator, analyzed_module);
+        var new_zir_module = try text.emit_zir(allocator, module);
         defer new_zir_module.deinit(allocator);
 
         var bos = std.io.bufferedOutStream(std.io.getStdOut().outStream());
         try new_zir_module.writeToStream(allocator, bos.outStream());
         try bos.flush();
     }
-
-    const link = @import("link.zig");
-    var result = try link.updateFilePath(allocator, analyzed_module, std.fs.cwd(), "zir.o");
-    defer result.deinit(allocator);
-    if (result.errors.len != 0) {
-        for (result.errors) |err_msg| {
-            const loc = std.zig.findLineColumn(source, err_msg.byte_offset);
-            std.debug.warn("{}:{}:{}: error: {}\n", .{ src_path, loc.line + 1, loc.column + 1, err_msg.msg });
-        }
-        if (debug_error_trace) return error.LinkFailure;
-        std.process.exit(1);
-    }
 }
 
 // Performance optimization ideas:
src-self-hosted/libc_installation.zig
@@ -1,6 +1,5 @@
 const std = @import("std");
 const builtin = @import("builtin");
-const util = @import("util.zig");
 const Target = std.Target;
 const fs = std.fs;
 const Allocator = std.mem.Allocator;
src-self-hosted/link.zig
@@ -9,50 +9,65 @@ const codegen = @import("codegen.zig");
 
 const default_entry_addr = 0x8000000;
 
-pub const ErrorMsg = struct {
-    byte_offset: usize,
-    msg: []const u8,
-};
-
-pub const Result = struct {
-    errors: []ErrorMsg,
-
-    pub fn deinit(self: *Result, allocator: *mem.Allocator) void {
-        for (self.errors) |err| {
-            allocator.free(err.msg);
-        }
-        allocator.free(self.errors);
-        self.* = undefined;
-    }
+pub const Options = struct {
+    target: std.Target,
+    output_mode: std.builtin.OutputMode,
+    link_mode: std.builtin.LinkMode,
+    object_format: std.builtin.ObjectFormat,
+    /// Used for calculating how much space to reserve for symbols in case the binary file
+    /// does not already have a symbol table.
+    symbol_count_hint: u64 = 32,
+    /// Used for calculating how much space to reserve for executable program code in case
+    /// the binary file deos not already have such a section.
+    program_code_size_hint: u64 = 256 * 1024,
 };
 
 /// Attempts incremental linking, if the file already exists.
 /// If incremental linking fails, falls back to truncating the file and rewriting it.
 /// A malicious file is detected as incremental link failure and does not cause Illegal Behavior.
 /// This operation is not atomic.
-pub fn updateFilePath(
+pub fn openBinFilePath(
     allocator: *Allocator,
-    module: ir.Module,
     dir: fs.Dir,
     sub_path: []const u8,
-) !Result {
-    const file = try dir.createFile(sub_path, .{ .truncate = false, .read = true, .mode = determineMode(module) });
+    options: Options,
+) !ElfFile {
+    const file = try dir.createFile(sub_path, .{ .truncate = false, .read = true, .mode = determineMode(options) });
     defer file.close();
 
-    return updateFile(allocator, module, file);
+    return openBinFile(allocator, file, options);
 }
 
 /// Atomically overwrites the old file, if present.
 pub fn writeFilePath(
     allocator: *Allocator,
-    module: ir.Module,
     dir: fs.Dir,
     sub_path: []const u8,
-) !Result {
-    const af = try dir.atomicFile(sub_path, .{ .mode = determineMode(module) });
+    module: ir.Module,
+    errors: *std.ArrayList(ir.ErrorMsg),
+) !void {
+    const options: Options = .{
+        .target = module.target,
+        .output_mode = module.output_mode,
+        .link_mode = module.link_mode,
+        .object_format = module.object_format,
+        .symbol_count_hint = module.decls.items.len,
+    };
+    const af = try dir.atomicFile(sub_path, .{ .mode = determineMode(options) });
     defer af.deinit();
 
-    const result = try writeFile(allocator, module, af.file);
+    const elf_file = try createElfFile(allocator, af.file, options);
+    for (module.decls.items) |decl| {
+        try elf_file.updateDecl(module, decl, errors);
+    }
+    try elf_file.flush();
+    if (elf_file.error_flags.no_entry_point_found) {
+        try errors.ensureCapacity(errors.items.len + 1);
+        errors.appendAssumeCapacity(.{
+            .byte_offset = 0,
+            .msg = try std.fmt.allocPrint(errors.allocator, "no entry point found", .{}),
+        });
+    }
     try af.finish();
     return result;
 }
@@ -62,49 +77,65 @@ pub fn writeFilePath(
 /// Returns an error if `file` is not already open with +read +write +seek abilities.
 /// A malicious file is detected as incremental link failure and does not cause Illegal Behavior.
 /// This operation is not atomic.
-pub fn updateFile(allocator: *Allocator, module: ir.Module, file: fs.File) !Result {
-    return updateFileInner(allocator, module, file) catch |err| switch (err) {
+pub fn openBinFile(allocator: *Allocator, file: fs.File, options: Options) !ElfFile {
+    return openBinFileInner(allocator, file, options) catch |err| switch (err) {
         error.IncrFailed => {
-            return writeFile(allocator, module, file);
+            return createElfFile(allocator, file, options);
         },
         else => |e| return e,
     };
 }
 
-const Update = struct {
+pub const ElfFile = struct {
+    allocator: *Allocator,
     file: fs.File,
-    module: *const ir.Module,
+    options: Options,
+    ptr_width: enum { p32, p64 },
 
     /// Stored in native-endian format, depending on target endianness needs to be bswapped on read/write.
     /// Same order as in the file.
-    sections: std.ArrayList(elf.Elf64_Shdr),
-    shdr_table_offset: ?u64,
+    sections: std.ArrayListUnmanaged(elf.Elf64_Shdr) = .{},
+    shdr_table_offset: ?u64 = null,
 
     /// Stored in native-endian format, depending on target endianness needs to be bswapped on read/write.
     /// Same order as in the file.
-    program_headers: std.ArrayList(elf.Elf64_Phdr),
-    phdr_table_offset: ?u64,
+    program_headers: std.ArrayListUnmanaged(elf.Elf64_Phdr) = .{},
+    phdr_table_offset: ?u64 = null,
     /// The index into the program headers of a PT_LOAD program header with Read and Execute flags
-    phdr_load_re_index: ?u16,
-    entry_addr: ?u64,
+    phdr_load_re_index: ?u16 = null,
+    entry_addr: ?u64 = null,
 
-    shstrtab: std.ArrayList(u8),
-    shstrtab_index: ?u16,
+    shstrtab: std.ArrayListUnmanaged(u8) = .{},
+    shstrtab_index: ?u16 = null,
 
-    text_section_index: ?u16,
-    symtab_section_index: ?u16,
+    text_section_index: ?u16 = null,
+    symtab_section_index: ?u16 = null,
 
     /// The same order as in the file
-    symbols: std.ArrayList(elf.Elf64_Sym),
+    symbols: std.ArrayListUnmanaged(elf.Elf64_Sym) = .{},
 
-    errors: std.ArrayList(ErrorMsg),
+    /// Same order as in the file.
+    offset_table: std.ArrayListUnmanaged(aoeu) = .{},
+
+    /// This means the entire read-only executable program code needs to be rewritten.
+    phdr_load_re_dirty: bool = false,
+    phdr_table_dirty: bool = false,
+    shdr_table_dirty: bool = false,
+    shstrtab_dirty: bool = false,
+    symtab_dirty: bool = false,
+
+    error_flags: ErrorFlags = ErrorFlags{},
 
-    fn deinit(self: *Update) void {
-        self.sections.deinit();
-        self.program_headers.deinit();
-        self.shstrtab.deinit();
-        self.symbols.deinit();
-        self.errors.deinit();
+    pub const ErrorFlags = struct {
+        no_entry_point_found: bool = false,
+    };
+
+    pub fn deinit(self: *ElfFile) void {
+        self.sections.deinit(self.allocator);
+        self.program_headers.deinit(self.allocator);
+        self.shstrtab.deinit(self.allocator);
+        self.symbols.deinit(self.allocator);
+        self.offset_table.deinit(self.allocator);
     }
 
     // `expand_num / expand_den` is the factor of padding when allocation
@@ -112,8 +143,8 @@ const Update = struct {
     const alloc_den = 3;
 
     /// Returns end pos of collision, if any.
-    fn detectAllocCollision(self: *Update, start: u64, size: u64) ?u64 {
-        const small_ptr = self.module.target.cpu.arch.ptrBitWidth() == 32;
+    fn detectAllocCollision(self: *ElfFile, start: u64, size: u64) ?u64 {
+        const small_ptr = self.options.target.cpu.arch.ptrBitWidth() == 32;
         const ehdr_size: u64 = if (small_ptr) @sizeOf(elf.Elf32_Ehdr) else @sizeOf(elf.Elf64_Ehdr);
         if (start < ehdr_size)
             return ehdr_size;
@@ -157,7 +188,7 @@ const Update = struct {
         return null;
     }
 
-    fn allocatedSize(self: *Update, start: u64) u64 {
+    fn allocatedSize(self: *ElfFile, start: u64) u64 {
         var min_pos: u64 = std.math.maxInt(u64);
         if (self.shdr_table_offset) |off| {
             if (off > start and off < min_pos) min_pos = off;
@@ -176,7 +207,7 @@ const Update = struct {
         return min_pos - start;
     }
 
-    fn findFreeSpace(self: *Update, object_size: u64, min_alignment: u16) u64 {
+    fn findFreeSpace(self: *ElfFile, object_size: u64, min_alignment: u16) u64 {
         var start: u64 = 0;
         while (self.detectAllocCollision(start, object_size)) |item_end| {
             start = mem.alignForwardGeneric(u64, item_end, min_alignment);
@@ -184,33 +215,21 @@ const Update = struct {
         return start;
     }
 
-    fn makeString(self: *Update, bytes: []const u8) !u32 {
+    fn makeString(self: *ElfFile, bytes: []const u8) !u32 {
         const result = self.shstrtab.items.len;
         try self.shstrtab.appendSlice(bytes);
         try self.shstrtab.append(0);
         return @intCast(u32, result);
     }
 
-    fn perform(self: *Update) !void {
-        const ptr_width: enum { p32, p64 } = switch (self.module.target.cpu.arch.ptrBitWidth()) {
-            32 => .p32,
-            64 => .p64,
-            else => return error.UnsupportedArchitecture,
-        };
-        const small_ptr = switch (ptr_width) {
+    pub fn populateMissingMetadata(self: *ElfFile) !void {
+        const small_ptr = switch (self.ptr_width) {
             .p32 => true,
             .p64 => false,
         };
-        // This means the entire read-only executable program code needs to be rewritten.
-        var phdr_load_re_dirty = false;
-        var phdr_table_dirty = false;
-        var shdr_table_dirty = false;
-        var shstrtab_dirty = false;
-        var symtab_dirty = false;
-
         if (self.phdr_load_re_index == null) {
             self.phdr_load_re_index = @intCast(u16, self.program_headers.items.len);
-            const file_size = 256 * 1024;
+            const file_size = self.options.program_code_size_hint;
             const p_align = 0x1000;
             const off = self.findFreeSpace(file_size, p_align);
             //std.debug.warn("found PT_LOAD free space 0x{x} to 0x{x}\n", .{ off, off + file_size });
@@ -225,24 +244,8 @@ const Update = struct {
                 .p_flags = elf.PF_X | elf.PF_R,
             });
             self.entry_addr = null;
-            phdr_load_re_dirty = true;
-            phdr_table_dirty = true;
-        }
-        if (self.sections.items.len == 0) {
-            // There must always be a null section in index 0
-            try self.sections.append(.{
-                .sh_name = 0,
-                .sh_type = elf.SHT_NULL,
-                .sh_flags = 0,
-                .sh_addr = 0,
-                .sh_offset = 0,
-                .sh_size = 0,
-                .sh_link = 0,
-                .sh_info = 0,
-                .sh_addralign = 0,
-                .sh_entsize = 0,
-            });
-            shdr_table_dirty = true;
+            self.phdr_load_re_dirty = true;
+            self.phdr_table_dirty = true;
         }
         if (self.shstrtab_index == null) {
             self.shstrtab_index = @intCast(u16, self.sections.items.len);
@@ -262,8 +265,8 @@ const Update = struct {
                 .sh_addralign = 1,
                 .sh_entsize = 0,
             });
-            shstrtab_dirty = true;
-            shdr_table_dirty = true;
+            self.shstrtab_dirty = true;
+            self.shdr_table_dirty = true;
         }
         if (self.text_section_index == null) {
             self.text_section_index = @intCast(u16, self.sections.items.len);
@@ -281,13 +284,13 @@ const Update = struct {
                 .sh_addralign = phdr.p_align,
                 .sh_entsize = 0,
             });
-            shdr_table_dirty = true;
+            self.shdr_table_dirty = true;
         }
         if (self.symtab_section_index == null) {
             self.symtab_section_index = @intCast(u16, self.sections.items.len);
             const min_align: u16 = if (small_ptr) @alignOf(elf.Elf32_Sym) else @alignOf(elf.Elf64_Sym);
             const each_size: u64 = if (small_ptr) @sizeOf(elf.Elf32_Sym) else @sizeOf(elf.Elf64_Sym);
-            const file_size = self.module.exports.len * each_size;
+            const file_size = self.options.symbol_count_hint * each_size;
             const off = self.findFreeSpace(file_size, min_align);
             //std.debug.warn("found symtab free space 0x{x} to 0x{x}\n", .{ off, off + file_size });
 
@@ -300,12 +303,12 @@ const Update = struct {
                 .sh_size = file_size,
                 // The section header index of the associated string table.
                 .sh_link = self.shstrtab_index.?,
-                .sh_info = @intCast(u32, self.module.exports.len),
+                .sh_info = @intCast(u32, self.symbols.items.len),
                 .sh_addralign = min_align,
                 .sh_entsize = each_size,
             });
-            symtab_dirty = true;
-            shdr_table_dirty = true;
+            self.symtab_dirty = true;
+            self.shdr_table_dirty = true;
         }
         const shsize: u64 = switch (ptr_width) {
             .p32 => @sizeOf(elf.Elf32_Shdr),
@@ -317,7 +320,7 @@ const Update = struct {
         };
         if (self.shdr_table_offset == null) {
             self.shdr_table_offset = self.findFreeSpace(self.sections.items.len * shsize, shalign);
-            shdr_table_dirty = true;
+            self.shdr_table_dirty = true;
         }
         const phsize: u64 = switch (ptr_width) {
             .p32 => @sizeOf(elf.Elf32_Phdr),
@@ -329,13 +332,15 @@ const Update = struct {
         };
         if (self.phdr_table_offset == null) {
             self.phdr_table_offset = self.findFreeSpace(self.program_headers.items.len * phsize, phalign);
-            phdr_table_dirty = true;
+            self.phdr_table_dirty = true;
         }
-        const foreign_endian = self.module.target.cpu.arch.endian() != std.Target.current.cpu.arch.endian();
+    }
 
-        try self.writeCodeAndSymbols(phdr_table_dirty, shdr_table_dirty);
+    /// Commit pending changes and write headers.
+    pub fn flush(self: *ElfFile) !void {
+        const foreign_endian = self.options.target.cpu.arch.endian() != std.Target.current.cpu.arch.endian();
 
-        if (phdr_table_dirty) {
+        if (self.phdr_table_dirty) {
             const allocated_size = self.allocatedSize(self.phdr_table_offset.?);
             const needed_size = self.program_headers.items.len * phsize;
 
@@ -345,7 +350,7 @@ const Update = struct {
             }
 
             const allocator = self.program_headers.allocator;
-            switch (ptr_width) {
+            switch (self.ptr_width) {
                 .p32 => {
                     const buf = try allocator.alloc(elf.Elf32_Phdr, self.program_headers.items.len);
                     defer allocator.free(buf);
@@ -371,11 +376,12 @@ const Update = struct {
                     try self.file.pwriteAll(mem.sliceAsBytes(buf), self.phdr_table_offset.?);
                 },
             }
+            self.phdr_table_offset = false;
         }
 
         {
             const shstrtab_sect = &self.sections.items[self.shstrtab_index.?];
-            if (shstrtab_dirty or self.shstrtab.items.len != shstrtab_sect.sh_size) {
+            if (self.shstrtab_dirty or self.shstrtab.items.len != shstrtab_sect.sh_size) {
                 const allocated_size = self.allocatedSize(shstrtab_sect.sh_offset);
                 const needed_size = self.shstrtab.items.len;
 
@@ -387,13 +393,14 @@ const Update = struct {
                 //std.debug.warn("shstrtab start=0x{x} end=0x{x}\n", .{ shstrtab_sect.sh_offset, shstrtab_sect.sh_offset + needed_size });
 
                 try self.file.pwriteAll(self.shstrtab.items, shstrtab_sect.sh_offset);
-                if (!shdr_table_dirty) {
+                if (!self.shdr_table_dirty) {
                     // Then it won't get written with the others and we need to do it.
                     try self.writeSectHeader(self.shstrtab_index.?);
                 }
+                self.shstrtab_dirty = false;
             }
         }
-        if (shdr_table_dirty) {
+        if (self.shdr_table_dirty) {
             const allocated_size = self.allocatedSize(self.shdr_table_offset.?);
             const needed_size = self.sections.items.len * phsize;
 
@@ -403,7 +410,7 @@ const Update = struct {
             }
 
             const allocator = self.sections.allocator;
-            switch (ptr_width) {
+            switch (self.ptr_width) {
                 .p32 => {
                     const buf = try allocator.alloc(elf.Elf32_Shdr, self.sections.items.len);
                     defer allocator.free(buf);
@@ -431,38 +438,36 @@ const Update = struct {
                 },
             }
         }
-        if (self.entry_addr == null and self.module.output_mode == .Exe) {
-            const msg = try std.fmt.allocPrint(self.errors.allocator, "no entry point found", .{});
-            errdefer self.errors.allocator.free(msg);
-            try self.errors.append(.{
-                .byte_offset = 0,
-                .msg = msg,
-            });
+        if (self.entry_addr == null and self.options.output_mode == .Exe) {
+            self.error_flags.no_entry_point_found = true;
         } else {
+            self.error_flags.no_entry_point_found = false;
             try self.writeElfHeader();
         }
         // TODO find end pos and truncate
+
+        // The point of flush() is to commit changes, so nothing should be dirty after this.
+        assert(!self.phdr_load_re_dirty);
+        assert(!self.phdr_table_dirty);
+        assert(!self.shdr_table_dirty);
+        assert(!self.shstrtab_dirty);
+        assert(!self.symtab_dirty);
     }
 
-    fn writeElfHeader(self: *Update) !void {
+    fn writeElfHeader(self: *ElfFile) !void {
         var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 = undefined;
 
         var index: usize = 0;
         hdr_buf[0..4].* = "\x7fELF".*;
         index += 4;
 
-        const ptr_width: enum { p32, p64 } = switch (self.module.target.cpu.arch.ptrBitWidth()) {
-            32 => .p32,
-            64 => .p64,
-            else => return error.UnsupportedArchitecture,
-        };
-        hdr_buf[index] = switch (ptr_width) {
+        hdr_buf[index] = switch (self.ptr_width) {
             .p32 => elf.ELFCLASS32,
             .p64 => elf.ELFCLASS64,
         };
         index += 1;
 
-        const endian = self.module.target.cpu.arch.endian();
+        const endian = self.options.target.cpu.arch.endian();
         hdr_buf[index] = switch (endian) {
             .Little => elf.ELFDATA2LSB,
             .Big => elf.ELFDATA2MSB,
@@ -480,10 +485,10 @@ const Update = struct {
 
         assert(index == 16);
 
-        const elf_type = switch (self.module.output_mode) {
+        const elf_type = switch (self.options.output_mode) {
             .Exe => elf.ET.EXEC,
             .Obj => elf.ET.REL,
-            .Lib => switch (self.module.link_mode) {
+            .Lib => switch (self.options.link_mode) {
                 .Static => elf.ET.REL,
                 .Dynamic => elf.ET.DYN,
             },
@@ -491,7 +496,7 @@ const Update = struct {
         mem.writeInt(u16, hdr_buf[index..][0..2], @enumToInt(elf_type), endian);
         index += 2;
 
-        const machine = self.module.target.cpu.arch.toElfMachine();
+        const machine = self.options.target.cpu.arch.toElfMachine();
         mem.writeInt(u16, hdr_buf[index..][0..2], @enumToInt(machine), endian);
         index += 2;
 
@@ -501,7 +506,7 @@ const Update = struct {
 
         const e_entry = if (elf_type == .REL) 0 else self.entry_addr.?;
 
-        switch (ptr_width) {
+        switch (self.ptr_width) {
             .p32 => {
                 mem.writeInt(u32, hdr_buf[index..][0..4], @intCast(u32, e_entry), endian);
                 index += 4;
@@ -533,14 +538,14 @@ const Update = struct {
         mem.writeInt(u32, hdr_buf[index..][0..4], e_flags, endian);
         index += 4;
 
-        const e_ehsize: u16 = switch (ptr_width) {
+        const e_ehsize: u16 = switch (self.ptr_width) {
             .p32 => @sizeOf(elf.Elf32_Ehdr),
             .p64 => @sizeOf(elf.Elf64_Ehdr),
         };
         mem.writeInt(u16, hdr_buf[index..][0..2], e_ehsize, endian);
         index += 2;
 
-        const e_phentsize: u16 = switch (ptr_width) {
+        const e_phentsize: u16 = switch (self.ptr_width) {
             .p32 => @sizeOf(elf.Elf32_Phdr),
             .p64 => @sizeOf(elf.Elf64_Phdr),
         };
@@ -551,7 +556,7 @@ const Update = struct {
         mem.writeInt(u16, hdr_buf[index..][0..2], e_phnum, endian);
         index += 2;
 
-        const e_shentsize: u16 = switch (ptr_width) {
+        const e_shentsize: u16 = switch (self.ptr_width) {
             .p32 => @sizeOf(elf.Elf32_Shdr),
             .p64 => @sizeOf(elf.Elf64_Shdr),
         };
@@ -570,81 +575,172 @@ const Update = struct {
         try self.file.pwriteAll(hdr_buf[0..index], 0);
     }
 
-    fn writeCodeAndSymbols(self: *Update, phdr_table_dirty: bool, shdr_table_dirty: bool) !void {
-        // index 0 is always a null symbol
-        try self.symbols.resize(1);
-        self.symbols.items[0] = .{
-            .st_name = 0,
-            .st_info = 0,
-            .st_other = 0,
-            .st_shndx = 0,
-            .st_value = 0,
-            .st_size = 0,
-        };
+    /// TODO Look into making this smaller to save memory.
+    /// Lots of redundant info here with the data stored in symbol structs.
+    const DeclSymbol = struct {
+        symbol_indexes: []usize,
+        vaddr: u64,
+        file_offset: u64,
+        size: u64,
+    };
 
+    const AllocatedBlock = struct {
+        vaddr: u64,
+        file_offset: u64,
+        size_capacity: u64,
+    };
+
+    fn allocateDeclSymbol(self: *ElfFile, size: u64) AllocatedBlock {
         const phdr = &self.program_headers.items[self.phdr_load_re_index.?];
-        var vaddr: u64 = phdr.p_vaddr;
-        var file_off: u64 = phdr.p_offset;
+        todo();
+        //{
+        //    // Now that we know the code size, we need to update the program header for executable code
+        //    phdr.p_memsz = vaddr - phdr.p_vaddr;
+        //    phdr.p_filesz = phdr.p_memsz;
+
+        //    const shdr = &self.sections.items[self.text_section_index.?];
+        //    shdr.sh_size = phdr.p_filesz;
+
+        //    self.phdr_table_dirty = true; // TODO look into making only the one program header dirty
+        //    self.shdr_table_dirty = true; // TODO look into making only the one section dirty
+        //}
+
+        //return self.writeSymbols();
+    }
+
+    fn findAllocatedBlock(self: *ElfFile, vaddr: u64) AllocatedBlock {
+        todo();
+    }
 
-        var code = std.ArrayList(u8).init(self.sections.allocator);
+    pub fn updateDecl(
+        self: *ElfFile,
+        module: ir.Module,
+        typed_value: ir.TypedValue,
+        decl_export_node: ?*std.LinkedList(std.builtin.ExportOptions).Node,
+        hash: ir.Module.Decl.Hash,
+        err_msg_allocator: *Allocator,
+    ) !?ir.ErrorMsg {
+        var code = std.ArrayList(u8).init(self.allocator);
         defer code.deinit();
 
-        for (self.module.exports) |exp| {
-            code.shrink(0);
-            var symbol = try codegen.generateSymbol(exp.typed_value, self.module.*, &code);
-            defer symbol.deinit(code.allocator);
-            if (symbol.errors.len != 0) {
-                for (symbol.errors) |err| {
-                    const msg = try mem.dupe(self.errors.allocator, u8, err.msg);
-                    errdefer self.errors.allocator.free(msg);
-                    try self.errors.append(.{
-                        .byte_offset = err.byte_offset,
-                        .msg = msg,
-                    });
+        const err_msg = try codegen.generateSymbol(typed_value, module, &code, err_msg_allocator);
+        if (err_msg != null) |em| return em;
+
+        const export_count = blk: {
+            var export_node = decl_export_node;
+            var i: usize = 0;
+            while (export_node) |node| : (export_node = node.next) i += 1;
+            break :blk i;
+        };
+
+        // Find or create a symbol from the decl
+        var valid_sym_index_len: usize = 0;
+        const decl_symbol = blk: {
+            if (self.decl_table.getValue(hash)) |decl_symbol| {
+                valid_sym_index_len = decl_symbol.symbol_indexes.len;
+                decl_symbol.symbol_indexes = try self.allocator.realloc(usize, export_count);
+
+                const existing_block = self.findAllocatedBlock(decl_symbol.vaddr);
+                if (code.items.len > existing_block.size_capacity) {
+                    const new_block = self.allocateDeclSymbol(code.items.len);
+                    decl_symbol.vaddr = new_block.vaddr;
+                    decl_symbol.file_offset = new_block.file_offset;
+                    decl_symbol.size = code.items.len;
                 }
-                continue;
+                break :blk decl_symbol;
+            } else {
+                const new_block = self.allocateDeclSymbol(code.items.len);
+
+                const decl_symbol = try self.allocator.create(DeclSymbol);
+                errdefer self.allocator.destroy(decl_symbol);
+
+                decl_symbol.* = .{
+                    .symbol_indexes = try self.allocator.alloc(usize, export_count),
+                    .vaddr = new_block.vaddr,
+                    .file_offset = new_block.file_offset,
+                    .size = code.items.len,
+                };
+                errdefer self.allocator.free(decl_symbol.symbol_indexes);
+
+                try self.decl_table.put(hash, decl_symbol);
+                break :blk decl_symbol;
+            }
+        };
+
+        // Allocate new symbols.
+        {
+            var i: usize = valid_sym_index_len;
+            const old_len = self.symbols.items.len;
+            try self.symbols.resize(old_len + (decl_symbol.symbol_indexes.len - i));
+            while (i < decl_symbol.symbol_indexes) : (i += 1) {
+                decl_symbol.symbol_indexes[i] = old_len + i;
             }
-            try self.file.pwriteAll(code.items, file_off);
+        }
 
-            if (mem.eql(u8, exp.name, "_start")) {
-                self.entry_addr = vaddr;
+        var export_node = decl_export_node;
+        var export_index: usize = 0;
+        while (export_node) |node| : ({
+            export_node = node.next;
+            export_index += 1;
+        }) {
+            if (node.data.section) |section_name| {
+                if (!mem.eql(u8, section_name, ".text")) {
+                    try errors.ensureCapacity(errors.items.len + 1);
+                    errors.appendAssumeCapacity(.{
+                        .byte_offset = 0,
+                        .msg = try std.fmt.allocPrint(errors.allocator, "Unimplemented: ExportOptions.section", .{}),
+                    });
+                }
             }
-            (try self.symbols.addOne()).* = .{
-                .st_name = try self.makeString(exp.name),
-                .st_info = (elf.STB_LOCAL << 4) | elf.STT_FUNC,
+            const stb_bits = switch (node.data.linkage) {
+                .Internal => elf.STB_LOCAL,
+                .Strong => blk: {
+                    if (mem.eql(u8, node.data.name, "_start")) {
+                        self.entry_addr = decl_symbol.vaddr;
+                    }
+                    break :blk elf.STB_GLOBAL;
+                },
+                .Weak => elf.STB_WEAK,
+                .LinkOnce => {
+                    try errors.ensureCapacity(errors.items.len + 1);
+                    errors.appendAssumeCapacity(.{
+                        .byte_offset = 0,
+                        .msg = try std.fmt.allocPrint(errors.allocator, "Unimplemented: GlobalLinkage.LinkOnce", .{}),
+                    });
+                },
+            };
+            const stt_bits = switch (typed_value.ty.zigTypeTag()) {
+                .Fn => elf.STT_FUNC,
+                else => elf.STT_OBJECT,
+            };
+            const sym_index = decl_symbol.symbol_indexes[export_index];
+            const name = blk: {
+                if (i < valid_sym_index_len) {
+                    const name_stroff = self.symbols.items[sym_index].st_name;
+                    const existing_name = self.getString(name_stroff);
+                    if (mem.eql(u8, existing_name, node.data.name)) {
+                        break :blk name_stroff;
+                    }
+                }
+                break :blk try self.makeString(node.data.name);
+            };
+            self.symbols.items[sym_index] = .{
+                .st_name = name,
+                .st_info = (stb_bits << 4) | stt_bits,
                 .st_other = 0,
                 .st_shndx = self.text_section_index.?,
-                .st_value = vaddr,
+                .st_value = decl_symbol.vaddr,
                 .st_size = code.items.len,
             };
-            vaddr += code.items.len;
         }
 
-        {
-            // Now that we know the code size, we need to update the program header for executable code
-            phdr.p_memsz = vaddr - phdr.p_vaddr;
-            phdr.p_filesz = phdr.p_memsz;
-
-            const shdr = &self.sections.items[self.text_section_index.?];
-            shdr.sh_size = phdr.p_filesz;
-
-            if (!phdr_table_dirty) {
-                // Then it won't get written with the others and we need to do it.
-                try self.writeProgHeader(self.phdr_load_re_index.?);
-            }
-            if (!shdr_table_dirty) {
-                // Then it won't get written with the others and we need to do it.
-                try self.writeSectHeader(self.text_section_index.?);
-            }
-        }
-
-        return self.writeSymbols();
+        try self.file.pwriteAll(code.items, decl_symbol.file_offset);
     }
 
-    fn writeProgHeader(self: *Update, index: usize) !void {
-        const foreign_endian = self.module.target.cpu.arch.endian() != std.Target.current.cpu.arch.endian();
+    fn writeProgHeader(self: *ElfFile, index: usize) !void {
+        const foreign_endian = self.options.target.cpu.arch.endian() != std.Target.current.cpu.arch.endian();
         const offset = self.program_headers.items[index].p_offset;
-        switch (self.module.target.cpu.arch.ptrBitWidth()) {
+        switch (self.options.target.cpu.arch.ptrBitWidth()) {
             32 => {
                 var phdr = [1]elf.Elf32_Phdr{progHeaderTo32(self.program_headers.items[index])};
                 if (foreign_endian) {
@@ -663,10 +759,10 @@ const Update = struct {
         }
     }
 
-    fn writeSectHeader(self: *Update, index: usize) !void {
-        const foreign_endian = self.module.target.cpu.arch.endian() != std.Target.current.cpu.arch.endian();
+    fn writeSectHeader(self: *ElfFile, index: usize) !void {
+        const foreign_endian = self.options.target.cpu.arch.endian() != std.Target.current.cpu.arch.endian();
         const offset = self.sections.items[index].sh_offset;
-        switch (self.module.target.cpu.arch.ptrBitWidth()) {
+        switch (self.options.target.cpu.arch.ptrBitWidth()) {
             32 => {
                 var shdr: [1]elf.Elf32_Shdr = undefined;
                 shdr[0] = sectHeaderTo32(self.sections.items[index]);
@@ -686,13 +782,8 @@ const Update = struct {
         }
     }
 
-    fn writeSymbols(self: *Update) !void {
-        const ptr_width: enum { p32, p64 } = switch (self.module.target.cpu.arch.ptrBitWidth()) {
-            32 => .p32,
-            64 => .p64,
-            else => return error.UnsupportedArchitecture,
-        };
-        const small_ptr = ptr_width == .p32;
+    fn writeSymbols(self: *ElfFile) !void {
+        const small_ptr = self.ptr_width == .p32;
         const syms_sect = &self.sections.items[self.symtab_section_index.?];
         const sym_align: u16 = if (small_ptr) @alignOf(elf.Elf32_Sym) else @alignOf(elf.Elf64_Sym);
         const sym_size: u64 = if (small_ptr) @sizeOf(elf.Elf32_Sym) else @sizeOf(elf.Elf64_Sym);
@@ -708,8 +799,8 @@ const Update = struct {
         syms_sect.sh_size = needed_size;
         syms_sect.sh_info = @intCast(u32, self.symbols.items.len);
         const allocator = self.symbols.allocator;
-        const foreign_endian = self.module.target.cpu.arch.endian() != std.Target.current.cpu.arch.endian();
-        switch (ptr_width) {
+        const foreign_endian = self.options.target.cpu.arch.endian() != std.Target.current.cpu.arch.endian();
+        switch (self.ptr_width) {
             .p32 => {
                 const buf = try allocator.alloc(elf.Elf32_Sym, self.symbols.items.len);
                 defer allocator.free(buf);
@@ -754,13 +845,13 @@ const Update = struct {
 
 /// Truncates the existing file contents and overwrites the contents.
 /// Returns an error if `file` is not already open with +read +write +seek abilities.
-pub fn writeFile(allocator: *Allocator, module: ir.Module, file: fs.File) !Result {
-    switch (module.output_mode) {
+pub fn createElfFile(allocator: *Allocator, file: fs.File, options: Options) !ElfFile {
+    switch (options.output_mode) {
         .Exe => {},
         .Obj => {},
         .Lib => return error.TODOImplementWritingLibFiles,
     }
-    switch (module.object_format) {
+    switch (options.object_format) {
         .unknown => unreachable, // TODO remove this tag from the enum
         .coff => return error.TODOImplementWritingCOFF,
         .elf => {},
@@ -768,38 +859,79 @@ pub fn writeFile(allocator: *Allocator, module: ir.Module, file: fs.File) !Resul
         .wasm => return error.TODOImplementWritingWasmObjects,
     }
 
-    var update = Update{
+    var self: ElfFile = .{
+        .allocator = allocator,
         .file = file,
-        .module = &module,
-        .sections = std.ArrayList(elf.Elf64_Shdr).init(allocator),
-        .shdr_table_offset = null,
-        .program_headers = std.ArrayList(elf.Elf64_Phdr).init(allocator),
-        .phdr_table_offset = null,
-        .phdr_load_re_index = null,
-        .entry_addr = null,
-        .shstrtab = std.ArrayList(u8).init(allocator),
-        .shstrtab_index = null,
-        .text_section_index = null,
-        .symtab_section_index = null,
-
-        .symbols = std.ArrayList(elf.Elf64_Sym).init(allocator),
-
-        .errors = std.ArrayList(ErrorMsg).init(allocator),
-    };
-    defer update.deinit();
-
-    try update.perform();
-    return Result{
-        .errors = update.errors.toOwnedSlice(),
+        .options = options,
+        .ptr_width = switch (self.options.target.cpu.arch.ptrBitWidth()) {
+            32 => .p32,
+            64 => .p64,
+            else => return error.UnsupportedELFArchitecture,
+        },
+        .symtab_dirty = true,
+        .shdr_table_dirty = true,
     };
+    errdefer self.deinit();
+
+    // Index 0 is always a null symbol.
+    try self.symbols.append(allocator, .{
+        .st_name = 0,
+        .st_info = 0,
+        .st_other = 0,
+        .st_shndx = 0,
+        .st_value = 0,
+        .st_size = 0,
+    });
+
+    // There must always be a null section in index 0
+    try self.sections.append(allocator, .{
+        .sh_name = 0,
+        .sh_type = elf.SHT_NULL,
+        .sh_flags = 0,
+        .sh_addr = 0,
+        .sh_offset = 0,
+        .sh_size = 0,
+        .sh_link = 0,
+        .sh_info = 0,
+        .sh_addralign = 0,
+        .sh_entsize = 0,
+    });
+
+    try self.populateMissingMetadata();
+
+    return self;
 }
 
 /// Returns error.IncrFailed if incremental update could not be performed.
-fn updateFileInner(allocator: *Allocator, module: ir.Module, file: fs.File) !Result {
-    //var ehdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 = undefined;
+fn openBinFileInner(allocator: *Allocator, file: fs.File, options: Options) !ElfFile {
+    switch (options.output_mode) {
+        .Exe => {},
+        .Obj => {},
+        .Lib => return error.IncrFailed,
+    }
+    switch (options.object_format) {
+        .unknown => unreachable, // TODO remove this tag from the enum
+        .coff => return error.IncrFailed,
+        .elf => {},
+        .macho => return error.IncrFailed,
+        .wasm => return error.IncrFailed,
+    }
+    var self: ElfFile = .{
+        .allocator = allocator,
+        .file = file,
+        .options = options,
+        .ptr_width = switch (self.options.target.cpu.arch.ptrBitWidth()) {
+            32 => .p32,
+            64 => .p64,
+            else => return error.UnsupportedELFArchitecture,
+        },
+    };
+    errdefer self.deinit();
 
-    // TODO implement incremental linking
+    // TODO implement reading the elf file
     return error.IncrFailed;
+    //try self.populateMissingMetadata();
+    //return self;
 }
 
 /// Saturating multiplication
@@ -840,14 +972,14 @@ fn sectHeaderTo32(shdr: elf.Elf64_Shdr) elf.Elf32_Shdr {
     };
 }
 
-fn determineMode(module: ir.Module) fs.File.Mode {
+fn determineMode(options: Options) fs.File.Mode {
     // On common systems with a 0o022 umask, 0o777 will still result in a file created
     // with 0o755 permissions, but it works appropriately if the system is configured
     // more leniently. As another data point, C's fopen seems to open files with the
     // 666 mode.
     const executable_mode = if (std.Target.current.os.tag == .windows) 0 else 0o777;
-    switch (module.output_mode) {
-        .Lib => return switch (module.link_mode) {
+    switch (options.output_mode) {
+        .Lib => return switch (options.link_mode) {
             .Dynamic => executable_mode,
             .Static => fs.File.default_mode,
         },
src-self-hosted/Package.zig
@@ -0,0 +1,52 @@
+pub const Table = std.StringHashMap(*Package);
+
+root_src_dir: std.fs.Dir,
+/// Relative to `root_src_dir`.
+root_src_path: []const u8,
+table: Table,
+
+/// No references to `root_src_dir` and `root_src_path` are kept.
+pub fn create(
+    allocator: *mem.Allocator,
+    base_dir: std.fs.Dir,
+    /// Relative to `base_dir`.
+    root_src_dir: []const u8,
+    /// Relative to `root_src_dir`.
+    root_src_path: []const u8,
+) !*Package {
+    const ptr = try allocator.create(Package);
+    errdefer allocator.destroy(ptr);
+    const root_src_path_dupe = try mem.dupe(allocator, u8, root_src_path);
+    errdefer allocator.free(root_src_path_dupe);
+    ptr.* = .{
+        .root_src_dir = try base_dir.openDir(root_src_dir, .{}),
+        .root_src_path = root_src_path_dupe,
+        .table = Table.init(allocator),
+    };
+    return ptr;
+}
+
+pub fn destroy(self: *Package) void {
+    const allocator = self.table.allocator;
+    self.root_src_dir.close();
+    allocator.free(self.root_src_path);
+    {
+        var it = self.table.iterator();
+        while (it.next()) |kv| {
+            allocator.free(kv.key);
+        }
+    }
+    self.table.deinit();
+    allocator.destroy(self);
+}
+
+pub fn add(self: *Package, name: []const u8, package: *Package) !void {
+    const name_dupe = try mem.dupe(self.table.allocator, u8, name);
+    errdefer self.table.allocator.deinit(name_dupe);
+    const entry = try self.table.put(name_dupe, package);
+    assert(entry == null);
+}
+
+const std = @import("std");
+const mem = std.mem;
+const assert = std.debug.assert;
src-self-hosted/package.zig
@@ -1,31 +0,0 @@
-const std = @import("std");
-const mem = std.mem;
-const assert = std.debug.assert;
-const ArrayListSentineled = std.ArrayListSentineled;
-
-pub const Package = struct {
-    root_src_dir: ArrayListSentineled(u8, 0),
-    root_src_path: ArrayListSentineled(u8, 0),
-
-    /// relative to root_src_dir
-    table: Table,
-
-    pub const Table = std.StringHashMap(*Package);
-
-    /// makes internal copies of root_src_dir and root_src_path
-    /// allocator should be an arena allocator because Package never frees anything
-    pub fn create(allocator: *mem.Allocator, root_src_dir: []const u8, root_src_path: []const u8) !*Package {
-        const ptr = try allocator.create(Package);
-        ptr.* = Package{
-            .root_src_dir = try ArrayListSentineled(u8, 0).init(allocator, root_src_dir),
-            .root_src_path = try ArrayListSentineled(u8, 0).init(allocator, root_src_path),
-            .table = Table.init(allocator),
-        };
-        return ptr;
-    }
-
-    pub fn add(self: *Package, name: []const u8, package: *Package) !void {
-        const entry = try self.table.put(try mem.dupe(self.table.allocator, u8, name), package);
-        assert(entry == null);
-    }
-};
src-self-hosted/scope.zig
@@ -1,418 +0,0 @@
-const std = @import("std");
-const Allocator = mem.Allocator;
-const Decl = @import("decl.zig").Decl;
-const Compilation = @import("compilation.zig").Compilation;
-const mem = std.mem;
-const ast = std.zig.ast;
-const Value = @import("value.zig").Value;
-const Type = @import("type.zig").Type;
-const ir = @import("ir.zig");
-const Span = @import("errmsg.zig").Span;
-const assert = std.debug.assert;
-const event = std.event;
-const llvm = @import("llvm.zig");
-
-pub const Scope = struct {
-    id: Id,
-    parent: ?*Scope,
-    ref_count: std.atomic.Int(usize),
-
-    /// Thread-safe
-    pub fn ref(base: *Scope) void {
-        _ = base.ref_count.incr();
-    }
-
-    /// Thread-safe
-    pub fn deref(base: *Scope, comp: *Compilation) void {
-        if (base.ref_count.decr() == 1) {
-            if (base.parent) |parent| parent.deref(comp);
-            switch (base.id) {
-                .Root => @fieldParentPtr(Root, "base", base).destroy(comp),
-                .Decls => @fieldParentPtr(Decls, "base", base).destroy(comp),
-                .Block => @fieldParentPtr(Block, "base", base).destroy(comp),
-                .FnDef => @fieldParentPtr(FnDef, "base", base).destroy(comp),
-                .CompTime => @fieldParentPtr(CompTime, "base", base).destroy(comp),
-                .Defer => @fieldParentPtr(Defer, "base", base).destroy(comp),
-                .DeferExpr => @fieldParentPtr(DeferExpr, "base", base).destroy(comp),
-                .Var => @fieldParentPtr(Var, "base", base).destroy(comp),
-                .AstTree => @fieldParentPtr(AstTree, "base", base).destroy(comp),
-            }
-        }
-    }
-
-    pub fn findRoot(base: *Scope) *Root {
-        var scope = base;
-        while (scope.parent) |parent| {
-            scope = parent;
-        }
-        assert(scope.id == .Root);
-        return @fieldParentPtr(Root, "base", scope);
-    }
-
-    pub fn findFnDef(base: *Scope) ?*FnDef {
-        var scope = base;
-        while (true) {
-            switch (scope.id) {
-                .FnDef => return @fieldParentPtr(FnDef, "base", scope),
-                .Root, .Decls => return null,
-
-                .Block,
-                .Defer,
-                .DeferExpr,
-                .CompTime,
-                .Var,
-                => scope = scope.parent.?,
-
-                .AstTree => unreachable,
-            }
-        }
-    }
-
-    pub fn findDeferExpr(base: *Scope) ?*DeferExpr {
-        var scope = base;
-        while (true) {
-            switch (scope.id) {
-                .DeferExpr => return @fieldParentPtr(DeferExpr, "base", scope),
-
-                .FnDef,
-                .Decls,
-                => return null,
-
-                .Block,
-                .Defer,
-                .CompTime,
-                .Root,
-                .Var,
-                => scope = scope.parent orelse return null,
-
-                .AstTree => unreachable,
-            }
-        }
-    }
-
-    fn init(base: *Scope, id: Id, parent: *Scope) void {
-        base.* = Scope{
-            .id = id,
-            .parent = parent,
-            .ref_count = std.atomic.Int(usize).init(1),
-        };
-        parent.ref();
-    }
-
-    pub const Id = enum {
-        Root,
-        AstTree,
-        Decls,
-        Block,
-        FnDef,
-        CompTime,
-        Defer,
-        DeferExpr,
-        Var,
-    };
-
-    pub const Root = struct {
-        base: Scope,
-        realpath: []const u8,
-        decls: *Decls,
-
-        /// Creates a Root scope with 1 reference
-        /// Takes ownership of realpath
-        pub fn create(comp: *Compilation, realpath: []u8) !*Root {
-            const self = try comp.gpa().create(Root);
-            self.* = Root{
-                .base = Scope{
-                    .id = .Root,
-                    .parent = null,
-                    .ref_count = std.atomic.Int(usize).init(1),
-                },
-                .realpath = realpath,
-                .decls = undefined,
-            };
-            errdefer comp.gpa().destroy(self);
-            self.decls = try Decls.create(comp, &self.base);
-            return self;
-        }
-
-        pub fn destroy(self: *Root, comp: *Compilation) void {
-            // TODO comp.fs_watch.removeFile(self.realpath);
-            self.decls.base.deref(comp);
-            comp.gpa().free(self.realpath);
-            comp.gpa().destroy(self);
-        }
-    };
-
-    pub const AstTree = struct {
-        base: Scope,
-        tree: *ast.Tree,
-
-        /// Creates a scope with 1 reference
-        /// Takes ownership of tree, will deinit and destroy when done.
-        pub fn create(comp: *Compilation, tree: *ast.Tree, root_scope: *Root) !*AstTree {
-            const self = try comp.gpa().create(AstTree);
-            self.* = AstTree{
-                .base = undefined,
-                .tree = tree,
-            };
-            self.base.init(.AstTree, &root_scope.base);
-
-            return self;
-        }
-
-        pub fn destroy(self: *AstTree, comp: *Compilation) void {
-            comp.gpa().free(self.tree.source);
-            self.tree.deinit();
-            comp.gpa().destroy(self);
-        }
-
-        pub fn root(self: *AstTree) *Root {
-            return self.base.findRoot();
-        }
-    };
-
-    pub const Decls = struct {
-        base: Scope,
-
-        /// This table remains Write Locked when the names are incomplete or possibly outdated.
-        /// So if a reader manages to grab a lock, it can be sure that the set of names is complete
-        /// and correct.
-        table: event.RwLocked(Decl.Table),
-
-        /// Creates a Decls scope with 1 reference
-        pub fn create(comp: *Compilation, parent: *Scope) !*Decls {
-            const self = try comp.gpa().create(Decls);
-            self.* = Decls{
-                .base = undefined,
-                .table = event.RwLocked(Decl.Table).init(Decl.Table.init(comp.gpa())),
-            };
-            self.base.init(.Decls, parent);
-            return self;
-        }
-
-        pub fn destroy(self: *Decls, comp: *Compilation) void {
-            self.table.deinit();
-            comp.gpa().destroy(self);
-        }
-    };
-
-    pub const Block = struct {
-        base: Scope,
-        incoming_values: std.ArrayList(*ir.Inst),
-        incoming_blocks: std.ArrayList(*ir.BasicBlock),
-        end_block: *ir.BasicBlock,
-        is_comptime: *ir.Inst,
-
-        safety: Safety,
-
-        const Safety = union(enum) {
-            Auto,
-            Manual: Manual,
-
-            const Manual = struct {
-                /// the source span that disabled the safety value
-                span: Span,
-
-                /// whether safety is enabled
-                enabled: bool,
-            };
-
-            fn get(self: Safety, comp: *Compilation) bool {
-                return switch (self) {
-                    .Auto => switch (comp.build_mode) {
-                        .Debug,
-                        .ReleaseSafe,
-                        => true,
-                        .ReleaseFast,
-                        .ReleaseSmall,
-                        => false,
-                    },
-                    .Manual => |man| man.enabled,
-                };
-            }
-        };
-
-        /// Creates a Block scope with 1 reference
-        pub fn create(comp: *Compilation, parent: *Scope) !*Block {
-            const self = try comp.gpa().create(Block);
-            self.* = Block{
-                .base = undefined,
-                .incoming_values = undefined,
-                .incoming_blocks = undefined,
-                .end_block = undefined,
-                .is_comptime = undefined,
-                .safety = Safety.Auto,
-            };
-            self.base.init(.Block, parent);
-            return self;
-        }
-
-        pub fn destroy(self: *Block, comp: *Compilation) void {
-            comp.gpa().destroy(self);
-        }
-    };
-
-    pub const FnDef = struct {
-        base: Scope,
-
-        /// This reference is not counted so that the scope can get destroyed with the function
-        fn_val: ?*Value.Fn,
-
-        /// Creates a FnDef scope with 1 reference
-        /// Must set the fn_val later
-        pub fn create(comp: *Compilation, parent: *Scope) !*FnDef {
-            const self = try comp.gpa().create(FnDef);
-            self.* = FnDef{
-                .base = undefined,
-                .fn_val = null,
-            };
-            self.base.init(.FnDef, parent);
-            return self;
-        }
-
-        pub fn destroy(self: *FnDef, comp: *Compilation) void {
-            comp.gpa().destroy(self);
-        }
-    };
-
-    pub const CompTime = struct {
-        base: Scope,
-
-        /// Creates a CompTime scope with 1 reference
-        pub fn create(comp: *Compilation, parent: *Scope) !*CompTime {
-            const self = try comp.gpa().create(CompTime);
-            self.* = CompTime{ .base = undefined };
-            self.base.init(.CompTime, parent);
-            return self;
-        }
-
-        pub fn destroy(self: *CompTime, comp: *Compilation) void {
-            comp.gpa().destroy(self);
-        }
-    };
-
-    pub const Defer = struct {
-        base: Scope,
-        defer_expr_scope: *DeferExpr,
-        kind: Kind,
-
-        pub const Kind = enum {
-            ScopeExit,
-            ErrorExit,
-        };
-
-        /// Creates a Defer scope with 1 reference
-        pub fn create(
-            comp: *Compilation,
-            parent: *Scope,
-            kind: Kind,
-            defer_expr_scope: *DeferExpr,
-        ) !*Defer {
-            const self = try comp.gpa().create(Defer);
-            self.* = Defer{
-                .base = undefined,
-                .defer_expr_scope = defer_expr_scope,
-                .kind = kind,
-            };
-            self.base.init(.Defer, parent);
-            defer_expr_scope.base.ref();
-            return self;
-        }
-
-        pub fn destroy(self: *Defer, comp: *Compilation) void {
-            self.defer_expr_scope.base.deref(comp);
-            comp.gpa().destroy(self);
-        }
-    };
-
-    pub const DeferExpr = struct {
-        base: Scope,
-        expr_node: *ast.Node,
-        reported_err: bool,
-
-        /// Creates a DeferExpr scope with 1 reference
-        pub fn create(comp: *Compilation, parent: *Scope, expr_node: *ast.Node) !*DeferExpr {
-            const self = try comp.gpa().create(DeferExpr);
-            self.* = DeferExpr{
-                .base = undefined,
-                .expr_node = expr_node,
-                .reported_err = false,
-            };
-            self.base.init(.DeferExpr, parent);
-            return self;
-        }
-
-        pub fn destroy(self: *DeferExpr, comp: *Compilation) void {
-            comp.gpa().destroy(self);
-        }
-    };
-
-    pub const Var = struct {
-        base: Scope,
-        name: []const u8,
-        src_node: *ast.Node,
-        data: Data,
-
-        pub const Data = union(enum) {
-            Param: Param,
-            Const: *Value,
-        };
-
-        pub const Param = struct {
-            index: usize,
-            typ: *Type,
-            llvm_value: *llvm.Value,
-        };
-
-        pub fn createParam(
-            comp: *Compilation,
-            parent: *Scope,
-            name: []const u8,
-            src_node: *ast.Node,
-            param_index: usize,
-            param_type: *Type,
-        ) !*Var {
-            const self = try create(comp, parent, name, src_node);
-            self.data = Data{
-                .Param = Param{
-                    .index = param_index,
-                    .typ = param_type,
-                    .llvm_value = undefined,
-                },
-            };
-            return self;
-        }
-
-        pub fn createConst(
-            comp: *Compilation,
-            parent: *Scope,
-            name: []const u8,
-            src_node: *ast.Node,
-            value: *Value,
-        ) !*Var {
-            const self = try create(comp, parent, name, src_node);
-            self.data = Data{ .Const = value };
-            value.ref();
-            return self;
-        }
-
-        fn create(comp: *Compilation, parent: *Scope, name: []const u8, src_node: *ast.Node) !*Var {
-            const self = try comp.gpa().create(Var);
-            self.* = Var{
-                .base = undefined,
-                .name = name,
-                .src_node = src_node,
-                .data = undefined,
-            };
-            self.base.init(.Var, parent);
-            return self;
-        }
-
-        pub fn destroy(self: *Var, comp: *Compilation) void {
-            switch (self.data) {
-                .Param => {},
-                .Const => |value| value.deref(comp),
-            }
-            comp.gpa().destroy(self);
-        }
-    };
-};
src-self-hosted/test.zig
@@ -3,15 +3,14 @@ const link = @import("link.zig");
 const ir = @import("ir.zig");
 const Allocator = std.mem.Allocator;
 
-var global_ctx: TestContext = undefined;
-
 test "self-hosted" {
-    try global_ctx.init();
-    defer global_ctx.deinit();
+    var ctx: TestContext = undefined;
+    try ctx.init();
+    defer ctx.deinit();
 
-    try @import("stage2_tests").addCases(&global_ctx);
+    try @import("stage2_tests").addCases(&ctx);
 
-    try global_ctx.run();
+    try ctx.run();
 }
 
 pub const TestContext = struct {
src-self-hosted/type.zig
@@ -52,6 +52,7 @@ pub const Type = extern union {
             .comptime_float => return .ComptimeFloat,
             .noreturn => return .NoReturn,
 
+            .fn_noreturn_no_args => return .Fn,
             .fn_naked_noreturn_no_args => return .Fn,
             .fn_ccc_void_no_args => return .Fn,
 
@@ -184,6 +185,7 @@ pub const Type = extern union {
                 => return out_stream.writeAll(@tagName(t)),
 
                 .const_slice_u8 => return out_stream.writeAll("[]const u8"),
+                .fn_noreturn_no_args => return out_stream.writeAll("fn() noreturn"),
                 .fn_naked_noreturn_no_args => return out_stream.writeAll("fn() callconv(.Naked) noreturn"),
                 .fn_ccc_void_no_args => return out_stream.writeAll("fn() callconv(.C) void"),
                 .single_const_pointer_to_comptime_int => return out_stream.writeAll("*const comptime_int"),
@@ -244,6 +246,7 @@ pub const Type = extern union {
             .comptime_int => return Value.initTag(.comptime_int_type),
             .comptime_float => return Value.initTag(.comptime_float_type),
             .noreturn => return Value.initTag(.noreturn_type),
+            .fn_noreturn_no_args => return Value.initTag(.fn_noreturn_no_args_type),
             .fn_naked_noreturn_no_args => return Value.initTag(.fn_naked_noreturn_no_args_type),
             .fn_ccc_void_no_args => return Value.initTag(.fn_ccc_void_no_args_type),
             .single_const_pointer_to_comptime_int => return Value.initTag(.single_const_pointer_to_comptime_int_type),
@@ -286,6 +289,7 @@ pub const Type = extern union {
             .array,
             .array_u8_sentinel_0,
             .const_slice_u8,
+            .fn_noreturn_no_args,
             .fn_naked_noreturn_no_args,
             .fn_ccc_void_no_args,
             .int_unsigned,
@@ -329,6 +333,7 @@ pub const Type = extern union {
             .array_u8_sentinel_0,
             .single_const_pointer,
             .single_const_pointer_to_comptime_int,
+            .fn_noreturn_no_args,
             .fn_naked_noreturn_no_args,
             .fn_ccc_void_no_args,
             .int_unsigned,
@@ -369,6 +374,7 @@ pub const Type = extern union {
             .noreturn,
             .array,
             .array_u8_sentinel_0,
+            .fn_noreturn_no_args,
             .fn_naked_noreturn_no_args,
             .fn_ccc_void_no_args,
             .int_unsigned,
@@ -410,6 +416,7 @@ pub const Type = extern union {
             .comptime_int,
             .comptime_float,
             .noreturn,
+            .fn_noreturn_no_args,
             .fn_naked_noreturn_no_args,
             .fn_ccc_void_no_args,
             .int_unsigned,
@@ -451,6 +458,7 @@ pub const Type = extern union {
             .comptime_int,
             .comptime_float,
             .noreturn,
+            .fn_noreturn_no_args,
             .fn_naked_noreturn_no_args,
             .fn_ccc_void_no_args,
             .single_const_pointer,
@@ -481,6 +489,7 @@ pub const Type = extern union {
             .comptime_int,
             .comptime_float,
             .noreturn,
+            .fn_noreturn_no_args,
             .fn_naked_noreturn_no_args,
             .fn_ccc_void_no_args,
             .array,
@@ -524,6 +533,7 @@ pub const Type = extern union {
             .comptime_int,
             .comptime_float,
             .noreturn,
+            .fn_noreturn_no_args,
             .fn_naked_noreturn_no_args,
             .fn_ccc_void_no_args,
             .array,
@@ -579,6 +589,7 @@ pub const Type = extern union {
     /// Asserts the type is a function.
     pub fn fnParamLen(self: Type) usize {
         return switch (self.tag()) {
+            .fn_noreturn_no_args => 0,
             .fn_naked_noreturn_no_args => 0,
             .fn_ccc_void_no_args => 0,
 
@@ -622,6 +633,7 @@ pub const Type = extern union {
     /// given by `fnParamLen`.
     pub fn fnParamTypes(self: Type, types: []Type) void {
         switch (self.tag()) {
+            .fn_noreturn_no_args => return,
             .fn_naked_noreturn_no_args => return,
             .fn_ccc_void_no_args => return,
 
@@ -664,6 +676,7 @@ pub const Type = extern union {
     /// Asserts the type is a function.
     pub fn fnReturnType(self: Type) Type {
         return switch (self.tag()) {
+            .fn_noreturn_no_args => Type.initTag(.noreturn),
             .fn_naked_noreturn_no_args => Type.initTag(.noreturn),
             .fn_ccc_void_no_args => Type.initTag(.void),
 
@@ -706,6 +719,7 @@ pub const Type = extern union {
     /// Asserts the type is a function.
     pub fn fnCallingConvention(self: Type) std.builtin.CallingConvention {
         return switch (self.tag()) {
+            .fn_noreturn_no_args => .Unspecified,
             .fn_naked_noreturn_no_args => .Naked,
             .fn_ccc_void_no_args => .C,
 
@@ -745,6 +759,49 @@ pub const Type = extern union {
         };
     }
 
+    /// Asserts the type is a function.
+    pub fn fnIsVarArgs(self: Type) bool {
+        return switch (self.tag()) {
+            .fn_noreturn_no_args => false,
+            .fn_naked_noreturn_no_args => false,
+            .fn_ccc_void_no_args => false,
+
+            .f16,
+            .f32,
+            .f64,
+            .f128,
+            .c_longdouble,
+            .c_void,
+            .bool,
+            .void,
+            .type,
+            .anyerror,
+            .comptime_int,
+            .comptime_float,
+            .noreturn,
+            .array,
+            .single_const_pointer,
+            .single_const_pointer_to_comptime_int,
+            .array_u8_sentinel_0,
+            .const_slice_u8,
+            .u8,
+            .i8,
+            .usize,
+            .isize,
+            .c_short,
+            .c_ushort,
+            .c_int,
+            .c_uint,
+            .c_long,
+            .c_ulong,
+            .c_longlong,
+            .c_ulonglong,
+            .int_unsigned,
+            .int_signed,
+            => unreachable,
+        };
+    }
+
     pub fn isNumeric(self: Type) bool {
         return switch (self.tag()) {
             .f16,
@@ -776,6 +833,7 @@ pub const Type = extern union {
             .type,
             .anyerror,
             .noreturn,
+            .fn_noreturn_no_args,
             .fn_naked_noreturn_no_args,
             .fn_ccc_void_no_args,
             .array,
@@ -812,6 +870,7 @@ pub const Type = extern union {
             .bool,
             .type,
             .anyerror,
+            .fn_noreturn_no_args,
             .fn_naked_noreturn_no_args,
             .fn_ccc_void_no_args,
             .single_const_pointer_to_comptime_int,
@@ -865,6 +924,7 @@ pub const Type = extern union {
             .bool,
             .type,
             .anyerror,
+            .fn_noreturn_no_args,
             .fn_naked_noreturn_no_args,
             .fn_ccc_void_no_args,
             .single_const_pointer_to_comptime_int,
@@ -902,11 +962,11 @@ pub const Type = extern union {
         c_longlong,
         c_ulonglong,
         c_longdouble,
-        c_void,
         f16,
         f32,
         f64,
         f128,
+        c_void,
         bool,
         void,
         type,
@@ -914,6 +974,7 @@ pub const Type = extern union {
         comptime_int,
         comptime_float,
         noreturn,
+        fn_noreturn_no_args,
         fn_naked_noreturn_no_args,
         fn_ccc_void_no_args,
         single_const_pointer_to_comptime_int,
src-self-hosted/util.zig
@@ -1,47 +0,0 @@
-const std = @import("std");
-const Target = std.Target;
-const llvm = @import("llvm.zig");
-
-pub fn getDarwinArchString(self: Target) [:0]const u8 {
-    switch (self.cpu.arch) {
-        .aarch64 => return "arm64",
-        .thumb,
-        .arm,
-        => return "arm",
-        .powerpc => return "ppc",
-        .powerpc64 => return "ppc64",
-        .powerpc64le => return "ppc64le",
-        // @tagName should be able to return sentinel terminated slice
-        else => @panic("TODO https://github.com/ziglang/zig/issues/3779"), //return @tagName(arch),
-    }
-}
-
-pub fn llvmTargetFromTriple(triple: [:0]const u8) !*llvm.Target {
-    var result: *llvm.Target = undefined;
-    var err_msg: [*:0]u8 = undefined;
-    if (llvm.GetTargetFromTriple(triple, &result, &err_msg) != 0) {
-        std.debug.warn("triple: {s} error: {s}\n", .{ triple, err_msg });
-        return error.UnsupportedTarget;
-    }
-    return result;
-}
-
-pub fn initializeAllTargets() void {
-    llvm.InitializeAllTargets();
-    llvm.InitializeAllTargetInfos();
-    llvm.InitializeAllTargetMCs();
-    llvm.InitializeAllAsmPrinters();
-    llvm.InitializeAllAsmParsers();
-}
-
-pub fn getLLVMTriple(allocator: *std.mem.Allocator, target: std.Target) ![:0]u8 {
-    var result = try std.ArrayListSentineled(u8, 0).initSize(allocator, 0);
-    defer result.deinit();
-
-    try result.outStream().print(
-        "{}-unknown-{}-{}",
-        .{ @tagName(target.cpu.arch), @tagName(target.os.tag), @tagName(target.abi) },
-    );
-
-    return result.toOwnedSlice();
-}
src-self-hosted/value.zig
@@ -6,6 +6,7 @@ const BigIntConst = std.math.big.int.Const;
 const BigIntMutable = std.math.big.int.Mutable;
 const Target = std.Target;
 const Allocator = std.mem.Allocator;
+const ir = @import("ir.zig");
 
 /// This is the raw data, with no bookkeeping, no memory awareness,
 /// no de-duplication, and no type system awareness.
@@ -45,6 +46,7 @@ pub const Value = extern union {
         comptime_int_type,
         comptime_float_type,
         noreturn_type,
+        fn_noreturn_no_args_type,
         fn_naked_noreturn_no_args_type,
         fn_ccc_void_no_args_type,
         single_const_pointer_to_comptime_int_type,
@@ -64,8 +66,8 @@ pub const Value = extern union {
         int_big_positive,
         int_big_negative,
         function,
-        ref,
-        ref_val,
+        decl_ref,
+        elem_ptr,
         bytes,
         repeated, // the value is a value repeated some number of times
 
@@ -136,6 +138,7 @@ pub const Value = extern union {
             .comptime_int_type => return out_stream.writeAll("comptime_int"),
             .comptime_float_type => return out_stream.writeAll("comptime_float"),
             .noreturn_type => return out_stream.writeAll("noreturn"),
+            .fn_noreturn_no_args_type => return out_stream.writeAll("fn() noreturn"),
             .fn_naked_noreturn_no_args_type => return out_stream.writeAll("fn() callconv(.Naked) noreturn"),
             .fn_ccc_void_no_args_type => return out_stream.writeAll("fn() callconv(.C) void"),
             .single_const_pointer_to_comptime_int_type => return out_stream.writeAll("*const comptime_int"),
@@ -153,11 +156,11 @@ pub const Value = extern union {
             .int_big_positive => return out_stream.print("{}", .{val.cast(Payload.IntBigPositive).?.asBigInt()}),
             .int_big_negative => return out_stream.print("{}", .{val.cast(Payload.IntBigNegative).?.asBigInt()}),
             .function => return out_stream.writeAll("(function)"),
-            .ref => return out_stream.writeAll("(ref)"),
-            .ref_val => {
-                try out_stream.writeAll("*const ");
-                val = val.cast(Payload.RefVal).?.val;
-                continue;
+            .decl_ref => return out_stream.writeAll("(decl ref)"),
+            .elem_ptr => {
+                const elem_ptr = val.cast(Payload.Int_u64).?;
+                try out_stream.print("&[{}] ", .{elem_ptr.index});
+                val = elem_ptr.array_ptr;
             },
             .bytes => return std.zig.renderStringLiteral(self.cast(Payload.Bytes).?.data, out_stream),
             .repeated => {
@@ -181,31 +184,32 @@ pub const Value = extern union {
         return switch (self.tag()) {
             .ty => self.cast(Payload.Ty).?.ty,
 
-            .u8_type => Type.initTag(.@"u8"),
-            .i8_type => Type.initTag(.@"i8"),
-            .isize_type => Type.initTag(.@"isize"),
-            .usize_type => Type.initTag(.@"usize"),
-            .c_short_type => Type.initTag(.@"c_short"),
-            .c_ushort_type => Type.initTag(.@"c_ushort"),
-            .c_int_type => Type.initTag(.@"c_int"),
-            .c_uint_type => Type.initTag(.@"c_uint"),
-            .c_long_type => Type.initTag(.@"c_long"),
-            .c_ulong_type => Type.initTag(.@"c_ulong"),
-            .c_longlong_type => Type.initTag(.@"c_longlong"),
-            .c_ulonglong_type => Type.initTag(.@"c_ulonglong"),
-            .c_longdouble_type => Type.initTag(.@"c_longdouble"),
-            .f16_type => Type.initTag(.@"f16"),
-            .f32_type => Type.initTag(.@"f32"),
-            .f64_type => Type.initTag(.@"f64"),
-            .f128_type => Type.initTag(.@"f128"),
-            .c_void_type => Type.initTag(.@"c_void"),
-            .bool_type => Type.initTag(.@"bool"),
-            .void_type => Type.initTag(.@"void"),
-            .type_type => Type.initTag(.@"type"),
-            .anyerror_type => Type.initTag(.@"anyerror"),
-            .comptime_int_type => Type.initTag(.@"comptime_int"),
-            .comptime_float_type => Type.initTag(.@"comptime_float"),
-            .noreturn_type => Type.initTag(.@"noreturn"),
+            .u8_type => Type.initTag(.u8),
+            .i8_type => Type.initTag(.i8),
+            .isize_type => Type.initTag(.isize),
+            .usize_type => Type.initTag(.usize),
+            .c_short_type => Type.initTag(.c_short),
+            .c_ushort_type => Type.initTag(.c_ushort),
+            .c_int_type => Type.initTag(.c_int),
+            .c_uint_type => Type.initTag(.c_uint),
+            .c_long_type => Type.initTag(.c_long),
+            .c_ulong_type => Type.initTag(.c_ulong),
+            .c_longlong_type => Type.initTag(.c_longlong),
+            .c_ulonglong_type => Type.initTag(.c_ulonglong),
+            .c_longdouble_type => Type.initTag(.c_longdouble),
+            .f16_type => Type.initTag(.f16),
+            .f32_type => Type.initTag(.f32),
+            .f64_type => Type.initTag(.f64),
+            .f128_type => Type.initTag(.f128),
+            .c_void_type => Type.initTag(.c_void),
+            .bool_type => Type.initTag(.bool),
+            .void_type => Type.initTag(.void),
+            .type_type => Type.initTag(.type),
+            .anyerror_type => Type.initTag(.anyerror),
+            .comptime_int_type => Type.initTag(.comptime_int),
+            .comptime_float_type => Type.initTag(.comptime_float),
+            .noreturn_type => Type.initTag(.noreturn),
+            .fn_noreturn_no_args_type => Type.initTag(.fn_noreturn_no_args),
             .fn_naked_noreturn_no_args_type => Type.initTag(.fn_naked_noreturn_no_args),
             .fn_ccc_void_no_args_type => Type.initTag(.fn_ccc_void_no_args),
             .single_const_pointer_to_comptime_int_type => Type.initTag(.single_const_pointer_to_comptime_int),
@@ -222,8 +226,8 @@ pub const Value = extern union {
             .int_big_positive,
             .int_big_negative,
             .function,
-            .ref,
-            .ref_val,
+            .decl_ref,
+            .elem_ptr,
             .bytes,
             .repeated,
             => unreachable,
@@ -259,6 +263,7 @@ pub const Value = extern union {
             .comptime_int_type,
             .comptime_float_type,
             .noreturn_type,
+            .fn_noreturn_no_args_type,
             .fn_naked_noreturn_no_args_type,
             .fn_ccc_void_no_args_type,
             .single_const_pointer_to_comptime_int_type,
@@ -267,8 +272,8 @@ pub const Value = extern union {
             .bool_false,
             .null_value,
             .function,
-            .ref,
-            .ref_val,
+            .decl_ref,
+            .elem_ptr,
             .bytes,
             .undef,
             .repeated,
@@ -314,6 +319,7 @@ pub const Value = extern union {
             .comptime_int_type,
             .comptime_float_type,
             .noreturn_type,
+            .fn_noreturn_no_args_type,
             .fn_naked_noreturn_no_args_type,
             .fn_ccc_void_no_args_type,
             .single_const_pointer_to_comptime_int_type,
@@ -322,8 +328,8 @@ pub const Value = extern union {
             .bool_false,
             .null_value,
             .function,
-            .ref,
-            .ref_val,
+            .decl_ref,
+            .elem_ptr,
             .bytes,
             .undef,
             .repeated,
@@ -370,6 +376,7 @@ pub const Value = extern union {
             .comptime_int_type,
             .comptime_float_type,
             .noreturn_type,
+            .fn_noreturn_no_args_type,
             .fn_naked_noreturn_no_args_type,
             .fn_ccc_void_no_args_type,
             .single_const_pointer_to_comptime_int_type,
@@ -378,8 +385,8 @@ pub const Value = extern union {
             .bool_false,
             .null_value,
             .function,
-            .ref,
-            .ref_val,
+            .decl_ref,
+            .elem_ptr,
             .bytes,
             .undef,
             .repeated,
@@ -431,6 +438,7 @@ pub const Value = extern union {
             .comptime_int_type,
             .comptime_float_type,
             .noreturn_type,
+            .fn_noreturn_no_args_type,
             .fn_naked_noreturn_no_args_type,
             .fn_ccc_void_no_args_type,
             .single_const_pointer_to_comptime_int_type,
@@ -439,8 +447,8 @@ pub const Value = extern union {
             .bool_false,
             .null_value,
             .function,
-            .ref,
-            .ref_val,
+            .decl_ref,
+            .elem_ptr,
             .bytes,
             .repeated,
             => unreachable,
@@ -521,6 +529,7 @@ pub const Value = extern union {
             .comptime_int_type,
             .comptime_float_type,
             .noreturn_type,
+            .fn_noreturn_no_args_type,
             .fn_naked_noreturn_no_args_type,
             .fn_ccc_void_no_args_type,
             .single_const_pointer_to_comptime_int_type,
@@ -529,8 +538,8 @@ pub const Value = extern union {
             .bool_false,
             .null_value,
             .function,
-            .ref,
-            .ref_val,
+            .decl_ref,
+            .elem_ptr,
             .bytes,
             .repeated,
             .undef,
@@ -573,6 +582,7 @@ pub const Value = extern union {
             .comptime_int_type,
             .comptime_float_type,
             .noreturn_type,
+            .fn_noreturn_no_args_type,
             .fn_naked_noreturn_no_args_type,
             .fn_ccc_void_no_args_type,
             .single_const_pointer_to_comptime_int_type,
@@ -581,8 +591,8 @@ pub const Value = extern union {
             .bool_false,
             .null_value,
             .function,
-            .ref,
-            .ref_val,
+            .decl_ref,
+            .elem_ptr,
             .bytes,
             .repeated,
             .undef,
@@ -636,7 +646,7 @@ pub const Value = extern union {
     }
 
     /// Asserts the value is a pointer and dereferences it.
-    pub fn pointerDeref(self: Value) Value {
+    pub fn pointerDeref(self: Value, module: *ir.Module) !Value {
         return switch (self.tag()) {
             .ty,
             .u8_type,
@@ -664,6 +674,7 @@ pub const Value = extern union {
             .comptime_int_type,
             .comptime_float_type,
             .noreturn_type,
+            .fn_noreturn_no_args_type,
             .fn_naked_noreturn_no_args_type,
             .fn_ccc_void_no_args_type,
             .single_const_pointer_to_comptime_int_type,
@@ -683,14 +694,21 @@ pub const Value = extern union {
             => unreachable,
 
             .the_one_possible_value => Value.initTag(.the_one_possible_value),
-            .ref => self.cast(Payload.Ref).?.cell.contents,
-            .ref_val => self.cast(Payload.RefVal).?.val,
+            .decl_ref => {
+                const index = self.cast(Payload.DeclRef).?.index;
+                return module.getDeclValue(index);
+            },
+            .elem_ptr => {
+                const elem_ptr = self.cast(ElemPtr).?;
+                const array_val = try elem_ptr.array_ptr.pointerDeref(module);
+                return self.elemValue(array_val, elem_ptr.index);
+            },
         };
     }
 
     /// Asserts the value is a single-item pointer to an array, or an array,
     /// or an unknown-length pointer, and returns the element value at the index.
-    pub fn elemValueAt(self: Value, allocator: *Allocator, index: usize) Allocator.Error!Value {
+    pub fn elemValue(self: Value, index: usize) Value {
         switch (self.tag()) {
             .ty,
             .u8_type,
@@ -718,6 +736,7 @@ pub const Value = extern union {
             .comptime_int_type,
             .comptime_float_type,
             .noreturn_type,
+            .fn_noreturn_no_args_type,
             .fn_naked_noreturn_no_args_type,
             .fn_ccc_void_no_args_type,
             .single_const_pointer_to_comptime_int_type,
@@ -733,13 +752,12 @@ pub const Value = extern union {
             .int_big_positive,
             .int_big_negative,
             .undef,
+            .elem_ptr,
+            .decl_ref,
             => unreachable,
 
-            .ref => @panic("TODO figure out how MemoryCell works"),
-            .ref_val => @panic("TODO figure out how MemoryCell works"),
-
             .bytes => {
-                const int_payload = try allocator.create(Value.Payload.Int_u64);
+                const int_payload = try allocator.create(Payload.Int_u64);
                 int_payload.* = .{ .int = self.cast(Payload.Bytes).?.data[index] };
                 return Value.initPayload(&int_payload.base);
             },
@@ -749,6 +767,17 @@ pub const Value = extern union {
         }
     }
 
+    /// Returns a pointer to the element value at the index.
+    pub fn elemPtr(self: Value, allocator: *Allocator, index: usize) !Value {
+        const payload = try allocator.create(Payload.ElemPtr);
+        if (self.cast(Payload.ElemPtr)) |elem_ptr| {
+            payload.* = .{ .array_ptr = elem_ptr.array_ptr, .index = elem_ptr.index + index };
+        } else {
+            payload.* = .{ .array_ptr = self, .index = index };
+        }
+        return Value.initPayload(&payload.base);
+    }
+
     pub fn isUndef(self: Value) bool {
         return self.tag() == .undef;
     }
@@ -783,6 +812,7 @@ pub const Value = extern union {
             .comptime_int_type,
             .comptime_float_type,
             .noreturn_type,
+            .fn_noreturn_no_args_type,
             .fn_naked_noreturn_no_args_type,
             .fn_ccc_void_no_args_type,
             .single_const_pointer_to_comptime_int_type,
@@ -796,8 +826,8 @@ pub const Value = extern union {
             .int_i64,
             .int_big_positive,
             .int_big_negative,
-            .ref,
-            .ref_val,
+            .decl_ref,
+            .elem_ptr,
             .bytes,
             .repeated,
             => false,
@@ -841,8 +871,7 @@ pub const Value = extern union {
 
         pub const Function = struct {
             base: Payload = Payload{ .tag = .function },
-            /// Index into the `fns` array of the `ir.Module`
-            index: usize,
+            func: *ir.Module.Fn,
         };
 
         pub const ArraySentinel0_u8_Type = struct {
@@ -855,14 +884,17 @@ pub const Value = extern union {
             elem_type: *Type,
         };
 
-        pub const Ref = struct {
-            base: Payload = Payload{ .tag = .ref },
-            cell: *MemoryCell,
+        /// Represents a pointer to a decl, not the value of the decl.
+        pub const DeclRef = struct {
+            base: Payload = Payload{ .tag = .decl_ref },
+            /// Index into the Module's decls list
+            index: usize,
         };
 
-        pub const RefVal = struct {
-            base: Payload = Payload{ .tag = .ref_val },
-            val: Value,
+        pub const ElemPtr = struct {
+            base: Payload = Payload{ .tag = .elem_ptr },
+            array_ptr: Value,
+            index: usize,
         };
 
         pub const Bytes = struct {
@@ -890,29 +922,3 @@ pub const Value = extern union {
         limbs: [(@sizeOf(u64) / @sizeOf(std.math.big.Limb)) + 1]std.math.big.Limb,
     };
 };
-
-/// This is the heart of resource management of the Zig compiler. The Zig compiler uses
-/// stop-the-world mark-and-sweep garbage collection during compilation to manage the resources
-/// associated with evaluating compile-time code and semantic analysis. Each `MemoryCell` represents
-/// a root.
-pub const MemoryCell = struct {
-    parent: Parent,
-    contents: Value,
-
-    pub const Parent = union(enum) {
-        none,
-        struct_field: struct {
-            struct_base: *MemoryCell,
-            field_index: usize,
-        },
-        array_elem: struct {
-            array_base: *MemoryCell,
-            elem_index: usize,
-        },
-        union_field: *MemoryCell,
-        err_union_code: *MemoryCell,
-        err_union_payload: *MemoryCell,
-        optional_payload: *MemoryCell,
-        optional_flag: *MemoryCell,
-    };
-};
src-self-hosted/visib.zig
@@ -1,4 +0,0 @@
-pub const Visib = enum {
-    Private,
-    Pub,
-};