| author | |
| committer | |
| log | 8fb392dbb443688cdf62e965935e17a4ae4a4267 |
| tree | 1f4c24d9f1b71a20e43984cd9a70c823f66ed716 |
| parent | abcd4ea5d88541a21062e63fe427b2d2ea16a041 |
4 files changed, 207 insertions(+), 11 deletions(-)
src-self-hosted/Module.zig+6| ... | ... | @@ -18,6 +18,7 @@ const Inst = ir.Inst; |
| 18 | 18 | const Body = ir.Body; |
| 19 | 19 | const ast = std.zig.ast; |
| 20 | 20 | const trace = @import("tracy.zig").trace; |
| 21 | const liveness = @import("liveness.zig"); | |
| 21 | 22 | |
| 22 | 23 | /// General-purpose allocator. |
| 23 | 24 | allocator: *Allocator, |
| ... | ... | @@ -986,6 +987,11 @@ pub fn performAllTheWork(self: *Module) error{OutOfMemory}!void { |
| 986 | 987 | .sema_failure, .dependency_failure => continue, |
| 987 | 988 | .success => {}, |
| 988 | 989 | } |
| 990 | // Here we tack on additional allocations to the Decl's arena. The allocations are | |
| 991 | // lifetime annotations in the ZIR. | |
| 992 | var decl_arena = decl.typed_value.most_recent.arena.?.promote(self.allocator); | |
| 993 | defer decl.typed_value.most_recent.arena.?.* = decl_arena.state; | |
| 994 | try liveness.analyze(self.allocator, &decl_arena.allocator, payload.func.analysis.success); | |
| 989 | 995 | } |
| 990 | 996 | |
| 991 | 997 | assert(decl.typed_value.most_recent.typed_value.ty.hasCodeGenBits()); |
src-self-hosted/codegen.zig+50-10| ... | ... | @@ -104,11 +104,18 @@ pub fn generateSymbol( |
| 104 | 104 | .bin_file = bin_file, |
| 105 | 105 | .mod_fn = module_fn, |
| 106 | 106 | .code = code, |
| 107 | .inst_table = std.AutoHashMap(*ir.Inst, Function.MCValue).init(bin_file.allocator), | |
| 108 | 107 | .err_msg = null, |
| 109 | 108 | .args = mc_args.items, |
| 109 | .branch_stack = .{}, | |
| 110 | 110 | }; |
| 111 | defer function.inst_table.deinit(); | |
| 111 | defer { | |
| 112 | assert(function.branch_stack.items.len == 1); | |
| 113 | function.branch_stack.items[0].inst_table.deinit(); | |
| 114 | function.branch_stack.deinit(bin_file.allocator); | |
| 115 | } | |
| 116 | try function.branch_stack.append(bin_file.allocator, .{ | |
| 117 | .inst_table = std.AutoHashMap(*ir.Inst, Function.MCValue).init(bin_file.allocator), | |
| 118 | }); | |
| 112 | 119 | |
| 113 | 120 | function.gen() catch |err| switch (err) { |
| 114 | 121 | error.CodegenFail => return Result{ .fail = function.err_msg.? }, |
| ... | ... | @@ -215,13 +222,29 @@ const Function = struct { |
| 215 | 222 | target: *const std.Target, |
| 216 | 223 | mod_fn: *const Module.Fn, |
| 217 | 224 | code: *std.ArrayList(u8), |
| 218 | inst_table: std.AutoHashMap(*ir.Inst, MCValue), | |
| 219 | 225 | err_msg: ?*ErrorMsg, |
| 220 | 226 | args: []MCValue, |
| 221 | 227 | |
| 228 | /// Whenever there is a runtime branch, we push a Branch onto this stack, | |
| 229 | /// and pop it off when the runtime branch joins. This provides an "overlay" | |
| 230 | /// of the table of mappings from instructions to `MCValue` from within the branch. | |
| 231 | /// This way we can modify the `MCValue` for an instruction in different ways | |
| 232 | /// within different branches. Special consideration is needed when a branch | |
| 233 | /// joins with its parent, to make sure all instructions have the same MCValue | |
| 234 | /// across each runtime branch upon joining. | |
| 235 | branch_stack: std.ArrayListUnmanaged(Branch), | |
| 236 | ||
| 237 | const Branch = struct { | |
| 238 | inst_table: std.AutoHashMap(*ir.Inst, MCValue), | |
| 239 | }; | |
| 240 | ||
| 222 | 241 | const MCValue = union(enum) { |
| 242 | /// No runtime bits. `void` types, empty structs, u0, enums with 1 tag, etc. | |
| 223 | 243 | none, |
| 244 | /// Control flow will not allow this value to be observed. | |
| 224 | 245 | unreach, |
| 246 | /// No more references to this value remain. | |
| 247 | dead, | |
| 225 | 248 | /// A pointer-sized integer that fits in a register. |
| 226 | 249 | immediate: u64, |
| 227 | 250 | /// The constant was emitted into the code, at this offset. |
| ... | ... | @@ -292,9 +315,10 @@ const Function = struct { |
| 292 | 315 | } |
| 293 | 316 | |
| 294 | 317 | fn genArch(self: *Function, comptime arch: std.Target.Cpu.Arch) !void { |
| 318 | const inst_table = &self.branch_stack.items[0].inst_table; | |
| 295 | 319 | for (self.mod_fn.analysis.success.instructions) |inst| { |
| 296 | 320 | const new_inst = try self.genFuncInst(inst, arch); |
| 297 | try self.inst_table.putNoClobber(inst, new_inst); | |
| 321 | try inst_table.putNoClobber(inst, new_inst); | |
| 298 | 322 | } |
| 299 | 323 | } |
| 300 | 324 | |
| ... | ... | @@ -525,7 +549,9 @@ const Function = struct { |
| 525 | 549 | fn genSetReg(self: *Function, src: usize, comptime arch: Target.Cpu.Arch, reg: Reg(arch), mcv: MCValue) error{ CodegenFail, OutOfMemory }!void { |
| 526 | 550 | switch (arch) { |
| 527 | 551 | .x86_64 => switch (mcv) { |
| 528 | .none, .unreach => unreachable, | |
| 552 | .dead => unreachable, | |
| 553 | .none => unreachable, | |
| 554 | .unreach => unreachable, | |
| 529 | 555 | .immediate => |x| { |
| 530 | 556 | if (reg.size() != 64) { |
| 531 | 557 | return self.fail(src, "TODO decide whether to implement non-64-bit loads", .{}); |
| ... | ... | @@ -708,12 +734,26 @@ const Function = struct { |
| 708 | 734 | if (self.inst_table.get(inst)) |mcv| { |
| 709 | 735 | return mcv; |
| 710 | 736 | } |
| 737 | // Constants have static lifetimes, so they are always memoized in the outer most table. | |
| 711 | 738 | if (inst.cast(ir.Inst.Constant)) |const_inst| { |
| 712 | const mcvalue = try self.genTypedValue(inst.src, .{ .ty = inst.ty, .val = const_inst.val }); | |
| 713 | try self.inst_table.putNoClobber(inst, mcvalue); | |
| 714 | return mcvalue; | |
| 715 | } else { | |
| 716 | return self.inst_table.get(inst).?; | |
| 739 | const branch = &self.branch_stack.items[0]; | |
| 740 | const gop = try branch.inst_table.getOrPut(inst); | |
| 741 | if (!gop.found_existing) { | |
| 742 | const mcv = try self.genTypedValue(inst.src, .{ .ty = inst.ty, .val = const_inst.val }); | |
| 743 | try branch.inst_table.putNoClobber(inst, mcv); | |
| 744 | gop.kv.value = mcv; | |
| 745 | return mcv; | |
| 746 | } | |
| 747 | return gop.kv.value; | |
| 748 | } | |
| 749 | ||
| 750 | // Treat each stack item as a "layer" on top of the previous one. | |
| 751 | var i: usize = self.branch_stack.items.len; | |
| 752 | while (true) { | |
| 753 | i -= 1; | |
| 754 | if (self.branch_stack.items[i].inst_table.getValue(inst)) |mcv| { | |
| 755 | return mcv; | |
| 756 | } | |
| 717 | 757 | } |
| 718 | 758 | } |
| 719 | 759 |
src-self-hosted/ir.zig+17-1| ... | ... | @@ -10,6 +10,16 @@ const Module = @import("Module.zig"); |
| 10 | 10 | /// a memory location for the value to survive after a const instruction. |
| 11 | 11 | pub const Inst = struct { |
| 12 | 12 | tag: Tag, |
| 13 | /// Each bit represents the index of an `Inst` parameter in the `args` field. | |
| 14 | /// If a bit is set, it marks the end of the lifetime of the corresponding | |
| 15 | /// instruction parameter. For example, 0b00000101 means that the first and | |
| 16 | /// third `Inst` parameters' lifetimes end after this instruction, and will | |
| 17 | /// not have any more following references. | |
| 18 | /// The most significant bit being set means that the instruction itself is | |
| 19 | /// never referenced, in other words its lifetime ends as soon as it finishes. | |
| 20 | /// If the byte is `0xff`, it means this is a special case and this data is | |
| 21 | /// encoded elsewhere. | |
| 22 | deaths: u8 = 0xff, | |
| 13 | 23 | ty: Type, |
| 14 | 24 | /// Byte offset into the source. |
| 15 | 25 | src: usize, |
| ... | ... | @@ -165,6 +175,12 @@ pub const Inst = struct { |
| 165 | 175 | true_body: Body, |
| 166 | 176 | false_body: Body, |
| 167 | 177 | }, |
| 178 | /// Set of instructions whose lifetimes end at the start of one of the branches. | |
| 179 | /// The `true` branch is first: `deaths[0..true_death_count]`. | |
| 180 | /// The `false` branch is next: `(deaths + true_death_count)[..false_death_count]`. | |
| 181 | deaths: [*]*Inst = undefined, | |
| 182 | true_death_count: u32 = 0, | |
| 183 | false_death_count: u32 = 0, | |
| 168 | 184 | }; |
| 169 | 185 | |
| 170 | 186 | pub const Constant = struct { |
| ... | ... | @@ -224,4 +240,4 @@ pub const Inst = struct { |
| 224 | 240 | |
| 225 | 241 | pub const Body = struct { |
| 226 | 242 | instructions: []*Inst, |
| 227 | }; | |
| 243 | }; | |
| \ No newline at end of file |
src-self-hosted/liveness.zig created+134| ... | ... | @@ -0,0 +1,134 @@ |
| 1 | const std = @import("std"); | |
| 2 | const ir = @import("ir.zig"); | |
| 3 | const trace = @import("tracy.zig").trace; | |
| 4 | ||
| 5 | /// Perform Liveness Analysis over the `Body`. Each `Inst` will have its `deaths` field populated. | |
| 6 | pub fn analyze( | |
| 7 | /// Used for temporary storage during the analysis. | |
| 8 | gpa: *std.mem.Allocator, | |
| 9 | /// Used to tack on extra allocations in the same lifetime as the existing instructions. | |
| 10 | arena: *std.mem.Allocator, | |
| 11 | body: ir.Body, | |
| 12 | ) error{OutOfMemory}!void { | |
| 13 | const tracy = trace(@src()); | |
| 14 | defer tracy.end(); | |
| 15 | ||
| 16 | var table = std.AutoHashMap(*ir.Inst, void).init(gpa); | |
| 17 | defer table.deinit(); | |
| 18 | try table.ensureCapacity(body.instructions.len); | |
| 19 | try analyzeWithTable(arena, &table, body); | |
| 20 | } | |
| 21 | ||
| 22 | fn analyzeWithTable(arena: *std.mem.Allocator, table: *std.AutoHashMap(*ir.Inst, void), body: ir.Body) error{OutOfMemory}!void { | |
| 23 | var i: usize = body.instructions.len; | |
| 24 | ||
| 25 | while (i != 0) { | |
| 26 | i -= 1; | |
| 27 | const base = body.instructions[i]; | |
| 28 | ||
| 29 | // Obtain the corresponding instruction type based on the tag type. | |
| 30 | inline for (std.meta.declarations(ir.Inst)) |decl| { | |
| 31 | switch (decl.data) { | |
| 32 | .Type => |T| { | |
| 33 | if (@hasDecl(T, "base_tag")) { | |
| 34 | if (T.base_tag == base.tag) { | |
| 35 | return analyzeInst(arena, table, T, @fieldParentPtr(T, "base", base)); | |
| 36 | } | |
| 37 | } | |
| 38 | }, | |
| 39 | else => continue, | |
| 40 | } | |
| 41 | } | |
| 42 | unreachable; | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 46 | fn analyzeInst(arena: *std.mem.Allocator, table: *std.AutoHashMap(*ir.Inst, void), comptime T: type, inst: *T) error{OutOfMemory}!void { | |
| 47 | inst.base.deaths = 0; | |
| 48 | ||
| 49 | switch (T) { | |
| 50 | ir.Inst.Constant => return, | |
| 51 | ir.Inst.Block => { | |
| 52 | try analyzeWithTable(arena, table, inst.args.body); | |
| 53 | // We let this continue so that it can possibly mark the block as | |
| 54 | // unreferenced below. | |
| 55 | }, | |
| 56 | ir.Inst.CondBr => { | |
| 57 | var true_table = std.AutoHashMap(*ir.Inst, void).init(table.allocator); | |
| 58 | defer true_table.deinit(); | |
| 59 | try true_table.ensureCapacity(inst.args.true_body.instructions.len); | |
| 60 | try analyzeWithTable(arena, &true_table, inst.args.true_body); | |
| 61 | ||
| 62 | var false_table = std.AutoHashMap(*ir.Inst, void).init(table.allocator); | |
| 63 | defer false_table.deinit(); | |
| 64 | try false_table.ensureCapacity(inst.args.false_body.instructions.len); | |
| 65 | try analyzeWithTable(arena, &false_table, inst.args.false_body); | |
| 66 | ||
| 67 | // Each death that occurs inside one branch, but not the other, needs | |
| 68 | // to be added as a death immediately upon entering the other branch. | |
| 69 | // During the iteration of the table, we additionally propagate the | |
| 70 | // deaths to the parent table. | |
| 71 | var true_entry_deaths = std.ArrayList(*ir.Inst).init(table.allocator); | |
| 72 | defer true_entry_deaths.deinit(); | |
| 73 | var false_entry_deaths = std.ArrayList(*ir.Inst).init(table.allocator); | |
| 74 | defer false_entry_deaths.deinit(); | |
| 75 | { | |
| 76 | var it = false_table.iterator(); | |
| 77 | while (it.next()) |entry| { | |
| 78 | const false_death = entry.key; | |
| 79 | if (!true_table.contains(false_death)) { | |
| 80 | try true_entry_deaths.append(false_death); | |
| 81 | // Here we are only adding to the parent table if the following iteration | |
| 82 | // would miss it. | |
| 83 | try table.putNoClobber(false_death, {}); | |
| 84 | } | |
| 85 | } | |
| 86 | } | |
| 87 | { | |
| 88 | var it = true_table.iterator(); | |
| 89 | while (it.next()) |entry| { | |
| 90 | const true_death = entry.key; | |
| 91 | try table.putNoClobber(true_death, {}); | |
| 92 | if (!false_table.contains(true_death)) { | |
| 93 | try false_entry_deaths.append(true_death); | |
| 94 | } | |
| 95 | } | |
| 96 | } | |
| 97 | inst.true_death_count = std.math.cast(@TypeOf(inst.true_death_count), true_entry_deaths.items.len) catch return error.OutOfMemory; | |
| 98 | inst.false_death_count = std.math.cast(@TypeOf(inst.false_death_count), false_entry_deaths.items.len) catch return error.OutOfMemory; | |
| 99 | const allocated_slice = try arena.alloc(*ir.Inst, true_entry_deaths.items.len + false_entry_deaths.items.len); | |
| 100 | inst.deaths = allocated_slice.ptr; | |
| 101 | ||
| 102 | // Continue on with the instruction analysis. The following code will find the condition | |
| 103 | // instruction, and the deaths flag for the CondBr instruction will indicate whether the | |
| 104 | // condition's lifetime ends immediately before entering any branch. | |
| 105 | }, | |
| 106 | else => {}, | |
| 107 | } | |
| 108 | ||
| 109 | if (!table.contains(&inst.base)) { | |
| 110 | // No tombstone for this instruction means it is never referenced, | |
| 111 | // and its birth marks its own death. Very metal 🤘 | |
| 112 | inst.base.deaths |= 1 << 7; | |
| 113 | } | |
| 114 | ||
| 115 | const Args = ir.Inst.Args(T); | |
| 116 | if (Args == void) { | |
| 117 | return; | |
| 118 | } | |
| 119 | ||
| 120 | comptime var arg_index: usize = 0; | |
| 121 | inline for (std.meta.fields(Args)) |field| { | |
| 122 | if (field.field_type == *ir.Inst) { | |
| 123 | if (arg_index >= 6) { | |
| 124 | @compileError("out of bits to mark deaths of operands"); | |
| 125 | } | |
| 126 | const prev = try table.put(@field(inst.args, field.name), {}); | |
| 127 | if (prev == null) { | |
| 128 | // Death. | |
| 129 | inst.base.deaths |= 1 << arg_index; | |
| 130 | } | |
| 131 | arg_index += 1; | |
| 132 | } | |
| 133 | } | |
| 134 | } | |
| \ No newline at end of file |