const std = @import("std"); const plugin = @import("plugin.zig"); pub const ParseError = error{ OutOfMemory, ArgRequired, ParseFail, }; pub const CommandType = enum { Noop, Load, Quicksave, RunQS, Amp, RFlanger, Eq, Phaser, Mbeq, Chorus, PitchScaler, Reverb, Highpass, Delay, Vinyl, RevDelay, Gate, Detune, Overdrive, Degrade, RePsycho, TalkBox, DynComp, ThruZero, Foverdrive, Gverb, Invert, TapeDelay, ModDelay, MultiChorus, Saturator, VintageDelay, Noise, WildNoise, Write, Embed, Rotate, }; pub const Type = enum { /// "LV2 Commands" are commands that receive split, index, and then receive /// any f64 arguments. lv2_command, }; pub const NewCommand = struct { tag: Tag, pub const Tag = enum { noop, load, quicksave, runqs, amp, rflanger, eq, phaser, mbeq, chorus, pitchscaler, reverb, highpass, delay, vinyl, revdelay, gate, detune, overdrive, degrade, repsycho, talkbox, dyncomp, thruzero, foverdrive, gverb, invert, tapedelay, moddelay, multichorus, saturator, vintagedelay, noise, wildnoise, write, embed, rotate, }; pub fn cast(base: *@This(), comptime T: type) ?*T { if (base.tag != T.base_tag) return null; return @fieldParentPtr(T, "base", base); } pub const Noop = struct { pub const base_tag = Tag.noop; base: NewCommand, }; pub const Load = struct { pub const base_tag = Tag.load; base: NewCommand, path: []const u8, }; pub const Quicksave = struct { pub const base_tag = Tag.quicksave; base: NewCommand, }; pub const RunQS = struct { pub const base_tag = Tag.runqs; program: []const u8, base: NewCommand, }; pub const Amp = struct { pub const base_tag = Tag.amp; pub const base_type = Type.lv2_command; base: NewCommand, gain: f64 }; pub const RFlanger = struct { pub const base_tag = Tag.rflanger; pub const base_type = Type.lv2_command; base: NewCommand, delay_depth_avg: f64, law_freq: f64, }; }; pub const Command = struct { command: CommandType, args: ArgList, cur_idx: usize = 0, pub fn print(self: Command) void { std.debug.warn("cmd:{}\n", .{self.command}); } pub fn argAt(self: Command, idx: usize) ![]const u8 { std.debug.warn("{} {}", .{ idx, self.args.items.len }); if (idx > (self.args.items.len - 1)) { std.debug.warn("Expected argument at index {}\n", .{idx}); return ParseError.ArgRequired; } return self.args.items[idx]; } pub fn usizeArgAt(self: 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: Command, idx: usize) !i32 { var arg = try self.argAt(idx); return try std.fmt.parseInt(i32, arg, 10); } pub fn floatArgAt(self: Command, idx: usize) !f32 { var arg = try self.argAt(idx); return try std.fmt.parseFloat(f32, arg); } pub fn floatArgMany( self: 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.items; } 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 fn copy(self: Command, allocator: *std.mem.Allocator) !*Command { var cmd = try allocator.create(Command); cmd.* = Command{ .command = self.command, .args = self.args, .cur_idx = self.cur_idx, }; return cmd; } }; pub const CommandList = std.ArrayList(NewCommand); pub const ArgList = std.ArrayList([]const u8); pub const KeywordMap = std.StringHashMap(CommandType); /// A parser. 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(); } pub fn reset(self: *Lang) void { self.has_error = false; self.line = 0; } fn fillKeywords(self: *Lang) !void { inline for (@typeInfo(NewCommand).Struct.decls) |cmd_struct_decl| { switch (cmd_struct_decl.data) { .Type => |typ| switch (@typeInfo(typ)) { .Struct => {}, else => continue, }, else => continue, } const struct_name = cmd_struct_decl.name; comptime var lowered_command_name = [_]u8{0} ** struct_name.len; comptime { for (struct_name) |c, i| { lowered_command_name[i] = std.ascii.toLower(c); } } _ = try self.keywords.put(&lowered_command_name, @field(CommandType, struct_name)); } } pub fn getCommand(self: *Lang, stmt: []const u8) ?CommandType { var kv_opt = self.keywords.get(stmt); if (kv_opt) |kv| { return kv.value; } else { return null; } } fn expectAny(self: *Lang, count: usize, args: ArgList) !void { if (args.items.len != count) { self.doError("expected {} arguments, found {}", .{ count, args.items.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.items.len != count) { self.doError("expected {} arguments, found {}", .{ count, args.items.len }); return error.ArgRequired; } while (i < count) : (i += 1) { var arg = args.items[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(17, 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(7, 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), .Embed => try self.expectAny(3, cmd.args), else => std.debug.warn("WARN unchecked command {}\n", .{cmd.command}), } } fn doError(self: *Lang, comptime fmt: []const u8, args: var) void { std.debug.warn("error at line {}: ", .{self.line}); std.debug.warn(fmt, args); std.debug.warn("\n", .{}); self.has_error = true; } fn parseCommandArguments( self: *@This(), comptime command_struct: type, tok_it: *std.mem.TokenIterator, commands: *CommandList, ) !void { // Based on the command struct fields, we can parse the arguments. var cmd = try self.allocator.create(command_struct); inline for (@typeInfo(command_struct).Struct.fields) |cmd_field| { comptime { if (std.mem.eql(u8, cmd_field.name, "base")) { continue; } } // TODO: crash when no arguments are left but we still need // arguments... const arg = tok_it.next().?; const argument_value = switch (cmd_field.field_type) { usize => try std.fmt.parseInt(usize, arg, 10), i32 => try std.fmt.parseInt(i32, arg, 10), f32 => try std.fmt.parseFloat(f32, arg), []const u8 => arg, else => @panic("Invalid parameter type (" ++ @typeName(cmd_field.field_type) ++ ") left on command struct " ++ @typeName(command_struct) ++ "."), }; @field(cmd, cmd_field.name) = argument_value; } try commands.append(cmd.base); } pub fn parse(self: *Lang, data: []const u8) ParseError!CommandList { var splitted_it = std.mem.split(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 (std.mem.startsWith(u8, stmt, "#")) continue; // TODO better tokenizer instead of just tokenize(" ")...maybe???? var tok_it = std.mem.tokenize(stmt, " "); var cmd_opt = tok_it.next(); if (cmd_opt == null) { self.doError("No command given", .{}); continue; } const command_string = cmd_opt.?; var found: bool = false; inline for (@typeInfo(NewCommand).Struct.decls) |cmd_struct_decl| { switch (cmd_struct_decl.data) { .Type => |typ| switch (@typeInfo(typ)) { .Struct => {}, else => continue, }, else => continue, } const struct_name = cmd_struct_decl.name; comptime var lowered_command_name = [_]u8{0} ** struct_name.len; comptime { for (struct_name) |c, i| { lowered_command_name[i] = std.ascii.toLower(c); } } // if we have a match, we know the proper struct type // to use. this actually works compared to storing command_struct // in a variable because then that variable must be comptime. // the drawback of this approach is that our emitted code is basically linear // because we don't use the hashmap anymore. maybe #5359 can help. if (std.mem.eql(u8, &lowered_command_name, command_string)) { found = true; const cmd_struct_type = cmd_struct_decl.data.Type; try self.parseCommandArguments(cmd_struct_type, &tok_it, &cmds); } } if (!found) { self.doError("Unknown command '{}' ({})", .{ command_string, command_string.len }); continue; } // try cmds.append(cmd); } 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.items[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.items[0].command, .Load); std.testing.expectEqual(cmds.items[1].command, .Phaser); std.testing.expectEqual(cmds.items[2].command, .Quicksave); }