Compare commits

...

5 Commits

Author SHA1 Message Date
d7e71d58e1 mvp treewalk interpreter
Some checks failed
Run unit tests / zig build test (push) Failing after 33s
2026-01-23 19:50:58 +01:00
69bd99a830 only allow funcs at top level 2026-01-14 01:56:23 +01:00
ecb9a63efb hide sexpr implementation detail from parser public api 2026-01-14 01:40:31 +01:00
a018213ec5 make parsing functions backwards (idk how to describe it) 2026-01-13 23:33:26 +01:00
bd95d6b779 fix formatting, add error sets 2026-01-10 20:52:26 +01:00
7 changed files with 253 additions and 150 deletions

View File

@@ -19,7 +19,9 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/main.zig"), .root_source_file = b.path("src/main.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.imports = &.{.{ .name = "libneon", .module = lib.root_module }}, .imports = &.{
.{ .name = "libneon", .module = lib.root_module },
},
}), }),
}); });
@@ -40,7 +42,9 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("tests/all.zig"), .root_source_file = b.path("tests/all.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.imports = &.{.{ .name = "libneon", .module = lib.root_module }}, .imports = &.{
.{ .name = "libneon", .module = lib.root_module },
},
}), }),
}); });

View File

@@ -1,14 +1,3 @@
(func print_fibs [limit] {
(let a 1)
(let b 1)
(while (< a limit) {
(printf "%d\n" a)
(set b (+ b a))
(set a (- b a))
})
})
(func main [] { (func main [] {
(print_fibs 100) (print_int (+ 1 (* 2 34)))
}) })

83
lib/interpreter.zig Normal file
View File

@@ -0,0 +1,83 @@
const std = @import("std");
const parser = @import("parser.zig");
pub const Interpreter = struct {
stdout: *std.Io.Writer,
pub fn interpret(self: @This(), program: parser.Program) !void {
const main = for (program.funcs) |func| {
if (std.mem.eql(u8, func.name, "main")) break func;
} else return error.NoMain;
try self.run_func(main);
try self.stdout.flush();
}
fn run_func(self: @This(), func: parser.Func) !void {
for (func.body) |stmt| {
try self.run_stmt(stmt);
}
}
fn run_stmt(self: @This(), stmt: parser.Stmt) !void {
switch (stmt) {
.expr => |expr| {
const val = try self.evaluate(expr);
if (val != .nil) {
try self.stdout.print("{any}\n", .{val});
}
},
else => {
std.debug.print("unimplemented run for '{t}' stmt\n", .{stmt});
return error.Todo;
},
}
}
fn evaluate(self: @This(), expr: parser.Expr) !Value {
switch (expr) {
.nil => return .nil,
.variable => |val| {
std.debug.print("unimplemented variable '{s}' access\n", .{val});
return error.Todo;
},
.integer => |val| return .{ .integer = @intCast(val) },
.string => |val| return .{ .string = val },
.func_call => |call| {
if (std.mem.eql(u8, call.name, "+")) {
var result: i64 = 0;
for (call.args) |arg| {
const val = try self.evaluate(arg);
if (val != .integer) return error.InvalidArg;
result += val.integer;
}
return .{ .integer = result };
} else if (std.mem.eql(u8, call.name, "*")) {
var result: i64 = 1;
for (call.args) |arg| {
const val = try self.evaluate(arg);
if (val != .integer) return error.InvalidArg;
result *= val.integer;
}
return .{ .integer = result };
} else if (std.mem.eql(u8, call.name, "print_int")) {
if (call.args.len != 1) return error.InvalidArg;
const arg = try self.evaluate(call.args[0]);
if (arg != .integer) return error.InvalidArg;
try self.stdout.print("{d}\n", .{arg.integer});
return .nil;
} else {
std.debug.print("unimplemented call to '{s}'\n", .{call.name});
return error.Todo;
}
},
}
}
};
const Value = union(enum) {
nil,
integer: i64,
string: []const u8,
};

View File

