//! Minimal C11 emitter — Phase 3 foundation for `rinne build`. //! //! Translates a parsed RinneScript program into standalone, dependency-free //! C11 source. Supported subset (v1): //! - primitives: int (-> long long), float (-> double), bool (-> bool) //! - functions (recursion, arity-checked calls, prototypes up front) //! - let/const bindings with type annotations or literal inference //! - if/else, while, break, continue, return //! - arithmetic/comparison/logical operators with C-compatible semantics //! (integer `/` and `%` truncate exactly like the bytecode VM) //! //! Everything else (strings, arrays, builtins, defer, structs, directives) //! fails with `error.UnsupportedConstruct` and a precise message naming the //! offending construct, so callers can surface actionable diagnostics. //! All emitted identifiers are `rg_`-prefixed to avoid C keyword clashes. const std = @import("std"); const parser = @import("../script/parser.zig"); pub const Error = error{ UnsupportedConstruct, OutOfMemory, WriteFailed }; pub const CType = enum { int, float, boolean, void_, fn name(self: CType) []const u8 { return switch (self) { .int => "long long", .float => "double", .boolean => "bool", .void_ => "void", }; } fn fromDataType(dt: parser.DataType) ?CType { return switch (dt) { .primitive => |p| switch (p) { .int => .int, .float => .float, .bool => .boolean, .void_type => .void_, else => null, }, else => null, }; } }; const FnSig = struct { params: []CType, ret: CType, }; pub const Emitter = struct { arena: std.mem.Allocator, w: *std.Io.Writer, fns: std.StringHashMapUnmanaged(FnSig) = .empty, globals: std.StringHashMapUnmanaged(CType) = .empty, scopes: std.ArrayList(std.StringHashMapUnmanaged(CType)) = .empty, indent: usize = 0, /// Human-readable explanation set when returning error.UnsupportedConstruct. err_msg: []const u8 = "", pub fn init(arena: std.mem.Allocator, w: *std.Io.Writer) Emitter { return .{ .arena = arena, .w = w }; } fn fail(self: *Emitter, comptime fmt: []const u8, args: anytype) Error { self.err_msg = std.fmt.allocPrint(self.arena, fmt, args) catch "unsupported construct"; return error.UnsupportedConstruct; } pub fn emitProgram(self: *Emitter, prog: *const parser.Program) Error!void { try self.prepass(prog); try self.w.writeAll("#include \n\n"); // Prototypes for every function (enables forward references). for (prog.fn_decls) |*fd| try self.emitPrototype(fd); try self.w.writeAll("\n"); // File-scope storage. Initializers are emitted as assignments at the // top of generated `main` so arbitrary runtime expressions (including // calls) stay available once those land. for (prog.global_stmts.stmts) |stmt| switch (stmt) { .var_decl => |d| { const t = self.globals.get(d.name).?; try self.w.print("static {s} {s};\n", .{ t.name(), try self.mangle(d.name) }); }, else => return self.fail("native build supports only top-level declarations", .{}), }; if (prog.global_stmts.stmts.len > 0) try self.w.writeAll("\n"); for (prog.fn_decls) |*fd| try self.emitFnBody(fd); // Generated entry point: initialize globals, run language main. if (self.fns.contains("main")) { try self.w.writeAll( \\int main(void) { \\ ); self.indent += 1; for (prog.global_stmts.stmts) |stmt| switch (stmt) { .var_decl => |d| { try self.writeIndent(); try self.w.print("{s} = ", .{try self.mangle(d.name)}); try self.emitExpr(&d.initializer); try self.w.writeAll(";\n"); }, else => unreachable, }; self.indent -= 1; try self.w.writeAll( \\ return (int)rg_main(); \\} \\ ); } } // ---- prepass: validate signatures, register fn/global tables ---- fn prepass(self: *Emitter, prog: *const parser.Program) Error!void { for (prog.fn_decls) |fd| { const params = try self.arena.alloc(CType, fd.params.len); for (fd.params, 0..) |p, i| { params[i] = CType.fromDataType(p.param_type) orelse return self.fail("in fn '{s}': parameter '{s}' has a type unsupported by native build", .{ fd.name, p.name }); } const ret = CType.fromDataType(fd.return_type) orelse return self.fail("in fn '{s}': return type unsupported by native build", .{fd.name}); if (self.fns.contains(fd.name)) { return self.fail("duplicate function '{s}'", .{fd.name}); } try self.fns.put(self.arena, fd.name, .{ .params = params, .ret = ret }); } // Language entry point must be callable as `int main(void)`. if (self.fns.get("main")) |sig| { if (sig.params.len != 0 or sig.ret != .int) { return self.fail("native build requires 'fn main() int' with no parameters", .{}); } } for (prog.global_stmts.stmts) |stmt| switch (stmt) { .var_decl => |d| { // Register sequentially so later initializers can reference // earlier globals (declaration-before-use, like the VM). const t = try self.declType(d.type_ann, d.initializer); try self.globals.put(self.arena, d.name, t); }, else => return self.fail("native build supports only top-level declarations", .{}), }; } // ---- declarations ---- /// Resolves a declared type: explicit annotation, else inferred from the /// initializer expression. fn declType(self: *Emitter, ann: ?parser.DataType, init_expr: parser.Expr) Error!CType { if (ann) |dt| { return CType.fromDataType(dt) orelse return self.fail("type unsupported by native build (only int, float, bool)", .{}); } return self.inferType(init_expr); } fn inferType(self: *Emitter, expr: parser.Expr) Error!CType { return switch (expr) { .int_lit => .int, .float_lit => .float, .bool_lit => .boolean, .unary => |u| switch (u.op) { .minus => self.inferType(u.operand.*), .bang => .boolean, else => self.fail("operator unsupported by native build", .{}), }, .binary => |b| switch (b.op) { .plus, .minus, .star, .slash => blk: { const lt = try self.inferType(b.lhs.*); const rt = try self.inferType(b.rhs.*); break :blk if (lt == .float or rt == .float) CType.float else CType.int; }, .percent => blk: { const lt = try self.inferType(b.lhs.*); const rt = try self.inferType(b.rhs.*); if (lt == .float or rt == .float) { return self.fail("modulo requires integer operands in native build", .{}); } break :blk CType.int; }, .equal_equal, .not_equal, .less, .less_equal, .greater, .greater_equal, .kw_and, .kw_or => .boolean, else => self.fail("operator unsupported by native build", .{}), }, .identifier => |name| self.lookup(name) orelse self.fail("unknown variable '{s}' in native build", .{name}), .call => |c| blk: { const name = switch (c.callee.*) { .identifier => |n| n, .field_access => return self.fail("method calls are not supported by the native C11 backend", .{}), else => return self.fail("only plain function calls are supported by native build", .{}), }; const sig = self.fns.get(name) orelse return self.fail("unknown function '{s}' in native build", .{name}); break :blk sig.ret; }, .call_directive => |cd| { if (std.mem.eql(u8, cd.name, "tensor")) { return self.fail("tensors are not supported by the native C11 backend", .{}); } return self.fail("@{s} is not supported by the native C11 backend", .{cd.name}); }, else => self.fail("expression kind unsupported by native build", .{}), }; } fn lookup(self: *Emitter, name: []const u8) ?CType { var i = self.scopes.items.len; while (i > 0) { i -= 1; if (self.scopes.items[i].get(name)) |t| return t; } if (self.globals.get(name)) |t| return t; return null; } fn mangle(self: *Emitter, name: []const u8) Error![]const u8 { return std.fmt.allocPrint(self.arena, "rg_{s}", .{name}); } // ---- expressions ---- fn binOp(self: *Emitter, op: parser.TokenType) Error![]const u8 { return switch (op) { .plus => "+", .minus => "-", .star => "*", .slash => "/", .percent => "%", .equal_equal => "==", .not_equal => "!=", .less => "<", .less_equal => "<=", .greater => ">", .greater_equal => ">=", .kw_and => "&&", .kw_or => "||", else => self.fail("operator unsupported by native build", .{}), }; } fn emitExpr(self: *Emitter, expr: *const parser.Expr) Error!void { switch (expr.*) { .int_lit => |v| try self.w.print("{d}", .{v}), .float_lit => |v| try self.w.print("{d}", .{v}), .bool_lit => |v| try self.w.writeAll(if (v) "true" else "false"), .identifier => |name| { if (self.lookup(name) == null) { return self.fail("unknown variable '{s}' in native build", .{name}); } try self.w.writeAll(try self.mangle(name)); }, .unary => |u| { const op: []const u8 = switch (u.op) { .minus => "-", .bang => "!", else => return self.fail("operator unsupported by native build", .{}), }; try self.w.writeAll(op); try self.wrap(u.operand); }, .binary => |b| { if (b.op == .percent) { const lt = try self.inferType(b.lhs.*); const rt = try self.inferType(b.rhs.*); if (lt == .float or rt == .float) { return self.fail("modulo requires integer operands in native build", .{}); } } try self.w.writeAll("("); try self.emitExpr(b.lhs); try self.w.print(" {s} ", .{try self.binOp(b.op)}); try self.emitExpr(b.rhs); try self.w.writeAll(")"); }, .call => |c| { const callee_name = switch (c.callee.*) { .identifier => |n| n, .field_access => return self.fail("method calls are not supported by the native C11 backend", .{}), else => return self.fail("only plain function calls are supported by native build", .{}), }; const sig = self.fns.get(callee_name) orelse return self.fail("unknown function '{s}' in native build", .{callee_name}); if (sig.params.len != c.args.len) { return self.fail("'{s}' expects {d} argument(s), got {d}", .{ callee_name, sig.params.len, c.args.len }); } try self.w.writeAll(try self.mangle(callee_name)); try self.w.writeAll("("); for (c.args, 0..) |*arg, i| { if (i > 0) try self.w.writeAll(", "); try self.emitExpr(arg); } try self.w.writeAll(")"); }, .call_directive => |cd| { if (std.mem.eql(u8, cd.name, "tensor")) { return self.fail("tensors are not supported by the native C11 backend", .{}); } return self.fail("@{s} is not supported by the native C11 backend", .{cd.name}); }, else => return self.fail("expression kind unsupported by native build", .{}), } } /// Emits `(expr)` — used where precedence would otherwise be ambiguous. fn wrap(self: *Emitter, expr: *const parser.Expr) Error!void { try self.w.writeAll("("); try self.emitExpr(expr); try self.w.writeAll(")"); } // ---- functions & statements ---- fn emitPrototype(self: *Emitter, fd: *const parser.FnDecl) Error!void { const sig = self.fns.get(fd.name).?; try self.w.print("{s} {s}(", .{ sig.ret.name(), try self.mangle(fd.name) }); if (sig.params.len == 0) { try self.w.writeAll("void"); } else { for (fd.params, 0..) |p, i| { if (i > 0) try self.w.writeAll(", "); try self.w.print("{s} {s}", .{ sig.params[i].name(), try self.mangle(p.name) }); } } try self.w.writeAll(");\n"); } fn emitFnBody(self: *Emitter, fd: *const parser.FnDecl) Error!void { const sig = self.fns.get(fd.name).?; // Fresh scope holding the parameters. var param_scope = std.StringHashMapUnmanaged(CType).empty; for (fd.params) |p| { try param_scope.put(self.arena, p.name, CType.fromDataType(p.param_type).?); } try self.scopes.append(self.arena, param_scope); defer _ = self.scopes.pop(); try self.w.print("{s} {s}(", .{ sig.ret.name(), try self.mangle(fd.name) }); if (fd.params.len == 0) { try self.w.writeAll("void"); } else { for (fd.params, 0..) |p, i| { if (i > 0) try self.w.writeAll(", "); try self.w.print("{s} {s}", .{ sig.params[i].name(), try self.mangle(p.name) }); } } try self.w.writeAll(") "); try self.emitBlock(&fd.body); try self.w.writeAll("\n"); } fn emitBlock(self: *Emitter, block: *const parser.Block) Error!void { const empty_map = std.StringHashMapUnmanaged(CType).empty; try self.scopes.append(self.arena, empty_map); defer _ = self.scopes.pop(); // Statements insert into `self.scopes.items[len-1]` directly, so the // by-value copy above is only the initial empty state. try self.w.writeAll("{\n"); self.indent += 1; for (block.stmts) |*stmt| try self.emitStmt(stmt); self.indent -= 1; try self.writeIndent(); try self.w.writeAll("}\n"); } fn writeIndent(self: *Emitter) Error!void { var i: usize = 0; while (i < self.indent) : (i += 1) try self.w.writeAll(" "); } fn emitStmt(self: *Emitter, stmt: *const parser.Stmt) Error!void { try self.writeIndent(); switch (stmt.*) { .var_decl => |d| { // Reject same-block shadowing (the VM rejects it too). const top = &self.scopes.items[self.scopes.items.len - 1]; if (top.contains(d.name)) { return self.fail("variable '{s}' redeclared in the same block", .{d.name}); } const t = try self.declType(d.type_ann, d.initializer); try top.put(self.arena, d.name, t); try self.w.print("{s} {s} = ", .{ t.name(), try self.mangle(d.name) }); try self.emitExpr(&d.initializer); try self.w.writeAll(";\n"); }, .assignment => |a| { const name = switch (a.target) { .identifier => |n| n, else => return self.fail("only plain variables can be assigned in native build", .{}), }; if (self.lookup(name) == null) { return self.fail("unknown variable '{s}' in native build", .{name}); } // Constants are immutable in RinneScript; the compiler flags // that before we ever see it, so plain assignment is safe. try self.w.writeAll(try self.mangle(name)); try self.w.writeAll(" = "); try self.emitExpr(&a.value); try self.w.writeAll(";\n"); }, .expr_stmt => |*e| { switch (e.*) { .call => {}, else => return self.fail("expression statements must be function calls in native build", .{}), } try self.w.writeAll("(void) "); try self.emitExpr(e); try self.w.writeAll(";\n"); }, .if_stmt => |i| { try self.w.writeAll("if "); try self.wrap(&i.condition); try self.w.writeAll(" "); try self.emitBlock(&i.then_branch); if (i.else_branch) |eb| { self.indent -= 1; // align `else` with `if` try self.writeIndent(); try self.w.writeAll("else "); self.indent += 1; try self.emitBlock(&eb); } }, .while_stmt => |w| { try self.w.writeAll("while "); try self.wrap(&w.condition); try self.w.writeAll(" "); try self.emitBlock(&w.body); }, .break_stmt => try self.w.writeAll("break;\n"), .continue_stmt => try self.w.writeAll("continue;\n"), .for_stmt => return self.fail("for-in loops are not supported by the native C11 backend yet (use while)", .{}), .range_for => return self.fail("range for-loops are not supported by the native C11 backend yet (use while)", .{}), .return_stmt => |r| { if (r) |e| { try self.w.writeAll("return "); try self.emitExpr(&e); try self.w.writeAll(";\n"); } else { try self.w.writeAll("return;\n"); } }, .defer_stmt => return self.fail("defer is not yet supported by native build", .{}), .fn_decl => return self.fail("nested functions are not supported by native build", .{}), } } }; // ---- Tests ---- test "emit arithmetic program to C11" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); var out_buf: [4096]u8 = undefined; var w = std.Io.Writer.fixed(&out_buf); var p = parser.Parser.init(arena, \\var base: int = 10; \\const scale: int = base + 2; \\ \\fn area(w: int, h: int) int { \\ return w * h; \\} \\ \\fn main() int { \\ var total: int = area(scale, 4) / 2; \\ while total > 20 { total = total - 5; } \\ if total == 15 { return 1; } else { return 2; } \\} ); const prog = try p.parseProgram(); try std.testing.expect(p.err_msg == null); var em = Emitter.init(arena, &w); em.emitProgram(&prog) catch |e| { std.debug.print("emit failed: {s}\n", .{em.err_msg}); return e; }; const src = w.buffered(); // Spot-check structural landmarks of the generated C. try std.testing.expect(std.mem.indexOf(u8, src, "#include ") != null); try std.testing.expect(std.mem.indexOf(u8, src, "long long rg_area(long long rg_w, long long rg_h);") != null); try std.testing.expect(std.mem.indexOf(u8, src, "static long long rg_base;") != null); try std.testing.expect(std.mem.indexOf(u8, src, "rg_scale = (rg_base + 2);") != null); try std.testing.expect(std.mem.indexOf(u8, src, "long long rg_main(void) {") != null); try std.testing.expect(std.mem.indexOf(u8, src, "rg_area(rg_scale, 4) / 2") != null); try std.testing.expect(std.mem.indexOf(u8, src, "int main(void) {") != null); } test "emit rejects strings and arrays with precise message" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); var out_buf: [512]u8 = undefined; var w = std.Io.Writer.fixed(&out_buf); var p = parser.Parser.init(arena, \\fn main() int { \\ const s: str = "nope"; \\ return 0; \\} ); const prog = try p.parseProgram(); var em = Emitter.init(arena, &w); try std.testing.expectError(error.UnsupportedConstruct, em.emitProgram(&prog)); try std.testing.expect(std.mem.indexOf(u8, em.err_msg, "unsupported") != null); } test "emit rejects tensors with precise message" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); var out_buf: [512]u8 = undefined; var w = std.Io.Writer.fixed(&out_buf); var p = parser.Parser.init(arena, \\fn main() int { \\ const t = @tensor(f32, [2, 3]); \\ return 0; \\} ); const prog = try p.parseProgram(); var em = Emitter.init(arena, &w); try std.testing.expectError(error.UnsupportedConstruct, em.emitProgram(&prog)); try std.testing.expect(std.mem.indexOf(u8, em.err_msg, "tensors are not supported") != null); }