const std = @import("std"); const CharSet = @import("char-set.zig"); const Character = @import("character.zig").Character; const string = @import("string.zig"); const Rule = @import("rule.zig"); const Self = @This(); id: usize, name: []const u8, rule_list: std.ArrayList(Rule), first: CharSet = CharSet {}, follows: CharSet = CharSet {}, allocator: std.mem.Allocator, pub fn init(name: []const u8, id: usize, allocator: std.mem.Allocator) !Self { const target = try allocator.alloc(u8, name.len); @memcpy(target, name); return Self { .name = target, .id = id, .rule_list = std.ArrayList(Rule).init(allocator), .allocator = allocator, }; } pub fn deinit(self: *Self) void { for (self.rules()) |rule| { self.allocator.free(rule.items); } self.rule_list.deinit(); self.allocator.free(self.name); } pub inline fn rules(self: *Self) []Rule { return self.rule_list.items; } pub fn add_rule(self: *Self, rule: Rule) !void { try self.rule_list.append(rule); } pub fn parse_name(buffer: []const u8) ![]const u8 { const arrow_start = std.mem.indexOf(u8, buffer, "->") orelse return error.MissingArrow; if (arrow_start == 0) { return error.MissingRuleName; } return std.mem.trim(u8, buffer[0..arrow_start], &std.ascii.whitespace); } pub fn parse_and_add_variants( self: *Self, buffer: []const u8, names: *const std.StringHashMap(usize), ) !void { const arrow_start = std.mem.indexOf(u8, buffer, "->") orelse return error.MissingArrow; var head: []const u8 = buffer[arrow_start + 2..]; if (std.mem.trimLeft(u8, head, &std.ascii.whitespace).len == 0) { return error.MissingBody; } var rule = std.ArrayList(Character).init(self.allocator); errdefer rule.deinit(); head = std.mem.trimLeft(u8, head, &std.ascii.whitespace); while (head.len > 0) { if (head[0] == '|') { try self.rule_list.append(Rule { .items = try rule.toOwnedSlice() }); rule = std.ArrayList(Character).init(self.allocator); head = head[1..]; } else if (head[0] == '\'') { head = head[1..]; if (head.len > 0 and head[0] == '\'') { try rule.append(Character { .epsilon = void{} }); } while (head.len > 0 and head[0] != '\'') { const char, head = try string.read_first_from_literal(head); try rule.append(Character { .terminal = char }); } if (head.len == 0) { return error.UnterminatedString; } head = head[1..]; } else { const child_name, head = string.read_name(head); if (child_name.len == 0) { return error.EmptyVariant; } try rule.append(Character { .non_terminal = names.get(child_name) orelse { return error.MissingRule; } }); } head = std.mem.trimLeft(u8, head, &std.ascii.whitespace); } if (rule.items.len == 0) { return error.EmptyVariant; } try self.rule_list.append(Rule { .items = try rule.toOwnedSlice() }); } pub inline fn is_empty(self: *const Self) bool { return self.rules().len == 0; } pub fn format( self: *const Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype, ) !void { _ = fmt; _ = options; try writer.print("{s} ->", .{ self.name }); if (self.rule_list.items.len > 0) { for (self.rule_list.items[0].items) |c| { try writer.print(" {}", .{c}); } for (self.rule_list.items[1..]) |r| { try writer.print(" |", .{}); for (r.items) |c| { try writer.print(" {}", .{c}); } } } } test "single-non-terminal" { const allocator = std.testing.allocator; var non_terminal = try Self.init("main", 0, allocator); defer non_terminal.deinit(); var names = std.StringHashMap(usize).init(allocator); defer names.deinit(); try names.put("main", 0); try names.put("one", 1); try non_terminal.parse_and_add_variants("main -> one", &names); try std.testing.expectEqualDeep( &[_] Character { Character { .non_terminal = 1 } }, non_terminal.rules()[0].items ); } test "multiple-non-terminal" { const allocator = std.testing.allocator; var non_terminal = try Self.init("main", 0, allocator); defer non_terminal.deinit(); var names = std.StringHashMap(usize).init(allocator); defer names.deinit(); try names.put("main", 0); try names.put("one", 1); try names.put("two", 2); try names.put("three", 3); try non_terminal.parse_and_add_variants("main -> one | two | three", &names); inline for (0..3) |i| { try std.testing.expectEqualDeep( &[_] Character { Character { .non_terminal = i + 1 } }, non_terminal.rules()[i].items ); } } test "multiple-non-terminal-sequence" { const allocator = std.testing.allocator; var non_terminal = try Self.init("main", 0, allocator); defer non_terminal.deinit(); var names = std.StringHashMap(usize).init(allocator); defer names.deinit(); try names.put("main", 0); try names.put("one", 1); try names.put("two", 2); try names.put("three", 3); try non_terminal.parse_and_add_variants("main -> one two | two | three", &names); try std.testing.expectEqualDeep( &[_] Character { Character { .non_terminal = 1 }, Character { .non_terminal = 2 }, }, non_terminal.rules()[0].items ); try std.testing.expectEqualDeep( &[_] Character { Character { .non_terminal = 2 } }, non_terminal.rules()[1].items ); try std.testing.expectEqualDeep( &[_] Character { Character { .non_terminal = 3 } }, non_terminal.rules()[2].items ); } test "single-terminal" { const allocator = std.testing.allocator; var non_terminal = try Self.init("main", 0, allocator); defer non_terminal.deinit(); var names = std.StringHashMap(usize).init(allocator); defer names.deinit(); try names.put("main", 0); try names.put("one", 1); try non_terminal.parse_and_add_variants("main -> 'one'", &names); try std.testing.expectEqualDeep( &[_] Character { Character { .terminal = 'o' }, Character { .terminal = 'n' }, Character { .terminal = 'e' } }, non_terminal.rules()[0].items ); } test "single-terminal-with-escapes" { const allocator = std.testing.allocator; var non_terminal = try Self.init("main", 0, allocator); defer non_terminal.deinit(); var names = std.StringHashMap(usize).init(allocator); defer names.deinit(); try names.put("main", 0); try non_terminal.parse_and_add_variants("main -> 'o\\r\\nn\\'e\\\\'", &names); try std.testing.expectEqualDeep( &[_] Character { Character { .terminal = 'o' }, Character { .terminal = '\r' }, Character { .terminal = '\n' }, Character { .terminal = 'n' }, Character { .terminal = '\'' }, Character { .terminal = 'e' }, Character { .terminal = '\\' }, }, non_terminal.rules()[0].items ); } test "mix" { const allocator = std.testing.allocator; var non_terminal = try Self.init("main", 0, allocator); defer non_terminal.deinit(); var names = std.StringHashMap(usize).init(allocator); defer names.deinit(); try names.put("main", 0); try names.put("other", 1); try names.put("some", 2); try non_terminal.parse_and_add_variants("main -> 'ab' other | other some 'a' | main", &names); try std.testing.expectEqualDeep( &[_] Character { Character { .terminal = 'a' }, Character { .terminal = 'b' }, Character { .non_terminal = 1 }, }, non_terminal.rules()[0].items, ); try std.testing.expectEqualDeep( &[_] Character { Character { .non_terminal = 1 }, Character { .non_terminal = 2 }, Character { .terminal = 'a' }, }, non_terminal.rules()[1].items, ); try std.testing.expectEqualDeep( &[_] Character { Character { .non_terminal = 0 }, }, non_terminal.rules()[2].items, ); }