@@ -11,7 +11,7 @@ pub const Token = union(enum) {
identifier: []const u8, identifier: []const u8,
string: []const u8, string: []const u8,
pub fn format(self: @This(), w: *std.Io.Writer) !void { pub fn format(self: @This(), w: *std.Io.Writer) error{WriteFailed}!void {
try switch (self) { try switch (self) {
.lparen => w.print("lparen", .{}), .lparen => w.print("lparen", .{}),
.rparen => w.print("rparen", .{}), .rparen => w.print("rparen", .{}),
@@ -26,7 +26,7 @@ pub const Token = union(enum) {
} }
}; };
pub fn lex(gpa: std.mem.Allocator, src: []const u8) ![]const Token { pub fn lex(gpa: std.mem.Allocator, src: []const u8) error{ OutOfMemory, Overflow, InvalidCharacter }![]const Token {
var result = std.ArrayList(Token).empty; var result = std.ArrayList(Token).empty;
var idx: usize = 0; var idx: usize = 0;
while (idx < src.len) { while (idx < src.len) {
@@ -51,11 +51,30 @@ pub fn lex(gpa: std.mem.Allocator, src: []const u8) ![]const Token {
idx -= 1; idx -= 1;
break :sw .{ .integer = int }; break :sw .{ .integer = int };
}, },
'A'...'Z', 'a'...'z', '!', '$'...'\'', '*'...'/', ':'...'@', '^', '_', '~' => { 'A'...'Z',
'a'...'z',
'!',
'$'...'\'',
'*'...'/',
':'...'@',
'^',
'_',
'~',
=> {
const start = idx; const start = idx;
while (idx < src.len) { while (idx < src.len) {
switch (src[idx]) { switch (src[idx]) {
'A'...'Z', 'a'...'z', '0'...'9', '!', '$'...'\'', '*'...'/', ':'...'@', '^', '_', '~' => { 'A'...'Z',
'a'...'z',
'0'...'9',
'!',
'$'...'\'',
'*'...'/',
':'...'@',
'^',
'_',
'~',
=> {
idx += 1; idx += 1;
}, },
else => break, else => break,

View File

@@ -1,129 +1,13 @@
const std = @import("std"); const std = @import("std");
const lexer = @import("lexer.zig"); const lexer = @import("lexer.zig");
pub const SExpr = union(enum) { const SExpr = union(enum) {
integer: u64, // 42 integer: u64, // 42
identifier: []const u8, // module.name identifier: []const u8, // module.name
string: []const u8, // "foo bar" string: []const u8, // "foo bar"
call: []const SExpr, // (+ 1 2 3) call: []const SExpr, // (+ 1 2 3)
block: []const SExpr, // { (print "hi") } block: []const SExpr, // { (print "hi") }
list: []const SExpr, // [1 2 3] list: []const SExpr, // [1 2 3]
fn format_(self: @This(), w: *std.Io.Writer, depth: usize) !void {
for (0..depth) |_| {
try w.print(" ", .{});
}
switch (self) {
.integer => |int| try w.print("int: {d}\n", .{int}),
.identifier => |id| try w.print("id: {s}\n", .{id}),
.string => |str| try w.print("str: {s}\n", .{str}),
.call => |call| {
try w.print("call: {s}\n", .{if (call.len == 0) "()" else ""});
for (call) |expr| {
try expr.format_(w, depth + 1);
}
},
.block => |block| {
try w.print("block: {s}\n", .{if (block.len == 0) "{}" else ""});
for (block) |expr| {
try expr.format_(w, depth + 1);
}
},
.list => |list| {
try w.print("list: {s}\n", .{if (list.len == 0) "[]" else ""});
for (list) |expr| {
try expr.format_(w, depth + 1);
}
},
}
}
pub fn format(self: @This(), w: *std.Io.Writer) !void {
try self.format_(w, 0);
}
pub fn parse_stmt(self: @This(), gpa: std.mem.Allocator) !Stmt {
switch (self) {
.call => |ss| {
if (ss.len == 0) {
return error.EmptyCall;
}
const name = ss[0].identifier;
if (std.mem.eql(u8, name, "func")) {
if (ss.len != 4 or ss[1] != .identifier or ss[2] != .list or ss[3] != .block) return error.InvalidFunc;
const func_name = ss[1].identifier;
var args = std.ArrayList([]const u8).empty;
for (ss[2].list) |arg| {
if (arg != .identifier) return error.InvalidFuncArg;
try args.append(gpa, arg.identifier);
}
var body = std.ArrayList(Stmt).empty;
for (ss[3].block) |sexpr| {
try body.append(gpa, try sexpr.parse_stmt(gpa));
}
return .{ .func_decl = .{
.name = func_name,
.args = try args.toOwnedSlice(gpa),
.body = try body.toOwnedSlice(gpa),
} };
} else if (std.mem.eql(u8, name, "let")) {
if (ss.len != 3 or ss[1] != .identifier) return error.InvalidLet;
const var_name = ss[1].identifier;
return .{ .var_let = .{
.name = var_name,
.value = try ss[2].parse_expr(gpa),
} };
} else if (std.mem.eql(u8, name, "set")) {
if (ss.len != 3 or ss[1] != .identifier) return error.InvalidLet;
const var_name = ss[1].identifier;
return .{ .var_set = .{
.name = var_name,
.value = try ss[2].parse_expr(gpa),
} };
} else if (std.mem.eql(u8, name, "while")) {
if (ss.len != 3 or ss[2] != .block) return error.InvalidWhile;
const cond = try ss[1].parse_expr(gpa);
var body = std.ArrayList(Stmt).empty;
for (ss[2].block) |sexpr| {
try body.append(gpa, try sexpr.parse_stmt(gpa));
}
return .{ .while_loop = .{
.cond = cond,
.body = try body.toOwnedSlice(gpa),
} };
} else {
return .{ .expr = try self.parse_expr(gpa) };
}
},
else => return .{ .expr = try self.parse_expr(gpa) },
}
}
pub fn parse_expr(self: @This(), gpa: std.mem.Allocator) !Expr {
switch (self) {
.integer => |int| return .{ .integer = int },
.identifier => |id| {
if (std.mem.eql(u8, id, "nil")) {
return .nil;
}
return .{ .variable = id };
},
.string => |str| return .{ .string = str },
.call => |ss| {
if (ss.len < 1 or ss[0] != .identifier) return error.InvalidCall;
var args = std.ArrayList(Expr).empty;
for (ss[1..]) |sexpr| {
try args.append(gpa, try sexpr.parse_expr(gpa));
}
return .{ .func_call = .{
.name = ss[0].identifier,
.args = try args.toOwnedSlice(gpa),
} };
},
.block => return error.BlockAsExpr,
.list => return error.ListsNotImplemented,
}
}
}; };
fn parse_sexpr(gpa: std.mem.Allocator, tokens: []const lexer.Token) !struct { SExpr, usize } { fn parse_sexpr(gpa: std.mem.Allocator, tokens: []const lexer.Token) !struct { SExpr, usize } {
@@ -183,7 +67,7 @@ fn parse_sexpr(gpa: std.mem.Allocator, tokens: []const lexer.Token) !struct { SE
} }
} }
pub fn parse_sexprs(gpa: std.mem.Allocator, tokens: []const lexer.Token) ![]const SExpr { fn parse_sexprs(gpa: std.mem.Allocator, tokens: []const lexer.Token) ![]const SExpr {
var result = std.ArrayList(SExpr).empty; var result = std.ArrayList(SExpr).empty;
var idx: usize = 0; var idx: usize = 0;
@@ -196,12 +80,44 @@ pub fn parse_sexprs(gpa: std.mem.Allocator, tokens: []const lexer.Token) ![]cons
return result.toOwnedSlice(gpa); return result.toOwnedSlice(gpa);
} }
pub const Stmt = union(enum) { pub const Program = struct {
func_decl: struct { funcs: []const Func,
};
pub const Func = struct {
name: []const u8, name: []const u8,
args: []const []const u8, args: []const []const u8,
body: []const Stmt, body: []const Stmt,
},
fn from(gpa: std.mem.Allocator, sexpr: SExpr) !Func {
if (sexpr != .call or sexpr.call.len != 4) {
return error.InvalidFunc;
}
const call = sexpr.call;
if (call[0] != .identifier or !std.mem.eql(u8, call[0].identifier, "func") or call[1] != .identifier) {
return error.InvalidFunc;
}
if (call[2] != .list or call[3] != .block) {
return error.InvalidFunc;
}
var args = std.ArrayList([]const u8).empty;
for (call[2].list) |arg| {
if (arg != .identifier) return error.InvalidFuncArg;
try args.append(gpa, arg.identifier);
}
var body = std.ArrayList(Stmt).empty;
for (call[3].block) |inner| {
try body.append(gpa, try Stmt.from(gpa, inner));
}
return Func{
.name = call[1].identifier,
.args = try args.toOwnedSlice(gpa),
.body = try body.toOwnedSlice(gpa),
};
}
};
pub const Stmt = union(enum) {
var_let: struct { var_let: struct {
name: []const u8, name: []const u8,
value: Expr, value: Expr,
@@ -215,6 +131,52 @@ pub const Stmt = union(enum) {
body: []const Stmt, body: []const Stmt,
}, },
expr: Expr, expr: Expr,
fn from(gpa: std.mem.Allocator, sexpr: SExpr) !Stmt {
switch (sexpr) {
.call => |ss| {
if (ss.len == 0 or ss[0] != .identifier) {
return error.InvalidCall;
}
const name = ss[0].identifier;
if (std.mem.eql(u8, name, "let")) {
if (ss.len != 3 or ss[1] != .identifier) {
return error.InvalidLet;
}
const var_name = ss[1].identifier;
return .{ .var_let = .{
.name = var_name,
.value = try Expr.from(gpa, ss[2]),
} };
} else if (std.mem.eql(u8, name, "set")) {
if (ss.len != 3 or ss[1] != .identifier) {
return error.InvalidSet;
}
const var_name = ss[1].identifier;
return .{ .var_set = .{
.name = var_name,
.value = try Expr.from(gpa, ss[2]),
} };
} else if (std.mem.eql(u8, name, "while")) {
if (ss.len != 3 or ss[2] != .block) {
return error.InvalidWhile;
}
const cond = try Expr.from(gpa, ss[1]);
var body = std.ArrayList(Stmt).empty;
for (ss[2].block) |inner| {
try body.append(gpa, try Stmt.from(gpa, inner));
}
return .{ .while_loop = .{
.cond = cond,
.body = try body.toOwnedSlice(gpa),
} };
} else {
return .{ .expr = try Expr.from(gpa, sexpr) };
}
},
else => return .{ .expr = try Expr.from(gpa, sexpr) },
}
}
}; };
pub const Expr = union(enum) { pub const Expr = union(enum) {
@@ -226,4 +188,42 @@ pub const Expr = union(enum) {
name: []const u8, name: []const u8,
args: []const Expr, args: []const Expr,
}, },
fn from(gpa: std.mem.Allocator, sexpr: SExpr) !Expr {
switch (sexpr) {
.integer => |int| return .{ .integer = int },
.identifier => |id| {
if (std.mem.eql(u8, id, "nil")) {
return .nil;
}
return .{ .variable = id };
},
.string => |str| return .{ .string = str },
.call => |ss| {
if (ss.len < 1 or ss[0] != .identifier) return error.InvalidCall;
var args = std.ArrayList(Expr).empty;
for (ss[1..]) |inner| {
try args.append(gpa, try Expr.from(gpa, inner));
}
return .{ .func_call = .{
.name = ss[0].identifier,
.args = try args.toOwnedSlice(gpa),
} };
},
.block => return error.BlockAsExpr,
.list => return error.ListsNotImplemented,
}
}
}; };
pub fn parse(gpa: std.mem.Allocator, tokens: []const lexer.Token) !Program {
const sexprs = try parse_sexprs(gpa, tokens);
var funcs = std.ArrayList(Func).empty;
for (sexprs) |sexpr| {
const func = try Func.from(gpa, sexpr);
try funcs.append(gpa, func);
}
return Program{
.funcs = try funcs.toOwnedSlice(gpa),
};
}

View File

@@ -1,2 +1,3 @@
pub const lexer = @import("lexer.zig"); pub const lexer = @import("lexer.zig");
pub const parser = @import("parser.zig"); pub const parser = @import("parser.zig");
pub const interpreter = @import("interpreter.zig");

View File

@@ -3,7 +3,12 @@ const std = @import("std");
const lib = @import("libneon"); const lib = @import("libneon");
fn usage(w: *std.Io.Writer, program: []const u8) error{WriteFailed}!void { fn usage(w: *std.Io.Writer, program: []const u8) error{WriteFailed}!void {
try w.print("Usage: {s} <input>\n", .{program}); try w.print(
\\Usage: {s} [flags] <input>
\\Flags:
\\ -h, --help Show this help message and quit
\\
, .{program});
} }
fn entry(init: struct { fn entry(init: struct {
@@ -16,7 +21,7 @@ fn entry(init: struct {
var input_path: ?[]const u8 = null; var input_path: ?[]const u8 = null;
for (init.args[1..]) |arg| { for (init.args[1..]) |arg| {
if (std.mem.eql(u8, arg, "--help")) { if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
try usage(init.stdout, program_name); try usage(init.stdout, program_name);
try init.stdout.flush(); try init.stdout.flush();
return; return;
@@ -37,17 +42,19 @@ fn entry(init: struct {
return; return;
}; };
const src = try std.fs.cwd().readFileAlloc(init.arena.allocator(), input_path.?, input_stat.size); const src = try std.fs.cwd().readFileAlloc(
init.arena.allocator(),
input_path.?,
input_stat.size,
);
const tokens = try lib.lexer.lex(init.arena.allocator(), src); const tokens = try lib.lexer.lex(init.arena.allocator(), src);
const sexprs = try lib.parser.parse_sexprs(init.arena.allocator(), tokens); const program = try lib.parser.parse(init.arena.allocator(), tokens);
for (sexprs) |sexpr| { const interpreter = lib.interpreter.Interpreter{ .stdout = init.stdout };
const stmt = try sexpr.parse_stmt(init.arena.allocator());
try init.stdout.print("{any}\n", .{stmt}); try interpreter.interpret(program);
try init.stdout.flush();
}
} }
pub fn main() !void { pub fn main() !void {