Compare commits

..

12 Commits

Author SHA1 Message Date
374a221e94 remove funcs for now
All checks were successful
Run unit tests / zig build test (push) Successful in 33s
2026-01-23 20:16:19 +01:00
a42770f4cc fix test
All checks were successful
Run unit tests / zig build test (push) Successful in 31s
2026-01-23 20:07:24 +01:00
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
73268f59b5 implement a very rough translator from s-expressions to AST
All checks were successful
Run unit tests / zig build test (push) Successful in 47s
2026-01-07 01:03:52 +01:00
8f5a3e00b8 clean up renaming 2026-01-07 00:32:53 +01:00
6991ef6bcc add build.zig.zon
All checks were successful
Run unit tests / zig build test (push) Successful in 43s
2026-01-03 22:47:43 +01:00
92f92f7e2b expand example
All checks were successful
Run unit tests / zig build test (push) Successful in 42s
2026-01-03 22:36:58 +01:00
9fe595e5f5 rework directory structure, add build.zig
All checks were successful
Run unit tests / zig build test (push) Successful in 44s
2026-01-03 22:20:40 +01:00
16 changed files with 658 additions and 328 deletions

View File

@@ -1,14 +1,13 @@
name: Run all tests
run-name: Run all tests
name: Run unit tests
on: [push]
jobs:
test:
name: zig test
name: zig build test
runs-on: ubuntu-latest
steps:
- uses: mlugg/setup-zig@v2
- uses: actions/checkout@v6
with:
token: ${{ secrets.ACCESS_TOKEN }}
- run: find src/ -type f -regex '.*\.zig' -exec zig test '{}' ';'
- run: zig build test --summary all

3
.gitignore vendored
View File

@@ -1 +1,2 @@
examples/
zig-out/
.zig-cache/

View File

@@ -2,7 +2,7 @@
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![test workflow](https://git.svecs.cz/svecs/neon/actions/workflows/test.yml/badge.svg?branch=main)](https://git.svecs.cz/svecs/neon/actions/?workflow=test.yml)
> a simple scripting language (hopefully one day)
> a simple programming language (hopefully one day)
## features
@@ -12,3 +12,13 @@
> for now the language uses an s-expression syntax (even though it's not a lisp) to simplify parsing. this syntax WILL change in the future!
- currently none lol
## quick start
required tools:
- [zig](https://ziglang.org/download/)
```shell
zig build run -- examples/first.ne
```

55
build.zig Normal file
View File

@@ -0,0 +1,55 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const lib = b.addLibrary(.{
.name = "neon",
.root_module = b.createModule(.{
.root_source_file = b.path("lib/root.zig"),
.target = target,
.optimize = optimize,
}),
});
const exe = b.addExecutable(.{
.name = "neon",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "libneon", .module = lib.root_module },
},
}),
});
b.installArtifact(exe);
const run_exe = b.addRunArtifact(exe);
if (b.args) |args| {
run_exe.addArgs(args);
}
const run_step = b.step("run", "Run the application");
run_step.dependOn(&run_exe.step);
const tests = b.addTest(.{
.name = "all",
.root_module = b.createModule(.{
.root_source_file = b.path("tests/all.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "libneon", .module = lib.root_module },
},
}),
});
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run the tests");
test_step.dependOn(&run_tests.step);
}

7
build.zig.zon Normal file
View File

@@ -0,0 +1,7 @@
.{
.name = .neon,
.fingerprint = 0xfcce6dd0bc6abcf3,
.version = "0.0.1",
.minimum_zig_version = "0.15.2",
.paths = .{""},
}

1
examples/first.ne Normal file
View File

@@ -0,0 +1 @@
(print_int (+ 1 (* 2 34)))

75
lib/interpreter.zig Normal file
View File

@@ -0,0 +1,75 @@
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 {
for (program.stmts) |stmt| {
try self.run_stmt(stmt);
try self.stdout.flush();
}
}
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,
};

106
lib/lexer.zig Normal file
View File

