Something went wrong. Try again.
A general purpose .. programming language written in zig ijadux2.tngl.sh/Rinnescript
zig language
Something went wrong. Try again.
22 kB · 546 lines
Zig
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547//! 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 <stdbool.h>\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 <stdbool.h>") != 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);}