From a75216a3d7855256c360131d3bb20fe0404ed0c0 Mon Sep 17 00:00:00 2001 From: bgk- Date: Thu, 16 May 2024 03:05:34 -0700 Subject: [PATCH] Add enumseq --- build.zig.zon | 9 +++--- docs/syntax.md | 39 ++++++++++++++++++++++-- src/ast.zig | 1 + src/builtins.zig | 67 +++++++++++++++++++++++++++--------------- src/compiler-error.zig | 3 +- src/compiler.zig | 1 + src/enum.zig | 8 ++--- src/export.zig | 50 +++++++++++++------------------ src/parser.zig | 4 ++- src/state.zig | 3 ++ src/token.zig | 5 ++++ src/values.zig | 4 ++- src/vm.test.zig | 55 +++++++++++++++++++++------------- src/vm.zig | 21 +++++++++++-- 14 files changed, 178 insertions(+), 92 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index f95becf..ab760ad 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,6 @@ .{ - .name = "topiary", - .version = "0.12.0", - .paths = .{""}, - .dependencies = .{ - }, + .name = "topiary", + .version = "0.12.1", + .paths = .{""}, + .dependencies = .{}, } diff --git a/docs/syntax.md b/docs/syntax.md index 41f7800..960cad2 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -316,8 +316,6 @@ map.clear() // Map{} #### While While loops will execute so long as the condition is met. -~~However there is an internal limit of 100,000 to catch infinite loops. -This can be adjusted by setting `Topiary.MaxWhile = -1 // no limit`~~ ```topi var i = 0 @@ -468,6 +466,43 @@ enum Cardinal = { var direction = Cardinal.North ``` +Under the hood they are just index integers, which cannot be changed. +This does mean you can use comparitive operators with enums. + +```topi +var north = Cardinal.North +var south = Cardinal.South + +if (north < south) print(true) // true +``` + +## Sequences + +Enum Sequences (`enumseq`) are special enums, they are the same except they cannot be changed to a previous value. +If attempted, topi will ignore the assignment and remain at the current value. + +```topi +enumseq QuestGiver = { + None, + LearnedOfQuestGiver, + MetQuestGiver, + AcceptedQuest, + CompletedQuest, + RecievedAward +} + +var seq = QuestGiver.None +seq = QuestGiver.MetQuestGiver +seq = QuestGiver.LearnedOfQuestGiver // ignored +print(seq) // QuestGiver.MetQuestGiver +``` + +Sequences are useful in that all previous states are inferred from the current. +If the player met the quest giver, they must have learned of them. +Same if they accepted the quest, they must have met them, and so on. +For more information you can watch this great talk by +[Inkle's Jon Ingold](https://www.youtube.com/watch?v=HZft_U4Fc-U). + ### Classes Classes are an encapsulation of named data. diff --git a/src/ast.zig b/src/ast.zig index c99680b..248741f 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -144,6 +144,7 @@ pub const Statement = struct { }, @"enum": struct { name: []const u8, + is_seq: bool, values: [][]const u8, }, expression: Expression, diff --git a/src/builtins.zig b/src/builtins.zig index bb4497a..9bc49e0 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -88,7 +88,30 @@ const Print = struct { const writer = std.debug; args[0].print(writer, null); writer.print("\n", .{}); - return values.Nil; + return values.Void; + } +}; + +pub const Assert = struct { + const Self = @This(); + var value: Value = .{ + .obj = &Self.obj, + }; + var obj: Value.Obj = .{ + .data = .{ + .builtin = .{ + .backing = Self.builtin, + .arity = 2, + .is_method = false, + .name = "assert", + }, + }, + }; + fn builtin(_: *Gc, args: []Value) Value { + const expr = args[0]; + const msg = args[1]; + if (!expr.eql(values.True)) return msg; + return values.Void; } }; @@ -97,24 +120,22 @@ const Definition = struct { value: *Value, }; -pub const builtins = [_]Definition{ - .{ - .name = "rnd", - .value = &Rnd.value, - }, - .{ - .name = "rnd01", - .value = &Rnd01.value, - }, - .{ - .name = "print", - .value = &Print.value, - }, - .{ - .name = "round", - .value = &Round.value, - }, -}; +pub const builtins = [_]Definition{ .{ + .name = "rnd", + .value = &Rnd.value, +}, .{ + .name = "rnd01", + .value = &Rnd01.value, +}, .{ + .name = "print", + .value = &Print.value, +}, .{ + .name = "round", + .value = &Round.value, +}, .{ + .name = "assert", + .value = &Assert.value, +} }; pub const Count = struct { const Self = @This(); @@ -155,7 +176,7 @@ pub const Add = struct { .set => args[0].obj.data.set.put(item, {}) catch {}, else => unreachable, } - return values.Nil; + return values.Void; } }; @@ -176,7 +197,7 @@ pub const AddMap = struct { .map => args[0].obj.data.map.put(key, item) catch {}, else => unreachable, } - return values.Nil; + return values.Void; } }; pub const Remove = struct { @@ -203,7 +224,7 @@ pub const Remove = struct { .map => _ = args[0].obj.data.map.orderedRemove(item), else => unreachable, } - return values.Nil; + return values.Void; } }; @@ -259,6 +280,6 @@ pub const Clear = struct { .set => data.set.clearAndFree(), else => {}, } - return values.Nil; + return values.Void; } }; diff --git a/src/compiler-error.zig b/src/compiler-error.zig index aa851a9..4fab906 100644 --- a/src/compiler-error.zig +++ b/src/compiler-error.zig @@ -73,7 +73,8 @@ pub const CompilerErrors = struct { var lineStart: usize = 0; if (lineNumber == 1) lineStart = if (std.mem.startsWith(u8, l, "\xEF\xBB\xBF")) 3 else @as(usize, 0); try writer.print("{s}\n", .{l[lineStart..]}); - try writer.writeByteNTimes(' ', column - 1); + try writer.writeByteNTimes(' ', @max(column - 1, 0)); + try writer.print("{s}", .{color_prefix}); try writer.writeByteNTimes('~', end - start); try writer.writeAll("\n\x1b[0m"); break; diff --git a/src/compiler.zig b/src/compiler.zig index d7f1cc4..1739869 100644 --- a/src/compiler.zig +++ b/src/compiler.zig @@ -459,6 +459,7 @@ pub const Compiler = struct { obj.* = .{ .data = .{ .@"enum" = .{ + .is_seq = e.is_seq, .name = try self.allocator.dupe(u8, e.name), .values = try names.toOwnedSlice(), }, diff --git a/src/enum.zig b/src/enum.zig index 0c38e0b..276e748 100644 --- a/src/enum.zig +++ b/src/enum.zig @@ -4,12 +4,10 @@ const Value = @import("values.zig").Value; pub const Enum = struct { name: []const u8, values: [][]const u8, + is_seq: bool, - pub fn init(name: []const u8, values: [][]const u8) Enum { - return .{ - .name = name, - .values = values, - }; + pub fn init(name: []const u8, values: [][]const u8, is_seq: bool) Enum { + return .{ .name = name, .values = values, .is_seq = is_seq }; } pub const Val = struct { diff --git a/src/export.zig b/src/export.zig index 9b763c8..99edea4 100644 --- a/src/export.zig +++ b/src/export.zig @@ -511,25 +511,13 @@ const ExportRunner = struct { const TestRunner = struct { pub fn onLine(vm_ptr: usize, dialogue: *ExportLine) void { - std.debug.print("{s}: {s} ", .{ - dialogue.speaker[0..dialogue.speaker_length], - dialogue.content[0..dialogue.content_length], - }); - for (dialogue.tags[0..dialogue.tags_length]) |t| { - std.debug.print("#{s} ", .{t}); - } - std.debug.print("\n", .{}); + _ = dialogue; selectContinue(vm_ptr); } pub fn onChoices(vm_ptr: usize, choices: [*]ExportChoice, choices_len: u8) void { - for (choices, 0..choices_len) |choice, i| { - std.debug.print("[{d}] {s} ", .{ i, choice.content }); - for (choice.tags[0..choice.tags_length]) |t| { - std.debug.print("#{s} ", .{t}); - } - std.debug.print("\n", .{}); - } + _ = choices; + _ = choices_len; selectChoice(vm_ptr, 0); } @@ -539,7 +527,7 @@ const TestRunner = struct { }; fn testSubscriber(value: ExportValue) void { - std.debug.print("ExportSubscriber: {s}\n", .{value.data.string}); + std.testing.expectEqualSlices(u8, "321 test", value.data.string[0..8]) catch {}; } test "Create and Destroy Vm" { @@ -568,9 +556,7 @@ test "Create and Destroy Vm" { ; debug_log = TestRunner.log; - debug_severity = .info; defer debug_log = null; - defer debug_severity = .err; const file = try std.fs.cwd().createFile("tmp.topi", .{ .read = true }); defer std.fs.cwd().deleteFile("tmp.topi") catch {}; @@ -595,9 +581,7 @@ test "Create and Destroy Vm" { const vm: *Vm = @ptrFromInt(vm_ptr); defer destroyVm(vm_ptr); - vm.bytecode.print(std.debug); defer vm.bytecode.free(alloc); - std.debug.print("\n=====\n", .{}); const val_name = "value"; subscribe( vm_ptr, @@ -628,10 +612,11 @@ test "Create and Destroy Vm" { list_name.len, &list_value, )) { - std.debug.print("List: {}\n", .{list_value.data.list}); - for (list_value.data.list.items[0..list_value.data.list.count]) |item| { - std.debug.print("List Item: {d}\n", .{item.data.number}); - } + const list = list_value.data.list.items; + try std.testing.expectEqual(1, list[0].data.number); + try std.testing.expectEqual(2, list[1].data.number); + try std.testing.expectEqual(3, list[2].data.number); + try std.testing.expectEqual(4, list[3].data.number); destroyValue(&list_value); } @@ -643,9 +628,10 @@ test "Create and Destroy Vm" { set_name.len, &set_value, )) { - for (set_value.data.list.items[0..set_value.data.list.count]) |item| { - std.debug.print("Set Item: {s}\n", .{item.data.string}); - } + const set = set_value.data.list.items; + try std.testing.expectEqualSlices(u8, "some", set[0].data.string[0..4]); + try std.testing.expectEqualSlices(u8, "string", set[1].data.string[0..6]); + try std.testing.expectEqualSlices(u8, "values", set[2].data.string[0..6]); destroyValue(&set_value); } @@ -657,9 +643,13 @@ test "Create and Destroy Vm" { map_name.len, &map_value, )) { - for (map_value.data.list.items[0 .. map_value.data.list.count * 2]) |item| { - std.debug.print("Map Item: {d}\n", .{item.data.number}); - } + const map = map_value.data.list.items; + try std.testing.expectEqual(0, map[0].data.number); + try std.testing.expectEqual(0.0001, map[1].data.number); + try std.testing.expectEqual(1, map[2].data.number); + try std.testing.expectEqual(1.1111, map[3].data.number); + try std.testing.expectEqual(2, map[4].data.number); + try std.testing.expectEqual(2.222, map[5].data.number); destroyValue(&map_value); } } diff --git a/src/parser.zig b/src/parser.zig index c214534..827588a 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -163,7 +163,7 @@ pub const Parser = struct { return switch (self.current_token.token_type) { .include => try self.includeStatement(), .class => try self.classDeclaration(), - .@"enum" => try self.enumDeclaration(), + .@"enum", .enumseq => try self.enumDeclaration(), .@"extern", .@"var", .@"const" => try self.varDeclaration(), .bough => try self.boughStatement(), .divert => try self.divertStatement(), @@ -274,6 +274,7 @@ pub const Parser = struct { fn enumDeclaration(self: *Parser) Error!Statement { const start = self.current_token; + const is_seq = start.token_type == .enumseq; self.next(); const name = try self.consumeIdentifier(); try self.expectCurrent(.equal); @@ -291,6 +292,7 @@ pub const Parser = struct { .token = start, .type = .{ .@"enum" = .{ + .is_seq = is_seq, .name = name, .values = try values.toOwnedSlice(), }, diff --git a/src/state.zig b/src/state.zig index e2205ad..882809c 100644 --- a/src/state.zig +++ b/src/state.zig @@ -88,6 +88,8 @@ pub const State = struct { try stream.beginObject(); try stream.objectField("name"); try stream.write(e.name); + try stream.objectField("is_seq"); + try stream.write(e.is_seq); try stream.objectField("values"); try stream.beginArray(); for (e.values) |v| try stream.write(v); @@ -275,6 +277,7 @@ pub const State = struct { for (values_items, 0..) |t, i| vals[i] = try vm.allocator.dupe(u8, t.string); var result = try vm.gc.create(vm, .{ .@"enum" = .{ .name = v.object.get("name").?.string, + .is_seq = v.object.get("is_seq").?.bool, .values = vals, } }); result.obj.id = id.?; diff --git a/src/token.zig b/src/token.zig index b9923a8..aabcbab 100644 --- a/src/token.zig +++ b/src/token.zig @@ -61,6 +61,7 @@ pub const TokenType = enum { @"continue", @"else", @"enum", + enumseq, @"extern", false, @"for", @@ -73,6 +74,7 @@ pub const TokenType = enum { @"or", @"return", self, + seq, set, class, @"switch", @@ -98,6 +100,7 @@ pub const Keywords = std.ComptimeStringMap(TokenType, .{ .{ "const", .@"const" }, .{ "else", .@"else" }, .{ "enum", .@"enum" }, + .{ "enumseq", .enumseq }, .{ "extern", .@"extern" }, .{ "false", .false }, .{ "for", .@"for" }, @@ -168,6 +171,7 @@ pub fn toString(token_type: TokenType) []const u8 { .@"continue" => "continue", .@"else" => "else", .@"enum" => "enum", + .enumseq => "enumseq", .@"extern" => "extern", .false => "false", .@"for" => "for", @@ -180,6 +184,7 @@ pub fn toString(token_type: TokenType) []const u8 { .@"or" => "or", .@"return" => "return", .self => "self", + .seq => "seq", .set => "Set", .class => "class", .@"switch" => "switch", diff --git a/src/values.zig b/src/values.zig index 42a351d..290b7ba 100644 --- a/src/values.zig +++ b/src/values.zig @@ -301,6 +301,7 @@ pub const Value = union(Type) { .@"enum" => |e| { try writer.writeByte(@intCast(e.name.len)); try writer.writeAll(e.name); + try writer.writeByte(if (e.is_seq) 1 else 0); try writer.writeByte(@intCast(e.values.len)); for (e.values) |value| { try writer.writeByte(@intCast(value.len)); @@ -425,9 +426,10 @@ pub const Value = union(Type) { const name_length = try reader.readByte(); const name_buf = try allocator.alloc(u8, name_length); try reader.readNoEof(name_buf); + const is_seq = try reader.readByte() == 1; const values_length = try reader.readByte(); const obj = try allocator.create(Value.Obj); - obj.* = .{ .data = .{ .@"enum" = .{ .name = name_buf, .values = try allocator.alloc([]const u8, values_length) } } }; + obj.* = .{ .data = .{ .@"enum" = .{ .name = name_buf, .values = try allocator.alloc([]const u8, values_length), .is_seq = is_seq } } }; for (0..values_length) |i| { const value_name_length = try reader.readByte(); const value_name_buf = try allocator.alloc(u8, value_name_length); diff --git a/src/vm.test.zig b/src/vm.test.zig index 1772972..3f4e381 100644 --- a/src/vm.test.zig +++ b/src/vm.test.zig @@ -330,7 +330,6 @@ test "Index" { \\ const mid = List{inner, 6,7,8} \\ const outer = List{mid,9} \\ outer[0][0][4] = 99 - \\ print(outer) \\ outer[0][0][4] , .value = 99.0, @@ -740,7 +739,6 @@ test "Loops" { return err; }; const value = vm.stack.previous(); - value.print(std.debug, vm.bytecode.constants); try testing.expectEqual(value.number, case.value); } } @@ -774,21 +772,21 @@ test "Instance" { \\ } \\ const test = new Test{} \\ test.value = 5 - \\ print(test) - \\ print(test.value) + \\ assert(test.value == 5, "test.value == 5") \\ test.value += 1 - \\ print(test.value) - \\ print(test.fn()) + \\ assert(test.value == 6, "test.value == 6") + \\ assert(test.fn() == "func", "test.fn() == ""func""") \\ test.incr(1) - \\ print(test.value) + \\ assert(test.value == 7, "test.value == 7") \\ test.list.add(1) - \\ print(test.list) + \\ assert(test.list.count() == 1, "test.list.count() == 1") + \\ assert(test.list[0] == 1, "test.list[0] == 1") \\ test.list[0] = 99 - \\ print(test.list) + \\ assert(test.list[0] == 99, "test.list[0] == 99") \\ test.nested.add(2) + \\ assert(test.nested[0] == 2, "test.nested[2] == 2") \\ test.list.add(test.nested) - \\ print(test.list) - \\ print(test.list[1][0]) + \\ assert(test.list[1][0] == 2, "test.list[1][0] == 2") ; var mod = Module.create(allocator); defer mod.deinit(); @@ -821,8 +819,21 @@ test "Enums" { \\ } \\ } \\ - \\ print(timeOfDay(5)) - \\ + \\ assert(timeOfDay(5) == TimeOfDay.Morning, "timeOfDay(5) == TimeOfDay.Morning") + \\ var tod = TimeOfDay.Morning + \\ assert(tod < TimeOfDay.Evening, "tod < TimeOfDay.Evening"); + \\ enumseq Quest = { + \\ None, + \\ KnowsOf, + \\ Started, + \\ Complete + \\ } + \\ var quest = Quest.None + \\ quest = Quest.Started + \\ quest = Quest.KnowsOf + \\ quest = Quest.Complete + \\ quest = Quest.None + \\ assert(quest == Quest.Complete, "Quest is not Complete") ; var mod = Module.create(allocator); @@ -831,7 +842,10 @@ test "Enums" { var vm = try initTestVm(input, &mod, false); defer vm.deinit(); defer vm.bytecode.free(testing.allocator); - try vm.interpret(); + vm.interpret() catch |err| { + vm.err.print(std.io.getStdErr().writer()); + return err; + }; } test "Boughs" { @@ -856,7 +870,6 @@ test "Boughs" { \\ while count > 0 { \\ result = result + str \\ count -= 1 - \\ print(count) \\ } \\ return result \\ } @@ -875,7 +888,10 @@ test "Boughs" { \\ === START { \\ if true :speaker: "True text goes here" \\ :speaker: "More text here" - \\ if false :speaker: "False text doesn't appear" + \\ if false { + \\ :speaker: "False text doesn't appear" + \\ assert(false, "should not be here") + \\ } \\ :speaker: "Final text here" \\ } }, @@ -887,7 +903,7 @@ test "Boughs" { \\ } \\ :speaker: "More goes here" \\ => INNER - \\ :speaker: "Final goes here" // should not be printed + \\ assert(false, "should not be here") \\ } }, .{ .input = @@ -901,7 +917,7 @@ test "Boughs" { \\ } \\ :speaker: "More goes here" \\ => OUTER.INNER - \\ :speaker: "Text doesn't appear here" // should not be printed + \\ assert(false, "should not be here") \\ } }, }; @@ -1468,7 +1484,6 @@ test "Save and Load State" { var data = std.ArrayList(u8).init(alloc); defer data.deinit(); try State.serialize(&vm, data.writer()); - std.debug.print("\n{s}\n", .{data.items}); const second_case = \\ var value = 10 @@ -1490,6 +1505,4 @@ test "Save and Load State" { try testing.expectEqual(vm2.globals[2].obj.data.instance.fields[1].number, 2); try vm2.interpret(); try testing.expectEqual(vm2.globals[0].number, 6); - - std.log.warn("CALC SIZE: {}, ACTUAL SIZE: {}", .{ try State.calculateSize(&vm), data.items.len }); } diff --git a/src/vm.zig b/src/vm.zig index 1a040dc..553a9e6 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -69,7 +69,7 @@ pub const Vm = struct { bytecode: Bytecode, /// Current instruction position ip: usize = 0, - debug: bool = false, + break_on_assert: bool = true, /// Used to cache the choices choices_list: std.ArrayList(Choice), @@ -202,6 +202,7 @@ pub const Vm = struct { for (self.subscribers) |*sub| sub.deinit(); self.allocator.free(self.subscribers); self.allocator.free(self.globals); + if (self.err.msg) |msg| self.allocator.free(msg); } /// Returns root values that should not be cleaned up by the garbage collector @@ -455,7 +456,11 @@ pub const Vm = struct { if (index > globals_size) return self.fail("Globals index {} is out of bounds of max size", .{index}); if (index >= self.globals.len) return self.fail("Globals index {} is out of bounds of current size {}", .{ index, self.globals.len }); - const value = self.pop(); + var value = self.pop(); + const current = self.globals[index]; + if (current == .enum_value and value == .enum_value and current.enum_value.base == value.enum_value.base and current.enum_value.base.data.@"enum".is_seq) { + if (current.enum_value.index > value.enum_value.index) value = current; + } self.globals[index] = value; self.subscribers[index].invoke(value); }, @@ -467,7 +472,12 @@ pub const Vm = struct { .set_local => { const index = self.readInt(OpCode.Size(.set_local)); const frame = self.currentFrame(); - self.stack.items[frame.bp + index] = self.pop(); + var value = self.pop(); + const current = self.stack.items[frame.bp + index]; + if (current == .enum_value and value == .enum_value and current.enum_value.base == value.enum_value.base and current.enum_value.base.data.@"enum".is_seq) { + if (current.enum_value.index > value.enum_value.index) value = current; + } + self.stack.items[frame.bp + index] = value; }, .get_local => { const index = self.readInt(OpCode.Size(.get_local)); @@ -861,6 +871,9 @@ pub const Vm = struct { .{ b.arity, arg_count }, ); const result = b.backing(&self.gc, self.stack.items[self.stack.count - arg_count .. self.stack.count]); + if (self.break_on_assert and std.mem.eql(u8, b.name, "assert") and result != .void) { + return self.fail("Assertion Failed: {s}", .{result.obj.data.string}); + } self.stack.count -= arg_count + 1; try self.push(result); }, @@ -1013,6 +1026,8 @@ pub const Vm = struct { if (@intFromEnum(right) != @intFromEnum(left)) { return self.fail("Cannot compare mismatched types '{s}' and '{s}'", .{ @tagName(left), @tagName(right) }); } + if (right == .enum_value) right = .{ .number = @floatFromInt(right.enum_value.index) }; + if (left == .enum_value) left = .{ .number = @floatFromInt(left.enum_value.index) }; switch (op) { .equal => try self.push(.{ .bool = right.eql(left) }), .not_equal => try self.push(.{ .bool = !right.eql(left) }),