Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions zig/README.md
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions zig/build.zig
Original file line number Diff line number Diff line change
@@ -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);
}
46 changes: 46 additions & 0 deletions zig/src/ffi.zig
Original file line number Diff line number Diff line change
@@ -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);
}
147 changes: 147 additions & 0 deletions zig/src/interpreter.zig
Original file line number Diff line number Diff line change
@@ -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;
}
},
}
}
47 changes: 47 additions & 0 deletions zig/src/main.zig
Original file line number Diff line number Diff line change
@@ -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 <script.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);
}
Loading