Something went wrong. Try again.
A general purpose .. programming language written in zig ijadux2.tngl.sh/Rinnescript
zig language
Something went wrong. Try again.
30 kB · 771 lines
Zig
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772//! RinneScript -> no-libc x86_64 Linux ELF lowering (the `.rgx` backend).//!//! This is the "Direct native ELF (no libc)" path for `rinne build`. It lowers//! a deliberately small, well-defined subset of RinneScript://!//! * `fn main() int` with no parameters is the program entry point.//! * Statements: `return <expr>;` and top-level `@stdout("literal")` calls.//! (The `.{}` tuple argument of `@stdout` must be empty for now.)//! * Expressions: integer literals, `+`, `-`, unary `-`, and parentheses.//!//! `@stdout("literal")` writes its literal bytes to fd 1 via the `write`//! syscall. The program's `main` return value becomes the exit status via the//! `exit` syscall.//!//! Everything outside this subset is rejected up front with a clear diagnostic//! (`error.UnsupportedRinne`); `rinne build` then falls back to the existing//! C11 + system `cc` pipeline. Integer `{}` formatting and string args are//! planned follow-ups, deliberately not silently miscompiled.//!//! The emitted binary is a static ET_EXEC assembled by `elf_emit.zig`.const std = @import("std");const parser = @import("../script/parser.zig");const elf_emit = @import("elf_emit.zig");
pub const Error = error{ OutOfMemory, UnsupportedArch, UnsupportedRinne, InvalidProgram };
const SYS_WRITE: u32 = 1;const SYS_EXIT: u32 = 60;const STDOUT_FD: u32 = 1;
/// A little assembler: appends x86-64 bytes and records the references that/// must be patched once the final text/rodata layout is known.const Asm = struct { alloc: std.mem.Allocator, bytes: std.ArrayList(u8) = .empty, relocs: std.ArrayList(Reloc) = .empty,
const Reloc = union(enum) { /// RIP-relative disp32 of a `lea rsi,[rip+disp]` pointing at rodata. rip_rodata: struct { field_off: usize, rip_off: usize, rodata: usize }, /// rel32 of an `E8` call pointing at a text offset. call_text: struct { field_off: usize, rip_off: usize, text: usize }, };
fn init(alloc: std.mem.Allocator) Asm { return .{ .alloc = alloc }; }
fn deinit(self: *Asm) void { self.bytes.deinit(self.alloc); self.relocs.deinit(self.alloc); }
fn len(self: *const Asm) usize { return self.bytes.items.len; }
fn byte(self: *Asm, b: u8) !void { try self.bytes.append(self.alloc, b); }
fn u32le(self: *Asm, v: u32) !void { try self.bytes.appendSlice(self.alloc, &std.mem.toBytes(v)); }
fn u64le(self: *Asm, v: u64) !void { try self.bytes.appendSlice(self.alloc, &std.mem.toBytes(v)); }
// ---- x86-64 instruction helpers (64-bit operand sizes) ----
/// mov rax, imm64 fn movRaxImm64(self: *Asm, imm: u64) !void { try self.byte(0x48); try self.byte(0xB8); try self.u64le(imm); }
/// mov eax, imm32 (zero-extends into rax) fn movEaxImm32(self: *Asm, imm: u32) !void { try self.byte(0xB8); try self.u32le(imm); }
/// mov edi, imm32 (zero-extends into rdi) fn movEdiImm32(self: *Asm, imm: u32) !void { try self.byte(0xBF); try self.u32le(imm); }
/// mov edx, imm32 (zero-extends into rdx) fn movEdxImm32(self: *Asm, imm: u32) !void { try self.byte(0xBA); try self.u32le(imm); }
/// mov rdi, rax fn movRdiRax(self: *Asm) !void { try self.appendSlice(&.{ 0x48, 0x89, 0xC7 }); }
/// mov rcx, rax fn movRcxRax(self: *Asm) !void { try self.appendSlice(&.{ 0x48, 0x89, 0xC1 }); }
/// add rax, rcx fn addRaxRcx(self: *Asm) !void { try self.appendSlice(&.{ 0x48, 0x01, 0xC8 }); }
/// sub rax, rcx fn subRaxRcx(self: *Asm) !void { try self.appendSlice(&.{ 0x48, 0x29, 0xC8 }); }
/// neg rax fn negRax(self: *Asm) !void { try self.appendSlice(&.{ 0x48, 0xF7, 0xD8 }); }
fn pushRax(self: *Asm) !void { try self.byte(0x50); }
fn popRax(self: *Asm) !void { try self.byte(0x58); }
fn syscall(self: *Asm) !void { try self.appendSlice(&.{ 0x0F, 0x05 }); }
fn ret(self: *Asm) !void { try self.byte(0xC3); }
/// lea rsi, [rip + disp32]; points at rodata[`rodata`]. fn leaRsiRip(self: *Asm, rodata: usize) !void { try self.appendSlice(&.{ 0x48, 0x8D, 0x35 }); const field_off = self.len(); try self.u32le(0); try self.relocs.append(self.alloc, .{ .rip_rodata = .{ .field_off = field_off, .rip_off = self.len(), .rodata = rodata } }); }
/// call <absolute text offset> as `E8 rel32`. fn callText(self: *Asm, text: usize) !void { try self.byte(0xE8); const field_off = self.len(); try self.u32le(0); try self.relocs.append(self.alloc, .{ .call_text = .{ .field_off = field_off, .rip_off = self.len(), .text = text } }); }
fn appendSlice(self: *Asm, s: []const u8) !void { try self.bytes.appendSlice(self.alloc, s); }};
/// Deduplicating string table forming the `.rodata` region (starts with a/// reserved scratch tail only used by future formatting; currently empty).const StrTab = struct { alloc: std.mem.Allocator, strings: std.ArrayList([]const u8) = .empty,
fn init(alloc: std.mem.Allocator) StrTab { return .{ .alloc = alloc }; }
fn deinit(self: *StrTab) void { for (self.strings.items) |s| self.alloc.free(s); self.strings.deinit(self.alloc); }
/// Offset within the rodata buffer of the string interned (added if new). fn intern(self: *StrTab, s: []const u8) !usize { for (self.strings.items, 0..) |existing, i| { if (std.mem.eql(u8, existing, s)) return offsetOf(self, i); } const copy = try self.alloc.dupe(u8, s); const idx = self.strings.items.len; try self.strings.append(self.alloc, copy); return offsetOf(self, idx); }
fn offsetOf(self: *const StrTab, idx: usize) usize { var off: usize = 0; for (self.strings.items[0..idx]) |s| { off += s.len + 1; // NUL-terminated } return off; }
/// Serialize as a NUL-terminated byte set. fn build(self: *const StrTab, out_alloc: std.mem.Allocator) ![]u8 { var buf: std.ArrayList(u8) = .empty; for (self.strings.items) |s| { try buf.appendSlice(out_alloc, s); try buf.append(out_alloc, 0); } return buf.toOwnedSlice(out_alloc); }};
/// `_start` fixed-length preamble:/// call rg_main ; 48 ... 5 bytes/// mov rdi, rax ; 3 bytes/// mov eax, 60 ; 5 bytes/// syscall ; 2 bytesconst START_LEN = 5 + 3 + 5 + 2;
fn emitStart(a: *Asm, main_off: usize) Error!void { try a.callText(main_off); try a.movRdiRax(); try a.movEaxImm32(SYS_EXIT); try a.syscall();}
fn findMain(prog: *const parser.Program) ?*const parser.FnDecl { for (prog.fn_decls) |*fd| { if (std.mem.eql(u8, fd.name, "main")) return fd; } return null;}
fn failCtx(err_buf: ?ErrBuf, msg: []const u8) Error { if (err_buf) |eb| eb.write("{s}\n", .{msg}); return error.UnsupportedRinne;}
/// Raise an `UnsupportedRinne` and record a reason into `err_buf`.fn unsupported(err_buf: ?ErrBuf, comptime fmt: []const u8, args: anytype) Error { if (err_buf) |eb| eb.write(fmt, args); return error.UnsupportedRinne;}
/// A write-through reporter for `UnsupportedRinne` diagnostics.pub const ErrBuf = struct { alloc: std.mem.Allocator, out: *std.ArrayList(u8),
fn write(self: ErrBuf, comptime fmt: []const u8, args: anytype) void { const s = std.fmt.allocPrint(self.alloc, fmt, args) catch return; defer self.alloc.free(s); self.out.appendSlice(self.alloc, s) catch {}; }};
/// Lower `prog` into complete `.rgx` executable bytes. On `error.UnsupportedRinne`,/// `err_buf` (if non-null) receives a human-readable reason.pub fn lower(alloc: std.mem.Allocator, prog: *const parser.Program, err_buf: ?ErrBuf) Error![]u8 { const main_fn = findMain(prog) orelse return failCtx(err_buf, "native .rgx build needs a 'fn main() int' entry point");
var a = Asm.init(alloc); defer a.deinit(); var strs = StrTab.init(alloc); defer strs.deinit();
// Text layout: [ _start ][ rg_main ]. `_start` is fixed-length so rg_main // begins immediately after it. const main_off = START_LEN; try emitStart(&a, main_off);
const main_len = try emitMain(&a, &strs, main_fn, err_buf); _ = main_len;
const rodata = try strs.build(alloc); defer alloc.free(rodata);
var img = elf_emit.Image{ .text = a.bytes.items, .rodata = rodata, }; _ = elf_emit.layout(&img); try patchRelocs(&a, &img);
return elf_emit.assemble(alloc, &img);}
/// Emit the body of `fn main`. Returns its length in bytes.fn emitMain(a: *Asm, strs: *StrTab, main_fn: *const parser.FnDecl, err_buf: ?ErrBuf) Error!usize { const start = a.len();
// Entry point must be `fn main() int` with no parameters. if (main_fn.params.len != 0) return unsupported(err_buf, "native .rgx build needs 'fn main()' with no parameters\n", .{});
for (main_fn.body.stmts) |*stmt| switch (stmt.*) { .expr_stmt => |e| { if (e != .call_directive or !std.mem.eql(u8, e.call_directive.name, "stdout")) return unsupported(err_buf, "only '@stdout' calls are supported in native build\n", .{}); try emitStdout(a, strs, e.call_directive.args, err_buf); }, .return_stmt => |r| { if (r) |e| { if (!isIntExpr(e)) return unsupported(err_buf, "only integer expressions are supported in 'fn main'\n", .{}); try lowerInt(a, e); } }, else => return unsupported(err_buf, "only 'return' and '@stdout' are supported in 'fn main'\n", .{}), };
// Fall-through path must still produce a value; rax holds whatever the // last statement left, plus an explicit ret to unwind. try a.ret(); return a.len() - start;}
/// Emit `write(fd=1, "content", n)`.fn emitStdout(a: *Asm, strs: *StrTab, args: []const parser.Expr, err_buf: ?ErrBuf) Error!void { // Expected shape: @stdout("literal", .{}) if (args.len != 2) return unsupported(err_buf, "@stdout expects a format string and a tuple\n", .{}); const fmt = args[0]; const tuple = args[1]; if (fmt != .string_lit) return unsupported(err_buf, "@stdout expects a string literal format\n", .{}); if (tuple != .array_lit) return unsupported(err_buf, "@stdout expects a tuple argument\n", .{}); if (tuple.array_lit.len != 0) return unsupported(err_buf, "integer '{{}}' formatting is not yet supported in native build\n", .{});
const s = fmt.string_lit; const ro = try strs.intern(s); try emitWriteLiteral(a, ro, s.len);}
/// `write(fd=1, buf=rodata[ro], len)`.fn emitWriteLiteral(a: *Asm, rodata: usize, len: usize) Error!void { try a.movEaxImm32(SYS_WRITE); try a.movEdiImm32(STDOUT_FD); try a.leaRsiRip(rodata); try a.movEdxImm32(@intCast(len)); try a.syscall();}
fn lowerInt(a: *Asm, e: parser.Expr) Error!void { switch (e) { .int_lit => |v| try a.movRaxImm64(@bitCast(v)), .unary => |u| { try lowerInt(a, u.operand.*); if (u.op == .minus) try a.negRax(); }, .binary => |b| { try lowerInt(a, b.lhs.*); try a.pushRax(); try lowerInt(a, b.rhs.*); try a.movRcxRax(); try a.popRax(); switch (b.op) { .plus => try a.addRaxRcx(), .minus => try a.subRaxRcx(), else => return error.InvalidProgram, } }, else => return error.InvalidProgram, }}
fn isIntExpr(e: parser.Expr) bool { switch (e) { .int_lit => return true, .unary => |u| return u.op == .minus and isIntExpr(u.operand.*), .binary => |b| { if (b.op == .plus or b.op == .minus) return isIntExpr(b.lhs.*) and isIntExpr(b.rhs.*); return false; }, else => return false, }}
/// Patch recorded relocations using the final layout. All arithmetic is done/// in offsets; `LOAD_BASE` cancels out for both text and rodata targets.fn patchRelocs(a: *Asm, img: *elf_emit.Image) Error!void { const text_off = elf_emit.ELF_HEADER_SIZE + elf_emit.PHDR_COUNT * elf_emit.PHDR_SIZE; // rodata begins at text_off + (rounded text length); recompute same as layout. for (a.relocs.items) |rel| switch (rel) { .call_text => |c| { // disp = text[c.text] - text[c.rip_off] const disp = @as(i64, @intCast(c.text)) - @as(i64, @intCast(c.rip_off)); writeI32(a.bytes.items, c.field_off, @intCast(disp)); }, .rip_rodata => |r| { // target = LOAD + text_off + (rodata_off - text_off) + r.rodata // rip = LOAD + text_off + r.rip_off const rodata_off = alignA(text_off + img.text.len, 16); const target = rodata_off - text_off + r.rodata; const disp = @as(i64, @intCast(target)) - @as(i64, @intCast(r.rip_off)); writeI32(a.bytes.items, r.field_off, @intCast(disp)); }, };}
fn alignA(v: usize, a: usize) usize { return (v + a - 1) & ~(a - 1);}
fn writeI32(bytes: []u8, off: usize, val: i32) void { const b = std.mem.toBytes(val); @memcpy(bytes[off .. off + 4], &b);}
const testing = std.testing;
/// Parse, lower, write+chmod, run, and check the resulting `.rgx` binary.fn runCompiled(alloc: std.mem.Allocator, src: []const u8, expect_exit: u8, expect_out: []const u8) !void { var p = parser.Parser.init(alloc, src); defer p.deinit(); var prog = try p.parseProgram(); defer prog.deinit(alloc);
var err_buf: std.ArrayList(u8) = .empty; defer err_buf.deinit(alloc);
const blob = try lower(alloc, &prog, .{ .alloc = alloc, .out = &err_buf }); defer alloc.free(blob);
const io = std.testing.io; const path = "/tmp/rg_rinnescript_native"; const cwd = std.Io.Dir.cwd(); cwd.writeFile(io, .{ .sub_path = path, .data = blob }) catch return error.TestUnexpectedResult; defer { cwd.deleteFile(io, path) catch {}; } const path_z = try alloc.dupeZ(u8, path); defer alloc.free(path_z); try testing.expect(std.os.linux.chmod(path_z, 0o755) == 0);
var child = std.process.spawn(io, .{ .argv = &.{path}, .stdout = .pipe, .stderr = .ignore, }) catch return error.TestUnexpectedResult;
// Read the (tiny) stdout while the child runs, then wait. var rbuf: [1024]u8 = undefined; const out_file = child.stdout orelse return error.TestUnexpectedResult; var rr = out_file.reader(io, &rbuf); const got = rr.interface.readSliceShort(rbuf[0..]) catch return error.TestUnexpectedResult; _ = &out_file;
const term = child.wait(io) catch return error.TestUnexpectedResult; switch (term) { .exited => |code| { if (expect_exit != 0) try testing.expectEqual(expect_exit, code); if (expect_out.len > 0) try testing.expect(std.mem.eql(u8, rbuf[0..got], expect_out)); }, else => return error.TestUnexpectedResult, }}
test "rgx: hello world writes via syscall and exits 0" { const src = \\fn main() int { \\ @stdout("hello native\n", .{}); \\ @stdout("world\n", .{}); \\ return 0; \\} ; try runCompiled(testing.allocator, src, 0, "hello native\nworld\n");}
test "rgx: integer expression becomes exit status" { const src = \\fn main() int { \\ return 1 + 2 + 3; \\} ; try runCompiled(testing.allocator, src, 6, "");}
test "rgx: integer args to @stdout are rejected (not miscompiled)" { const src = \\fn main() int { \\ @stdout("x={}\n", .{4}); \\ return 0; \\} ; var p = parser.Parser.init(testing.allocator, src); defer p.deinit(); var prog = try p.parseProgram(); defer prog.deinit(testing.allocator); var err_buf: std.ArrayList(u8) = .empty; defer err_buf.deinit(testing.allocator); const res = lower(testing.allocator, &prog, .{ .alloc = testing.allocator, .out = &err_buf }); try testing.expectError(error.UnsupportedRinne, res);}
// ---------------------------------------------------------------------------// Linked assembly backend (`--native`, links against libc & other .so files).//// Rather than hand-assembling a relocation-free ELF, this emits x86-64 AT&T// assembly and lets the system linker (`cc -nostartfiles`) assemble + link it// against libc and any `-l`/`-L` the user passes. `_start` calls the Rinne// `main`, then hands the result to libc `exit`; `@stdout` goes through libc// `write`. This is the path that supports "link with libc and other libs".// ---------------------------------------------------------------------------
/// Lower `prog` into x86-64 AT&T assembly source. On `error.UnsupportedRinne`,/// `err_buf` (if non-null) receives a human-readable reason.pub fn lowerAssembly(alloc: std.mem.Allocator, prog: *const parser.Program, err_buf: ?ErrBuf) Error![]u8 { const main_fn = findMain(prog) orelse return failCtx(err_buf, "native build needs a 'fn main() int' entry point");
var buf: std.ArrayList(u8) = .empty; defer buf.deinit(alloc);
// Externs resolved against libc / linked libs by the system linker. try wline(&buf, alloc, " .extern write"); try wline(&buf, alloc, " .extern exit");
var seen: std.ArrayList([]const u8) = .empty; defer seen.deinit(alloc); try collectLibcallSyms(&seen, alloc, main_fn); for (seen.items) |sym| { try wprint(&buf, alloc, " .extern {s}", .{sym}); }
try buf.appendSlice(alloc, \\ .text \\ .globl _start \\_start: \\ call rg_main \\ mov %rax, %rdi \\ call exit \\ );
try emitMainAsm(&buf, alloc, main_fn, err_buf);
try buf.appendSlice(alloc, \\ .section .rodata \\ ); try appendRosym(&buf, alloc, main_fn); // rodata labels + bytes
return buf.toOwnedSlice(alloc);}
/// Emit `rg_main` body (statements), leaving the result in rax, then `ret`.fn emitMainAsm(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, main_fn: *const parser.FnDecl, err_buf: ?ErrBuf) Error!void { try buf.appendSlice(alloc, "rg_main:\n"); var str_i: usize = 0; for (main_fn.body.stmts) |*stmt| switch (stmt.*) { .expr_stmt => |e| { if (e != .call_directive) return unsupported(err_buf, "only '@stdout' and '@libcall' calls are supported in 'fn main'\n", .{}); if (std.mem.eql(u8, e.call_directive.name, "stdout")) { emitStdoutAsm(buf, alloc, &str_i, e.call_directive.args, err_buf) catch |er| switch (er) { error.UnsupportedRinne => return er, else => return er, }; } else if (std.mem.eql(u8, e.call_directive.name, "libcall")) { try emitLibcallAsm(buf, alloc, e.call_directive.args, err_buf); } else { return unsupported(err_buf, "only '@stdout' and '@libcall' calls are supported in 'fn main'\n", .{}); } }, .return_stmt => |r| { if (r) |ex| { if (ex == .call_directive and std.mem.eql(u8, ex.call_directive.name, "libcall")) { try emitLibcallAsm(buf, alloc, ex.call_directive.args, err_buf); } else { if (!isIntExpr(ex)) return unsupported(err_buf, "only integer expressions (or @libcall) are supported in 'return'\n", .{}); try lowerIntAsm(buf, alloc, ex); } } }, else => return unsupported(err_buf, "only 'return', '@stdout' and '@libcall' are supported in 'fn main'\n", .{}), }; try buf.appendSlice(alloc, " ret\n");}
/// Collect the unique `.extern` symbol names referenced via `@libcall`.fn collectLibcallSyms(seen: *std.ArrayList([]const u8), alloc: std.mem.Allocator, main_fn: *const parser.FnDecl) Error!void { for (main_fn.body.stmts) |*stmt| switch (stmt.*) { .expr_stmt => |e| { if (e == .call_directive and std.mem.eql(u8, e.call_directive.name, "libcall")) { if (e.call_directive.args.len == 2 and e.call_directive.args[0] == .string_lit) try addSym(seen, alloc, e.call_directive.args[0].string_lit); } }, .return_stmt => |r| { if (r) |ex| { if (ex == .call_directive and std.mem.eql(u8, ex.call_directive.name, "libcall")) { if (ex.call_directive.args.len == 2 and ex.call_directive.args[0] == .string_lit) try addSym(seen, alloc, ex.call_directive.args[0].string_lit); } } }, else => {}, };}
fn addSym(seen: *std.ArrayList([]const u8), alloc: std.mem.Allocator, sym: []const u8) Error!void { for (seen.items) |s| if (std.mem.eql(u8, s, sym)) return; try seen.append(alloc, sym);}
/// Emit `call <sym>` with the given integer args in System V registers.fn emitLibcallAsm(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, args: []const parser.Expr, err_buf: ?ErrBuf) Error!void { // Expected shape: @libcall("symbol", .{arg1, arg2, ...}). if (args.len != 2) return unsupported(err_buf, "@libcall expects a symbol name and a tuple of integer args (e.g. a call like @libcall(\"getpid\", tuple) )\n", .{}); if (args[0] != .string_lit) return unsupported(err_buf, "@libcall expects a string symbol name as its first argument\n", .{}); if (args[1] != .array_lit) return unsupported(err_buf, "@libcall expects a tuple of arguments as its second argument\n", .{});
const sym = args[0].string_lit; const tuple = args[1].array_lit; const max_args = 6; // rdi, rsi, rdx, rcx, r8, r9 if (tuple.len > max_args) return unsupported(err_buf, "@libcall supports at most 6 integer arguments in native build\n", .{});
for (tuple, 0..) |arg_expr, i| { if (!isIntExpr(arg_expr)) return unsupported(err_buf, "@libcall arguments must be integer expressions in native build\n", .{}); const reg = svereg(alloc, i) catch return error.OutOfMemory; defer alloc.free(reg); try lowerIntAsm(buf, alloc, arg_expr); try wprint(buf, alloc, " mov %rax, %{s}", .{reg}); }
// Result lands in rax (System V). The caller may use or discard it. try wprint(buf, alloc, " call {s}", .{sym});}
/// The i-th System V integer argument register (0-based).fn svereg(alloc: std.mem.Allocator, i: usize) ![]const u8 { const reg = switch (i) { 0 => "rdi", 1 => "rsi", 2 => "rdx", 3 => "rcx", 4 => "r8", 5 => "r9", else => return error.InvalidProgram, }; return alloc.dupe(u8, reg);}
/// Emit a `write(fd=1, "literal", len)` through libc's `write`.fn emitStdoutAsm(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, str_i: *usize, args: []const parser.Expr, err_buf: ?ErrBuf) Error!void { if (args.len != 2) return unsupported(err_buf, "@stdout expects a format string and a tuple\n", .{}); const fmt = args[0]; const tuple = args[1]; if (fmt != .string_lit) return unsupported(err_buf, "@stdout expects a string literal format\n", .{}); if (tuple != .array_lit) return unsupported(err_buf, "@stdout expects a tuple argument\n", .{}); if (tuple.array_lit.len != 0) return unsupported(err_buf, "integer '{{}}' formatting is not yet supported in native build\n", .{});
const s = fmt.string_lit; const label = str_i.*;
try wline(buf, alloc, " mov $1, %eax"); try wline(buf, alloc, " mov $1, %edi"); try wprint(buf, alloc, " lea .Ls{d}(%rip), %rsi", .{label}); try wprint(buf, alloc, " mov ${d}, %edx", .{s.len}); try wline(buf, alloc, " call write"); str_i.* += 1;}
/// Lower an integer expression to assembly; result in rax.fn lowerIntAsm(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, e: parser.Expr) Error!void { switch (e) { .int_lit => |v| try wprint(buf, alloc, " mov ${d}, %rax", .{v}), .unary => |u| { try lowerIntAsm(buf, alloc, u.operand.*); if (u.op == .minus) try wline(buf, alloc, " neg %rax"); }, .binary => |b| { try lowerIntAsm(buf, alloc, b.lhs.*); try wline(buf, alloc, " push %rax"); try lowerIntAsm(buf, alloc, b.rhs.*); try wline(buf, alloc, " mov %rax, %rcx"); try wline(buf, alloc, " pop %rax"); switch (b.op) { .plus => try wline(buf, alloc, " add %rcx, %rax"), .minus => try wline(buf, alloc, " sub %rcx, %rax"), else => return error.InvalidProgram, } }, else => return error.InvalidProgram, }}
/// Append `.rodata` string labels/bytes collected by `emitStdoutAsm`. Emits/// byte values explicitly to avoid escaping/quoting surprises.fn appendRosym(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, main_fn: *const parser.FnDecl) Error!void { var str_i: usize = 0; for (main_fn.body.stmts) |*stmt| switch (stmt.*) { .expr_stmt => |e| { if (e == .call_directive and std.mem.eql(u8, e.call_directive.name, "stdout")) { const fmt = e.call_directive.args[0]; if (fmt == .string_lit) { try appendLabel(buf, alloc, str_i); try buf.appendSlice(alloc, " .byte "); const s = fmt.string_lit; for (s, 0..) |c, i| { if (i > 0) try buf.appendSlice(alloc, ","); const b = std.fmt.allocPrint(alloc, "{d}", .{c}) catch return error.OutOfMemory; defer alloc.free(b); try buf.appendSlice(alloc, b); } try buf.appendSlice(alloc, "\n"); str_i += 1; } } }, else => {}, };}
/// Append a `.rodata` string label line (`.Ls{N}:`).fn appendLabel(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, i: usize) Error!void { const s = std.fmt.allocPrint(alloc, ".Ls{d}:\n", .{i}) catch return error.OutOfMemory; defer alloc.free(s); try buf.appendSlice(alloc, s);}
/// Append a literal line (already newline-terminated) to the assembly buffer.fn wline(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, s: []const u8) Error!void { try wprint(buf, alloc, "{s}\n", .{s});}
/// Append a formatted string (newline appended) to the assembly buffer.fn wprint(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, comptime fmt: []const u8, args: anytype) Error!void { const s = std.fmt.allocPrint(alloc, fmt ++ "\n", args) catch return error.OutOfMemory; defer alloc.free(s); try buf.appendSlice(alloc, s);}
test "rgx assembly backend: emits linked libc skeleton + rodata bytes" { const src = \\fn main() int { \\ @stdout("hi\n", .{}); \\ return 3; \\} ; var p = parser.Parser.init(testing.allocator, src); defer p.deinit(); var prog = try p.parseProgram(); defer prog.deinit(testing.allocator); var err_buf: std.ArrayList(u8) = .empty; defer err_buf.deinit(testing.allocator); const asm_src = try lowerAssembly(testing.allocator, &prog, .{ .alloc = testing.allocator, .out = &err_buf }); defer testing.allocator.free(asm_src); try testing.expect(std.mem.indexOf(u8, asm_src, ".extern write") != null); try testing.expect(std.mem.indexOf(u8, asm_src, ".extern exit") != null); try testing.expect(std.mem.indexOf(u8, asm_src, "call rg_main") != null); try testing.expect(std.mem.indexOf(u8, asm_src, "call write") != null); try testing.expect(std.mem.indexOf(u8, asm_src, ".byte 104,105,10") != null); // "hi\n"}
test "rgx assembly backend: @libcall emits extern + sysv arg registers + call" { const src = \\fn main() int { \\ return @libcall("addtwo", .{3, 4}); \\} ; var p = parser.Parser.init(testing.allocator, src); defer p.deinit(); var prog = try p.parseProgram(); defer prog.deinit(testing.allocator); var err_buf: std.ArrayList(u8) = .empty; defer err_buf.deinit(testing.allocator); const asm_src = try lowerAssembly(testing.allocator, &prog, .{ .alloc = testing.allocator, .out = &err_buf }); defer testing.allocator.free(asm_src); try testing.expect(std.mem.indexOf(u8, asm_src, ".extern addtwo") != null); try testing.expect(std.mem.indexOf(u8, asm_src, "mov %rax, %rdi") != null); // arg0 try testing.expect(std.mem.indexOf(u8, asm_src, "mov %rax, %rsi") != null); // arg1 try testing.expect(std.mem.indexOf(u8, asm_src, "call addtwo") != null);}