diff --git a/.gitattributes b/.gitattributes index ef01f614..9e23cff3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ *.zig text eol=lf + +deps/** linguist-vendored diff --git a/README.md b/README.md index 97a4a849..010f06f1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,23 @@ # Aro A C compiler with the goal of providing fast compilation and low memory usage with good diagnostics. -Currently it can preprocess, parse and semantically analyze ~75% of standard C17 with +Currently it can preprocess, parse and semantically analyze ~85% of standard C17 with work still being needed to support all of the usual extensions. +Basic code generation is supported for x86-64 linux and can produce a valid hello world: +```shell +$ cat hello.c +extern int printf(const char *restrict fmt, ...); +int main(void) { + pritnf("Hello, world!\n"); + return 0; +} +$ zig build run -- hello.c -c +$ zig run hello.o -lc +Hello, world! +$ +``` + Future plans for the project include making it the C backend of Zig's `translate-c` feature and making it an optional C frontend for the self-hosted Zig compiler. ```c @@ -50,4 +64,4 @@ fn_def: 'fn (argc: int, argv: **const char) int' value: 4 ``` -types are printed in Zig style as C types are more confusing than necessary +types are printed in Zig style as C types are more confusing than necessary, actual error messages contain proper C like types diff --git a/build.zig b/build.zig index 472389d4..9e9dd59c 100644 --- a/build.zig +++ b/build.zig @@ -12,9 +12,15 @@ pub fn build(b: *Builder) void { // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. const mode = b.standardReleaseOptions(); + const zig_pkg = std.build.Pkg{ + .name = "zig", + .path = .{ .path = "deps/zig/lib.zig" }, + }; + const exe = b.addExecutable("arocc", "src/main.zig"); exe.setTarget(target); exe.setBuildMode(mode); + exe.addPackage(zig_pkg); exe.install(); const run_cmd = exe.run(); @@ -30,12 +36,18 @@ pub fn build(b: *Builder) void { tests_step.dependOn(&exe.step); var unit_tests = b.addTest("src/main.zig"); + unit_tests.addPackage(zig_pkg); tests_step.dependOn(&unit_tests.step); const integration_tests = b.addExecutable("arocc", "test/runner.zig"); - integration_tests.addPackagePath("aro", "src/lib.zig"); + integration_tests.addPackage(.{ + .name = "aro", + .path = .{ .path = "src/lib.zig" }, + .dependencies = &.{zig_pkg}, + }); const integration_test_runner = integration_tests.run(); integration_test_runner.addArg(b.pathFromRoot("test/cases")); + integration_test_runner.addArg(b.zig_exe); tests_step.dependOn(&integration_test_runner.step); } diff --git a/deps/zig/codegen/x86_64.zig b/deps/zig/codegen/x86_64.zig new file mode 100644 index 00000000..72a74680 --- /dev/null +++ b/deps/zig/codegen/x86_64.zig @@ -0,0 +1,716 @@ +const std = @import("std"); +const testing = std.testing; +const mem = std.mem; +const assert = std.debug.assert; +const ArrayList = std.ArrayList; +const Allocator = std.mem.Allocator; +const DW = std.dwarf; + +// zig fmt: off + +/// Definitions of all of the x64 registers. The order is semantically meaningful. +/// The registers are defined such that IDs go in descending order of 64-bit, +/// 32-bit, 16-bit, and then 8-bit, and each set contains exactly sixteen +/// registers. This results in some useful properties: +/// +/// Any 64-bit register can be turned into its 32-bit form by adding 16, and +/// vice versa. This also works between 32-bit and 16-bit forms. With 8-bit, it +/// works for all except for sp, bp, si, and di, which do *not* have an 8-bit +/// form. +/// +/// If (register & 8) is set, the register is extended. +/// +/// The ID can be easily determined by figuring out what range the register is +/// in, and then subtracting the base. +pub const Register = enum(u8) { + // 0 through 15, 64-bit registers. 8-15 are extended. + // id is just the int value. + rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, + r8, r9, r10, r11, r12, r13, r14, r15, + + // 16 through 31, 32-bit registers. 24-31 are extended. + // id is int value - 16. + eax, ecx, edx, ebx, esp, ebp, esi, edi, + r8d, r9d, r10d, r11d, r12d, r13d, r14d, r15d, + + // 32-47, 16-bit registers. 40-47 are extended. + // id is int value - 32. + ax, cx, dx, bx, sp, bp, si, di, + r8w, r9w, r10w, r11w, r12w, r13w, r14w, r15w, + + // 48-63, 8-bit registers. 56-63 are extended. + // id is int value - 48. + al, cl, dl, bl, ah, ch, dh, bh, + r8b, r9b, r10b, r11b, r12b, r13b, r14b, r15b, + + /// Returns the bit-width of the register. + pub fn size(self: Register) u7 { + return switch (@enumToInt(self)) { + 0...15 => 64, + 16...31 => 32, + 32...47 => 16, + 48...64 => 8, + else => unreachable, + }; + } + + /// Returns whether the register is *extended*. Extended registers are the + /// new registers added with amd64, r8 through r15. This also includes any + /// other variant of access to those registers, such as r8b, r15d, and so + /// on. This is needed because access to these registers requires special + /// handling via the REX prefix, via the B or R bits, depending on context. + pub fn isExtended(self: Register) bool { + return @enumToInt(self) & 0x08 != 0; + } + + /// This returns the 4-bit register ID, which is used in practically every + /// opcode. Note that bit 3 (the highest bit) is *never* used directly in + /// an instruction (@see isExtended), and requires special handling. The + /// lower three bits are often embedded directly in instructions (such as + /// the B8 variant of moves), or used in R/M bytes. + pub fn id(self: Register) u4 { + return @truncate(u4, @enumToInt(self)); + } + + /// Like id, but only returns the lower 3 bits. + pub fn low_id(self: Register) u3 { + return @truncate(u3, @enumToInt(self)); + } + + /// Returns the index into `callee_preserved_regs`. + pub fn allocIndex(self: Register) ?u4 { + return switch (self) { + .rax, .eax, .ax, .al => 0, + .rcx, .ecx, .cx, .cl => 1, + .rdx, .edx, .dx, .dl => 2, + .rsi, .esi, .si => 3, + .rdi, .edi, .di => 4, + .r8, .r8d, .r8w, .r8b => 5, + .r9, .r9d, .r9w, .r9b => 6, + .r10, .r10d, .r10w, .r10b => 7, + .r11, .r11d, .r11w, .r11b => 8, + else => null, + }; + } + + /// Convert from any register to its 64 bit alias. + pub fn to64(self: Register) Register { + return @intToEnum(Register, self.id()); + } + + /// Convert from any register to its 32 bit alias. + pub fn to32(self: Register) Register { + return @intToEnum(Register, @as(u8, self.id()) + 16); + } + + /// Convert from any register to its 16 bit alias. + pub fn to16(self: Register) Register { + return @intToEnum(Register, @as(u8, self.id()) + 32); + } + + /// Convert from any register to its 8 bit alias. + pub fn to8(self: Register) Register { + return @intToEnum(Register, @as(u8, self.id()) + 48); + } + + pub fn dwarfLocOp(self: Register) u8 { + return switch (self.to64()) { + .rax => DW.OP.reg0, + .rdx => DW.OP.reg1, + .rcx => DW.OP.reg2, + .rbx => DW.OP.reg3, + .rsi => DW.OP.reg4, + .rdi => DW.OP.reg5, + .rbp => DW.OP.reg6, + .rsp => DW.OP.reg7, + + .r8 => DW.OP.reg8, + .r9 => DW.OP.reg9, + .r10 => DW.OP.reg10, + .r11 => DW.OP.reg11, + .r12 => DW.OP.reg12, + .r13 => DW.OP.reg13, + .r14 => DW.OP.reg14, + .r15 => DW.OP.reg15, + + else => unreachable, + }; + } +}; + +// zig fmt: on + +/// These registers belong to the called function. +pub const callee_preserved_regs = [_]Register{ .rax, .rcx, .rdx, .rsi, .rdi, .r8, .r9, .r10, .r11 }; +pub const c_abi_int_param_regs = [_]Register{ .rdi, .rsi, .rdx, .rcx, .r8, .r9 }; +pub const c_abi_int_return_regs = [_]Register{ .rax, .rdx }; + +/// Encoding helper functions for x86_64 instructions +/// +/// Many of these helpers do very little, but they can help make things +/// slightly more readable with more descriptive field names / function names. +/// +/// Some of them also have asserts to ensure that we aren't doing dumb things. +/// For example, trying to use register 4 (esp) in an indirect modr/m byte is illegal, +/// you need to encode it with an SIB byte. +/// +/// Note that ALL of these helper functions will assume capacity, +/// so ensure that the `code` has sufficient capacity before using them. +/// The `init` method is the recommended way to ensure capacity. +pub const Encoder = struct { + /// Non-owning reference to the code array + code: *ArrayList(u8), + + const Self = @This(); + + /// Wrap `code` in Encoder to make it easier to call these helper functions + /// + /// maximum_inst_size should contain the maximum number of bytes + /// that the encoded instruction will take. + /// This is because the helper functions will assume capacity + /// in order to avoid bounds checking. + pub fn init(code: *ArrayList(u8), maximum_inst_size: u8) !Self { + try code.ensureUnusedCapacity(maximum_inst_size); + return Self{ .code = code }; + } + + /// Directly write a number to the code array with big endianness + pub fn writeIntBig(self: Self, comptime T: type, value: T) void { + mem.writeIntBig( + T, + self.code.addManyAsArrayAssumeCapacity(@divExact(@typeInfo(T).Int.bits, 8)), + value, + ); + } + + /// Directly write a number to the code array with little endianness + pub fn writeIntLittle(self: Self, comptime T: type, value: T) void { + mem.writeIntLittle( + T, + self.code.addManyAsArrayAssumeCapacity(@divExact(@typeInfo(T).Int.bits, 8)), + value, + ); + } + + // -------- + // Prefixes + // -------- + + pub const LegacyPrefixes = packed struct { + /// LOCK + prefix_f0: bool = false, + /// REPNZ, REPNE, REP, Scalar Double-precision + prefix_f2: bool = false, + /// REPZ, REPE, REP, Scalar Single-precision + prefix_f3: bool = false, + + /// CS segment override or Branch not taken + prefix_2e: bool = false, + /// DS segment override + prefix_36: bool = false, + /// ES segment override + prefix_26: bool = false, + /// FS segment override + prefix_64: bool = false, + /// GS segment override + prefix_65: bool = false, + + /// Branch taken + prefix_3e: bool = false, + + /// Operand size override (enables 16 bit operation) + prefix_66: bool = false, + + /// Address size override (enables 16 bit address size) + prefix_67: bool = false, + + padding: u5 = 0, + }; + + /// Encodes legacy prefixes + pub fn legacyPrefixes(self: Self, prefixes: LegacyPrefixes) void { + if (@bitCast(u16, prefixes) != 0) { + // Hopefully this path isn't taken very often, so we'll do it the slow way for now + + // LOCK + if (prefixes.prefix_f0) self.code.appendAssumeCapacity(0xf0); + // REPNZ, REPNE, REP, Scalar Double-precision + if (prefixes.prefix_f2) self.code.appendAssumeCapacity(0xf2); + // REPZ, REPE, REP, Scalar Single-precision + if (prefixes.prefix_f3) self.code.appendAssumeCapacity(0xf3); + + // CS segment override or Branch not taken + if (prefixes.prefix_2e) self.code.appendAssumeCapacity(0x2e); + // DS segment override + if (prefixes.prefix_36) self.code.appendAssumeCapacity(0x36); + // ES segment override + if (prefixes.prefix_26) self.code.appendAssumeCapacity(0x26); + // FS segment override + if (prefixes.prefix_64) self.code.appendAssumeCapacity(0x64); + // GS segment override + if (prefixes.prefix_65) self.code.appendAssumeCapacity(0x65); + + // Branch taken + if (prefixes.prefix_3e) self.code.appendAssumeCapacity(0x3e); + + // Operand size override + if (prefixes.prefix_66) self.code.appendAssumeCapacity(0x66); + + // Address size override + if (prefixes.prefix_67) self.code.appendAssumeCapacity(0x67); + } + } + + /// Use 16 bit operand size + /// + /// Note that this flag is overridden by REX.W, if both are present. + pub fn prefix16BitMode(self: Self) void { + self.code.appendAssumeCapacity(0x66); + } + + /// From section 2.2.1.2 of the manual, REX is encoded as b0100WRXB + pub const Rex = struct { + /// Wide, enables 64-bit operation + w: bool = false, + /// Extends the reg field in the ModR/M byte + r: bool = false, + /// Extends the index field in the SIB byte + x: bool = false, + /// Extends the r/m field in the ModR/M byte, + /// or the base field in the SIB byte, + /// or the reg field in the Opcode byte + b: bool = false, + }; + + /// Encodes a REX prefix byte given all the fields + /// + /// Use this byte whenever you need 64 bit operation, + /// or one of reg, index, r/m, base, or opcode-reg might be extended. + /// + /// See struct `Rex` for a description of each field. + /// + /// Does not add a prefix byte if none of the fields are set! + pub fn rex(self: Self, byte: Rex) void { + var value: u8 = 0b0100_0000; + + if (byte.w) value |= 0b1000; + if (byte.r) value |= 0b0100; + if (byte.x) value |= 0b0010; + if (byte.b) value |= 0b0001; + + if (value != 0b0100_0000) { + self.code.appendAssumeCapacity(value); + } + } + + // ------ + // Opcode + // ------ + + /// Encodes a 1 byte opcode + pub fn opcode_1byte(self: Self, opcode: u8) void { + self.code.appendAssumeCapacity(opcode); + } + + /// Encodes a 2 byte opcode + /// + /// e.g. IMUL has the opcode 0x0f 0xaf, so you use + /// + /// encoder.opcode_2byte(0x0f, 0xaf); + pub fn opcode_2byte(self: Self, prefix: u8, opcode: u8) void { + self.code.appendAssumeCapacity(prefix); + self.code.appendAssumeCapacity(opcode); + } + + /// Encodes a 1 byte opcode with a reg field + /// + /// Remember to add a REX prefix byte if reg is extended! + pub fn opcode_withReg(self: Self, opcode: u8, reg: u3) void { + assert(opcode & 0b111 == 0); + self.code.appendAssumeCapacity(opcode | reg); + } + + // ------ + // ModR/M + // ------ + + /// Construct a ModR/M byte given all the fields + /// + /// Remember to add a REX prefix byte if reg or rm are extended! + pub fn modRm(self: Self, mod: u2, reg_or_opx: u3, rm: u3) void { + self.code.appendAssumeCapacity( + @as(u8, mod) << 6 | @as(u8, reg_or_opx) << 3 | rm, + ); + } + + /// Construct a ModR/M byte using direct r/m addressing + /// r/m effective address: r/m + /// + /// Note reg's effective address is always just reg for the ModR/M byte. + /// Remember to add a REX prefix byte if reg or rm are extended! + pub fn modRm_direct(self: Self, reg_or_opx: u3, rm: u3) void { + self.modRm(0b11, reg_or_opx, rm); + } + + /// Construct a ModR/M byte using indirect r/m addressing + /// r/m effective address: [r/m] + /// + /// Note reg's effective address is always just reg for the ModR/M byte. + /// Remember to add a REX prefix byte if reg or rm are extended! + pub fn modRm_indirectDisp0(self: Self, reg_or_opx: u3, rm: u3) void { + assert(rm != 4 and rm != 5); + self.modRm(0b00, reg_or_opx, rm); + } + + /// Construct a ModR/M byte using indirect SIB addressing + /// r/m effective address: [SIB] + /// + /// Note reg's effective address is always just reg for the ModR/M byte. + /// Remember to add a REX prefix byte if reg or rm are extended! + pub fn modRm_SIBDisp0(self: Self, reg_or_opx: u3) void { + self.modRm(0b00, reg_or_opx, 0b100); + } + + /// Construct a ModR/M byte using RIP-relative addressing + /// r/m effective address: [RIP + disp32] + /// + /// Note reg's effective address is always just reg for the ModR/M byte. + /// Remember to add a REX prefix byte if reg or rm are extended! + pub fn modRm_RIPDisp32(self: Self, reg_or_opx: u3) void { + self.modRm(0b00, reg_or_opx, 0b101); + } + + /// Construct a ModR/M byte using indirect r/m with a 8bit displacement + /// r/m effective address: [r/m + disp8] + /// + /// Note reg's effective address is always just reg for the ModR/M byte. + /// Remember to add a REX prefix byte if reg or rm are extended! + pub fn modRm_indirectDisp8(self: Self, reg_or_opx: u3, rm: u3) void { + assert(rm != 4); + self.modRm(0b01, reg_or_opx, rm); + } + + /// Construct a ModR/M byte using indirect SIB with a 8bit displacement + /// r/m effective address: [SIB + disp8] + /// + /// Note reg's effective address is always just reg for the ModR/M byte. + /// Remember to add a REX prefix byte if reg or rm are extended! + pub fn modRm_SIBDisp8(self: Self, reg_or_opx: u3) void { + self.modRm(0b01, reg_or_opx, 0b100); + } + + /// Construct a ModR/M byte using indirect r/m with a 32bit displacement + /// r/m effective address: [r/m + disp32] + /// + /// Note reg's effective address is always just reg for the ModR/M byte. + /// Remember to add a REX prefix byte if reg or rm are extended! + pub fn modRm_indirectDisp32(self: Self, reg_or_opx: u3, rm: u3) void { + assert(rm != 4); + self.modRm(0b10, reg_or_opx, rm); + } + + /// Construct a ModR/M byte using indirect SIB with a 32bit displacement + /// r/m effective address: [SIB + disp32] + /// + /// Note reg's effective address is always just reg for the ModR/M byte. + /// Remember to add a REX prefix byte if reg or rm are extended! + pub fn modRm_SIBDisp32(self: Self, reg_or_opx: u3) void { + self.modRm(0b10, reg_or_opx, 0b100); + } + + // --- + // SIB + // --- + + /// Construct a SIB byte given all the fields + /// + /// Remember to add a REX prefix byte if index or base are extended! + pub fn sib(self: Self, scale: u2, index: u3, base: u3) void { + self.code.appendAssumeCapacity( + @as(u8, scale) << 6 | @as(u8, index) << 3 | base, + ); + } + + /// Construct a SIB byte with scale * index + base, no frills. + /// r/m effective address: [base + scale * index] + /// + /// Remember to add a REX prefix byte if index or base are extended! + pub fn sib_scaleIndexBase(self: Self, scale: u2, index: u3, base: u3) void { + assert(base != 5); + + self.sib(scale, index, base); + } + + /// Construct a SIB byte with scale * index + disp32 + /// r/m effective address: [scale * index + disp32] + /// + /// Remember to add a REX prefix byte if index or base are extended! + pub fn sib_scaleIndexDisp32(self: Self, scale: u2, index: u3) void { + assert(index != 4); + + // scale is actually ignored + // index = 4 means no index + // base = 5 means no base, if mod == 0. + self.sib(scale, index, 5); + } + + /// Construct a SIB byte with just base + /// r/m effective address: [base] + /// + /// Remember to add a REX prefix byte if index or base are extended! + pub fn sib_base(self: Self, base: u3) void { + assert(base != 5); + + // scale is actually ignored + // index = 4 means no index + self.sib(0, 4, base); + } + + /// Construct a SIB byte with just disp32 + /// r/m effective address: [disp32] + /// + /// Remember to add a REX prefix byte if index or base are extended! + pub fn sib_disp32(self: Self) void { + // scale is actually ignored + // index = 4 means no index + // base = 5 means no base, if mod == 0. + self.sib(0, 4, 5); + } + + /// Construct a SIB byte with scale * index + base + disp8 + /// r/m effective address: [base + scale * index + disp8] + /// + /// Remember to add a REX prefix byte if index or base are extended! + pub fn sib_scaleIndexBaseDisp8(self: Self, scale: u2, index: u3, base: u3) void { + self.sib(scale, index, base); + } + + /// Construct a SIB byte with base + disp8, no index + /// r/m effective address: [base + disp8] + /// + /// Remember to add a REX prefix byte if index or base are extended! + pub fn sib_baseDisp8(self: Self, base: u3) void { + // scale is ignored + // index = 4 means no index + self.sib(0, 4, base); + } + + /// Construct a SIB byte with scale * index + base + disp32 + /// r/m effective address: [base + scale * index + disp32] + /// + /// Remember to add a REX prefix byte if index or base are extended! + pub fn sib_scaleIndexBaseDisp32(self: Self, scale: u2, index: u3, base: u3) void { + self.sib(scale, index, base); + } + + /// Construct a SIB byte with base + disp32, no index + /// r/m effective address: [base + disp32] + /// + /// Remember to add a REX prefix byte if index or base are extended! + pub fn sib_baseDisp32(self: Self, base: u3) void { + // scale is ignored + // index = 4 means no index + self.sib(0, 4, base); + } + + // ------------------------- + // Trivial (no bit fiddling) + // ------------------------- + + /// Encode an 8 bit immediate + /// + /// It is sign-extended to 64 bits by the cpu. + pub fn imm8(self: Self, imm: i8) void { + self.code.appendAssumeCapacity(@bitCast(u8, imm)); + } + + /// Encode an 8 bit displacement + /// + /// It is sign-extended to 64 bits by the cpu. + pub fn disp8(self: Self, disp: i8) void { + self.code.appendAssumeCapacity(@bitCast(u8, disp)); + } + + /// Encode an 16 bit immediate + /// + /// It is sign-extended to 64 bits by the cpu. + pub fn imm16(self: Self, imm: i16) void { + self.writeIntLittle(i16, imm); + } + + /// Encode an 32 bit immediate + /// + /// It is sign-extended to 64 bits by the cpu. + pub fn imm32(self: Self, imm: i32) void { + self.writeIntLittle(i32, imm); + } + + /// Encode an 32 bit displacement + /// + /// It is sign-extended to 64 bits by the cpu. + pub fn disp32(self: Self, disp: i32) void { + self.writeIntLittle(i32, disp); + } + + /// Encode an 64 bit immediate + /// + /// It is sign-extended to 64 bits by the cpu. + pub fn imm64(self: Self, imm: u64) void { + self.writeIntLittle(u64, imm); + } +}; + +test "x86_64 Encoder helpers" { + var code = ArrayList(u8).init(testing.allocator); + defer code.deinit(); + + // simple integer multiplication + + // imul eax,edi + // 0faf c7 + { + try code.resize(0); + const encoder = try Encoder.init(&code, 4); + encoder.rex(.{ + .r = Register.eax.isExtended(), + .b = Register.edi.isExtended(), + }); + encoder.opcode_2byte(0x0f, 0xaf); + encoder.modRm_direct( + Register.eax.low_id(), + Register.edi.low_id(), + ); + + try testing.expectEqualSlices(u8, &[_]u8{ 0x0f, 0xaf, 0xc7 }, code.items); + } + + // simple mov + + // mov eax,edi + // 89 f8 + { + try code.resize(0); + const encoder = try Encoder.init(&code, 3); + encoder.rex(.{ + .r = Register.edi.isExtended(), + .b = Register.eax.isExtended(), + }); + encoder.opcode_1byte(0x89); + encoder.modRm_direct( + Register.edi.low_id(), + Register.eax.low_id(), + ); + + try testing.expectEqualSlices(u8, &[_]u8{ 0x89, 0xf8 }, code.items); + } + + // signed integer addition of 32-bit sign extended immediate to 64 bit register + + // add rcx, 2147483647 + // + // Using the following opcode: REX.W + 81 /0 id, we expect the following encoding + // + // 48 : REX.W set for 64 bit operand (*r*cx) + // 81 : opcode for " with immediate" + // c1 : id = rcx, + // : c1 = 11 <-- mod = 11 indicates r/m is register (rcx) + // : 000 <-- opcode_extension = 0 because opcode extension is /0. /0 specifies ADD + // : 001 <-- 001 is rcx + // ffffff7f : 2147483647 + { + try code.resize(0); + const encoder = try Encoder.init(&code, 7); + encoder.rex(.{ .w = true }); // use 64 bit operation + encoder.opcode_1byte(0x81); + encoder.modRm_direct( + 0, + Register.rcx.low_id(), + ); + encoder.imm32(2147483647); + + try testing.expectEqualSlices(u8, &[_]u8{ 0x48, 0x81, 0xc1, 0xff, 0xff, 0xff, 0x7f }, code.items); + } +} + +// TODO add these registers to the enum and populate dwarfLocOp +// // Return Address register. This is stored in `0(%rsp, "")` and is not a physical register. +// RA = (16, "RA"), +// +// XMM0 = (17, "xmm0"), +// XMM1 = (18, "xmm1"), +// XMM2 = (19, "xmm2"), +// XMM3 = (20, "xmm3"), +// XMM4 = (21, "xmm4"), +// XMM5 = (22, "xmm5"), +// XMM6 = (23, "xmm6"), +// XMM7 = (24, "xmm7"), +// +// XMM8 = (25, "xmm8"), +// XMM9 = (26, "xmm9"), +// XMM10 = (27, "xmm10"), +// XMM11 = (28, "xmm11"), +// XMM12 = (29, "xmm12"), +// XMM13 = (30, "xmm13"), +// XMM14 = (31, "xmm14"), +// XMM15 = (32, "xmm15"), +// +// ST0 = (33, "st0"), +// ST1 = (34, "st1"), +// ST2 = (35, "st2"), +// ST3 = (36, "st3"), +// ST4 = (37, "st4"), +// ST5 = (38, "st5"), +// ST6 = (39, "st6"), +// ST7 = (40, "st7"), +// +// MM0 = (41, "mm0"), +// MM1 = (42, "mm1"), +// MM2 = (43, "mm2"), +// MM3 = (44, "mm3"), +// MM4 = (45, "mm4"), +// MM5 = (46, "mm5"), +// MM6 = (47, "mm6"), +// MM7 = (48, "mm7"), +// +// RFLAGS = (49, "rFLAGS"), +// ES = (50, "es"), +// CS = (51, "cs"), +// SS = (52, "ss"), +// DS = (53, "ds"), +// FS = (54, "fs"), +// GS = (55, "gs"), +// +// FS_BASE = (58, "fs.base"), +// GS_BASE = (59, "gs.base"), +// +// TR = (62, "tr"), +// LDTR = (63, "ldtr"), +// MXCSR = (64, "mxcsr"), +// FCW = (65, "fcw"), +// FSW = (66, "fsw"), +// +// XMM16 = (67, "xmm16"), +// XMM17 = (68, "xmm17"), +// XMM18 = (69, "xmm18"), +// XMM19 = (70, "xmm19"), +// XMM20 = (71, "xmm20"), +// XMM21 = (72, "xmm21"), +// XMM22 = (73, "xmm22"), +// XMM23 = (74, "xmm23"), +// XMM24 = (75, "xmm24"), +// XMM25 = (76, "xmm25"), +// XMM26 = (77, "xmm26"), +// XMM27 = (78, "xmm27"), +// XMM28 = (79, "xmm28"), +// XMM29 = (80, "xmm29"), +// XMM30 = (81, "xmm30"), +// XMM31 = (82, "xmm31"), +// +// K0 = (118, "k0"), +// K1 = (119, "k1"), +// K2 = (120, "k2"), +// K3 = (121, "k3"), +// K4 = (122, "k4"), +// K5 = (123, "k5"), +// K6 = (124, "k6"), +// K7 = (125, "k7"), diff --git a/deps/zig/lib.zig b/deps/zig/lib.zig new file mode 100644 index 00000000..24924dcb --- /dev/null +++ b/deps/zig/lib.zig @@ -0,0 +1,4 @@ +pub const codegen = struct { + pub const x86_64 = @import("codegen/x86_64.zig"); +}; +pub const RegisterManager = @import("register_manager.zig").RegisterManager; diff --git a/deps/zig/register_manager.zig b/deps/zig/register_manager.zig new file mode 100644 index 00000000..b3fb7cb6 --- /dev/null +++ b/deps/zig/register_manager.zig @@ -0,0 +1,407 @@ +const std = @import("std"); +const math = std.math; +const mem = std.mem; +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Index = u32; +const expect = std.testing.expect; +const expectEqual = std.testing.expectEqual; +const expectEqualSlices = std.testing.expectEqualSlices; + +const log = std.log.scoped(.register_manager); + +pub fn RegisterManager( + comptime Function: type, + comptime Register: type, + comptime callee_preserved_regs: []const Register, +) type { + return struct { + /// The key must be canonical register. + registers: [callee_preserved_regs.len]?Index = [_]?Index{null} ** callee_preserved_regs.len, + free_registers: FreeRegInt = math.maxInt(FreeRegInt), + /// Tracks all registers allocated in the course of this function + allocated_registers: FreeRegInt = 0, + + const Self = @This(); + + /// An integer whose bits represent all the registers and whether they are free. + const FreeRegInt = std.meta.Int(.unsigned, callee_preserved_regs.len); + const ShiftInt = math.Log2Int(FreeRegInt); + + fn getFunction(self: *Self) *Function { + return @fieldParentPtr(Function, "register_manager", self); + } + + fn markRegUsed(self: *Self, reg: Register) void { + if (FreeRegInt == u0) return; + const index = reg.allocIndex() orelse return; + const shift = @intCast(ShiftInt, index); + const mask = @as(FreeRegInt, 1) << shift; + self.free_registers &= ~mask; + self.allocated_registers |= mask; + } + + fn markRegFree(self: *Self, reg: Register) void { + if (FreeRegInt == u0) return; + const index = reg.allocIndex() orelse return; + const shift = @intCast(ShiftInt, index); + self.free_registers |= @as(FreeRegInt, 1) << shift; + } + + /// Returns true when this register is not tracked + pub fn isRegFree(self: Self, reg: Register) bool { + if (FreeRegInt == u0) return true; + const index = reg.allocIndex() orelse return true; + const shift = @intCast(ShiftInt, index); + return self.free_registers & @as(FreeRegInt, 1) << shift != 0; + } + + /// Returns whether this register was allocated in the course + /// of this function. + /// Returns false when this register is not tracked + pub fn isRegAllocated(self: Self, reg: Register) bool { + if (FreeRegInt == u0) return false; + const index = reg.allocIndex() orelse return false; + const shift = @intCast(ShiftInt, index); + return self.allocated_registers & @as(FreeRegInt, 1) << shift != 0; + } + + /// Allocates a specified number of registers, optionally + /// tracking them. Returns `null` if not enough registers are + /// free. + pub fn tryAllocRegs( + self: *Self, + comptime count: comptime_int, + insts: [count]?Index, + exceptions: []const Register, + ) ?[count]Register { + comptime if (callee_preserved_regs.len == 0) return null; + comptime assert(count > 0 and count <= callee_preserved_regs.len); + assert(count + exceptions.len <= callee_preserved_regs.len); + + const free_registers = @popCount(FreeRegInt, self.free_registers); + if (free_registers < count) return null; + + var regs: [count]Register = undefined; + var i: usize = 0; + for (callee_preserved_regs) |reg| { + if (i >= count) break; + if (mem.indexOfScalar(Register, exceptions, reg) != null) continue; + if (self.isRegFree(reg)) { + regs[i] = reg; + i += 1; + } + } + + if (i == count) { + for (regs) |reg, j| { + if (insts[j]) |inst| { + // Track the register + const index = reg.allocIndex().?; // allocIndex() on a callee-preserved reg should never return null + self.registers[index] = inst; + self.markRegUsed(reg); + } + } + + return regs; + } else return null; + } + + /// Allocates a register and optionally tracks it with a + /// corresponding instruction. Returns `null` if all registers + /// are allocated. + pub fn tryAllocReg(self: *Self, inst: ?Index, exceptions: []const Register) ?Register { + return if (tryAllocRegs(self, 1, .{inst}, exceptions)) |regs| regs[0] else null; + } + + /// Allocates a specified number of registers, optionally + /// tracking them. Asserts that count + exceptions.len is not + /// larger than the total number of registers available. + pub fn allocRegs( + self: *Self, + comptime count: comptime_int, + insts: [count]?Index, + exceptions: []const Register, + ) ![count]Register { + comptime assert(count > 0 and count <= callee_preserved_regs.len); + assert(count + exceptions.len <= callee_preserved_regs.len); + + return self.tryAllocRegs(count, insts, exceptions) orelse blk: { + // We'll take over the first count registers. Spill + // the instructions that were previously there to a + // stack allocations. + var regs: [count]Register = undefined; + var i: usize = 0; + for (callee_preserved_regs) |reg| { + if (i >= count) break; + if (mem.indexOfScalar(Register, exceptions, reg) != null) continue; + regs[i] = reg; + + const index = reg.allocIndex().?; // allocIndex() on a callee-preserved reg should never return null + if (insts[i]) |inst| { + // Track the register + if (self.isRegFree(reg)) { + self.markRegUsed(reg); + } else { + const spilled_inst = self.registers[index].?; + try self.getFunction().spillInstruction(reg, spilled_inst); + } + self.registers[index] = inst; + } else { + // Don't track the register + if (!self.isRegFree(reg)) { + const spilled_inst = self.registers[index].?; + try self.getFunction().spillInstruction(reg, spilled_inst); + self.freeReg(reg); + } + } + + i += 1; + } + + break :blk regs; + }; + } + + /// Allocates a register and optionally tracks it with a + /// corresponding instruction. + pub fn allocReg(self: *Self, inst: ?Index, exceptions: []const Register) !Register { + return (try self.allocRegs(1, .{inst}, exceptions))[0]; + } + + /// Spills the register if it is currently allocated. If a + /// corresponding instruction is passed, will also track this + /// register. + pub fn getReg(self: *Self, reg: Register, inst: ?Index) !void { + const index = reg.allocIndex() orelse return; + + if (inst) |tracked_inst| + if (!self.isRegFree(reg)) { + // Move the instruction that was previously there to a + // stack allocation. + const spilled_inst = self.registers[index].?; + self.registers[index] = tracked_inst; + try self.getFunction().spillInstruction(reg, spilled_inst); + } else { + self.getRegAssumeFree(reg, tracked_inst); + } + else { + if (!self.isRegFree(reg)) { + // Move the instruction that was previously there to a + // stack allocation. + const spilled_inst = self.registers[index].?; + try self.getFunction().spillInstruction(reg, spilled_inst); + self.freeReg(reg); + } + } + } + + /// Allocates the specified register with the specified + /// instruction. Asserts that the register is free and no + /// spilling is necessary. + pub fn getRegAssumeFree(self: *Self, reg: Register, inst: Index) void { + const index = reg.allocIndex() orelse return; + + assert(self.registers[index] == null); + self.registers[index] = inst; + self.markRegUsed(reg); + } + + /// Marks the specified register as free + pub fn freeReg(self: *Self, reg: Register) void { + const index = reg.allocIndex() orelse return; + + self.registers[index] = null; + self.markRegFree(reg); + } + }; +} + +const MockRegister1 = enum(u2) { + r0, + r1, + r2, + r3, + + pub fn allocIndex(self: MockRegister1) ?u2 { + inline for (callee_preserved_regs) |cpreg, i| { + if (self == cpreg) return i; + } + return null; + } + + const callee_preserved_regs = [_]MockRegister1{ .r2, .r3 }; +}; + +const MockRegister2 = enum(u2) { + r0, + r1, + r2, + r3, + + pub fn allocIndex(self: MockRegister2) ?u2 { + inline for (callee_preserved_regs) |cpreg, i| { + if (self == cpreg) return i; + } + return null; + } + + const callee_preserved_regs = [_]MockRegister2{ .r0, .r1, .r2, .r3 }; +}; + +fn MockFunction(comptime Register: type) type { + return struct { + allocator: *Allocator, + register_manager: RegisterManager(Self, Register, &Register.callee_preserved_regs) = .{}, + spilled: std.ArrayListUnmanaged(Register) = .{}, + + const Self = @This(); + + pub fn deinit(self: *Self) void { + self.spilled.deinit(self.allocator); + } + + pub fn spillInstruction(self: *Self, reg: Register, inst: Index) !void { + _ = inst; + try self.spilled.append(self.allocator, reg); + } + }; +} + +const MockFunction1 = MockFunction(MockRegister1); +const MockFunction2 = MockFunction(MockRegister2); + +test "default state" { + const allocator = std.testing.allocator; + + var function = MockFunction1{ + .allocator = allocator, + }; + defer function.deinit(); + + try expect(!function.register_manager.isRegAllocated(.r2)); + try expect(!function.register_manager.isRegAllocated(.r3)); + try expect(function.register_manager.isRegFree(.r2)); + try expect(function.register_manager.isRegFree(.r3)); +} + +test "tryAllocReg: no spilling" { + const allocator = std.testing.allocator; + + var function = MockFunction1{ + .allocator = allocator, + }; + defer function.deinit(); + + const mock_instruction: Index = 1; + + try expectEqual(@as(?MockRegister1, .r2), function.register_manager.tryAllocReg(mock_instruction, &.{})); + try expectEqual(@as(?MockRegister1, .r3), function.register_manager.tryAllocReg(mock_instruction, &.{})); + try expectEqual(@as(?MockRegister1, null), function.register_manager.tryAllocReg(mock_instruction, &.{})); + + try expect(function.register_manager.isRegAllocated(.r2)); + try expect(function.register_manager.isRegAllocated(.r3)); + try expect(!function.register_manager.isRegFree(.r2)); + try expect(!function.register_manager.isRegFree(.r3)); + + function.register_manager.freeReg(.r2); + function.register_manager.freeReg(.r3); + + try expect(function.register_manager.isRegAllocated(.r2)); + try expect(function.register_manager.isRegAllocated(.r3)); + try expect(function.register_manager.isRegFree(.r2)); + try expect(function.register_manager.isRegFree(.r3)); +} + +test "allocReg: spilling" { + const allocator = std.testing.allocator; + + var function = MockFunction1{ + .allocator = allocator, + }; + defer function.deinit(); + + const mock_instruction: Index = 1; + + try expectEqual(@as(?MockRegister1, .r2), try function.register_manager.allocReg(mock_instruction, &.{})); + try expectEqual(@as(?MockRegister1, .r3), try function.register_manager.allocReg(mock_instruction, &.{})); + + // Spill a register + try expectEqual(@as(?MockRegister1, .r2), try function.register_manager.allocReg(mock_instruction, &.{})); + try expectEqualSlices(MockRegister1, &[_]MockRegister1{.r2}, function.spilled.items); + + // No spilling necessary + function.register_manager.freeReg(.r3); + try expectEqual(@as(?MockRegister1, .r3), try function.register_manager.allocReg(mock_instruction, &.{})); + try expectEqualSlices(MockRegister1, &[_]MockRegister1{.r2}, function.spilled.items); + + // Exceptions + function.register_manager.freeReg(.r2); + function.register_manager.freeReg(.r3); + try expectEqual(@as(?MockRegister1, .r3), try function.register_manager.allocReg(mock_instruction, &.{.r2})); +} + +test "tryAllocRegs" { + const allocator = std.testing.allocator; + + var function = MockFunction2{ + .allocator = allocator, + }; + defer function.deinit(); + + try expectEqual([_]MockRegister2{ .r0, .r1, .r2 }, function.register_manager.tryAllocRegs(3, .{ null, null, null }, &.{}).?); + + // Exceptions + function.register_manager.freeReg(.r0); + function.register_manager.freeReg(.r1); + function.register_manager.freeReg(.r2); + try expectEqual([_]MockRegister2{ .r0, .r2, .r3 }, function.register_manager.tryAllocRegs(3, .{ null, null, null }, &.{.r1}).?); +} + +test "allocRegs" { + const allocator = std.testing.allocator; + + var function = MockFunction2{ + .allocator = allocator, + }; + defer function.deinit(); + + const mock_instruction: Index = 1; + + try expectEqual([_]MockRegister2{ .r0, .r1, .r2 }, try function.register_manager.allocRegs(3, .{ + mock_instruction, + mock_instruction, + mock_instruction, + }, &.{})); + + // Exceptions + try expectEqual([_]MockRegister2{ .r0, .r2, .r3 }, try function.register_manager.allocRegs(3, .{ null, null, null }, &.{.r1})); + try expectEqualSlices(MockRegister2, &[_]MockRegister2{ .r0, .r2 }, function.spilled.items); +} + +test "getReg" { + const allocator = std.testing.allocator; + + var function = MockFunction1{ + .allocator = allocator, + }; + defer function.deinit(); + + const mock_instruction: Index = 1; + + try function.register_manager.getReg(.r3, mock_instruction); + + try expect(!function.register_manager.isRegAllocated(.r2)); + try expect(function.register_manager.isRegAllocated(.r3)); + try expect(function.register_manager.isRegFree(.r2)); + try expect(!function.register_manager.isRegFree(.r3)); + + // Spill r3 + try function.register_manager.getReg(.r3, mock_instruction); + + try expect(!function.register_manager.isRegAllocated(.r2)); + try expect(function.register_manager.isRegAllocated(.r3)); + try expect(function.register_manager.isRegFree(.r2)); + try expect(!function.register_manager.isRegFree(.r3)); + try expectEqualSlices(MockRegister1, &[_]MockRegister1{.r3}, function.spilled.items); +} diff --git a/src/Codegen.zig b/src/Codegen.zig new file mode 100644 index 00000000..aebeb053 --- /dev/null +++ b/src/Codegen.zig @@ -0,0 +1,109 @@ +const std = @import("std"); +const Compilation = @import("Compilation.zig"); +const Tree = @import("Tree.zig"); +const NodeIndex = Tree.NodeIndex; +const Object = @import("Object.zig"); +const x86_64 = @import("codegen/x86_64.zig"); + +const Codegen = @This(); + +comp: *Compilation, +tree: Tree, +obj: *Object, +node_tag: []const Tree.Tag, +node_data: []const Tree.Node.Data, + +pub const Error = Compilation.Error || error{CodegenFailed}; + +/// Generate tree to an object file. +/// Caller is responsible for flushing and freeing the returned object. +pub fn generateTree(comp: *Compilation, tree: Tree) Compilation.Error!*Object { + var c = Codegen{ + .comp = comp, + .tree = tree, + .obj = try Object.create(comp), + .node_tag = tree.nodes.items(.tag), + .node_data = tree.nodes.items(.data), + }; + errdefer c.obj.deinit(); + + const node_tags = tree.nodes.items(.tag); + for (tree.root_decls) |decl| { + switch (node_tags[@enumToInt(decl)]) { + // these produce no code + .static_assert, + .typedef, + .struct_decl_two, + .union_decl_two, + .enum_decl_two, + .struct_decl, + .union_decl, + .enum_decl, + => {}, + + // define symbol + .fn_proto, + .static_fn_proto, + .inline_fn_proto, + .inline_static_fn_proto, + .noreturn_fn_proto, + .noreturn_static_fn_proto, + .noreturn_inline_fn_proto, + .noreturn_inline_static_fn_proto, + .extern_var, + .threadlocal_extern_var, + => { + const name = c.tree.tokSlice(c.node_data[@enumToInt(decl)].decl.name); + _ = try c.obj.declareSymbol(.@"undefined", name, .Strong, .external, 0, 0); + }, + + // function definition + .fn_def, + .static_fn_def, + .inline_fn_def, + .inline_static_fn_def, + .noreturn_fn_def, + .noreturn_static_fn_def, + .noreturn_inline_fn_def, + .noreturn_inline_static_fn_def, + => c.genFn(decl) catch |err| switch (err) { + error.FatalError => return error.FatalError, + error.OutOfMemory => return error.OutOfMemory, + error.CodegenFailed => continue, + }, + + .@"var", + .static_var, + .threadlocal_var, + .threadlocal_static_var, + => c.genVar(decl) catch |err| switch (err) { + error.FatalError => return error.FatalError, + error.OutOfMemory => return error.OutOfMemory, + error.CodegenFailed => continue, + }, + + else => unreachable, + } + } + + return c.obj; +} + +fn genFn(c: *Codegen, decl: NodeIndex) Error!void { + const section: Object.Section = .func; + const data = try c.obj.getSection(section); + const start_len = data.items.len; + switch (c.comp.target.cpu.arch) { + .x86_64 => try x86_64.genFn(c, decl, data), + else => unreachable, + } + const name = c.tree.tokSlice(c.node_data[@enumToInt(decl)].decl.name); + _ = try c.obj.declareSymbol(section, name, .Strong, .func, start_len, data.items.len - start_len); +} + +fn genVar(c: *Codegen, decl: NodeIndex) Error!void { + switch (c.comp.target.cpu.arch) { + .x86_64 => try x86_64.genVar(c, decl), + else => unreachable, + } +} diff --git a/src/Compilation.zig b/src/Compilation.zig index b3906723..033095ba 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -26,6 +26,8 @@ output_name: ?[]const u8 = null, builtin_header_path: ?[]u8 = null, target: std.Target = std.Target.current, only_preprocess: bool = false, +only_compile: bool = false, +verbose_ast: bool = false, langopts: LangOpts = .{}, pub fn init(gpa: *Allocator) Compilation { diff --git a/src/Object.zig b/src/Object.zig new file mode 100644 index 00000000..c0c4120c --- /dev/null +++ b/src/Object.zig @@ -0,0 +1,73 @@ +const std = @import("std"); +const Compilation = @import("Compilation.zig"); +const Elf = @import("object/Elf.zig"); + +const Object = @This(); + +format: std.Target.ObjectFormat, +comp: *Compilation, + +pub fn create(comp: *Compilation) !*Object { + switch (comp.target.getObjectFormat()) { + .elf => return Elf.create(comp), + else => unreachable, + } +} + +pub fn deinit(obj: *Object) void { + switch (obj.format) { + .elf => @fieldParentPtr(Elf, "obj", obj).deinit(), + else => unreachable, + } +} + +pub const Section = union(enum) { + @"undefined", + data, + read_only_data, + func, + strings, + custom: []const u8, +}; + +pub fn getSection(obj: *Object, section: Section) !*std.ArrayList(u8) { + switch (obj.format) { + .elf => return @fieldParentPtr(Elf, "obj", obj).getSection(section), + else => unreachable, + } +} + +pub const SymbolType = enum { + func, + variable, + external, +}; + +pub fn declareSymbol( + obj: *Object, + section: Section, + name: ?[]const u8, + linkage: std.builtin.GlobalLinkage, + @"type": SymbolType, + offset: u64, + size: u64, +) ![]const u8 { + switch (obj.format) { + .elf => return @fieldParentPtr(Elf, "obj", obj).declareSymbol(section, name, linkage, @"type", offset, size), + else => unreachable, + } +} + +pub fn addRelocation(obj: *Object, name: []const u8, section: Section, address: u64, addend: i64) !void { + switch (obj.format) { + .elf => return @fieldParentPtr(Elf, "obj", obj).addRelocation(name, section, address, addend), + else => unreachable, + } +} + +pub fn finish(obj: *Object, file: std.fs.File) !void { + switch (obj.format) { + .elf => return @fieldParentPtr(Elf, "obj", obj).finish(file), + else => unreachable, + } +} diff --git a/src/codegen/x86_64.zig b/src/codegen/x86_64.zig new file mode 100644 index 00000000..84cc30f4 --- /dev/null +++ b/src/codegen/x86_64.zig @@ -0,0 +1,211 @@ +const std = @import("std"); +const Codegen = @import("../Codegen.zig"); +const Tree = @import("../Tree.zig"); +const NodeIndex = Tree.NodeIndex; +const x86_64 = @import("zig").codegen.x86_64; +const Register = x86_64.Register; +const RegisterManager = @import("zig").RegisterManager; + +const Fn = @This(); + +const Value = union(enum) { + symbol: []const u8, + immediate: i64, + register: Register, + none, +}; + +register_manager: RegisterManager(Fn, Register, &x86_64.callee_preserved_regs) = .{}, +data: *std.ArrayList(u8), +c: *Codegen, + +pub fn deinit(func: *Fn) void { + func.* = undefined; +} + +pub fn genFn(c: *Codegen, decl: NodeIndex, data: *std.ArrayList(u8)) Codegen.Error!void { + var func = Fn{ .data = data, .c = c }; + defer func.deinit(); + + // function prologue + try func.data.appendSlice(&.{ + 0x55, // push rbp + 0x48, 0x89, 0xe5, // mov rbp,rsp + }); + _ = try func.genNode(c.node_data[@enumToInt(decl)].decl.node); + // all functions are guaranteed to end in a return statement so no extra work required here +} + +pub fn spillInst(f: *Fn, reg: Register, inst: u32) !void { + _ = inst; + _ = reg; + _ = f; +} + +fn setReg(func: *Fn, val: Value, reg: Register) !void { + switch (val) { + .none => unreachable, + .symbol => |sym| { + // lea address with 0 and add relocation + const encoder = try x86_64.Encoder.init(func.data, 8); + encoder.rex(.{ .w = true }); + encoder.opcode_1byte(0x8D); + encoder.modRm_RIPDisp32(reg.low_id()); + + const offset = func.data.items.len; + encoder.imm32(0); + + try func.c.obj.addRelocation(sym, .func, offset, -4); + }, + .immediate => |x| if (x == 0) { + // 32-bit moves zero-extend to 64-bit, so xoring the 32-bit + // register is the fastest way to zero a register. + // The encoding for `xor r32, r32` is `0x31 /r`. + const encoder = try x86_64.Encoder.init(func.data, 3); + + // If we're accessing e.g. r8d, we need to use a REX prefix before the actual operation. Since + // this is a 32-bit operation, the W flag is set to zero. X is also zero, as we're not using a SIB. + // Both R and B are set, as we're extending, in effect, the register bits *and* the operand. + encoder.rex(.{ .r = reg.isExtended(), .b = reg.isExtended() }); + encoder.opcode_1byte(0x31); + // Section 3.1.1.1 of the Intel x64 Manual states that "/r indicates that the + // ModR/M byte of the instruction contains a register operand and an r/m operand." + encoder.modRm_direct(reg.low_id(), reg.low_id()); + } else if (x <= std.math.maxInt(i32)) { + // Next best case: if we set the lower four bytes, the upper four will be zeroed. + // + // The encoding for `mov IMM32 -> REG` is (0xB8 + R) IMM. + + const encoder = try x86_64.Encoder.init(func.data, 6); + // Just as with XORing, we need a REX prefix. This time though, we only + // need the B bit set, as we're extending the opcode's register field, + // and there is no Mod R/M byte. + encoder.rex(.{ .b = reg.isExtended() }); + encoder.opcode_withReg(0xB8, reg.low_id()); + + // no ModR/M byte + + // IMM + encoder.imm32(@intCast(i32, x)); + } else { + // Worst case: we need to load the 64-bit register with the IMM. GNU's assemblers calls + // this `movabs`, though this is officially just a different variant of the plain `mov` + // instruction. + // + // This encoding is, in fact, the *same* as the one used for 32-bit loads. The only + // difference is that we set REX.W before the instruction, which extends the load to + // 64-bit and uses the full bit-width of the register. + { + const encoder = try x86_64.Encoder.init(func.data, 10); + encoder.rex(.{ .w = true, .b = reg.isExtended() }); + encoder.opcode_withReg(0xB8, reg.low_id()); + encoder.imm64(@bitCast(u64, x)); + } + }, + .register => |src_reg| { + // If the registers are the same, nothing to do. + if (src_reg.id() == reg.id()) + return; + + // This is a variant of 8B /r. + const encoder = try x86_64.Encoder.init(func.data, 3); + encoder.rex(.{ + .w = true, + .r = reg.isExtended(), + .b = src_reg.isExtended(), + }); + encoder.opcode_1byte(0x8B); + encoder.modRm_direct(reg.low_id(), src_reg.low_id()); + }, + } +} + +fn genNode(func: *Fn, node: NodeIndex) Codegen.Error!Value { + if (func.c.tree.value_map.get(node)) |some| return Value{ .immediate = @bitCast(i64, some) }; + + const data = func.c.node_data[@enumToInt(node)]; + switch (func.c.node_tag[@enumToInt(node)]) { + .static_assert => return Value{ .none = {} }, + .compound_stmt_two => { + if (data.bin.lhs != .none) _ = try func.genNode(data.bin.lhs); + if (data.bin.rhs != .none) _ = try func.genNode(data.bin.rhs); + return Value{ .none = {} }; + }, + .compound_stmt => { + for (func.c.tree.data[data.range.start..data.range.end]) |stmt| { + _ = try func.genNode(stmt); + } + return Value{ .none = {} }; + }, + .call_expr_one => if (data.bin.rhs != .none) + return func.genCall(data.bin.lhs, &.{data.bin.rhs}) + else + return func.genCall(data.bin.lhs, &.{}), + .call_expr => return func.genCall(func.c.tree.data[data.range.start], func.c.tree.data[data.range.start + 1 .. data.range.end]), + .function_to_pointer => return func.genNode(data.un), // no-op + .array_to_pointer => return func.genNode(data.un), // no-op + .decl_ref_expr => { + // TODO locals and arguments + return Value{ .symbol = func.c.tree.tokSlice(data.decl_ref) }; + }, + .return_stmt => { + const value = try func.genNode(data.un); + try func.setReg(value, x86_64.c_abi_int_return_regs[0]); + try func.data.appendSlice(&.{ + 0x5d, // pop rbp + 0xc3, // ret + }); + return Value{ .none = {} }; + }, + .implicit_return => { + try func.setReg(.{ .immediate = 0 }, x86_64.c_abi_int_return_regs[0]); + try func.data.appendSlice(&.{ + 0x5d, // pop rbp + 0xc3, // ret + }); + return Value{ .none = {} }; + }, + .int_literal => return Value{ .immediate = @bitCast(i64, data.int) }, + .string_literal_expr => { + const str_bytes = func.c.tree.strings[data.str.index..][0..data.str.len]; + const section = try func.c.obj.getSection(.strings); + const start = section.items.len; + try section.appendSlice(str_bytes); + const symbol_name = try func.c.obj.declareSymbol(.strings, null, .Internal, .variable, start, str_bytes.len); + return Value{ .symbol = symbol_name }; + }, + else => return func.c.comp.diag.fatalNoSrc("TODO x86_64 genNode {}\n", .{func.c.node_tag[@enumToInt(node)]}), + } +} + +fn genCall(func: *Fn, lhs: NodeIndex, args: []const NodeIndex) Codegen.Error!Value { + if (args.len > x86_64.c_abi_int_param_regs.len) + return func.c.comp.diag.fatalNoSrc("TODO more than args {d}\n", .{x86_64.c_abi_int_param_regs.len}); + + const func_value = try func.genNode(lhs); + for (args) |arg, i| { + const value = try func.genNode(arg); + try func.setReg(value, x86_64.c_abi_int_param_regs[i]); + } + + switch (func_value) { + .none => unreachable, + .symbol => |sym| { + const encoder = try x86_64.Encoder.init(func.data, 5); + encoder.opcode_1byte(0xe8); + + const offset = func.data.items.len; + encoder.imm32(0); + + try func.c.obj.addRelocation(sym, .func, offset, -4); + }, + .immediate => return func.c.comp.diag.fatalNoSrc("TODO call immediate\n", .{}), + .register => return func.c.comp.diag.fatalNoSrc("TODO call reg\n", .{}), + } + return Value{ .register = x86_64.c_abi_int_return_regs[0] }; +} + +pub fn genVar(c: *Codegen, decl: NodeIndex) Codegen.Error!void { + _ = c; + _ = decl; +} diff --git a/src/lib.zig b/src/lib.zig index 7f6e1dae..f5a760a0 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -1,6 +1,8 @@ +pub const Codegen = @import("Codegen.zig"); pub const Compilation = @import("Compilation.zig"); pub const Diagnostics = @import("Diagnostics.zig"); pub const Parser = @import("Parser.zig"); +pub const Object = @import("Object.zig"); pub const Preprocessor = @import("Preprocessor.zig"); pub const Source = @import("Source.zig"); pub const Tokenizer = @import("Tokenizer.zig"); diff --git a/src/main.zig b/src/main.zig index 771a54fc..54bcb3db 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,6 +2,7 @@ const std = @import("std"); const mem = std.mem; const Allocator = mem.Allocator; const process = std.process; +const Codegen = @import("Codegen.zig"); const Compilation = @import("Compilation.zig"); const Source = @import("Source.zig"); const Preprocessor = @import("Preprocessor.zig"); @@ -47,22 +48,27 @@ const usage = \\ -h, --help Print this message. \\ -v, --version Print aro version. \\ - \\Feature options: - \\ -E Only run the preprocessor + \\Compile options: + \\ -c Only run preprocess, compile, and assemble steps \\ -D = Define to (defaults to 1) - \\ -U Undefine + \\ -E Only run the preprocessor \\ -fcolor-diagnostics Enable colors in diagnostics \\ -fno-color-diagnostics Disable colors in diagnostics \\ -I Add directory to include search path \\ -isystem Add directory to SYSTEM include search path \\ -o Write output to \\ -std= Specify language standard + \\ --target= Generate code for the given target + \\ -U Undefine \\ -Wall Enable all warnings \\ -Werror Treat all warnings as errors \\ -Werror= Treat warning as error \\ -W Enable the specified warning \\ -Wno- Disable the specified warning \\ + \\Depub options: + \\ --verbose-ast Dump produced AST to stdout + \\ \\ ; @@ -95,6 +101,8 @@ fn handleArgs(comp: *Compilation, args: [][]const u8) !void { return comp.diag.fatalNoSrc("{s} when trying to print version", .{@errorName(err)}); }; return; + } else if (mem.eql(u8, arg, "-c")) { + comp.only_compile = true; } else if (mem.startsWith(u8, arg, "-D")) { var macro = arg["-D".len..]; if (macro.len == 0) { @@ -161,7 +169,15 @@ fn handleArgs(comp: *Compilation, args: [][]const u8) !void { try comp.diag.set(option, .warning); } else if (mem.startsWith(u8, arg, "-std=")) { const standard = arg["-std=".len..]; - comp.langopts.setStandard(standard) catch return comp.diag.fatalNoSrc("Invalid standard '{s}'", .{standard}); + comp.langopts.setStandard(standard) catch + return comp.diag.fatalNoSrc("Invalid standard '{s}'", .{standard}); + } else if (mem.startsWith(u8, arg, "--target=")) { + const triple = arg["--target=".len..]; + const cross = std.zig.CrossTarget.parse(.{ .arch_os_abi = triple }) catch + return comp.diag.fatalNoSrc("Invalid target '{s}'", .{triple}); + comp.target = cross.toTarget(); // TODO deprecated + } else if (mem.eql(u8, arg, "--verbose-ast")) { + comp.verbose_ast = true; } else { const std_out = std.io.getStdErr().writer(); std_out.print(usage, .{args[0]}) catch {}; @@ -233,12 +249,48 @@ fn processSource(comp: *Compilation, source: Source, builtin: Source, user_macro var tree = try Parser.parse(&pp); defer tree.deinit(); + if (comp.verbose_ast) { + var buf_writer = std.io.bufferedWriter(std.io.getStdOut().writer()); + tree.dump(buf_writer.writer()) catch {}; + buf_writer.flush() catch {}; + } + + const prev_errors = comp.diag.errors; comp.renderErrors(); - tree.dump(std.io.getStdOut().writer()) catch {}; + if (comp.diag.errors != prev_errors) return; // do not compile if there were errors + + if (comp.target.getObjectFormat() != .elf or comp.target.cpu.arch != .x86_64) { + return comp.diag.fatalNoSrc( + "unsupported target {s}-{s}-{s}, currently only x86-64 elf is supported", + .{ @tagName(comp.target.cpu.arch), @tagName(comp.target.os.tag), @tagName(comp.target.abi) }, + ); + } + + const obj = try Codegen.generateTree(comp, tree); + defer obj.deinit(); + + const basename = std.fs.path.basename(source.path); + const out_file_name = comp.output_name orelse try std.fmt.allocPrint(comp.gpa, "{s}{s}", .{ + basename[0 .. basename.len - std.fs.path.extension(source.path).len], + comp.target.getObjectFormat().fileExt(comp.target.cpu.arch), + }); + defer if (comp.output_name == null) comp.gpa.free(out_file_name); + + const out_file = std.fs.cwd().createFile(out_file_name, .{}) catch |err| + return comp.diag.fatalNoSrc("could not create output file '{s}': {s}", .{ out_file_name, @errorName(err) }); + defer out_file.close(); + + obj.finish(out_file) catch |err| + return comp.diag.fatalNoSrc("could output to object file '{s}': {s}", .{ out_file_name, @errorName(err) }); + + if (comp.only_compile) return; + + // TODO invoke linker } test { + _ = @import("Codegen.zig"); _ = @import("Compilation.zig"); _ = @import("Diagnostics.zig"); _ = @import("InitList.zig"); diff --git a/src/object/Elf.zig b/src/object/Elf.zig new file mode 100644 index 00000000..b22933bd --- /dev/null +++ b/src/object/Elf.zig @@ -0,0 +1,377 @@ +const std = @import("std"); +const Compilation = @import("../Compilation.zig"); +const Object = @import("../Object.zig"); + +const Elf = @This(); + +const Section = struct { + data: std.ArrayList(u8), + relocations: std.ArrayListUnmanaged(Relocation) = .{}, + flags: u64, + type: u32, + index: u16 = undefined, +}; + +const Symbol = struct { + section: ?*Section, + size: u64, + offset: u64, + index: u16 = undefined, + info: u8, +}; + +const Relocation = packed struct { + symbol: *Symbol, + addend: i64, + offset: u48, + type: u8, +}; + +const additional_sections = 3; // null section, strtab, symtab +const strtab_index = 1; +const symtab_index = 2; +const strtab_default = "\x00.strtab\x00.symtab\x00"; +const strtab_name = 1; +const symtab_name = "\x00.strtab\x00".len; + +obj: Object, +/// The keys are owned by the Codegen.tree +sections: std.StringHashMapUnmanaged(*Section) = .{}, +local_symbols: std.StringHashMapUnmanaged(*Symbol) = .{}, +global_symbols: std.StringHashMapUnmanaged(*Symbol) = .{}, +unnamed_symbol_mangle: u32 = 0, +strtab_len: u64 = strtab_default.len, +arena: std.heap.ArenaAllocator, + +pub fn create(comp: *Compilation) !*Object { + const elf = try comp.gpa.create(Elf); + elf.* = .{ + .obj = .{ .format = .elf, .comp = comp }, + .arena = std.heap.ArenaAllocator.init(comp.gpa), + }; + return &elf.obj; +} + +pub fn deinit(elf: *Elf) void { + const gpa = elf.arena.child_allocator; + { + var it = elf.sections.valueIterator(); + while (it.next()) |sect| { + sect.*.data.deinit(); + sect.*.relocations.deinit(gpa); + } + } + elf.sections.deinit(gpa); + elf.local_symbols.deinit(gpa); + elf.global_symbols.deinit(gpa); + elf.arena.deinit(); + gpa.destroy(elf); +} + +fn sectionString(sec: Object.Section) []const u8 { + return switch (sec) { + .@"undefined" => unreachable, + .data => "data", + .read_only_data => "rodata", + .func => "text", + .strings => "rodata.str", + .custom => |name| name, + }; +} + +pub fn getSection(elf: *Elf, section_kind: Object.Section) !*std.ArrayList(u8) { + const section_name = sectionString(section_kind); + const section = elf.sections.get(section_name) orelse blk: { + const section = try elf.arena.allocator.create(Section); + section.* = .{ + .data = std.ArrayList(u8).init(elf.arena.child_allocator), + .type = std.elf.SHT_PROGBITS, + .flags = switch (section_kind) { + .func, .custom => std.elf.SHF_ALLOC + std.elf.SHF_EXECINSTR, + .strings => std.elf.SHF_ALLOC + std.elf.SHF_MERGE + std.elf.SHF_STRINGS, + .read_only_data => std.elf.SHF_ALLOC, + .data => std.elf.SHF_ALLOC + std.elf.SHF_WRITE, + .@"undefined" => unreachable, + }, + }; + try elf.sections.putNoClobber(elf.arena.child_allocator, section_name, section); + elf.strtab_len += section_name.len + ".\x00".len; + break :blk section; + }; + return §ion.data; +} + +pub fn declareSymbol( + elf: *Elf, + section_kind: Object.Section, + maybe_name: ?[]const u8, + linkage: std.builtin.GlobalLinkage, + @"type": Object.SymbolType, + offset: u64, + size: u64, +) ![]const u8 { + const section = blk: { + if (section_kind == .@"undefined") break :blk null; + const section_name = sectionString(section_kind); + break :blk elf.sections.get(section_name); + }; + const binding: u8 = switch (linkage) { + .Internal => std.elf.STB_LOCAL, + .Strong => std.elf.STB_GLOBAL, + .Weak => std.elf.STB_WEAK, + .LinkOnce => unreachable, + }; + const sym_type: u8 = switch (@"type") { + .func => std.elf.STT_FUNC, + .variable => std.elf.STT_OBJECT, + .external => std.elf.STT_NOTYPE, + }; + const name = if (maybe_name) |some| some else blk: { + defer elf.unnamed_symbol_mangle += 1; + break :blk try std.fmt.allocPrint(&elf.arena.allocator, ".L.{d}", .{elf.unnamed_symbol_mangle}); + }; + + const gop = if (linkage == .Internal) + try elf.local_symbols.getOrPut(elf.arena.child_allocator, name) + else + try elf.global_symbols.getOrPut(elf.arena.child_allocator, name); + + if (!gop.found_existing) { + gop.value_ptr.* = try elf.arena.allocator.create(Symbol); + elf.strtab_len += name.len + 1; // +1 for null byte + } + gop.value_ptr.*.* = .{ + .section = section, + .size = size, + .offset = offset, + .info = (binding << 4) + sym_type, + }; + return name; +} + +pub fn addRelocation(elf: *Elf, name: []const u8, section_kind: Object.Section, address: u64, addend: i64) !void { + const section_name = sectionString(section_kind); + const symbol = elf.local_symbols.get(name) orelse elf.global_symbols.get(name).?; // reference to undeclared symbol + const section = elf.sections.get(section_name).?; + if (section.relocations.items.len == 0) elf.strtab_len += ".rela".len; + + try section.relocations.append(elf.arena.child_allocator, .{ + .symbol = symbol, + .offset = @intCast(u48, address), + .addend = addend, + .type = if (symbol.section == null) 4 else 2, // TODO + }); +} + +/// elf header +/// sections contents +/// symbols +/// relocations +/// strtab +/// section headers +pub fn finish(elf: *Elf, file: std.fs.File) !void { + var buf_writer = std.io.bufferedWriter(file.writer()); + const w = buf_writer.writer(); + + var num_sections: std.elf.Elf64_Half = additional_sections; + var relocations_len: std.elf.Elf64_Off = 0; + var sections_len: std.elf.Elf64_Off = 0; + { + var it = elf.sections.valueIterator(); + while (it.next()) |sect| { + sections_len += sect.*.data.items.len; + relocations_len += sect.*.relocations.items.len * @sizeOf(std.elf.Elf64_Rela); + sect.*.index = num_sections; + num_sections += 1; + num_sections += @boolToInt(sect.*.relocations.items.len != 0); + } + } + const symtab_len = (elf.local_symbols.count() + elf.global_symbols.count() + 1) * @sizeOf(std.elf.Elf64_Sym); + + const symtab_offset = @sizeOf(std.elf.Elf64_Ehdr) + sections_len; + const symtab_offset_aligned = std.mem.alignForward(symtab_offset, 8); + const rela_offset = symtab_offset_aligned + symtab_len; + const strtab_offset = rela_offset + relocations_len; + const sh_offset = strtab_offset + elf.strtab_len; + const sh_offset_aligned = std.mem.alignForward(sh_offset, 16); + + var elf_header = std.elf.Elf64_Ehdr{ + .e_ident = .{ 0x7F, 'E', 'L', 'F', 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + .e_type = std.elf.ET.REL, // we only produce relocatables + .e_machine = elf.obj.comp.target.cpu.arch.toElfMachine(), + .e_version = 1, + .e_entry = 0, // linker will handle this + .e_phoff = 0, // no program header + .e_shoff = sh_offset_aligned, // section headers offset + .e_flags = 0, // no flags + .e_ehsize = @sizeOf(std.elf.Elf64_Ehdr), + .e_phentsize = 0, // no program header + .e_phnum = 0, // no program header + .e_shentsize = @sizeOf(std.elf.Elf64_Shdr), + .e_shnum = num_sections, + .e_shstrndx = strtab_index, + }; + try w.writeStruct(elf_header); + + // write contents of sections + { + var it = elf.sections.valueIterator(); + while (it.next()) |sect| try w.writeAll(sect.*.data.items); + } + + // pad to 8 bytes + try w.writeByteNTimes(0, symtab_offset_aligned - symtab_offset); + + var name_offset: u32 = strtab_default.len; + // write symbols + { + // first symbol must be null + try w.writeStruct(std.mem.zeroes(std.elf.Elf64_Sym)); + + var sym_index: u16 = 1; + var it = elf.local_symbols.iterator(); + while (it.next()) |entry| { + const sym = entry.value_ptr.*; + try w.writeStruct(std.elf.Elf64_Sym{ + .st_name = name_offset, + .st_info = sym.info, + .st_other = 0, + .st_shndx = if (sym.section) |some| some.index else 0, + .st_value = sym.offset, + .st_size = sym.size, + }); + sym.index = sym_index; + sym_index += 1; + name_offset += @intCast(u32, entry.key_ptr.len + 1); // +1 for null byte + } + it = elf.global_symbols.iterator(); + while (it.next()) |entry| { + const sym = entry.value_ptr.*; + try w.writeStruct(std.elf.Elf64_Sym{ + .st_name = name_offset, + .st_info = sym.info, + .st_other = 0, + .st_shndx = if (sym.section) |some| some.index else 0, + .st_value = sym.offset, + .st_size = sym.size, + }); + sym.index = sym_index; + sym_index += 1; + name_offset += @intCast(u32, entry.key_ptr.len + 1); // +1 for null byte + } + } + + // write relocations + { + var it = elf.sections.valueIterator(); + while (it.next()) |sect| { + for (sect.*.relocations.items) |rela| { + try w.writeStruct(std.elf.Elf64_Rela{ + .r_offset = rela.offset, + .r_addend = rela.addend, + .r_info = (@as(u64, rela.symbol.index) << 32) | rela.type, + }); + } + } + } + + // write strtab + try w.writeAll(strtab_default); + { + var it = elf.local_symbols.keyIterator(); + while (it.next()) |key| try w.print("{s}\x00", .{key.*}); + it = elf.global_symbols.keyIterator(); + while (it.next()) |key| try w.print("{s}\x00", .{key.*}); + } + { + var it = elf.sections.iterator(); + while (it.next()) |entry| { + if (entry.value_ptr.*.relocations.items.len != 0) try w.writeAll(".rela"); + try w.print(".{s}\x00", .{entry.key_ptr.*}); + } + } + + // pad to 16 bytes + try w.writeByteNTimes(0, sh_offset_aligned - sh_offset); + // mandatory null header + try w.writeStruct(std.mem.zeroes(std.elf.Elf64_Shdr)); + + // write strtab section header + { + var sect_header = std.elf.Elf64_Shdr{ + .sh_name = strtab_name, + .sh_type = std.elf.SHT_STRTAB, + .sh_flags = 0, + .sh_addr = 0, + .sh_offset = strtab_offset, + .sh_size = elf.strtab_len, + .sh_link = 0, + .sh_info = 0, + .sh_addralign = 1, + .sh_entsize = 0, + }; + try w.writeStruct(sect_header); + } + + // write symtab section header + { + var sect_header = std.elf.Elf64_Shdr{ + .sh_name = symtab_name, + .sh_type = std.elf.SHT_SYMTAB, + .sh_flags = 0, + .sh_addr = 0, + .sh_offset = symtab_offset_aligned, + .sh_size = symtab_len, + .sh_link = strtab_index, + .sh_info = elf.local_symbols.size + 1, + .sh_addralign = 8, + .sh_entsize = @sizeOf(std.elf.Elf64_Sym), + }; + try w.writeStruct(sect_header); + } + + // remaining section headers + { + var sect_offset: u64 = @sizeOf(std.elf.Elf64_Ehdr); + var rela_sect_offset: u64 = rela_offset; + var it = elf.sections.iterator(); + while (it.next()) |entry| { + const sect = entry.value_ptr.*; + const rela_count = sect.relocations.items.len; + const rela_name_offset = if (rela_count != 0) @truncate(u32, ".rela".len) else 0; + try w.writeStruct(std.elf.Elf64_Shdr{ + .sh_name = rela_name_offset + name_offset, + .sh_type = sect.type, + .sh_flags = sect.flags, + .sh_addr = 0, + .sh_offset = sect_offset, + .sh_size = sect.data.items.len, + .sh_link = 0, + .sh_info = 0, + .sh_addralign = if (sect.flags & std.elf.SHF_EXECINSTR != 0) 16 else 1, + .sh_entsize = 0, + }); + + if (rela_count != 0) { + const size = rela_count * @sizeOf(std.elf.Elf64_Rela); + try w.writeStruct(std.elf.Elf64_Shdr{ + .sh_name = name_offset, + .sh_type = std.elf.SHT_RELA, + .sh_flags = 0, + .sh_addr = 0, + .sh_offset = rela_sect_offset, + .sh_size = rela_count * @sizeOf(std.elf.Elf64_Rela), + .sh_link = symtab_index, + .sh_info = sect.index, + .sh_addralign = 8, + .sh_entsize = @sizeOf(std.elf.Elf64_Rela), + }); + rela_sect_offset += size; + } + + sect_offset += sect.data.items.len; + name_offset += @intCast(u32, entry.key_ptr.len + ".\x00".len) + rela_name_offset; + } + } + try buf_writer.flush(); +} diff --git a/test/cases/hello world.c b/test/cases/hello world.c index 0f9454e4..0b04a794 100644 --- a/test/cases/hello world.c +++ b/test/cases/hello world.c @@ -1,16 +1,17 @@ -// our builtin macros and include searches are not advanced enough to work in the CI -#define TESTS_SKIPPED 1 - -#if 0 -#include +extern int printf(const char*, ...); +static int foo(void); int main(int argc, char **argv) { + return foo(); +} + +static int foo(void) { printf("Hello, world!\n"); return 0; } -#define EXPECTED_ERRORS "warning: '__WORDSIZE' macro redefined" \ - "warning: '__WORDSIZE_TIME64_COMPAT32' macro redefined" \ - "warning: '__SYSCALL_WORDSIZE' macro redefined" \ - "warning: '__LDOUBLE_REDIRECTS_TO_FLOAT128_ABI' macro redefined" -#endif +#if defined __linux__ && defined __x86_64__ +#define EXPECTED_OUTPUT "Hello, world!\n" +#else +#define TESTS_SKIPPED 1 +#endif \ No newline at end of file diff --git a/test/runner.zig b/test/runner.zig index dea50ffd..130d4f0f 100644 --- a/test/runner.zig +++ b/test/runner.zig @@ -1,6 +1,7 @@ const std = @import("std"); const print = std.debug.print; const aro = @import("aro"); +const Codegen = aro.Codegen; const Tree = aro.Tree; const Token = Tree.Token; const NodeIndex = Tree.NodeIndex; @@ -19,8 +20,8 @@ pub fn main() !void { var args = try std.process.argsAlloc(gpa); defer std.process.argsFree(gpa, args); - if (args.len != 2) { - print("expected test case directory as only argument\n", .{}); + if (args.len != 3) { + print("expected test case directory and zig executable as only arguments\n", .{}); return error.InvalidArguments; } @@ -74,6 +75,13 @@ pub fn main() !void { break :blk source; }; + // apparently we can't use setAstCwd without libc on windows yet + const win = @import("builtin").os.tag == .windows; + var tmp_dir = if (!win) std.testing.tmpDir(.{}); + defer if (!win) tmp_dir.cleanup(); + + if (!win) try tmp_dir.dir.setAsCwd(); + // iterate over all cases var ok_count: u32 = 0; var fail_count: u32 = 0; @@ -283,6 +291,79 @@ pub fn main() !void { } comp.renderErrors(); + + if (pp.defines.get("EXPECTED_OUTPUT")) |macro| blk: { + if (comp.diag.errors != 0) break :blk; + + if (macro != .simple) { + fail_count += 1; + progress.log("invalid EXPECTED_OUTPUT {}\n", .{macro}); + continue; + } + + if (macro.simple.tokens.len != 1 or macro.simple.tokens[0].id != .string_literal) { + fail_count += 1; + progress.log("EXPECTED_OUTPUT takes exactly one string", .{}); + continue; + } + + const start = path_buf.items.len; + defer path_buf.items.len = start; + // realistically the strings will only contain \" if any escapes so we can use Zig's string parsing + std.debug.assert((try std.zig.string_literal.parseAppend(&path_buf, pp.tokSliceSafe(macro.simple.tokens[0]))) == .success); + const expected_output = path_buf.items[start..]; + + const obj_name = "test_object.o"; + { + const obj = try Codegen.generateTree(&comp, tree); + defer obj.deinit(); + + const out_file = try std.fs.cwd().createFile(obj_name, .{}); + defer out_file.close(); + + try obj.finish(out_file); + } + + var child = try std.ChildProcess.init(&.{ args[2], "run", "-lc", obj_name }, comp.gpa); + defer child.deinit(); + + child.stdout_behavior = .Pipe; + + try child.spawn(); + + const stdout = try child.stdout.?.reader().readAllAlloc(comp.gpa, std.math.maxInt(u16)); + defer comp.gpa.free(stdout); + + switch (try child.wait()) { + .Exited => |code| if (code != 0) { + fail_count += 1; + continue; + }, + else => { + fail_count += 1; + continue; + }, + } + + if (!std.mem.eql(u8, expected_output, stdout)) { + fail_count += 1; + progress.log( + \\ + \\======= expected output ======= + \\{s} + \\ + \\=== but output does not contain it === + \\{s} + \\ + \\ + , .{ expected_output, stdout }); + break; + } + + ok_count += 1; + continue; + } + if (comp.diag.errors != 0) fail_count += 1 else ok_count += 1; }