@@ -0,0 +1,106 @@
const std = @import("std");
pub const Token = union(enum) {
lparen,
rparen,
lbrace,
rbrace,
lbracket,
rbracket,
integer: u64,
identifier: []const u8,
string: []const u8,
pub fn format(self: @This(), w: *std.Io.Writer) error{WriteFailed}!void {
try switch (self) {
.lparen => w.print("lparen", .{}),
.rparen => w.print("rparen", .{}),
.lbrace => w.print("lbrace", .{}),
.rbrace => w.print("rbrace", .{}),
.lbracket => w.print("lbracket", .{}),
.rbracket => w.print("rbracket", .{}),
.integer => |int| w.print("int: {d}", .{int}),
.identifier => |id| w.print("id: {s}", .{id}),
.string => |str| w.print("str: {s}", .{str}),
};
}
};
pub fn lex(gpa: std.mem.Allocator, src: []const u8) error{ OutOfMemory, Overflow, InvalidCharacter }![]const Token {
var result = std.ArrayList(Token).empty;
var idx: usize = 0;
while (idx < src.len) {
const token: ?Token = sw: switch (src[idx]) {
' ', '\n', '\r', '\t' => null,
'#' => {
while (idx < src.len and src[idx] != '\n') idx += 1;
break :sw null;
},
'(' => .lparen,
')' => .rparen,
'{' => .lbrace,
'}' => .rbrace,
'[' => .lbracket,
']' => .rbracket,
'0'...'9' => {
const start = idx;
while (idx < src.len and std.ascii.isDigit(src[idx])) {
idx += 1;
}
const int = try std.fmt.parseInt(u64, src[start..idx], 10);
idx -= 1;
break :sw .{ .integer = int };
},
'A'...'Z',
'a'...'z',
'!',
'$'...'\'',
'*'...'/',
':'...'@',
'^',
'_',
'~',
=> {
const start = idx;
while (idx < src.len) {
switch (src[idx]) {
'A'...'Z',
'a'...'z',
'0'...'9',
'!',
'$'...'\'',
'*'...'/',
':'...'@',
'^',
'_',
'~',
=> {
idx += 1;
},
else => break,
}
}
idx -= 1;
break :sw .{ .identifier = src[start .. idx + 1] };
},
'"' => {
idx += 1;
const start = idx;
while (idx < src.len and src[idx] != '"') idx += 1;
break :sw .{ .string = src[start..idx] };
},
else => {
std.debug.print("invalid character `{c}' at position {d}\n", .{ src[idx], idx });
return error.InvalidCharacter;
},
};
idx += 1;
if (token) |t| {
try result.append(gpa, t);
}
}
return result.toOwnedSlice(gpa);
}

196
lib/parser.zig Normal file
View File

