summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2025-01-31 07:36:52 +0100
committerNathan Reiner <nathan@nathanreiner.xyz>2025-01-31 07:36:52 +0100
commit2ce14aec655589f00442ab469b9d877a143eeefd (patch)
tree8cd12b1869d10ef16a449a8e182a3077b86c9810
init commit
-rw-r--r--.envrc1
-rw-r--r--.gitignore3
-rw-r--r--README.md145
-rw-r--r--build.zig39
-rw-r--r--build.zig.zon15
-rw-r--r--eostre/build.zig20
-rw-r--r--eostre/build.zig.zon10
-rw-r--r--eostre/lib/cursor.zig69
-rw-r--r--eostre/lib/parser/common.zig28
-rw-r--r--eostre/lib/parser/context.zig9
-rw-r--r--eostre/lib/parser/root.zig284
-rw-r--r--eostre/lib/root.zig8
-rw-r--r--flake.lock27
-rw-r--r--flake.nix18
-rw-r--r--src/main.zig157
15 files changed, 833 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6781a58
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.direnv
+.zig-cache
+zig-out
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0c16cd8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,145 @@
+# Implementation of *Ēostre’s Shell*
+
+# Syntax
+## Piping
+```esh
+program_0 | program_1 | program_2 | ... | program_n
+```
+
+## Comments
+```esh
+# Comments are written like this
+```
+
+## Forcing Pipe Mode
+### Force String
+```esh
+mode(string) { program_0 | program_1 } | program_2 | program_3
+```
+### Force *EAPM*
+```esh
+unsafe {
+ mode(raw) { program_0 | program_1 } | program_2 | program_3
+}
+```
+
+Forcing `EAPM` can only happen in a unsafe block.
+All pipes in a forced mode scope will be forced to this mode.
+In this example `program_0` and `program_1` will run in the forced
+mode.
+
+## If-Statement
+```esh
+if (program arg_1 arg_2 ... arg_n) {
+ # do something here
+}
+```
+```esh
+if (program arg_1 arg_2 ... arg_n) {
+ # do something here
+} else {
+ # do something else
+}
+```
+```esh
+if (program0 arg_1 arg_2 ... arg_n) {
+ # do something here
+} else if program1 arg_1 arg_2 ... arg_n {
+ # do something here
+} else {
+ # do something else
+}
+```
+
+## For-Loop
+For loops are always *line-based*
+```esh
+for line in (program arg_1 arg_2 ... arg_n) {
+ # do something here
+}
+```
+
+## While-Loop
+```esh
+while (program arg_1 arg_2 ... arg_n) {
+ # do something here
+}
+```
+
+## Variables
+### Set
+```esh
+var = {
+ program arg_1 arg_2 ... arg_n
+}
+```
+
+### Get
+This will pass `var` as `arg_1`
+``` esh
+program arg_1 (var) arg_3 ... arg_n
+```
+
+## String
+### Normal String
+``` esh
+var = "Some random string"
+```
+
+#### Embedding other variables and program calls into string definition.
+``` esh
+var2 = "this is var: (var) and program gives the output: {program arg_1 arg_2 ... arg_n}"
+```
+If you want to use `{` or `}` as a real character use the escape character `\` or use a raw
+string:
+
+``` esh
+var = "The following is a curly bracket: \{"
+```
+
+### Raw String
+A raw string is a string where no character except the end character
+is treated special
+``` esh
+var = 'Some string where I can use " without ending the string'
+```
+
+## Functions
+``` esh
+fn my-function(arg_1 arg_2 arg_3) {
+ # do something here
+}
+```
+
+### `Use`-Keyword
+
+The `use` keyword passes a variable from the parent to the child scope.
+If the variable does not exist in the parent context yet it will
+set it to a empty string.
+
+``` esh
+fn my-function(arg_1 arg_2 arg_3) use (var0 var1) {
+ # do something here
+}
+```
+
+## Multi-Line Expressions
+``` esh
+program arg_1 \
+ arg_2 \
+ ...
+ arg_n
+```
+
+## Merging Lines
+``` esh
+program0 arg_1 arg_2 ... arg_n; program1 arg_1 arg_2 ... arg_n
+```
+
+``` esh
+program0 && program1
+```
+
+``` esh
+program0 || program1
+```
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..40cae2d
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,39 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ const target = b.standardTargetOptions(.{});
+ const optimize = b.standardOptimizeOption(.{});
+
+ const pkg = b.dependency("eostre", .{
+ .target = target,
+ .optimize = optimize,
+ });
+
+ const exe = b.addExecutable(.{
+ .name = "shell",
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize
+ });
+
+ const run_exe = b.addRunArtifact(exe);
+ const run_step = b.step("run", "Run the application");
+ run_step.dependOn(&run_exe.step);
+
+ exe.root_module.addImport("eostre", pkg.module("eostre"));
+
+ const test_step = b.step("test", "Run unit tests");
+
+ const tests = b.addTest(.{
+ .name = "shell",
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ });
+ tests.root_module.addImport("eostre", pkg.module("eostre"));
+
+ const run_tests = b.addRunArtifact(tests);
+ test_step.dependOn(&run_tests.step);
+
+ const lib_tests = b.addRunArtifact(pkg.artifact("eostre"));
+ test_step.dependOn(&lib_tests.step);
+}
diff --git a/build.zig.zon b/build.zig.zon
new file mode 100644
index 0000000..311d2b8
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,15 @@
+.{
+ .name = "shell",
+ .version = "0.0.0",
+ .dependencies = .{
+ .eostre = .{
+ .path = "./eostre",
+ },
+ },
+
+ .paths = .{
+ "src",
+ "build.zig",
+ "build.zig.zon"
+ },
+}
diff --git a/eostre/build.zig b/eostre/build.zig
new file mode 100644
index 0000000..da302f9
--- /dev/null
+++ b/eostre/build.zig
@@ -0,0 +1,20 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ const target = b.standardTargetOptions(.{});
+ const optimize = b.standardOptimizeOption(.{});
+
+ _ = b.addModule("eostre", .{
+ .root_source_file = b.path("lib/root.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ const tests = b.addTest(.{
+ .name = "eostre",
+ .root_source_file = b.path("lib/root.zig"),
+ .target = target,
+ });
+
+ b.installArtifact(tests);
+}
diff --git a/eostre/build.zig.zon b/eostre/build.zig.zon
new file mode 100644
index 0000000..39514b6
--- /dev/null
+++ b/eostre/build.zig.zon
@@ -0,0 +1,10 @@
+.{
+ .name = "eostre",
+ .version = "0.0.0",
+
+ .paths = .{
+ "lib",
+ "build.zig",
+ "build.zig.zon"
+ }
+}
diff --git a/eostre/lib/cursor.zig b/eostre/lib/cursor.zig
new file mode 100644
index 0000000..c0a4a32
--- /dev/null
+++ b/eostre/lib/cursor.zig
@@ -0,0 +1,69 @@
+const std = @import("std");
+
+pub const ReadError = error {
+ EndOfBuffer
+};
+
+pub fn Cursor(comptime T: type) type {
+ return struct {
+ const Self = @This();
+
+ buffer: []const T,
+ index: usize,
+ touched_index: usize,
+
+ pub fn init(buffer: []const T) Self {
+ return .{
+ .buffer = buffer,
+ .index = 0,
+ .touched_index = 0,
+ };
+ }
+
+ pub fn peek(self: Self, amount: usize) ReadError![]const T {
+ if (self.index + amount > self.buffer.len) {
+ return ReadError.EndOfBuffer;
+ }
+
+ return self.buffer[self.index..self.index + amount];
+ }
+
+ pub fn consume(self: *Self, amount: usize) ReadError![]const T {
+ const slice = try self.peek(amount);
+ self.index += amount;
+ self.touched_index = self.index;
+ return slice;
+ }
+
+ pub fn snapshot(self: Self) usize {
+ return self.index;
+ }
+
+ pub fn rollback(self: *Self, index: usize) void {
+ self.index = index;
+ }
+
+ pub fn touched_slice(self: Self) []const T {
+ return self.buffer[0..self.touched_index];
+ }
+ };
+}
+
+test "peek" {
+ const buffer = "1234";
+ const cursor = Cursor(u8).init(buffer);
+ try std.testing.expectEqualSlices(u8, "1", try cursor.peek(1));
+ try std.testing.expectEqualSlices(u8, "12", try cursor.peek(2));
+ try std.testing.expectEqualSlices(u8, "123", try cursor.peek(3));
+ try std.testing.expectEqualSlices(u8, "1234", try cursor.peek(4));
+ try std.testing.expectError(ReadError.EndOfBuffer, cursor.peek(5));
+}
+
+test "consume" {
+ const buffer = "12345678";
+ var cursor = Cursor(u8).init(buffer);
+ try std.testing.expectEqualSlices(u8, "1", try cursor.consume(1));
+ try std.testing.expectEqualSlices(u8, "23", try cursor.consume(2));
+ try std.testing.expectEqualSlices(u8, "456", try cursor.consume(3));
+ try std.testing.expectError(ReadError.EndOfBuffer, cursor.consume(4));
+}
diff --git a/eostre/lib/parser/common.zig b/eostre/lib/parser/common.zig
new file mode 100644
index 0000000..986af58
--- /dev/null
+++ b/eostre/lib/parser/common.zig
@@ -0,0 +1,28 @@
+const std = @import("std");
+const root = @import("root.zig");
+const Context = @import("context.zig").Context;
+const ParseFn = root.ParseFn;
+
+pub fn LiteralFn(comptime I: type, comptime O: type) type {
+ return *const fn (comptime literal: []const I, value: O) ParseFn(I, O);
+}
+
+pub fn Literal(comptime I: type, comptime O: type) LiteralFn(I, O) {
+ return struct {
+ pub fn literal(comptime lit: []const I, value: O) ParseFn(I, O) {
+ return struct {
+ pub fn parse(ctx: *Context(I)) !O {
+ const snapshot = ctx.cursor.snapshot();
+ errdefer ctx.cursor.rollback(snapshot);
+
+ const slice = try ctx.cursor.consume(lit.len);
+ if (std.mem.eql(I, lit, slice)) {
+ return value;
+ }
+
+ return error.UnexpectedLiteral;
+ }
+ }.parse;
+ }
+ }.literal;
+}
diff --git a/eostre/lib/parser/context.zig b/eostre/lib/parser/context.zig
new file mode 100644
index 0000000..56fb025
--- /dev/null
+++ b/eostre/lib/parser/context.zig
@@ -0,0 +1,9 @@
+const std = @import("std");
+const Cursor = @import("../cursor.zig").Cursor;
+
+pub fn Context(comptime I: type) type {
+ return struct {
+ cursor: Cursor(I),
+ allocator: std.mem.Allocator,
+ };
+}
diff --git a/eostre/lib/parser/root.zig b/eostre/lib/parser/root.zig
new file mode 100644
index 0000000..b168a88
--- /dev/null
+++ b/eostre/lib/parser/root.zig
@@ -0,0 +1,284 @@
+const std = @import("std");
+const Cursor = @import("../cursor.zig").Cursor;
+pub const common = @import("common.zig");
+pub const Context = @import("context.zig").Context;
+
+
+pub fn ParseFn(comptime I: type, comptime O: type) type {
+ return *const fn (ctx: *Context(I)) anyerror!O;
+}
+
+pub fn Parser(comptime I: type, comptime O: type, comptime parse_fn: ParseFn(I, O)) type {
+ return struct {
+ const Self = @This();
+
+ pub fn parse(ctx: *Context(I)) !O {
+ return try parse_fn(ctx);
+ }
+
+ pub fn or_else(comptime other: ParseFn(I, O)) type {
+ return Parser(I, O, struct {
+ pub fn parse(ctx: *Context(I)) !O {
+ return parse_fn(ctx) catch other(ctx);
+ }
+ }.parse);
+ }
+
+ pub fn and_skip(comptime skip: anytype) type {
+ return Parser(I, O, struct {
+ pub fn parse(ctx: *Context(I)) !O {
+ const snapshot = ctx.cursor.snapshot();
+ errdefer ctx.cursor.rollback(snapshot);
+
+ _ = try skip(ctx);
+ return try parse_fn(ctx);
+ }
+ }.parse);
+ }
+
+ pub fn then_skip(comptime skip: anytype) type {
+ return Parser(I, O, struct {
+ pub fn parse(ctx: *Context(I)) !O {
+ const snapshot = ctx.cursor.snapshot();
+ errdefer ctx.cursor.rollback(snapshot);
+ const value = try parse_fn(ctx);
+ _ = try skip(ctx);
+ return value;
+ }
+ }.parse);
+ }
+
+ pub fn then(comptime O2: type, comptime other: ParseFn(I, O2)) type {
+ const CombinedOutput = struct { O, O2 };
+
+ return Parser(I, CombinedOutput, struct {
+ pub fn parse(ctx: *Context(I)) !CombinedOutput {
+ const snapshot = ctx.cursor.snapshot();
+ errdefer ctx.cursor.rollback(snapshot);
+
+ return .{ try parse_fn(ctx), try other(ctx) };
+ }
+ }.parse);
+ }
+
+ pub fn zero_or_more() type {
+ return Parser(I, std.ArrayList(O), struct {
+ pub fn parse(ctx: *Context(I)) !std.ArrayList(O) {
+ var values = std.ArrayList(O).init(ctx.allocator);
+ errdefer values.deinit();
+
+ while (parse_fn(ctx)) |value| {
+ try values.append(value);
+ } else |_| {}
+
+ return values;
+ }
+ }.parse);
+ }
+
+ pub fn repeat(n: usize) type {
+ return Parser(I, [n]O, struct {
+ pub fn parse(ctx: *Context(I)) ![n]O {
+ const snapshot = ctx.cursor.snapshot();
+ errdefer ctx.cursor.rollback(snapshot);
+
+ var values: [n]O = undefined;
+
+ for (&values) |*value| {
+ value.* = try parse_fn(ctx);
+ }
+
+ return values;
+ }
+ }.parse);
+ }
+
+ pub fn map(M: type, map_fn: *const fn (value: O) M) type {
+ return Parser(I, M, struct {
+ pub fn parse(ctx: *Context(I)) !M {
+ return map_fn(try parse_fn(ctx));
+ }
+ }.parse);
+ }
+
+ pub fn end() type {
+ return Parser(I, O, struct {
+ pub fn parse(ctx: *Context(I)) !O {
+ const value = try parse_fn(ctx);
+ _ = ctx.cursor.peek(1) catch return value;
+ return error.NotEnd;
+ }
+ }.parse);
+ }
+ };
+}
+
+fn parse_zero(ctx: *Context(u8)) !u8 {
+ const snapshot = ctx.cursor.snapshot();
+ errdefer ctx.cursor.rollback(snapshot);
+
+ const value = try ctx.cursor.consume(1);
+
+ if (value[0] == '0') {
+ return 0;
+ }
+
+ return error.NotZero;
+}
+
+fn parse_one(ctx: *Context(u8)) !u8 {
+ const snapshot = ctx.cursor.snapshot();
+ errdefer ctx.cursor.rollback(snapshot);
+
+ const value = try ctx.cursor.consume(1);
+
+ if (value[0] == '1') {
+ return 1;
+ }
+
+ return error.NotOne;
+}
+
+
+test "direct" {
+ const ZeroParser = Parser(u8, u8, parse_zero);
+
+ var ctx = Context(u8) {
+ .cursor = Cursor(u8).init("0"),
+ .allocator = std.testing.allocator,
+ };
+ try std.testing.expectEqual(0, try ZeroParser.parse(&ctx));
+
+ ctx.cursor = Cursor(u8).init("1");
+ try std.testing.expectError(error.NotZero, ZeroParser.parse(&ctx));
+}
+
+test "or_else" {
+ const ZeroParser = Parser(u8, u8, parse_zero);
+ const ZeroAndOneParser = ZeroParser.or_else(parse_one);
+
+ var ctx = Context(u8) {
+ .cursor = Cursor(u8).init("0"),
+ .allocator = std.testing.allocator,
+ };
+ try std.testing.expectEqual(0, try ZeroAndOneParser.parse(&ctx));
+
+ ctx.cursor = Cursor(u8).init("1");
+ try std.testing.expectEqual(1, try ZeroAndOneParser.parse(&ctx));
+
+ ctx.cursor = Cursor(u8).init("2");
+ try std.testing.expectError(error.NotZero, ZeroParser.parse(&ctx));
+}
+
+test "then" {
+ const ZeroParser = Parser(u8, u8, parse_zero);
+ const ZeroThenOneParser = ZeroParser.then(u8, parse_one);
+
+ var ctx = Context(u8) {
+ .cursor = Cursor(u8).init("01"),
+ .allocator = std.testing.allocator,
+ };
+ try std.testing.expectEqual(.{ 0, 1 }, try ZeroThenOneParser.parse(&ctx));
+
+ ctx.cursor = Cursor(u8).init("11");
+ try std.testing.expectError(error.NotZero, ZeroThenOneParser.parse(&ctx));
+
+ ctx.cursor = Cursor(u8).init("00");
+ try std.testing.expectError(error.NotOne, ZeroThenOneParser.parse(&ctx));
+}
+
+test "zero_or_more" {
+ const ZeroParser = Parser(u8, u8, parse_zero);
+ const ZeroWildcardParser = ZeroParser.zero_or_more();
+ const ZeroOrOneWildcardParser = ZeroParser.or_else(parse_one).zero_or_more();
+
+ var ctx = Context(u8) {
+ .cursor = Cursor(u8).init("0000"),
+ .allocator = std.testing.allocator,
+ };
+ var result = try ZeroWildcardParser.parse(&ctx);
+ try std.testing.expectEqualSlices(u8, &[_]u8{0, 0, 0, 0}, result.items);
+ result.deinit();
+
+ ctx.cursor = Cursor(u8).init("00001");
+ result = try ZeroWildcardParser.parse(&ctx);
+ try std.testing.expectEqualSlices(u8, &[_]u8{0, 0, 0, 0}, result.items);
+ result.deinit();
+
+ ctx.cursor = Cursor(u8).init("0101110");
+ result = try ZeroOrOneWildcardParser.parse(&ctx);
+ try std.testing.expectEqualSlices(u8, &[_]u8{0, 1, 0, 1, 1, 1, 0}, result.items);
+ result.deinit();
+
+ ctx.cursor = Cursor(u8).init("0101110abc");
+ result = try ZeroOrOneWildcardParser.parse(&ctx);
+ try std.testing.expectEqualSlices(u8, &[_]u8{0, 1, 0, 1, 1, 1, 0}, result.items);
+ result.deinit();
+}
+
+test "repeat" {
+ const ZeroParser = Parser(u8, u8, parse_zero);
+ const FourZerosParser = ZeroParser.repeat(4);
+
+ var ctx = Context(u8) {
+ .cursor = Cursor(u8).init("00001"),
+ .allocator = std.testing.allocator,
+ };
+ try std.testing.expectEqualSlices(
+ u8,
+ &[_]u8{0, 0, 0, 0},
+ &(try FourZerosParser.parse(&ctx))
+ );
+
+ ctx.cursor = Cursor(u8).init("0001");
+ try std.testing.expectError(
+ error.NotZero,
+ FourZerosParser.parse(&ctx)
+ );
+}
+
+test "map" {
+ const ZeroOrOneParser = Parser(u8, u8, parse_zero).or_else(parse_one);
+ const ZeroOrOneMappedParser = ZeroOrOneParser.map(u8, struct {
+ pub fn map(i: u8) u8 {
+ return i * 2;
+ }
+ }.map).zero_or_more();
+
+ var ctx = Context(u8) {
+ .cursor = Cursor(u8).init("00110101"),
+ .allocator = std.testing.allocator,
+ };
+ const result = try ZeroOrOneMappedParser.parse(&ctx);
+ try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0, 2, 2, 0, 2, 0, 2 }, result.items);
+ defer result.deinit();
+}
+
+test "cursor rollback" {
+ const DoubleZeroOrOneParser = Parser(u8, u8, parse_zero).repeat(3).or_else(
+ Parser(u8, u8, parse_zero).repeat(2).then(u8, parse_one).map([3]u8, struct {
+ pub fn map(r: struct { [2]u8, u8 }) [3]u8 {
+ const array, const last = r;
+ return array ++ .{ last };
+ }
+ }.map).parse
+ );
+ var ctx = Context(u8) {
+ .cursor = Cursor(u8).init("0001"),
+ .allocator = std.testing.allocator,
+ };
+ try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0, 0 }, &(try DoubleZeroOrOneParser.parse(&ctx)));
+
+ ctx.cursor = Cursor(u8).init("0010");
+ try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0, 1 }, &(try DoubleZeroOrOneParser.parse(&ctx)));
+}
+
+test "cursor error touched slice" {
+ const OneThenDoubleZeroParser = Parser(u8, u8, parse_one).then([2]u8, Parser(u8, u8, parse_zero).repeat(2).parse);
+ var ctx = Context(u8) {
+ .cursor = Cursor(u8).init("10110"),
+ .allocator = std.testing.allocator,
+ };
+ try std.testing.expectError(error.NotZero, OneThenDoubleZeroParser.parse(&ctx));
+ try std.testing.expectEqualSlices(u8, "101", ctx.cursor.touched_slice());
+}
diff --git a/eostre/lib/root.zig b/eostre/lib/root.zig
new file mode 100644
index 0000000..86af090
--- /dev/null
+++ b/eostre/lib/root.zig
@@ -0,0 +1,8 @@
+const std = @import("std");
+
+pub const cursor = @import("cursor.zig");
+pub const parser = @import("parser/root.zig");
+
+test {
+ std.testing.refAllDecls(@This());
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..fbd8c73
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1737299813,
+ "narHash": "sha256-Qw2PwmkXDK8sPQ5YQ/y/icbQ+TYgbxfjhgnkNJyT1X8=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "107d5ef05c0b1119749e381451389eded30fb0d5",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-24.11",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..8ffc51e
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,18 @@
+{
+ description = "Parz";
+
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-24.11";
+ };
+
+ outputs = { self, nixpkgs }:
+ let
+ pkgs = import nixpkgs { system = "x86_64-linux"; };
+ in
+ {
+ devShells.x86_64-linux.default = pkgs.zigStdenv.mkDerivation {
+ name = "zig-dev-shell";
+ buildInputs = [ pkgs.zig ];
+ };
+ };
+}
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..2144798
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,157 @@
+const std = @import("std");
+const eostre = @import("eostre");
+const Cursor = eostre.cursor.Cursor;
+const Parser = eostre.parser.Parser;
+const Context = eostre.parser.Context;
+const Literal = eostre.parser.common.Literal;
+const ParseFn = eostre.parser.ParseFn;
+
+const Keyword = enum {
+ if_statement,
+ other
+};
+
+const String = struct {
+};
+
+const Token = union(enum) {
+ openparenthesis: void,
+ closeparenthesis: void,
+ openscope: void,
+ closescope: void,
+ backslash: void,
+ pipe: void,
+ semicolon: void,
+ logical_and: void,
+ logical_or: void,
+ assign: void,
+ keyword: Keyword,
+ string: String,
+};
+
+pub fn parse_string(ctx: *Context(u8)) !Token {
+ const snapshot = ctx.cursor.snapshot();
+ errdefer ctx.cursor.rollback(snapshot);
+
+ const first_char = (try ctx.cursor.consume(1))[0];
+
+ if (first_char == ' ') {
+ return error.NotAString;
+ }
+
+ if (first_char == '"') {
+ while (ctx.cursor.consume(1) catch null) |c| {
+ if (c[0] == '\"') {
+ return Token.string;
+ } else if (c[0] == '{') {
+ while (ctx.cursor.consume(1) catch null) |inner| {
+ if (inner[0] == '}') {
+ break;
+ }
+ } else {
+ return error.UnterminatedString;
+ }
+ }
+ } else {
+ return error.UnterminatedString;
+ }
+ } else if (first_char == '\'') {
+ while (ctx.cursor.consume(1) catch null) |c| {
+ if (c[0] == '\'') {
+ return Token.string;
+ }
+ } else {
+ return error.UnterminatedString;
+ }
+ }
+
+ while (ctx.cursor.consume(1) catch null) |c| {
+ if (c[0] == ' ') {
+ return Token.string;
+ } else if (c[0] == '{') {
+ while (ctx.cursor.consume(1) catch null) |inner| {
+ if (inner[0] == '}') {
+ break;
+ }
+ } else {
+ return error.UnterminatedString;
+ }
+ }
+ } else {
+ return error.UnterminatedString;
+ }
+}
+
+const unterminated_keyword = Parser(u8, Keyword, keyword_literal("if", Keyword.if_statement));
+
+fn keyword(ctx: *Context(u8)) !Token {
+ const snapshot = ctx.cursor.snapshot();
+ errdefer ctx.cursor.rollback(snapshot);
+
+ const token = try unterminated_keyword.parse(ctx);
+ const next_char = (try ctx.cursor.peek(1))[0];
+
+ switch (next_char) {
+ '(', ')', '{', '}', '\\', '&', '|', ';', '=', ' ' => return .{ .keyword = token },
+ else => return error.UnterminatedKeyword,
+ }
+}
+
+const literal = Literal(u8, Token);
+const keyword_literal = Literal(u8, Keyword);
+const skip_literal = Literal(u8, void);
+
+const Tokenizer = Parser(u8, Token, literal("(", Token.openparenthesis))
+ .or_else(literal(")", Token.closeparenthesis))
+ .or_else(literal("{", Token.openscope))
+ .or_else(literal("}", Token.closescope))
+ .or_else(literal("\\", Token.backslash))
+ .or_else(literal("&&", Token.logical_and))
+ .or_else(literal("||", Token.logical_or))
+ .or_else(literal("|", Token.pipe))
+ .or_else(literal(";", Token.semicolon))
+ .or_else(literal("=", Token.assign))
+ .or_else(keyword)
+ .or_else(parse_string)
+ .and_skip(Parser(u8, void, skip_literal(" ", {})).zero_or_more().parse)
+ .zero_or_more()
+ .then_skip(Parser(u8, void, skip_literal(" ", {})).zero_or_more().parse)
+ .end();
+
+pub fn main() void {
+ std.debug.print("Hello, World!\n", .{});
+}
+
+test "tokenizer" {
+ const example = " | 'asdf' ( || \"hello\" ) or_something &&{ =}\\ ; if ";
+
+ var ctx = Context(u8) {
+ .cursor = Cursor(u8).init(example),
+ .allocator = std.testing.allocator,
+ };
+
+ const tokens = try Tokenizer.parse(&ctx);
+ defer tokens.deinit();
+
+
+ try std.testing.expectEqualSlices(
+ Token,
+ &[_]Token {
+ .pipe,
+ .string,
+ .openparenthesis,
+ .logical_or,
+ .string,
+ .closeparenthesis,
+ .string,
+ .logical_and,
+ .openscope,
+ .assign,
+ .closescope,
+ .backslash,
+ .semicolon,
+ .{ .keyword = Keyword.if_statement }
+ },
+ tokens.items
+ );
+}