repo: hackvr-turbo action: commit revision: path_from: revision_from: 5bf9a27176a9e6f3c1c7004909032e25229239c0: path_to: revision_to:
commit 5bf9a27176a9e6f3c1c7004909032e25229239c0 Author: Felix (xq) QueißnerDate: Thu Jun 25 11:02:22 2020 +0200 Improves parser, adds tests to parser, improves spec. diff --git a/SPEC.md b/SPEC.md
--- a/SPEC.md +++ b/SPEC.md @@ -3,45 +3,81 @@ **Note:** This specification is inofficial and based on the development of HackVR Turbo (which is based on the original hackvr) +## I/O +HackVR communicates via `stdin`/`stdout`. Commands are passed on a line-by-line basis, each line is terminated by a LF character, optionally preceeded by a CR character. +Each line must be encoded in valid UTF-8. + +## Commands + +Commands in a line are separated by one or more whitespace characters (SPACE, TAB). Commands are usually prefixed by a group selector `[group]`, only exception to that are `version` and `help`. + +Everything after a `#` in a line is considered a comment and will be ignored by the command processor. The same is true for lines not containing any non-space characters. + +> TODO: group names can be globbed in some cases to operate on multiple groups +> some commands that take numbers as arguments can be made to behave relative by putting + before the number. makes negative relative a bit odd like: +> ``` +> user move +-2 +-2 0 +> groupnam* command arguments +> ``` + +## Input Commands + +### `help` + +### `version` + +### `[groupspec] deleteallexcept grou*` + +### `[groupspec] deletegroup grou*` + +### `[groupspec] assimilate grou*` + +### `[groupspec] renamegroup group` + +### `[groupspec] status +**deprecated** + +Just outputs a variable that is supposed to be loops per second.` + +### `[groupspec] dump +Tries to let you output the various things that can be set.` + +### `[groupspec] quit +Closes hackvr only if the id that is doing it is the same as yours.` + +### `[groupspec] set` + +### `[groupspec] physics` + +### `[groupspec] control grou* +> Globbing this group could have fun effects + +### `[groupspec] addshape color N x1 y1 z1 ... xN yN zN` + +### `[groupspec] export grou*` + +### `[groupspec] ping any-string-without-spaces` + +### `[groupspec] scale x y z` + +### `[groupspec] rotate [+]x [+]y [+]z` + +### `[groupspec] periodic +Flushes out locally-cached movement and rotation` + +### `[groupspec] flatten +combines group attributes to the shapes. + +### `[groupspec] move [+]x [+]y [+]z` + +### `[groupspec] move forward|backward|up|down|left|right` + +## Output Commands + +### `name action targetgroup` + +when a group is clicked on using the name set from the command line. usually `$USER` -hackvr help output: - -``` -commands that don't get prepended with groupname: help, version -command format: -group names can be globbed in some cases to operate on multiple groups -some commands that take numbers as arguments can be made to behave relative -by putting + before the number. makes negative relative a bit odd like: - user move +-2 +-2 0 -groupnam* command arguments -commands: - deleteallexcept grou* -_ deletegroup grou* - assimilate grou* - renamegroup group - status # old. just outputs a variable that is supposed to be loops per second. - dump # tries to let you output the various things that can be set. - quit #closes hackvr only if the id that is doing it is the same as yours. - set - physics - control grou* [globbing this group could have fun effects] - addshape color N x1 y1 z1 ... xN yN zN - export grou* - ping any-string-without-spaces -* scale x y z -* rotate [+]x [+]y [+]z - periodic # flushes out locally-cached movement and rotation - flatten # combines group attributes to the shapes. -* move [+]x [+]y [+]z -* move forward|backward|up|down|left|right -``` - -hackvr also outputs - -``` -name action targetgroup -when a group is clicked on using the name set from the command line. usually $USER -``` ## Changes diff --git a/build.zig b/build.zig
--- a/build.zig
+++ b/build.zig
@@ -3,6 +3,14 @@ const std = @import("std");
const hackvr = std.build.Pkg{
.name = "hackvr",
.path = "lib/hackvr/lib.zig",
+ .dependencies = &[_]std.build.Pkg{
+ fixed_list,
+ },
+};
+
+const fixed_list = std.build.Pkg{
+ .name = "fixed-list",
+ .path = "lib/fixed_list.zig",
};
pub fn build(b: *std.build.Builder) void {
@@ -27,8 +35,11 @@ pub fn build(b: *std.build.Builder) void {
exe.install();
+ const hackvr_tests = b.addTest("lib/hackvr/lib.zig");
+ hackvr_tests.addPackage(fixed_list);
+
const test_step = b.step("test", "Runs all tests");
- test_step.dependOn(&b.addTest("lib/hackvr/lib.zig").step);
+ test_step.dependOn(&hackvr_tests.step);
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
diff --git a/lib/fixed_list.zig b/lib/fixed_list.zig
new file mode 100644
index 0000000000000000000000000000000000000000..59fff865f365847cfdb9440442c39b9e25a3b197
--- /dev/null
+++ b/lib/fixed_list.zig
@@ -0,0 +1,41 @@
+const std = @import("std");
+
+pub fn FixedList(comptime T: type, comptime limit: usize) type {
+ return struct {
+ const Self = @This();
+
+ buffer: [limit]T,
+ count: usize,
+
+ pub fn init() Self {
+ return Self{
+ .buffer = undefined,
+ .count = 0,
+ };
+ }
+
+ pub fn append(self: *Self, value: T) !void {
+ if (self.count >= limit)
+ return error.OutOfMemory;
+ self.buffer[self.count] = value;
+ self.count += 1;
+ }
+
+ pub fn pop(self: *Self) ?T {
+ if (self.count > 0) {
+ self.count -= 1;
+ var value = self.buffer[self.count];
+ self.buffer[self.count] = undefined;
+ return value;
+ } else {
+ return null;
+ }
+ }
+
+ /// Return contents as a slice. Only valid while the list
+ /// doesn't change size.
+ pub fn span(self: *Self) []T {
+ return self.buffer[0..self.count];
+ }
+ };
+}
diff --git a/lib/hackvr/parser.zig b/lib/hackvr/parser.zig
--- a/lib/hackvr/parser.zig
+++ b/lib/hackvr/parser.zig
@@ -2,6 +2,8 @@ const std = @import("std");
const hvr = @import("lib.zig");
+const FixedList = @import("fixed-list").FixedList;
+
/// A push-event based parser for HackVR input.
/// Parses hackvr text into serialized, preparsed commands.
/// It's not interactive and can eat any amount of data.
@@ -20,31 +22,99 @@ pub const Parser = struct {
};
}
+ fn parseLine(line: []const u8) !?Event {
+ // max number of slices separated by a
+ var items = FixedList([]const u8, 512).init();
+
+ {
+ var current_start: usize = 0;
+ for (line) |c, i| {
+ if (std.ascii.isSpace(c)) {
+ if (current_start < i) {
+ try items.append(line[current_start..i]);
+ }
+ current_start = i + 1;
+ } else if (c == '#') {
+ // rest is comment
+ current_start = line.len;
+ break;
+ }
+ }
+ if (current_start < line.len) {
+ try items.append(line[current_start..]);
+ }
+ }
+
+ switch (items.count) {
+ 0 => return null,
+ 1 => {
+ if (std.mem.eql(u8, "help", items.buffer[0])) {
+ return Event{
+ .event_type = .help,
+ };
+ } else if (std.mem.eql(u8, "version", items.buffer[0])) {
+ return Event{
+ .event_type = .version,
+ };
+ } else {
+ return error.UnknownCommand;
+ }
+ },
+ else => {
+ return Event{
+ .event_type = .not_implemented_yet,
+ };
+ },
+ }
+ }
+
/// Pushes data into the parser.
pub fn push(self: *Self, source: []const u8) !PushResult {
var offset: usize = 0;
while (offset < source.len) {
- defer offset += 1;
if (source[offset] == '\n') {
var line = self.line_buffer[0..self.line_offset];
+ self.line_offset = 0;
+ offset += 1;
+
+ if (line.len > 0 and line[line.len - 1] == '\r') {
+ // strip off CR
+ line = line[0 .. line.len - 1];
+ }
+
if (!std.unicode.utf8ValidateSlice(line))
return error.InvalidEncoding;
- std.log.debug(.HackVR, "Emit event for line '{}'\n", .{line});
-
- return PushResult{
- .event = .{
- .rest = source[0..offset],
- .event = Event{},
- },
- };
+ const rest = source[offset..];
+
+ if (line.len > 0) {
+ const event = parseLine(line) catch |err| switch (err) {
+ error.UnknownCommand => return PushResult{
+ .parse_error = .{
+ .rest = rest,
+ .error_type = .unknown_command,
+ },
+ },
+ else => return err,
+ };
+
+ if (event) |ev| {
+ return PushResult{
+ .event = .{
+ .rest = rest,
+ .event = ev,
+ },
+ };
+ }
+ }
+ } else {
+ if (self.line_offset > self.line_buffer.len)
+ return error.OutOfMemory;
+ self.line_buffer[self.line_offset] = source[offset];
+ self.line_offset += 1;
+ offset += 1;
}
-
- if (self.line_offset > self.line_buffer.len)
- return error.OutOfMemory;
- self.line_buffer[self.line_offset] = source[offset];
- self.line_offset += 1;
}
return PushResult{
@@ -73,6 +143,7 @@ pub const PushResult = union(enum) {
const Error = enum {
syntax_error,
invalid_format,
+ unknown_command,
};
/// The portion of the input data that was not parsed
@@ -85,7 +156,16 @@ pub const PushResult = union(enum) {
},
};
-pub const Event = struct {};
+pub const Event = struct {
+ const Type = enum {
+ help,
+ version,
+
+ not_implemented_yet,
+ };
+
+ event_type: Type,
+};
test "parser: invalid encoding" {
var parser = Parser.init();
@@ -96,3 +176,48 @@ test "parser: invalid encoding" {
};
unreachable;
}
+
+test "parser: empty" {
+ var parser = Parser.init();
+
+ const result = try parser.push("");
+ std.testing.expect(result == .needs_data);
+}
+
+test "parser: whitespace lines" {
+ var parser = Parser.init();
+
+ const result = try parser.push("\n \n\n \n \n \n\n\n \n");
+ std.testing.expect(result == .needs_data);
+}
+
+test "parser: ParseResult.rest/error" {
+ var parser = Parser.init();
+
+ const result = try parser.push("a\nb");
+ std.testing.expect(result == .parse_error);
+ std.testing.expectEqualStrings("b", result.parse_error.rest);
+ std.testing.expect(result.parse_error.error_type == .unknown_command);
+}
+
+test "parser: ParseResult.rest/event" {
+ var parser = Parser.init();
+
+ const result = try parser.push("help\nb");
+ std.testing.expect(result == .event);
+ std.testing.expectEqualStrings("b", result.event.rest);
+}
+
+test "parser: parse line" {
+ _ = try Parser.parseLine(" a bb cccc ");
+}
+
+test "parser: cmd help" {
+ const result = (try Parser.parseLine("help")) orelse return error.ExpectedEvent;
+ std.testing.expect(result.event_type == .help);
+}
+
+test "parser: cmd version" {
+ const result = (try Parser.parseLine("version")) orelse return error.ExpectedEvent;
+ std.testing.expect(result.event_type == .version);
+}
-----END OF PAGE-----