@@ -0,0 +1,196 @@
const std = @import("std");
const lexer = @import("lexer.zig");
const SExpr = union(enum) {
integer: u64, // 42
identifier: []const u8, // module.name
string: []const u8, // "foo bar"
call: []const SExpr, // (+ 1 2 3)
block: []const SExpr, // { (print "hi") }
list: []const SExpr, // [1 2 3]
};
fn parse_sexpr(gpa: std.mem.Allocator, tokens: []const lexer.Token) !struct { SExpr, usize } {
if (tokens.len == 0) {
return error.UnexpectedEOF;
}
switch (tokens[0]) {
.integer => |int| return .{ .{ .integer = int }, 1 },
.identifier => |id| return .{ .{ .identifier = id }, 1 },
.string => |str| return .{ .{ .string = str }, 1 },
.lparen => {
var call = std.ArrayList(SExpr).empty;
var tokens_rest = tokens[1..];
while (tokens_rest.len > 0 and tokens_rest[0] != .rparen) {
const sexpr, const consumed = try parse_sexpr(gpa, tokens_rest);
try call.append(gpa, sexpr);
tokens_rest = tokens_rest[consumed..];
}
if (tokens_rest.len == 0) {
return error.UnmatchedParen;
}
const consumed = tokens_rest[1..].ptr - tokens.ptr;
return .{ .{ .call = try call.toOwnedSlice(gpa) }, consumed };
},
.lbrace => {
var block = std.ArrayList(SExpr).empty;
var tokens_rest = tokens[1..];
while (tokens_rest.len > 0 and tokens_rest[0] != .rbrace) {
const sexpr, const consumed = try parse_sexpr(gpa, tokens_rest);
try block.append(gpa, sexpr);
tokens_rest = tokens_rest[consumed..];
}
if (tokens_rest.len == 0) {
return error.UnmatchedBrace;
}
const consumed = tokens_rest[1..].ptr - tokens.ptr;
return .{ .{ .block = try block.toOwnedSlice(gpa) }, consumed };
},
.lbracket => {
var list = std.ArrayList(SExpr).empty;
var tokens_rest = tokens[1..];
while (tokens_rest.len > 0 and tokens_rest[0] != .rbracket) {
const sexpr, const consumed = try parse_sexpr(gpa, tokens_rest);
try list.append(gpa, sexpr);
tokens_rest = tokens_rest[consumed..];
}
if (tokens_rest.len == 0) {
return error.UnmatchedBracket;
}
const consumed = tokens_rest[1..].ptr - tokens.ptr;
return .{ .{ .list = try list.toOwnedSlice(gpa) }, consumed };
},
else => {
return error.UnexpectedToken;
},
}
}
fn parse_sexprs(gpa: std.mem.Allocator, tokens: []const lexer.Token) ![]const SExpr {
var result = std.ArrayList(SExpr).empty;
var idx: usize = 0;
while (idx < tokens.len) {
const sexpr, const consumed = try parse_sexpr(gpa, tokens[idx..]);
try result.append(gpa, sexpr);
idx += consumed;
}
return result.toOwnedSlice(gpa);
}
pub const Program = struct {
stmts: []const Stmt,
};
pub const Stmt = union(enum) {
var_let: struct {
name: []const u8,
value: Expr,
},
var_set: struct {
name: []const u8,
value: Expr,
},
while_loop: struct {
cond: Expr,
body: []const Stmt,
},
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) {
nil,
variable: []const u8,
integer: u64,
string: []const u8,
func_call: struct {
name: []const u8,
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 stmts = std.ArrayList(Stmt).empty;
for (sexprs) |sexpr| {
const stmt = try Stmt.from(gpa, sexpr);
try stmts.append(gpa, stmt);
}
return Program{
.stmts = try stmts.toOwnedSlice(gpa),
};
}

3
lib/root.zig Normal file
View File

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

View File

@@ -1,131 +0,0 @@
const std = @import("std");
pub const Token = union(enum) {
lparen,
rparen,
lbrace,
rbrace,
lbracket,
rbracket,
integer: u64,
identifier: []const u8,
string: []const u8,
pub fn format(self: @This(), writer: anytype) !void {
try switch (self) {
.lparen => writer.print("lparen", .{}),
.rparen => writer.print("rparen", .{}),
.lbrace => writer.print("lbrace", .{}),
.rbrace => writer.print("rbrace", .{}),
.lbracket => writer.print("lbracket", .{}),
.rbracket => writer.print("rbracket", .{}),
.integer => |int| writer.print("int: {d}", .{int}),
.identifier => |id| writer.print("id: {s}", .{id}),
.string => |str| writer.print("str: {s}", .{str}),
};
}
};
pub fn lex(gpa: std.mem.Allocator, src: []const u8) ![]const Token {
var result = std.ArrayList(Token).empty;
var idx: usize = 0;
while (idx < src.len) {
const token: ?Token = sw: switch (src[idx]) {
' ', '\n', '\r', '\t' => null,
'#' => {
while (idx < src.len and src[idx] != '\n') idx += 1;
break :sw null;
},
'(' => .lparen,
')' => .rparen,
'{' => .lbrace,
'}' => .rbrace,
'[' => .lbracket,
']' => .rbracket,
'0'...'9' => {
const start = idx;
while (idx < src.len and std.ascii.isDigit(src[idx])) {
idx += 1;
}
const int = try std.fmt.parseInt(u64, src[start..idx], 10);
idx -= 1;
break :sw .{ .integer = int };
},
'A'...'Z', 'a'...'z', '!', '$'...'\'', '*'...'/', ':'...'@', '^', '_', '~' => {
const start = idx;
while (idx < src.len) {
switch (src[idx]) {
'A'...'Z', 'a'...'z', '0'...'9', '!', '$'...'\'', '*'...'/', ':'...'@', '^', '_', '~' => {
idx += 1;
},
else => break,
}
}
idx -= 1;
break :sw .{ .identifier = src[start .. idx + 1] };
},
'"' => {
idx += 1;
const start = idx;
while (idx < src.len and src[idx] != '"') idx += 1;
break :sw .{ .string = src[start..idx] };
},
else => {
std.debug.print("invalid character `{c}'\n", .{src[idx]});
return error.InvalidCharacter;
},
};
idx += 1;
if (token) |t| {
try result.append(gpa, t);
}
}
return result.toOwnedSlice(gpa);
}
test "lexer" {
const t = std.testing;
var gpa = std.heap.DebugAllocator(.{}).init;
const src =
\\(let input (fs.read_file "input.txt")) # comment test
\\{
\\ (!_~special ch4racter test')
\\ (print [1 2 3 input])
\\}
;
const tokens = try lex(gpa.allocator(), src);
defer gpa.allocator().free(tokens);
try t.expectEqualDeep(tokens, &[_]Token{
.lparen,
.{ .identifier = "let" },
.{ .identifier = "input" },
.lparen,
.{ .identifier = "fs.read_file" },
.{ .string = "input.txt" },
.rparen,
.rparen,
.lbrace,
.lparen,
.{ .identifier = "!_~special" },
.{ .identifier = "ch4racter" },
.{ .identifier = "test'" },
.rparen,
.lparen,
.{ .identifier = "print" },
.lbracket,
.{ .integer = 1 },
.{ .integer = 2 },
.{ .integer = 3 },
.{ .identifier = "input" },
.rbracket,
.rparen,
.rbrace,
});
}

View File

@@ -1,22 +1,83 @@
const std = @import("std");
const lexer = @import("lexer.zig");
const parser = @import("parser.zig");
const lib = @import("libneon");
fn usage(w: *std.Io.Writer, program: []const u8) error{WriteFailed}!void {
try w.print(
\\Usage: {s} [flags] <input>
\\Flags:
\\ -h, --help Show this help message and quit
\\
, .{program});
}
fn entry(init: struct {
arena: *std.heap.ArenaAllocator,
args: []const []const u8,
stdout: *std.Io.Writer,
stderr: *std.Io.Writer,
}) !void {
const program_name = init.args[0];
var input_path: ?[]const u8 = null;
for (init.args[1..]) |arg| {
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
try usage(init.stdout, program_name);
try init.stdout.flush();
return;
}
if (input_path == null) input_path = arg;
}
if (input_path == null) {
try usage(init.stderr, program_name);
try init.stderr.print("Error: no input file\n", .{});
try init.stderr.flush();
return;
}
const input_stat = std.fs.cwd().statFile(input_path.?) catch {
try init.stderr.print("Error: could not read file {s}\n", .{input_path.?});
try init.stderr.flush();
return;
};
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 program = try lib.parser.parse(init.arena.allocator(), tokens);
const interpreter = lib.interpreter.Interpreter{ .stdout = init.stdout };
try interpreter.interpret(program);
}
pub fn main() !void {
var gpa = std.heap.DebugAllocator(.{}).init;
var debug_allocator = std.heap.DebugAllocator(.{}).init;
defer std.debug.assert(debug_allocator.deinit() == .ok);
const src: []const u8 =
\\(func main [] {
\\ (let input (fs.read_file "input.txt"))
\\ (print input)
\\})
;
var arena = std.heap.ArenaAllocator.init(debug_allocator.allocator());
defer arena.deinit();
const tokens = try lexer.lex(gpa.allocator(), src);
defer gpa.allocator().free(tokens);
var stdout_buffer: [1024]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
const stdout = &stdout_writer.interface;
const expr = try parser.parse_expr(gpa.allocator(), tokens);
var stderr_buffer: [1024]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
const stderr = &stderr_writer.interface;
std.debug.print("{f}", .{expr});
const args = try std.process.argsAlloc(arena.allocator());
return entry(.{
.arena = &arena,
.args = args,
.stdout = stdout,
.stderr = stderr,
});
}

View File

@@ -1,178 +0,0 @@
const std = @import("std");
const lexer = @import("lexer.zig");
pub const Expr = union(enum) {
integer: u64, // 42
identifier: []const u8, // module.name
string: []const u8, // "foo bar"
call: []const Expr, // (+ 1 2 3)
block: []const Expr, // { (print "hi") }
list: []const Expr, // [1 2 3]
fn format_(self: @This(), writer: anytype, depth: usize) !void {
for (0..depth) |_| {
try writer.print(" ", .{});
}
switch (self) {
.integer => |int| try writer.print("int: {d}\n", .{int}),
.identifier => |id| try writer.print("id: {s}\n", .{id}),
.string => |str| try writer.print("str: {s}\n", .{str}),
.call => |call| {
try writer.print("call: {s}\n", .{if (call.len == 0) "()" else ""});
for (call) |expr| {
try expr.format_(writer, depth + 1);
}
},
.block => |block| {
try writer.print("block: {s}\n", .{if (block.len == 0) "{}" else ""});
for (block) |expr| {
try expr.format_(writer, depth + 1);
}
},
.list => |list| {
try writer.print("list: {s}\n", .{if (list.len == 0) "[]" else ""});
for (list) |expr| {
try expr.format_(writer, depth + 1);
}
},
}
}
pub fn format(self: @This(), writer: anytype) !void {
try self.format_(writer, 0);
}
};
fn parse_expr_(gpa: std.mem.Allocator, tokens: []const lexer.Token) !struct { Expr, usize } {
if (tokens.len == 0) {
return error.UnexpectedEOF;
}
switch (tokens[0]) {
.integer => |int| return .{ .{ .integer = int }, 1 },
.identifier => |id| return .{ .{ .identifier = id }, 1 },
.string => |str| return .{ .{ .string = str }, 1 },
.lparen => {
var call = std.ArrayList(Expr).empty;
var tokens_rest = tokens[1..];
while (tokens_rest.len > 0 and tokens_rest[0] != .rparen) {
const expr, const consumed = try parse_expr_(gpa, tokens_rest);
try call.append(gpa, expr);
tokens_rest = tokens_rest[consumed..];
}
if (tokens_rest.len == 0) {
return error.UnmatchedParen;
}
const consumed = tokens_rest[1..].ptr - tokens.ptr;
return .{ .{ .call = try call.toOwnedSlice(gpa) }, consumed };
},
.lbrace => {
var block = std.ArrayList(Expr).empty;
var tokens_rest = tokens[1..];
while (tokens_rest.len > 0 and tokens_rest[0] != .rbrace) {
const expr, const consumed = try parse_expr_(gpa, tokens_rest);
try block.append(gpa, expr);
tokens_rest = tokens_rest[consumed..];
}
if (tokens_rest.len == 0) {
return error.UnmatchedBrace;
}
const consumed = tokens_rest[1..].ptr - tokens.ptr;
return .{ .{ .block = try block.toOwnedSlice(gpa) }, consumed };
},
.lbracket => {
var list = std.ArrayList(Expr).empty;
var tokens_rest = tokens[1..];
while (tokens_rest.len > 0 and tokens_rest[0] != .rbracket) {
const expr, const consumed = try parse_expr_(gpa, tokens_rest);
try list.append(gpa, expr);
tokens_rest = tokens_rest[consumed..];
}
if (tokens_rest.len == 0) {
return error.UnmatchedBracket;
}
const consumed = tokens_rest[1..].ptr - tokens.ptr;
return .{ .{ .list = try list.toOwnedSlice(gpa) }, consumed };
},
else => {
return error.UnexpectedToken;
},
}
}
pub fn parse_expr(gpa: std.mem.Allocator, tokens: []const lexer.Token) !Expr {
const expr, const consumed = try parse_expr_(gpa, tokens);
if (tokens[consumed..].len > 0) return error.ExtraInput;
return expr;
}
test "parser" {
const t = std.testing;
var gpa = std.heap.DebugAllocator(.{}).init;
try t.expectEqualDeep(
Expr{ .call = &.{
.{ .identifier = "foo" },
.{ .identifier = "bar" },
.{ .string = "baz" },
.{ .list = &.{
.{ .integer = 1 },
.{ .integer = 2 },
.{ .integer = 3 },
} },
.{ .block = &.{} },
} },
try parse_expr(gpa.allocator(), &.{
.lparen,
.{ .identifier = "foo" },
.{ .identifier = "bar" },
.{ .string = "baz" },
.lbracket,
.{ .integer = 1 },
.{ .integer = 2 },
.{ .integer = 3 },
.rbracket,
.lbrace,
.rbrace,
.rparen,
}),
);
try t.expectError(
error.UnexpectedEOF,
parse_expr(gpa.allocator(), &.{}),
);
try t.expectError(
error.ExtraInput,
parse_expr(gpa.allocator(), &.{
.lparen,
.{ .integer = 123 },
.rparen,
.{ .identifier = "extra" },
}),
);
try t.expectError(
error.UnmatchedParen,
parse_expr(gpa.allocator(), &.{
.lbracket,
.{ .integer = 34 },
.lparen,
.{ .identifier = "name" },
}),
);
try t.expectError(
error.UnmatchedBracket,
parse_expr(gpa.allocator(), &.{
.lparen,
.{ .integer = 34 },
.lbracket,
.{ .identifier = "name" },
}),
);
}

4
tests/all.zig Normal file
View File

@@ -0,0 +1,4 @@
test {
_ = @import("lexer_test.zig");
_ = @import("parser_test.zig");
}

45
tests/lexer_test.zig Normal file
View File

@@ -0,0 +1,45 @@
const std = @import("std");
const t = std.testing;
const lib = @import("libneon");
test "lexer" {
const tokens = try lib.lexer.lex(t.allocator,
\\(let input (fs.read_file "input.txt")) # comment test
\\{
\\ (!_~special ch4racter test')
\\ (print [1 2 3 input])
\\}
);
defer t.allocator.free(tokens);
try t.expectEqualDeep(
&[_]lib.lexer.Token{
.lparen,
.{ .identifier = "let" },
.{ .identifier = "input" },
.lparen,
.{ .identifier = "fs.read_file" },
.{ .string = "input.txt" },
.rparen,
.rparen,
.lbrace,
.lparen,
.{ .identifier = "!_~special" },
.{ .identifier = "ch4racter" },
.{ .identifier = "test'" },
.rparen,
.lparen,
.{ .identifier = "print" },
.lbracket,
.{ .integer = 1 },
.{ .integer = 2 },
.{ .integer = 3 },
.{ .identifier = "input" },
.rbracket,
.rparen,
.rbrace,
},
tokens,
);
}

76
tests/parser_test.zig Normal file
View File

@@ -0,0 +1,76 @@
const std = @import("std");
const t = std.testing;
const lib = @import("libneon");
test "program" {
var arena = std.heap.ArenaAllocator.init(t.allocator);
defer arena.deinit();
const gpa = arena.allocator();
try t.expectEqualDeep(
lib.parser.Program{
.stmts = &.{
.{ .expr = .{ .func_call = .{ .name = "foo", .args = &.{
.{ .variable = "bar" },
.{ .string = "baz" },
.{ .func_call = .{ .name = "+", .args = &.{
.{ .integer = 1 },
.{ .integer = 2 },
} } },
} } } },
},
},
try lib.parser.parse(gpa, &.{
.lparen,
.{ .identifier = "foo" },
.{ .identifier = "bar" },
.{ .string = "baz" },
.lparen,
.{ .identifier = "+" },
.{ .integer = 1 },
.{ .integer = 2 },
.rparen,
.rparen,
}),
);
}
test "empty program" {
var arena = std.heap.ArenaAllocator.init(t.allocator);
defer arena.deinit();
const gpa = arena.allocator();
try t.expectEqualDeep(
lib.parser.Program{
.stmts = &.{},
},
lib.parser.parse(gpa, &.{}),
);
}
test "unmatched delimiters" {
var arena = std.heap.ArenaAllocator.init(t.allocator);
defer arena.deinit();
const gpa = arena.allocator();
try t.expectError(
error.UnmatchedParen,
lib.parser.parse(gpa, &.{
.lbracket,
.{ .integer = 34 },
.lparen,
.{ .identifier = "name" },
}),
);
try t.expectError(
error.UnmatchedBracket,
lib.parser.parse(gpa, &.{
.lparen,
.{ .integer = 34 },
.lbracket,
.{ .identifier = "name" },
}),
);
}