aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--build.zig79
-rw-r--r--build.zig.zon86
-rw-r--r--src/lexer.zig66
-rw-r--r--src/main.zig46
-rw-r--r--src/parse.zig50
-rw-r--r--src/peek.zig51
7 files changed, 380 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..db3cb54
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/.zig-cache/
+/zig-out/
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..33e8492
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,79 @@
+const std = @import("std");
+
+// Although this function looks imperative, note that its job is to
+// declaratively construct a build graph that will be executed by an external
+// runner.
+pub fn build(b: *std.Build) void {
+ // Standard target options allows the person running `zig build` to choose
+ // what target to build for. Here we do not override the defaults, which
+ // means any target is allowed, and the default is native. Other options
+ // for restricting supported target set are available.
+ const target = b.standardTargetOptions(.{});
+
+ // Standard optimization options allow the person running `zig build` to select
+ // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
+ // set a preferred release mode, allowing the user to decide how to optimize.
+ const optimize = b.standardOptimizeOption(.{});
+
+ // We will also create a module for our other entry point, 'main.zig'.
+ const exe_mod = b.createModule(.{
+ // `root_source_file` is the Zig "entry point" of the module. If a module
+ // only contains e.g. external object files, you can make this `null`.
+ // In this case the main source file is merely a path, however, in more
+ // complicated build scripts, this could be a generated file.
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ // This creates another `std.Build.Step.Compile`, but this one builds an executable
+ // rather than a static library.
+ const exe = b.addExecutable(.{
+ .name = "huginn2",
+ .root_module = exe_mod,
+ .use_llvm = false,
+ .use_lld = false,
+ });
+
+ // This declares intent for the executable to be installed into the
+ // standard location when the user invokes the "install" step (the default
+ // step when running `zig build`).
+ b.installArtifact(exe);
+
+ // This *creates* a Run step in the build graph, to be executed when another
+ // step is evaluated that depends on it. The next line below will establish
+ // such a dependency.
+ const run_cmd = b.addRunArtifact(exe);
+
+ // By making the run step depend on the install step, it will be run from the
+ // installation directory rather than directly from within the cache directory.
+ // This is not necessary, however, if the application depends on other installed
+ // files, this ensures they will be present and in the expected location.
+ run_cmd.step.dependOn(b.getInstallStep());
+
+ // This allows the user to pass arguments to the application in the build
+ // command itself, like this: `zig build run -- arg1 arg2 etc`
+ if (b.args) |args| {
+ run_cmd.addArgs(args);
+ }
+
+ // This creates a build step. It will be visible in the `zig build --help` menu,
+ // and can be selected like this: `zig build run`
+ // This will evaluate the `run` step rather than the default, which is "install".
+ const run_step = b.step("run", "Run the app");
+ run_step.dependOn(&run_cmd.step);
+
+ const exe_unit_tests = b.addTest(.{
+ .root_module = exe_mod,
+ .use_llvm = false,
+ .use_lld = false,
+ });
+
+ const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
+
+ // Similar to creating the run step earlier, this exposes a `test` step to
+ // the `zig build --help` menu, providing a way for the user to request
+ // running the unit tests.
+ const test_step = b.step("test", "Run unit tests");
+ test_step.dependOn(&run_exe_unit_tests.step);
+}
diff --git a/build.zig.zon b/build.zig.zon
new file mode 100644
index 0000000..0ddded9
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,86 @@
+.{
+ // This is the default name used by packages depending on this one. For
+ // example, when a user runs `zig fetch --save <url>`, this field is used
+ // as the key in the `dependencies` table. Although the user can choose a
+ // different name, most users will stick with this provided value.
+ //
+ // It is redundant to include "zig" in this name because it is already
+ // within the Zig package namespace.
+ .name = .huginn2,
+
+ // This is a [Semantic Version](https://semver.org/).
+ // In a future version of Zig it will be used for package deduplication.
+ .version = "0.0.0",
+
+ // Together with name, this represents a globally unique package
+ // identifier. This field is generated by the Zig toolchain when the
+ // package is first created, and then *never changes*. This allows
+ // unambiguous detection of one package being an updated version of
+ // another.
+ //
+ // When forking a Zig project, this id should be regenerated (delete the
+ // field and run `zig build`) if the upstream project is still maintained.
+ // Otherwise, the fork is *hostile*, attempting to take control over the
+ // original project's identity. Thus it is recommended to leave the comment
+ // on the following line intact, so that it shows up in code reviews that
+ // modify the field.
+ .fingerprint = 0x6206a3d97ac08ad8, // Changing this has security and trust implications.
+
+ // Tracks the earliest Zig version that the package considers to be a
+ // supported use case.
+ .minimum_zig_version = "0.14.0",
+
+ // This field is optional.
+ // Each dependency must either provide a `url` and `hash`, or a `path`.
+ // `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
+ // Once all dependencies are fetched, `zig build` no longer requires
+ // internet connectivity.
+ .dependencies = .{
+ // See `zig fetch --save <url>` for a command-line interface for adding dependencies.
+ //.example = .{
+ // // When updating this field to a new URL, be sure to delete the corresponding
+ // // `hash`, otherwise you are communicating that you expect to find the old hash at
+ // // the new URL. If the contents of a URL change this will result in a hash mismatch
+ // // which will prevent zig from using it.
+ // .url = "https://example.com/foo.tar.gz",
+ //
+ // // This is computed from the file contents of the directory of files that is
+ // // obtained after fetching `url` and applying the inclusion rules given by
+ // // `paths`.
+ // //
+ // // This field is the source of truth; packages do not come from a `url`; they
+ // // come from a `hash`. `url` is just one of many possible mirrors for how to
+ // // obtain a package matching this `hash`.
+ // //
+ // // Uses the [multihash](https://multiformats.io/multihash/) format.
+ // .hash = "...",
+ //
+ // // When this is provided, the package is found in a directory relative to the
+ // // build root. In this case the package's hash is irrelevant and therefore not
+ // // computed. This field and `url` are mutually exclusive.
+ // .path = "foo",
+ //
+ // // When this is set to `true`, a package is declared to be lazily
+ // // fetched. This makes the dependency only get fetched if it is
+ // // actually used.
+ // .lazy = false,
+ //},
+ },
+
+ // Specifies the set of files and directories that are included in this package.
+ // Only files and directories listed here are included in the `hash` that
+ // is computed for this package. Only files listed here will remain on disk
+ // when using the zig package manager. As a rule of thumb, one should list
+ // files required for compilation plus any license(s).
+ // Paths are relative to the build root. Use the empty string (`""`) to refer to
+ // the build root itself.
+ // A directory listed here means that all files within, recursively, are included.
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ // For example...
+ //"LICENSE",
+ //"README.md",
+ },
+}
diff --git a/src/lexer.zig b/src/lexer.zig
new file mode 100644
index 0000000..93ed6cc
--- /dev/null
+++ b/src/lexer.zig
@@ -0,0 +1,66 @@
+pub const Token = struct {
+ start: usize,
+ end: usize,
+ type: Type,
+
+ pub const Type = union(enum) {
+ LeftParen,
+ RightParen,
+ IntegerLiteral: usize,
+ Plus,
+ Invalid,
+ Eof,
+ };
+};
+
+source: []const u8,
+last_end: usize = 0,
+pos: usize = 0,
+
+pub fn next(self: *Self) ?Token {
+ return s: switch (self.eat() orelse return self.create(.Eof)) {
+ '(' => self.create(.LeftParen),
+ ')' => self.create(.RightParen),
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => |d| self.integerLiteral(d),
+ '+' => self.create(.Plus),
+ ' ' => {
+ self.last_end = self.pos;
+ continue :s (self.eat() orelse return self.create(.Eof));
+ },
+ else => self.create(.Invalid),
+ };
+}
+
+fn integerLiteral(self: *Self, first: u8) Token {
+ var value: usize = @intCast(digitValue(first).?);
+ while (digitValue(self.peek())) |d| {
+ _ = self.eat();
+ value = value *| 10 +| d;
+ }
+ return self.create(.{ .IntegerLiteral = value });
+}
+
+fn create(self: *Self, tajp: Token.Type) Token {
+ const start = self.last_end;
+ self.last_end = self.pos;
+ return .{ .start = start, .end = self.pos, .type = tajp };
+}
+
+fn eat(self: *Self) ?u8 {
+ const token = self.peek();
+ if (token != null) self.pos += 1;
+ return token;
+}
+
+fn peek(self: *Self) ?u8 {
+ return if (self.pos < self.source.len) self.source[self.pos] else null;
+}
+
+const Self = @This();
+
+fn digitValue(c: ?u8) ?u8 {
+ return switch (c orelse return null) {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => c.? - '0',
+ else => null,
+ };
+}
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..e897d41
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,46 @@
+pub const peekable = peek.peekable;
+pub const Peekable = peek.Peekable;
+
+pub fn main() !void {
+ var arena: std.heap.ArenaAllocator = .init(std.heap.smp_allocator);
+ defer arena.deinit();
+ const allocator = arena.allocator();
+
+ var bw = std.io.bufferedWriter(std.io.getStdOut().writer());
+ const stdout = bw.writer();
+
+ // var br = std.io.bufferedReader(std.io.getStdIn().reader());
+ // const stdin = br.reader();
+ //
+ // var line: std.ArrayList(u8) = .init(alloc);
+ // defer line.deinit();
+ // while (true) {
+ // try stdin.streamUntilDelimiter(line.writer(), '\n', null);
+ //
+ // const lexer = Lexer{.source = line};
+ //
+ // try stdout.print("{s}\n", .{line.items});
+ // }
+
+ const source = "69 + (32 + 1) + 2";
+ // var lexer = Lexer{ .source = source };
+ // while (true) {
+ // const token = lexer.next().?;
+ // try stdout.print("{}\n", .{token});
+ // if (token.type == .Eof) break;
+ // }
+ // try stdout.print("\n", .{});
+ var lexer2 = peekable(Lexer{ .source = source });
+ try stdout.print("{}\n", .{try parse.expression(allocator, &lexer2)});
+
+ try bw.flush(); // Don't forget to flush!
+}
+
+const std = @import("std");
+const Lexer = @import("./lexer.zig");
+const parse = @import("./parse.zig");
+const peek = @import("./peek.zig");
+
+test {
+ _ = peek;
+}
diff --git a/src/parse.zig b/src/parse.zig
new file mode 100644
index 0000000..8ceabeb
--- /dev/null
+++ b/src/parse.zig
@@ -0,0 +1,50 @@
+pub const Expr = union(enum) {
+ IntegerLiteral: Token,
+ BinOp: struct { lhs: *const Expr, op: Token, rhs: *const Expr },
+ Invalid: Token,
+};
+
+pub fn expression(allocator: Allocator, lexer: *Peekable(Lexer)) error{OutOfMemory}!*Expr {
+ return addExpr(allocator, lexer);
+}
+
+pub fn addExpr(allocator: Allocator, lexer: *Peekable(Lexer)) !*Expr {
+ const lhs = try primaryExpr(allocator, lexer);
+ const token: ?Lexer.Token = lexer.peek();
+ const op = (if (token) |t| if (t.type == .Plus) t else null else null) orelse return lhs;
+ _ = lexer.next();
+
+ const rhs = try primaryExpr(allocator, lexer);
+ return allocate(allocator, .{ .BinOp = .{ .lhs = lhs, .op = op, .rhs = rhs } });
+}
+
+pub fn primaryExpr(allocator: Allocator, lexer: *Peekable(Lexer)) !*Expr {
+ const token = lexer.next().?;
+ // std.debug.print("term {}\n", .{token});
+ return allocate(allocator, switch (token.type) {
+ .LeftParen => {
+ const res = expression(allocator, lexer);
+ const right_paren = lexer.next().?;
+ if (right_paren.type != .RightParen)
+ return allocate(allocator, .{ .Invalid = right_paren });
+ return res;
+ },
+ .IntegerLiteral => .{ .IntegerLiteral = token },
+ else => .{ .Invalid = token },
+ });
+}
+
+fn allocate(allocator: Allocator, expr: Expr) !*Expr {
+ const res = try allocator.create(Expr);
+ res.* = expr;
+ return res;
+}
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const Lexer = @import("./lexer.zig");
+const Token = Lexer.Token;
+const root = @import("root");
+const Peekable = root.Peekable;
+const peekable = root.peekable;
diff --git a/src/peek.zig b/src/peek.zig
new file mode 100644
index 0000000..58911e9
--- /dev/null
+++ b/src/peek.zig
@@ -0,0 +1,51 @@
+pub fn peekable(iterator: anytype) Peekable(@TypeOf(iterator)) {
+ return .{ .iterator = iterator };
+}
+
+pub fn Peekable(Iterator: type) type {
+ const ActualIterator = switch (@typeInfo(Iterator)) {
+ .pointer => |p| p.child,
+ else => Iterator,
+ };
+ const Next = @typeInfo(@TypeOf(ActualIterator.next)).@"fn";
+ const Item = switch (@typeInfo(Next.return_type.?)) {
+ .optional => |o| o.child,
+ else => |i| i,
+ };
+ return struct {
+ iterator: Iterator,
+ peeked: ?Item = null,
+
+ pub fn peek(self: *Self) ?Item {
+ if (self.peeked) |peeked| return peeked;
+ const item = self.iterator.next();
+ self.peeked = item;
+ return item;
+ }
+
+ pub fn next(self: *Self) ?Item {
+ const item = if (self.peeked) |peeked| peeked else self.iterator.next();
+ self.peeked = null;
+ return item;
+ }
+
+ const Self = @This();
+ };
+}
+
+test peekable {
+ const expect = std.testing.expect;
+ // std.meta.de
+
+ var it = std.mem.window(u8, &[_]u8{ 1, 2, 3 }, 1, 1);
+ var peek = peekable(&it);
+ try expect(peek.next().?[0] == 1);
+ try expect(peek.peek().?[0] == 2);
+ try expect(peek.peek().?[0] == 2);
+ try expect(peek.next().?[0] == 2);
+ try expect(peek.next().?[0] == 3);
+ try expect(peek.peek() == null);
+ try expect(peek.next() == null);
+}
+
+const std = @import("std");