From 23a80b4146b999c221aeda0068c1fee41ccbc284 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:39:18 +0000 Subject: [PATCH 1/2] Initial plan From 4ae45b3d89d880b71f202bf57b9a510f2d2b5abf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:42:42 +0000 Subject: [PATCH 2/2] Add Zig rewrite skeleton in zig/ Co-authored-by: sidju <13993607+sidju@users.noreply.github.com> --- zig/README.md | 78 +++++++++++++++++++++ zig/build.zig | 28 ++++++++ zig/src/ffi.zig | 46 +++++++++++++ zig/src/interpreter.zig | 147 ++++++++++++++++++++++++++++++++++++++++ zig/src/main.zig | 47 +++++++++++++ zig/src/types.zig | 83 +++++++++++++++++++++++ 6 files changed, 429 insertions(+) create mode 100644 zig/README.md create mode 100644 zig/build.zig create mode 100644 zig/src/ffi.zig create mode 100644 zig/src/interpreter.zig create mode 100644 zig/src/main.zig create mode 100644 zig/src/types.zig diff --git a/zig/README.md b/zig/README.md new file mode 100644 index 0000000..d8c004e --- /dev/null +++ b/zig/README.md @@ -0,0 +1,78 @@ +# sid — Zig rewrite skeleton + +> **Status: work-in-progress skeleton.** +> The Rust implementation in `src/` remains authoritative. +> This directory is a proof-of-concept to explore a potential rewrite. + +--- + +## Why Zig? + +The primary motivation is **zero-cost, zero-boilerplate C interoperability**. + +| | Rust | Zig | +|---|---|---| +| Import a C header | `bindgen` + `build.rs` + `unsafe {}` everywhere | `@cImport({ @cInclude("math.h"); })` | +| Call `cos(x)` | `unsafe { libc::cos(x) }` | `c.cos(x)` | +| Runtime dlopen | `libloading` crate | `std.DynLib` (built-in) | + +`sid` needs to call into arbitrary C libraries (math, system, user-provided) at +runtime. Zig makes this natural without additional tooling. + +### C FFI example — `cos` + +**Rust** +```rust +// Cargo.toml: libc = "0.2" +extern "C" { fn cos(x: f64) -> f64; } + +fn call_cos(x: f64) -> f64 { + unsafe { cos(x) } +} +``` + +**Zig** (see [`src/ffi.zig`](src/ffi.zig)) +```zig +const c = @cImport({ @cInclude("math.h"); }); + +fn callCFunction(x: f64) f64 { + return c.cos(x); +} +``` + +--- + +## Building and running + +Requires **Zig 0.14** (stable, early 2026). + +```sh +# From this directory (zig/) +zig build run -- path/to/script.sid + +# Or build only +zig build +./zig-out/bin/sid path/to/script.sid +``` + +--- + +## Module map + +| Rust module | Zig module | Notes | +|---|---|---| +| `src/types.rs` | `src/types.zig` | Tagged unions replace Rust enums | +| `src/invoke/mod.rs` | `src/interpreter.zig` | `ExeState` struct + `interpret` loop | +| `src/built_in/mod.rs` | `src/ffi.zig` + TBD | C dispatch lives in `ffi.zig` | +| `src/parse/` | TBD | Parser not yet implemented | +| `src/render/` | TBD | Renderer not yet implemented | +| `src/bin/sid.rs` | `src/main.zig` | Entry point: read file → `run()` | + +--- + +## What is not yet implemented + +- **Parser** (`src/parse/` → `src/parse.zig`) — tokenise source into `ProgramValue`s +- **Renderer** — resolve template values with parent-stack captures +- **Built-in functions** — concrete implementations behind `src/ffi.zig`'s dispatch table +- **Error handling** — panics are used for now; a proper error union is planned diff --git a/zig/build.zig b/zig/build.zig new file mode 100644 index 0000000..7088535 --- /dev/null +++ b/zig/build.zig @@ -0,0 +1,28 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "sid", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + // Link libc so that C FFI (see src/ffi.zig) works + exe.linkLibC(); + + b.installArtifact(exe); + + // `zig build run -- path/to/script.sid` + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Build and run sid"); + run_step.dependOn(&run_cmd.step); +} diff --git a/zig/src/ffi.zig b/zig/src/ffi.zig new file mode 100644 index 0000000..e5fb973 --- /dev/null +++ b/zig/src/ffi.zig @@ -0,0 +1,46 @@ +/// C interoperability layer for sid. +/// +/// This file demonstrates the key advantage of rewriting in Zig: zero-boilerplate +/// C interop via @cImport / @cInclude. No bindgen, no unsafe blocks, no separate +/// header-parsing step — the C headers are imported directly at compile time. +/// +/// # Dynamic library loading +/// +/// For runtime-loaded C libraries (dlopen / LoadLibrary), use std.DynLib: +/// +/// var lib = try std.DynLib.open("libm.so.6"); +/// defer lib.close(); +/// +/// // Look up a symbol by name; provide its function type. +/// const cos_fn = lib.lookup(*const fn (f64) callconv(.C) f64, "cos") orelse +/// return error.SymbolNotFound; +/// +/// const result = cos_fn(1.0); +/// +/// This is where runtime-dispatched sid built-in functions (e.g. those loaded +/// from a user-provided shared library) would be resolved. + +const std = @import("std"); + +// Import the C standard math library. +// Zig translates the header declarations into Zig types automatically. +const c = @cImport({ + @cInclude("math.h"); +}); + +/// Demo: call the C `cos` function through Zig's C interop. +/// +/// In the full implementation, a dispatch table would map sid built-in names +/// such as "cos" to functions like this one. +/// +/// Example mapping: +/// // sid program: `1.0 "cos" !` +/// // ^^^^^^^^^^^ push Float(1.0), push label "cos", invoke +/// if (std.mem.eql(u8, built_in_name, "cos")) { +/// const arg = popFloat(state); // pop Float from data stack +/// const result = callCFunction(arg); // call C cos() +/// pushFloat(state, result); // push result back +/// } +pub fn callCFunction(x: f64) f64 { + return c.cos(x); +} diff --git a/zig/src/interpreter.zig b/zig/src/interpreter.zig new file mode 100644 index 0000000..f5a61ca --- /dev/null +++ b/zig/src/interpreter.zig @@ -0,0 +1,147 @@ +/// Minimal interpreter loop for the sid language. +/// +/// Mirrors the structure of src/invoke/mod.rs. + +const std = @import("std"); +const types = @import("types.zig"); +const ffi = @import("ffi.zig"); + +const ProgramValue = types.ProgramValue; +const DataValue = types.DataValue; +const RealValue = types.RealValue; + +// --------------------------------------------------------------------------- +// ExeState +// --------------------------------------------------------------------------- + +/// Execution state, equivalent to the Rust ExeState in src/invoke/mod.rs. +/// +/// In the full implementation there would be separate local_scope and +/// global_scope maps; a single `scope` is used here for simplicity. +pub const ExeState = struct { + program_stack: std.ArrayList(ProgramValue), + data_stack: std.ArrayList(DataValue), + /// Variable bindings: label → RealValue + scope: std.StringHashMap(RealValue), + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator) !ExeState { + return ExeState{ + .program_stack = std.ArrayList(ProgramValue).init(allocator), + .data_stack = std.ArrayList(DataValue).init(allocator), + .scope = std.StringHashMap(RealValue).init(allocator), + .allocator = allocator, + }; + } + + pub fn deinit(self: *ExeState) void { + for (self.program_stack.items) |*pv| pv.deinit(self.allocator); + self.program_stack.deinit(); + + for (self.data_stack.items) |*dv| dv.deinit(self.allocator); + self.data_stack.deinit(); + + var it = self.scope.iterator(); + while (it.next()) |entry| { + self.allocator.free(entry.key_ptr.*); + entry.value_ptr.deinit(self.allocator); + } + self.scope.deinit(); + } +}; + +// --------------------------------------------------------------------------- +// Main interpreter loop +// --------------------------------------------------------------------------- + +/// Repeatedly pop and execute each ProgramValue from the program stack. +/// +/// Mirrors `interpret` / `interpret_one` in src/invoke/mod.rs. +pub fn interpret(state: *ExeState) !void { + while (state.program_stack.items.len > 0) { + try interpretOne(state); + } +} + +fn interpretOne(state: *ExeState) !void { + const op = state.program_stack.pop(); + switch (op) { + // Push a real value onto the data stack + .Real => |rv| try state.data_stack.append(.{ .Real = rv }), + + // Push a label reference onto the data stack + .Label => |l| try state.data_stack.append(.{ .Label = l }), + + // Invoke: pop the top of the data stack and dispatch + .Invoke => try invoke(state), + } +} + +// --------------------------------------------------------------------------- +// Invoke dispatch +// --------------------------------------------------------------------------- + +fn invoke(state: *ExeState) !void { + if (state.data_stack.items.len == 0) { + std.debug.print("sid: invoke on empty data stack\n", .{}); + return error.EmptyStack; + } + + const top = state.data_stack.pop(); + + switch (top) { + .Real => |rv| switch (rv) { + // Invoking a Substack: push its contents onto the program stack + // so execution continues in the current context. + .Substack => |ss| { + // Append in reverse order so the first item is popped first. + // Ownership of each ProgramValue transfers to program_stack; + // only the ArrayList wrapper needs to be freed here. + var mut_ss = ss; + var i = mut_ss.items.len; + while (i > 0) { + i -= 1; + try state.program_stack.append(mut_ss.items[i]); + } + mut_ss.deinit(); + }, + + // Invoking a built-in function: look it up and call it. + // TODO: replace the stub below with a real dispatch table once + // built-in functions are implemented. + .BuiltInFunction => |name| { + std.debug.print("sid: invoke built-in '{s}'\n", .{name}); + + // --- C FFI hook-in point ----------------------------------- + // This is where a built-in like "cos" would be dispatched to + // a C function. Example: + // + // if (std.mem.eql(u8, name, "cos")) { + // const arg = popFloat(state); + // const result = ffi.callCFunction(arg); + // try state.data_stack.append(.{ .Real = .{ .Float = result } }); + // } + // ---------------------------------------------------------- + _ = ffi.callCFunction; // keep the import live + }, + + else => { + std.debug.print("sid: cannot invoke value of type {s}\n", .{@tagName(rv)}); + return error.InvalidInvoke; + }, + }, + + // Invoking a label: look it up in scope, then invoke the result + .Label => |l| { + std.debug.print("sid: invoke label '{s}'\n", .{l}); + if (state.scope.get(l)) |val| { + // Re-push resolved value and invoke it + try state.data_stack.append(.{ .Real = val }); + try invoke(state); + } else { + std.debug.print("sid: undefined label '{s}'\n", .{l}); + return error.UndefinedLabel; + } + }, + } +} diff --git a/zig/src/main.zig b/zig/src/main.zig new file mode 100644 index 0000000..149a3a4 --- /dev/null +++ b/zig/src/main.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const interpreter = @import("interpreter.zig"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const args = std.os.argv; + + if (args.len < 2) { + const stderr = std.io.getStdErr().writer(); + try stderr.writeAll( + \\sid (zig rewrite) — a stack-based RPN language interpreter + \\ + \\Usage: sid + \\ + ); + std.process.exit(1); + } + + const path = std.mem.span(args[1]); + + const source = std.fs.cwd().readFileAlloc(allocator, path, 1024 * 1024) catch |err| { + const stderr = std.io.getStdErr().writer(); + try stderr.print("sid: cannot read '{s}': {}\n", .{ path, err }); + std.process.exit(1); + }; + defer allocator.free(source); + + try run(allocator, source); +} + +/// Parse, render, and interpret a sid source string. +/// This mirrors the Rust `run()` in src/bin/sid.rs. +fn run(allocator: std.mem.Allocator, source: []const u8) !void { + // TODO: call parse(source) once the parser is implemented + // TODO: call render(parsed) once the renderer is implemented + // For now, just hand the source to the interpreter stub so the pipeline + // is visible end-to-end. + _ = source; + + var state = try interpreter.ExeState.init(allocator); + defer state.deinit(); + + try interpreter.interpret(&state); +} diff --git a/zig/src/types.zig b/zig/src/types.zig new file mode 100644 index 0000000..f435d28 --- /dev/null +++ b/zig/src/types.zig @@ -0,0 +1,83 @@ +/// Core value types for the sid interpreter. +/// +/// These mirror the Rust types in src/types.rs, adapted to Zig idioms: +/// - Rust enums with data → Zig tagged unions +/// - Rust Vec → std.ArrayList(T) +/// - Rust String → []const u8 (owned slices where needed) + +const std = @import("std"); + +// --------------------------------------------------------------------------- +// RealValue — a concrete, fully-evaluated value +// --------------------------------------------------------------------------- + +pub const RealValue = union(enum) { + Bool: bool, + /// Owned UTF-8 string + Str: []const u8, + /// A full grapheme cluster (may be multiple bytes) + Char: []const u8, + Int: i64, + Float: f64, + /// An embedded program (substack) that can be invoked + Substack: std.ArrayList(ProgramValue), + /// A flat list of data values + List: std.ArrayList(DataValue), + /// Name of a built-in function + BuiltInFunction: []const u8, + + pub fn deinit(self: *RealValue, allocator: std.mem.Allocator) void { + switch (self.*) { + .Str => |s| allocator.free(s), + .Char => |c| allocator.free(c), + .BuiltInFunction => |f| allocator.free(f), + .Substack => |*ss| { + for (ss.items) |*pv| pv.deinit(allocator); + ss.deinit(); + }, + .List => |*lst| { + for (lst.items) |*dv| dv.deinit(allocator); + lst.deinit(); + }, + else => {}, + } + } +}; + +// --------------------------------------------------------------------------- +// DataValue — a value that lives on the data stack +// --------------------------------------------------------------------------- + +pub const DataValue = union(enum) { + Real: RealValue, + /// A symbolic label (looked up in scope at invoke time) + Label: []const u8, + + pub fn deinit(self: *DataValue, allocator: std.mem.Allocator) void { + switch (self.*) { + .Real => |*rv| rv.deinit(allocator), + .Label => |l| allocator.free(l), + } + } +}; + +// --------------------------------------------------------------------------- +// ProgramValue — a value on the program (instruction) stack +// --------------------------------------------------------------------------- + +pub const ProgramValue = union(enum) { + Real: RealValue, + /// Push a label reference onto the data stack + Label: []const u8, + /// Pop and invoke the top of the data stack + Invoke, + // TODO: Template — captures parent-stack slots; see src/types.rs Template + + pub fn deinit(self: *ProgramValue, allocator: std.mem.Allocator) void { + switch (self.*) { + .Real => |*rv| rv.deinit(allocator), + .Label => |l| allocator.free(l), + .Invoke => {}, + } + } +};