const std = @import("std"); const plugin = @import("plugin.zig"); pub const ParseError = error{ OutOfMemory, ArgRequired, ParseFail, }; pub const CommandType = enum { /// "LV2 Commands" are commands that receive split, index, and then receive /// any f64 arguments. lv2_command, }; fn LV2Command( comptime tag: Command.Tag, comptime plugin_url: []const u8, comptime LV2Parameters: type, ) type { return struct { pub const base_tag = tag; pub const command_type = CommandType.lv2_command; pub const lv2_url = plugin_url; base: Command, split: usize, index: usize, parameters: LV2Parameters, }; } pub const Command = 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 tagToType(tag: Tag) type { return switch (tag) { .noop => Noop, .load => Load, .quicksave => Quicksave, .runqs => RunQS, .amp => Amp, .rflanger => RFlanger, .eq => Eq, .phaser => Phaser, // .mbeq => Mbeq, // .chorus => Chorus, // .pitchscaler => Pitchscaler, // .reverb => Reverb, // .highpass => Highpass, // .delay => Delay, // .vinyl => Vinyl, // .revdelay => Revdelay, // .gate => Gate, // .detune => Detune, // .overdrive => Overdrive, // .degrade => Degrade, // .repsycho => Repsycho, // .talkbox => Talkbox, // .dyncomp => Dyncomp, // .thruzero => Thruzero, // .foverdrive => Foverdrive, // .gverb => Gverb, // .invert => Invert, // .tapedelay => Tapedelay, // .moddelay => Moddelay, // .multichorus => Multichorus, // .saturator => Saturator, // .vintagedelay => Vintagedelay, else => @panic("TODO"), }; } pub fn cast(base: *const @This(), comptime T: type) ?*const T { if (base.tag != T.base_tag) return null; return @fieldParentPtr(T, "base", base); } pub fn print(base: *const @This()) void { std.debug.warn("tag: {}\n", .{base.tag}); } pub const Noop = struct { pub const base_tag = Tag.noop; base: Command, }; pub const Load = struct { pub const base_tag = Tag.load; base: Command, path: []const u8, }; pub const Quicksave = struct { pub const base_tag = Tag.quicksave; base: Command, }; pub const RunQS = struct { pub const base_tag = Tag.runqs; program: []const u8, base: Command, }; pub const Amp = LV2Command( .amp, "http://lv2plug.in/plugins/eg-amp", struct { gain: f32 }, ); pub const RFlanger = LV2Command( .rflanger, "http://plugin.org.uk/swh-plugins/retroFlange", struct { delay_depth_avg: f32, law_freq: f32 }, ); pub const Eq = LV2Command( .rflanger, "http://plugin.org.uk/swh-plugins/dj_eq_mono", struct { lo: f32, mid: f32, hi: f32 }, ); pub const Phaser = LV2Command( .rflanger, "http://plugin.org.uk/swh-plugins/lfoPhaser", struct { lfo_rate: f32, lfo_depth: f32, fb: f32, spread: f32 }, ); }; pub const CommandList = std.ArrayList(*Command); pub const ArgList = std.ArrayList([]const u8); pub const KeywordMap = std.StringHashMap(CommandType); /// A parser. pub const Lang = struct { allocator: *std.mem.Allocator, has_error: bool = false, line: usize = 0, pub fn init(allocator: *std.mem.Allocator) Lang { return Lang{ .allocator = allocator, }; } pub fn deinit(self: *Lang) void {} pub fn reset(self: *Lang) void { self.has_error = false; self.line = 0; } 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); const is_lv2_command = switch (command_struct.base_tag) { .noop, .load, .quicksave, .runqs => false, else => command_struct.command_type == .lv2_command, }; // TODO: crash when no arguments are left but we still need // arguments... if (is_lv2_command) { cmd.split = try std.fmt.parseInt(usize, tok_it.next().?, 10); cmd.index = try std.fmt.parseInt(usize, tok_it.next().?, 10); inline for (@typeInfo(@TypeOf(cmd.parameters)).Struct.fields) |cmd_field| { const arg = tok_it.next().?; const argument_value = switch (cmd_field.field_type) { f32 => try std.fmt.parseFloat(f32, arg), else => @compileError("LV2 parameter struct can only have f32 fields"), }; std.debug.warn("parsing {}, arg of type {} => {}\n", .{ @typeName(command_struct), @typeName(@TypeOf(argument_value)), argument_value, }); @field(cmd.parameters, cmd_field.name) = argument_value; } } else { inline for (@typeInfo(command_struct).Struct.fields) |cmd_field| { comptime { if (std.mem.eql(u8, cmd_field.name, "base")) { continue; } } 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 => try self.allocator.dupe(u8, arg), else => @panic("Invalid parameter type (" ++ @typeName(cmd_field.field_type) ++ ") left on command struct " ++ @typeName(command_struct) ++ "."), }; std.debug.warn("parsing {}, arg of type {} => {}\n", .{ @typeName(command_struct), @typeName(@TypeOf(argument_value)), argument_value, }); @field(cmd, cmd_field.name) = argument_value; } } cmd.base.tag = command_struct.base_tag; const command = cmd.base.cast(command_struct).?; std.debug.warn("cmd: {}\n", .{command}); try commands.append(&cmd.base); } pub fn parse(self: *Lang, data: []const u8) !CommandList { var splitted_it = std.mem.split(data, ";"); 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(Command).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); }