const std = @import("std"); const plugin = @import("plugin.zig"); pub const ParseError = error{ NoCommandGiven, UnknownCommand, ArgRequired, FloatParseFail, ParseFail, }; pub const CommandType = enum { Noop, Load, Quicksave, RunQS, Amp, RFlanger, Eq, Phaser, Mbeq, Chorus, PitchScaler, Reverb, Highpass, Delay, Vinyl, RevDelay, Noise, WildNoise, Write, Rotate, }; pub const Command = struct { command: CommandType, args: ArgList, cur_idx: usize = 0, pub fn print(self: *const Command) void { std.debug.warn("cmd:{}\n", self.command); } pub fn argAt(self: *const Command, idx: usize) ![]const u8 { const args = self.args.toSliceConst(); if (idx > (args.len - 1)) { std.debug.warn("Expected argument at index {}\n", idx); return ParseError.ArgRequired; } return args[idx]; } pub fn usizeArgAt(self: *const Command, idx: usize) !usize { var arg = try self.argAt(idx); return try std.fmt.parseInt(usize, arg, 10); } pub fn consumePosition(self: *Command) !plugin.Position { self.cur_idx = 2; return plugin.Position{ .split = try self.usizeArgAt(0), .index = try self.usizeArgAt(1), }; } pub fn intArgAt(self: *const Command, idx: usize) !i32 { var arg = try self.argAt(idx); return try std.fmt.parseInt(i32, arg, 10); } pub fn floatArgAt(self: *const Command, idx: usize) !f32 { var arg = try self.argAt(idx); return try std.fmt.parseFloat(f32, arg); } pub fn floatArgMany( self: *const Command, allocator: *std.mem.Allocator, start_index: usize, elements: usize, default: f32, ) ![]const f32 { var i: usize = start_index; var arr = std.ArrayList(f32).init(allocator); while (i < elements) : (i += 1) { var value: f32 = self.floatArgAt(i) catch |err| blk: { std.debug.warn("\tdoing default on arg {}\n", i); break :blk default; }; try arr.append(value); } return arr.toSliceConst(); } pub fn appendParam( self: *Command, params: *plugin.ParamList, symbol: []const u8, ) !void { var val = try self.floatArgAt(self.cur_idx); self.cur_idx += 1; try params.append(plugin.Param{ .sym = symbol, .value = val, }); } pub fn appendParamMap( self: *Command, map: *plugin.ParamMap, symbol: []const u8, ) !void { var val = try self.floatArgAt(self.cur_idx); self.cur_idx += 1; _ = try map.put(symbol, val); } }; pub const CommandList = std.ArrayList(*Command); pub const ArgList = std.ArrayList([]const u8); pub const KeywordMap = std.AutoHashMap([]const u8, CommandType); pub const Lang = struct { allocator: *std.mem.Allocator, keywords: KeywordMap, has_error: bool = false, line: usize = 0, pub fn init(allocator: *std.mem.Allocator) Lang { return Lang{ .allocator = allocator, .keywords = KeywordMap.init(allocator), }; } pub fn deinit(self: *Lang) void { self.keywords.deinit(); } fn fillKeywords(self: *Lang) !void { _ = try self.keywords.put("noop", .Noop); _ = try self.keywords.put("load", .Load); _ = try self.keywords.put("quicksave", .Quicksave); _ = try self.keywords.put("runqs", .RunQS); _ = try self.keywords.put("amp", .Amp); _ = try self.keywords.put("rflanger", .RFlanger); _ = try self.keywords.put("eq", .Eq); _ = try self.keywords.put("mbeq", .Mbeq); _ = try self.keywords.put("phaser", .Phaser); _ = try self.keywords.put("chorus", .Chorus); _ = try self.keywords.put("pitchscaler", .PitchScaler); _ = try self.keywords.put("reverb", .Reverb); _ = try self.keywords.put("highpass", .Highpass); _ = try self.keywords.put("delay", .Delay); _ = try self.keywords.put("vinyl", .Vinyl); _ = try self.keywords.put("revdelay", .RevDelay); // custom implementations (not lv2) _ = try self.keywords.put("noise", .Noise); _ = try self.keywords.put("wildnoise", .WildNoise); _ = try self.keywords.put("write", .Write); // even more custom _ = try self.keywords.put("rotate", .Rotate); } // TODO remove this once AutoHashMap is fixed. pub fn getCommand(self: *Lang, stmt: []const u8) ?CommandType { const commands = [_][]const u8{ "noop", "load", "quicksave", "runqs", "amp", "rflanger", "eq", "phaser", "mbeq", "chorus", "pitchscaler", "reverb", "highpass", "delay", "vinyl", "revdelay", "noise", "wildnoise", "write", "rotate", }; const command_types = [_]CommandType{ .Noop, .Load, .Quicksave, .RunQS, .Amp, .RFlanger, .Eq, .Phaser, .Mbeq, .Chorus, .PitchScaler, .Reverb, .Highpass, .Delay, .Vinyl, .RevDelay, .Noise, .WildNoise, .Write, .Rotate, }; std.debug.assert(commands.len == command_types.len); for (commands) |command, idx| { if (std.mem.eql(u8, stmt, command)) return command_types[idx]; } return null; } fn expectAny(self: *Lang, count: usize, args: ArgList) !void { if (args.len != count) { std.debug.warn("expected {} arguments, found {}\n", count, args.len); return error.ArgRequired; } } fn expectSingle(self: *Lang, args: ArgList) !void { return try self.expectAny(1, args); } fn expectFloat(self: *Lang, count: usize, args: ArgList) !void { var i: usize = 0; if (args.len != count) { std.debug.warn("expected {} arguments, found {}\n", count, args.len); return error.ArgRequired; } while (i < count) : (i += 1) { var arg = args.at(i); _ = std.fmt.parseFloat(f32, arg) catch |err| { std.debug.warn("failed to parse f32: {}\n", err); return error.FloatParseFail; }; } } fn validateCommand(self: *Lang, cmd: *Command) !void { switch (cmd.command) { .Quicksave, .Noop => {}, .Load, .RunQS => try self.expectSingle(cmd.args), .Amp => try self.expectFloat(3, cmd.args), .RFlanger => try self.expectFloat(4, cmd.args), .Eq => try self.expectFloat(5, cmd.args), .Phaser => try self.expectFloat(6, cmd.args), .Mbeq => try self.expectFloat(15, cmd.args), .Chorus => try self.expectFloat(8, cmd.args), .PitchScaler => try self.expectFloat(3, cmd.args), .Reverb => try self.expectFloat(12, cmd.args), .Highpass => try self.expectFloat(5, cmd.args), .Delay => try self.expectFloat(12, cmd.args), .Vinyl => try self.expectFloat(7, cmd.args), .RevDelay => try self.expectFloat(5, cmd.args), .Noise => try self.expectFloat(4, cmd.args), .WildNoise => try self.expectFloat(4, cmd.args), .Write => try self.expectFloat(3, cmd.args), .Rotate => try self.expectAny(2, cmd.args), else => std.debug.warn("WARN unchecked command {}\n", cmd.command), } } fn doError(self: *Lang, comptime fmt: []const u8, args: ...) void { std.debug.warn("error at line {}: ", self.line); std.debug.warn(fmt, args); std.debug.warn("\n"); self.has_error = true; } pub fn parse(self: *Lang, data: []const u8) !CommandList { var splitted_it = std.mem.separate(data, ";"); try self.fillKeywords(); var cmds = CommandList.init(self.allocator); while (splitted_it.next()) |stmt_orig| { self.line += 1; var stmt = std.mem.trimRight(u8, stmt_orig, "\n"); stmt = std.mem.trimLeft(u8, stmt, "\n"); if (stmt.len == 0) continue; if (stmt[0] == '#') continue; // TODO better tokenizer instead of just tokenize(" "); var tok_it = std.mem.tokenize(stmt, " "); var cmd_opt = tok_it.next(); if (cmd_opt == null) { self.doError("No command given"); continue; } var command = cmd_opt.?; var ctype_opt = self.getCommand(command); var ctype: CommandType = undefined; if (ctype_opt) |ctype_val| { ctype = ctype_val; } else { self.doError("Unknown command '{}' ({})", command, command.len); continue; } var args = ArgList.init(self.allocator); while (tok_it.next()) |arg| { try args.append(arg); } // construct final Command based on command var cmd_ptr = try self.allocator.create(Command); errdefer self.allocator.destroy(cmd_ptr); cmd_ptr.* = Command{ .command = ctype, .args = args }; self.validateCommand(cmd_ptr) catch |err| { self.doError("Unknown command '{}' (length {})", command, command.len); continue; }; try cmds.append(cmd_ptr); } if (self.has_error) return ParseError.ParseFail; return cmds; } }; test "noop" { var lang = Lang.init(std.heap.direct_allocator); defer lang.deinit(); var cmds = try lang.parse("noop;"); defer cmds.deinit(); std.testing.expectEqual(cmds.len, 1); std.testing.expectEqual(cmds.at(0).command, .Noop); } test "load, phaser, quicksave" { var lang = Lang.init(std.heap.direct_allocator); defer lang.deinit(); const prog = \\load :0; \\phaser 3 1 25 0.25 0 1; \\quicksave; ; var cmds = try lang.parse(prog); defer cmds.deinit(); std.testing.expectEqual(cmds.len, 3); std.testing.expectEqual(cmds.at(0).command, .Load); std.testing.expectEqual(cmds.at(1).command, .Phaser); std.testing.expectEqual(cmds.at(2).command, .Quicksave); }