Something went wrong. Try again.
A general purpose .. programming language written in zig ijadux2.tngl.sh/Rinnescript
zig language
Something went wrong. Try again.
49 kB · 1229 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230//! RinneScript -> no-libc 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.//!//! Text is emitted for the *build host architecture*: x86-64 and AArch64//! Linux are supported (`elf_emit.zig` selects the ELF machine type). The//! `--native` (linked) path additionally emits GNU assembly for the same two//! architectures and hands it to the system linker.//!//! 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 builtin = @import("builtin");const parser = @import("../script/parser.zig");const elf_emit = @import("elf_emit.zig");
pub const Error = error{ OutOfMemory, UnsupportedArch, UnsupportedRinne, InvalidProgram };
/// Native instruction set selected at build time from the host CPU.pub const Arch = enum { x86_64, aarch64 };
fn hostArch() Error!Arch { return switch (builtin.cpu.arch) { .x86_64 => .x86_64, .aarch64 => .aarch64, else => return error.UnsupportedArch, };}
const SYS_WRITE: u32 = 1;const SYS_EXIT: u32 = 60;const STDOUT_FD: u32 = 1;const ARM64_SYS_WRITE: u32 = 64;const ARM64_SYS_EXIT: u32 = 93;
/// A little assembler: appends instruction bytes for the selected `arch` and/// records the references that must be patched once the final text/rodata/// layout is known.const Asm = struct { alloc: std.mem.Allocator, arch: Arch, bytes: std.ArrayList(u8) = .empty, relocs: std.ArrayList(Reloc) = .empty,
const Reloc = union(enum) { // ---- x86-64 (RIP-relative) ---- /// 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 }, // ---- AArch64 (PC-relative) ---- /// imm26 of a `bl` pointing at a text offset. arm64_bl_text: struct { field_off: usize, pc_off: usize, text: usize }, /// immhi:immlo (page delta) of an `adrp x1, #0` pointing at rodata. arm64_adrp_rodata: struct { field_off: usize, pc_off: usize, rodata: usize }, /// imm12 (page low 12 bits) of an `add x1, x1, #0` pointing at rodata. arm64_add_lo12: struct { field_off: usize, rodata: usize }, };
fn init(alloc: std.mem.Allocator, arch: Arch) Asm { return .{ .alloc = alloc, .arch = arch }; }
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); }
// ---- AArch64 instruction helpers (64-bit fixed-width instructions) ----
/// `movz rd, #imm16, lsl #(hw*16)` (zeroes the other lanes). fn arm64Movz(self: *Asm, rd: u5, imm: u16, hw: u32) !void { try self.u32le(0xD2800000 | (hw << 21) | (@as(u32, imm) << 5) | rd); }
/// `movk rd, #imm16, lsl #(hw*16)` (keeps the other lanes). fn arm64Movk(self: *Asm, rd: u5, imm: u16, hw: u32) !void { try self.u32le(0xF2800000 | (hw << 21) | (@as(u32, imm) << 5) | rd); }
/// `mov rd, #imm` as a movz/movk chain (handles arbitrary 64-bit values). fn arm64MovImm(self: *Asm, rd: u5, imm: u64) !void { var hw: u6 = 0; var first = true; while (!first or (imm >> (16 * hw)) != 0) { const chunk: u16 = @intCast((imm >> (16 * hw)) & 0xffff); if (chunk != 0 or first) { if (first) try self.arm64Movz(rd, chunk, hw) else try self.arm64Movk(rd, chunk, hw); first = false; } if (hw == 3) break; // all 64 bits visited; shifting by 64 is UB hw += 1; } }
/// `svc #0` (kernel syscall entry). fn arm64Svc(self: *Asm) !void { try self.u32le(0xD4000001); }
/// `ret` (x30). fn arm64Ret(self: *Asm) !void { try self.u32le(0xD65F03C0); }
/// `str x0, [sp, #-16]!` (pre-index; keeps sp 16-byte aligned). fn arm64PushX0(self: *Asm) !void { try self.u32le(0xF81F0FE0); }
/// `ldr x1, [sp], #16` (post-index; unwinds the push). fn arm64PopToX1(self: *Asm) !void { try self.u32le(0xF84107E1); }
/// `str x30, [sp, #-16]!` (callee-save of the link register). fn arm64PushLR(self: *Asm) !void { try self.u32le(0xF81F0FFE); }
/// `ldr x30, [sp], #16` (restore of the link register). fn arm64PopLR(self: *Asm) !void { try self.u32le(0xF84107FE); }
/// `add x0, x1, x0`. fn arm64AddX1X0(self: *Asm) !void { try self.u32le(0x8B000020); }
/// `sub x0, x1, x0`. fn arm64SubX1X0(self: *Asm) !void { try self.u32le(0xCB000020); }
/// `neg x0, x0` (sub x0, xzr, x0). fn arm64NegX0(self: *Asm) !void { try self.u32le(0xCB0003E0); }
/// `bl <absolute text offset>` as `bl #0` + record the arm64 imm26 reloc. fn arm64BlText(self: *Asm, text: usize) !void { try self.u32le(0x94000000); const field_off = self.len() - 4; try self.relocs.append(self.alloc, .{ .arm64_bl_text = .{ .field_off = field_off, .pc_off = field_off, .text = text } }); }
/// `adrp x1, #0`; points at the page containing rodata[`rodata`]. fn arm64AdrpX1(self: *Asm, rodata: usize) !void { try self.u32le(0x90000001); const field_off = self.len() - 4; try self.relocs.append(self.alloc, .{ .arm64_adrp_rodata = .{ .field_off = field_off, .pc_off = field_off, .rodata = rodata } }); }
/// `add x1, x1, #0`; patched with the rodata address's low 12 bits. fn arm64AddX1Lo12(self: *Asm, rodata: usize) !void { try self.u32le(0x91000021); try self.relocs.append(self.alloc, .{ .arm64_add_lo12 = .{ .field_off = self.len() - 4, .rodata = rodata } }); }};
/// 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 (x86-64):/// 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;
/// AArch64 `_start` fixed-length preamble:/// bl rg_main ; 4 bytes/// movz x8, #93 ; 4 bytes/// svc #0 ; 4 bytesconst START_LEN_ARM64 = 12;
fn startLen(arch: Arch) usize { return switch (arch) { .x86_64 => START_LEN, .aarch64 => START_LEN_ARM64, };}
fn emitStart(a: *Asm, main_off: usize) Error!void { switch (a.arch) { .x86_64 => { try a.callText(main_off); try a.movRdiRax(); try a.movEaxImm32(SYS_EXIT); try a.syscall(); }, .aarch64 => { // rg_main returns the exit status in x0 (AArch64 return register), // so `_start` only needs the exit syscall after the call. try a.arm64BlText(main_off); try a.arm64MovImm(8, ARM64_SYS_EXIT); try a.arm64Svc(); }, }}
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"); const arch = try hostArch();
var a = Asm.init(alloc, arch); 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 = startLen(arch); 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", .{});
// AArch64 `bl`/`ret` go through the link register (x30), which nested // `bl` calls inside the body would clobber, so rg_main saves/restores it // around the body. switch (a.arch) { .x86_64 => {}, .aarch64 => try a.arm64PushLR(), }
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; the return register holds // whatever the last statement left, plus an explicit ret to unwind. switch (a.arch) { .x86_64 => try a.ret(), .aarch64 => { try a.arm64PopLR(); try a.arm64Ret(); }, } 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)` for the host arch.fn emitWriteLiteral(a: *Asm, rodata: usize, len: usize) Error!void { switch (a.arch) { .x86_64 => { try a.movEaxImm32(SYS_WRITE); try a.movEdiImm32(STDOUT_FD); try a.leaRsiRip(rodata); try a.movEdxImm32(@intCast(len)); try a.syscall(); }, .aarch64 => { try a.arm64MovImm(8, ARM64_SYS_WRITE); try a.arm64MovImm(0, STDOUT_FD); try a.arm64AdrpX1(rodata); try a.arm64AddX1Lo12(rodata); try a.arm64MovImm(2, len); try a.arm64Svc(); }, }}
/// Lower an integer expression; result in the arch return register (rax/x0).fn lowerInt(a: *Asm, e: parser.Expr) Error!void { switch (a.arch) { .x86_64 => try lowerIntX64(a, e), .aarch64 => try lowerIntArm64(a, e), }}
fn lowerIntX64(a: *Asm, e: parser.Expr) Error!void { switch (e) { .int_lit => |v| try a.movRaxImm64(@bitCast(v)), .unary => |u| { try lowerIntX64(a, u.operand.*); if (u.op == .minus) try a.negRax(); }, .binary => |b| { try lowerIntX64(a, b.lhs.*); try a.pushRax(); try lowerIntX64(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 lowerIntArm64(a: *Asm, e: parser.Expr) Error!void { switch (e) { .int_lit => |v| try a.arm64MovImm(0, @bitCast(v)), .unary => |u| { try lowerIntArm64(a, u.operand.*); if (u.op == .minus) try a.arm64NegX0(); }, .binary => |b| { try lowerIntArm64(a, b.lhs.*); try a.arm64PushX0(); try lowerIntArm64(a, b.rhs.*); try a.arm64PopToX1(); switch (b.op) { .plus => try a.arm64AddX1X0(), .minus => try a.arm64SubX1X0(), 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 x86-64 arithmetic/// is done in offsets; `LOAD_BASE` cancels out for both text and rodata/// targets. AArch64 uses the absolute virtual addresses (LOAD_BASE is/// page-aligned, so page deltas are just bare offset differences).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; switch (a.arch) { .x86_64 => { // 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)); }, else => unreachable, }; }, .aarch64 => { for (a.relocs.items) |rel| switch (rel) { .arm64_bl_text => |c| { // imm26 = (target - pc) >> 2, within ±128 MiB. const pc = elf_emit.LOAD_BASE + @as(u64, text_off + c.pc_off); const target = elf_emit.LOAD_BASE + @as(u64, text_off + c.text); const imm26: u32 = @intCast((target - pc) >> 2); const instr = readU32(a.bytes.items, c.field_off); writeU32(a.bytes.items, c.field_off, (instr & 0xFC000000) | (imm26 & 0x03FFFFFF)); }, .arm64_adrp_rodata => |r| { // imm21 = (page(rodata) - page(pc)) >> 12 -> immhi:immlo. const rodata_off = alignA(text_off + img.text.len, 16); const pc = elf_emit.LOAD_BASE + @as(u64, text_off + r.pc_off); const target = elf_emit.LOAD_BASE + @as(u64, rodata_off + r.rodata); const dp: i64 = @as(i64, @intCast(target & ~@as(u64, 0xfff))) - @as(i64, @intCast(pc & ~@as(u64, 0xfff))); const imm21: u32 = @bitCast(@as(i32, @intCast(dp >> 12))); const immlo = imm21 & 0x3; const immhi = (imm21 >> 2) & 0x7FFFF; const instr = readU32(a.bytes.items, r.field_off); writeU32(a.bytes.items, r.field_off, (instr & 0x9E00001F) | (immlo << 29) | (immhi << 5)); }, .arm64_add_lo12 => |r| { // imm12 = rodata & 0xfff (page low bits of the target). const rodata_off = alignA(text_off + img.text.len, 16); const target = elf_emit.LOAD_BASE + @as(u64, rodata_off + r.rodata); const lo12: u32 = @intCast(target & 0xfff); const instr = readU32(a.bytes.items, r.field_off); writeU32(a.bytes.items, r.field_off, (instr & ~@as(u32, 0x003FFC00)) | (lo12 << 10)); }, else => unreachable, }; }, }}
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);}
fn readU32(bytes: []const u8, off: usize) u32 { var b: [4]u8 = undefined; @memcpy(&b, bytes[off .. off + 4]); return std.mem.readInt(u32, &b, .little);}
fn writeU32(bytes: []u8, off: usize, val: u32) 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 GNU assembly// for the host architecture (x86-64 AT&T syntax, or AArch64 syntax) 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 GNU assembly source for the build-host architecture. 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 { switch (try hostArch()) { .x86_64 => return lowerAssemblyX64(alloc, prog, err_buf), .aarch64 => return lowerAssemblyArm64(alloc, prog, err_buf), }}
/// x86-64 AT&T assembly emitter.fn lowerAssemblyX64(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, }}
/// AArch64 assembly emitter: `_start` calls `rg_main` (result in x0) and/// forwards it to libc `exit`; statements and integer expressions follow the/// AArch64 calling convention (x0..x7 for args, x0 for the result).fn lowerAssemblyArm64(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: \\ bl rg_main \\ bl exit \\ );
try emitMainAsmArm64(&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 for AArch64 (statements), leaving the result in x0,/// then `ret`.fn emitMainAsmArm64(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"); // Save the link register: `bl write`/`bl <libcall>` inside the body would // otherwise clobber it and break the final `ret`. try wline(buf, alloc, " str x30, [sp, #-16]!"); 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")) { try emitStdoutAsmArm64(buf, alloc, &str_i, e.call_directive.args, err_buf); } else if (std.mem.eql(u8, e.call_directive.name, "libcall")) { try emitLibcallAsmArm64(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 emitLibcallAsmArm64(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 lowerIntAsmArm64(buf, alloc, ex); } } }, else => return unsupported(err_buf, "only 'return', '@stdout' and '@libcall' are supported in 'fn main'\n", .{}), }; try wline(buf, alloc, " ldr x30, [sp], #16"); try buf.appendSlice(alloc, " ret\n");}
/// Emit `mov x<rd>, #imm` as a movz/movk sequence the assembler will accept/// for any 64-bit constant.fn arm64MovImmAsm(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, rd: usize, imm: u64) Error!void { var hw: u6 = 0; var first = true; while (!first or (imm >> (16 * hw)) != 0) { const chunk: u16 = @intCast((imm >> (16 * hw)) & 0xffff); if (chunk != 0 or first) { if (first) { try wprint(buf, alloc, " movz x{d}, #0x{x}", .{ rd, chunk }); } else { try wprint(buf, alloc, " movk x{d}, #0x{x}, lsl #{d}", .{ rd, chunk, 16 * hw }); } first = false; } if (hw == 3) break; // all 64 bits visited; shifting by 64 is UB hw += 1; }}
/// Emit `call <sym>` with the given integer args in AArch64 registers x0..x7.fn emitLibcallAsmArm64(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 = 8; // x0..x7 if (tuple.len > max_args) return unsupported(err_buf, "@libcall supports at most 8 integer arguments in native build\n", .{});
// x0 is both the evaluation scratch and arg0, so park arg0 in x16 (the // caller-saved IP0 scratch register) until every argument is materialized. for (tuple, 0..) |arg_expr, i| { if (!isIntExpr(arg_expr)) return unsupported(err_buf, "@libcall arguments must be integer expressions in native build\n", .{}); try lowerIntAsmArm64(buf, alloc, arg_expr); if (i == 0) { if (tuple.len > 1) try wline(buf, alloc, " mov x16, x0"); } else { try wprint(buf, alloc, " mov x{d}, x0", .{i}); } } if (tuple.len > 1) try wline(buf, alloc, " mov x0, x16");
// Result lands in x0 (AArch64). The caller may use or discard it. try wprint(buf, alloc, " bl {s}", .{sym});}
/// Emit a `write(fd=1, "literal", len)` through libc's `write` (AArch64).fn emitStdoutAsmArm64(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 w0, #1"); try wprint(buf, alloc, " adrp x1, .Ls{d}", .{label}); try wprint(buf, alloc, " add x1, x1, :lo12:.Ls{d}", .{label}); try arm64MovImmAsm(buf, alloc, 2, @intCast(s.len)); try wline(buf, alloc, " bl write"); str_i.* += 1;}
/// Lower an integer expression to AArch64 assembly; result in x0.fn lowerIntAsmArm64(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, e: parser.Expr) Error!void { switch (e) { .int_lit => |v| try arm64MovImmAsm(buf, alloc, 0, @bitCast(v)), .unary => |u| { try lowerIntAsmArm64(buf, alloc, u.operand.*); if (u.op == .minus) try wline(buf, alloc, " neg x0, x0"); }, .binary => |b| { try lowerIntAsmArm64(buf, alloc, b.lhs.*); try wline(buf, alloc, " str x0, [sp, #-16]!"); try lowerIntAsmArm64(buf, alloc, b.rhs.*); try wline(buf, alloc, " ldr x1, [sp], #16"); switch (b.op) { .plus => try wline(buf, alloc, " add x0, x1, x0"), .minus => try wline(buf, alloc, " sub x0, x1, x0"), 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); switch (builtin.cpu.arch) { .x86_64 => { 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); }, .aarch64 => { try testing.expect(std.mem.indexOf(u8, asm_src, "bl rg_main") != null); try testing.expect(std.mem.indexOf(u8, asm_src, "bl write") != null); }, else => return error.SkipZigTest, } 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); switch (builtin.cpu.arch) { .x86_64 => { 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); }, .aarch64 => { try testing.expect(std.mem.indexOf(u8, asm_src, "mov x16, x0") != null); // arg0 parked try testing.expect(std.mem.indexOf(u8, asm_src, "mov x1, x0") != null); // arg1 try testing.expect(std.mem.indexOf(u8, asm_src, "bl addtwo") != null); }, else => return error.SkipZigTest, }}