From 889f678dd8da386ee7998f05af68853a2f399918 Mon Sep 17 00:00:00 2001 From: Brett Broadhurst Date: Mon, 2 Mar 2026 21:16:24 -0700 Subject: [PATCH] feat: code generation and execution for simple choice statements --- src/AstGen.zig | 337 +++++++++++++++++++++++++++++-------------- src/Story.zig | 109 ++++++++++---- src/Story/Dumper.zig | 96 ++++++------ src/main.zig | 26 +++- 4 files changed, 389 insertions(+), 179 deletions(-) diff --git a/src/AstGen.zig b/src/AstGen.zig index d7488bf..e84df84 100644 --- a/src/AstGen.zig +++ b/src/AstGen.zig @@ -18,68 +18,16 @@ jump_stack: std.ArrayListUnmanaged(Jump) = .empty, errors: std.ArrayListUnmanaged(Ast.Error) = .empty, default_knot_name: [:0]const u8 = "@main@", -pub const CheckError = anyerror; - -pub const Symbol = union(enum) { - global_variable: struct { - is_constant: bool, - constant_slot: usize, - node: *const Ast.Node, - }, - local_variable: struct { - stack_slot: usize, - node: *const Ast.Node, - }, - parameter: struct { - constant_slot: usize, - stack_slot: usize, - }, +pub const CheckError = error{ + OutOfMemory, + CompilerBug, + SemanticError, + TooManyConstants, + InvalidCharacter, + NotImplemented, }; -pub const Jump = struct { - label_index: usize, - code_offset: usize, -}; - -pub const Label = struct { - code_offset: usize, -}; - -pub const Chunk = struct { - name: [:0]const u8, - arity: usize, - locals_count: usize, - const_pool: std.ArrayListUnmanaged(*Story.Object), - bytes: std.ArrayListUnmanaged(u8), - - pub fn finalize(chunk: *Chunk, scope: *Scope) !*Story.Object.ContentPath { - const gpa = scope.global.gpa; - const story = scope.global.story; - - try scope.backpatch(); - - const const_pool = try chunk.const_pool.toOwnedSlice(gpa); - const bytes = try chunk.bytes.toOwnedSlice(gpa); - const knot_name = try Story.Object.String.create(story, chunk.name); - const content_path = try Story.Object.ContentPath.create( - story, - knot_name, - chunk.arity, - chunk.locals_count, - const_pool, - bytes, - ); - return content_path; - } - - pub fn deinit(chunk: *Chunk, gpa: std.mem.Allocator) void { - chunk.const_pool.deinit(gpa); - chunk.bytes.deinit(gpa); - chunk.* = undefined; - } -}; - -pub const Scope = struct { +const Scope = struct { parent: ?*Scope, global: *AstGen, chunk: *Chunk, @@ -122,7 +70,11 @@ pub const Scope = struct { return null; } - pub fn insertIdentifier(scope: *Scope, node: *const Ast.Node, symbol: Symbol) !void { + pub fn insertIdentifier( + scope: *Scope, + node: *const Ast.Node, + symbol: Symbol, + ) error{OutOfMemory}!void { const gpa = scope.global.gpa; const symbol_table = &scope.symbol_table; const token_bytes = scope.global.getLexemeFromNode(node); @@ -144,27 +96,7 @@ pub const Scope = struct { }; } - pub fn makeLabel(scope: *Scope) !usize { - const gpa = scope.global.gpa; - const label_stack = &scope.global.label_stack; - const label_id = label_stack.items.len; - const label_data: Label = .{ .code_offset = 0xffffffff }; - - try label_stack.append(gpa, label_data); - return label_id; - } - - pub fn setLabel(scope: *Scope, label_id: usize) void { - const chunk = scope.chunk; - const code_offset = chunk.bytes.items.len; - const label_stack = &scope.global.label_stack; - assert(label_id <= label_stack.items.len); - - const label_data = &label_stack.items[label_id]; - label_data.code_offset = code_offset; - } - - pub fn makeConstant(scope: *Scope, object: *Story.Object) !usize { + pub fn makeConstant(scope: *Scope, object: *Story.Object) error{OutOfMemory}!usize { const gpa = scope.global.gpa; const chunk = scope.chunk; const const_id = chunk.const_pool.items.len; @@ -178,7 +110,7 @@ pub const Scope = struct { len: u32, }; - pub fn makeString(scope: *Scope, bytes: []const u8) !IndexSlice { + pub fn makeString(scope: *Scope, bytes: []const u8) error{OutOfMemory}!IndexSlice { const global = scope.global; const gpa = scope.global.gpa; const str_index: u32 = @intCast(global.string_bytes.items.len); @@ -201,7 +133,11 @@ pub const Scope = struct { } } - pub fn makeGlobal(scope: *Scope, node: *const Ast.Node, symbol: Symbol) !void { + pub fn makeGlobal( + scope: *Scope, + node: *const Ast.Node, + symbol: Symbol, + ) error{OutOfMemory}!void { const gpa = scope.global.gpa; const global_data = scope.global; const token_bytes = global_data.getLexemeFromNode(node); @@ -209,11 +145,11 @@ pub const Scope = struct { return scope.insertIdentifier(node, symbol); } - pub fn emitByte(scope: *Scope, byte: u8) !void { + pub fn emitByte(scope: *Scope, byte: u8) error{OutOfMemory}!void { return scope.chunk.bytes.append(scope.global.gpa, byte); } - pub fn emitSimpleInst(scope: *Scope, op: Story.Opcode) !void { + pub fn emitSimpleInst(scope: *Scope, op: Story.Opcode) error{OutOfMemory}!void { return scope.emitByte(@intFromEnum(op)); } @@ -224,7 +160,7 @@ pub const Scope = struct { try scope.emitByte(@intCast(arg)); } - pub fn emitJumpInst(scope: *Scope, op: Story.Opcode) !void { + pub fn emitJumpInst(scope: *Scope, op: Story.Opcode) error{OutOfMemory}!usize { const bytes = &scope.chunk.bytes; try scope.emitSimpleInst(op); try scope.emitByte(0xff); @@ -232,7 +168,125 @@ pub const Scope = struct { return bytes.items.len - 2; } - pub fn backpatch(_: *Scope) !void {} + // Create a new code label. + pub fn makeLabel(scope: *Scope) error{OutOfMemory}!usize { + const gpa = scope.global.gpa; + const dummy_address = 0xffffffff; + const label_stack = &scope.global.label_stack; + const label_index = label_stack.items.len; + const label_data: Label = .{ .code_offset = dummy_address }; + + try label_stack.append(gpa, label_data); + return label_index; + } + + // Sets the code offset pointed to by a label. + pub fn setLabel(scope: *Scope, label_id: usize) void { + const chunk = scope.chunk; + const code_offset = chunk.bytes.items.len; + const label_stack = &scope.global.label_stack; + assert(label_id <= label_stack.items.len); + + const label_data = &label_stack.items[label_id]; + label_data.code_offset = code_offset; + } + + pub fn makeJump(scope: *Scope, jump: Jump) !usize { + const gpa = scope.global.gpa; + const jump_stack = &scope.global.jump_stack; + const jump_index = jump_stack.items.len; + try jump_stack.append(gpa, jump); + return jump_index; + } + + pub fn resolveLabels(scope: *Scope, start_index: usize, end_index: usize) !void { + assert(start_index <= end_index); + const jump_stack = &scope.global.jump_stack; + const label_stack = &scope.global.label_stack; + const chunk_bytes = &scope.chunk.bytes; + const jump_list = jump_stack.items[start_index..end_index]; + + for (jump_list) |jump| { + const label = label_stack.items[jump.label_index]; + const jump_offset: usize = switch (jump.mode) { + .relative => label.code_offset - jump.code_offset - 2, + .absolute => label.code_offset, + }; + if (jump_offset >= std.math.maxInt(u16)) { + std.debug.print("Too much code to jump over!\n", .{}); + return error.CompilerBug; + } + + assert(chunk_bytes.capacity >= jump.code_offset + 2); + chunk_bytes.items[jump.code_offset] = @intCast((jump_offset >> 8) & 0xff); + chunk_bytes.items[jump.code_offset + 1] = @intCast(jump_offset & 0xff); + } + } +}; + +pub const Symbol = union(enum) { + global_variable: struct { + is_constant: bool, + constant_slot: usize, + node: *const Ast.Node, + }, + local_variable: struct { + stack_slot: usize, + node: *const Ast.Node, + }, + parameter: struct { + constant_slot: usize, + stack_slot: usize, + }, +}; + +pub const Jump = struct { + mode: enum { + relative, + absolute, + }, + label_index: usize, + code_offset: usize, +}; + +pub const Label = struct { + code_offset: usize, +}; + +// Code chunk to emitting bytes and constants to during code generation. +pub const Chunk = struct { + name: [:0]const u8, + arity: usize, + locals_count: usize, + const_pool: std.ArrayListUnmanaged(*Story.Object), + bytes: std.ArrayListUnmanaged(u8), + + pub fn finalize(chunk: *Chunk, scope: *Scope) !*Story.Object.ContentPath { + const gpa = scope.global.gpa; + const story = scope.global.story; + const jump_stack_top = scope.global.jump_stack.items.len; + + try scope.resolveLabels(scope.jump_stack_top, jump_stack_top); + + const const_pool = try chunk.const_pool.toOwnedSlice(gpa); + const bytes = try chunk.bytes.toOwnedSlice(gpa); + const knot_name = try Story.Object.String.create(story, chunk.name); + const content_path = try Story.Object.ContentPath.create( + story, + knot_name, + chunk.arity, + chunk.locals_count, + const_pool, + bytes, + ); + return content_path; + } + + pub fn deinit(chunk: *Chunk, gpa: std.mem.Allocator) void { + chunk.const_pool.deinit(gpa); + chunk.bytes.deinit(gpa); + chunk.* = undefined; + } }; pub fn deinit(astgen: *AstGen) void { @@ -363,11 +417,11 @@ fn checkContentExpr(scope: *Scope, expr: *const Ast.Node) CheckError!void { switch (child_node.tag) { .string_literal => { try checkStringLiteral(scope, child_node); - try scope.emitSimpleInst(.content); + try scope.emitSimpleInst(.stream_push); }, .inline_logic_expr => { try checkInlineLogicExpr(scope, child_node); - try scope.emitSimpleInst(.content); + try scope.emitSimpleInst(.stream_push); }, else => return error.NotImplemented, } @@ -379,13 +433,6 @@ fn checkContentStmt(scope: *Scope, stmt: *const Ast.Node) CheckError!void { return checkContentExpr(scope, expr_node); } -fn checkBlockStmt(parent_scope: *Scope, block_stmt: *const Ast.Node) CheckError!void { - var block_scope = parent_scope.makeSubBlock(); - defer block_scope.deinit(); - const children = block_stmt.data.list.items orelse return; - for (children) |child_stmt| try checkStmt(&block_scope, child_stmt); -} - fn checkAssignStmt(scope: *Scope, stmt: *const Ast.Node) CheckError!void { const lhs = stmt.data.bin.lhs orelse return error.CompilerBug; const rhs = stmt.data.bin.rhs orelse return error.CompilerBug; @@ -413,9 +460,91 @@ fn checkAssignStmt(scope: *Scope, stmt: *const Ast.Node) CheckError!void { return scope.fail(.unknown_identifier, lhs); } +fn checkBlockStmt(parent_scope: *Scope, block_stmt: *const Ast.Node) CheckError!void { + var block_scope = parent_scope.makeSubBlock(); + defer block_scope.deinit(); + const children = block_stmt.data.list.items orelse return; + for (children) |child_stmt| try checkStmt(&block_scope, child_stmt); +} + +fn checkChoiceStmt(scope: *Scope, stmt_node: *const Ast.Node) CheckError!void { + const Choice = struct { + label_index: usize, + start_expression: ?*const Ast.Node, + option_expression: ?*const Ast.Node, + inner_expression: ?*const Ast.Node, + block_stmt: ?*const Ast.Node, + }; + + const branch_list = stmt_node.data.list.items orelse unreachable; + assert(branch_list.len != 0); + + const gpa = scope.global.gpa; + var choice_list: std.ArrayListUnmanaged(Choice) = .empty; + defer choice_list.deinit(gpa); + try choice_list.ensureUnusedCapacity(gpa, branch_list.len); + + for (branch_list) |branch_stmt| { + assert(branch_stmt.tag == .choice_star_stmt or branch_stmt.tag == .choice_plus_stmt); + + const branch_data = branch_stmt.data.bin; + const branch_expr = branch_data.lhs orelse unreachable; + const branch_expr_data = branch_expr.data.choice_expr; + const label_index = try scope.makeLabel(); + + if (branch_expr_data.start_expr) |node| { + try checkStringLiteral(scope, node); + try scope.emitSimpleInst(.stream_push); + } + if (branch_expr_data.option_expr) |node| { + try checkStringLiteral(scope, node); + try scope.emitSimpleInst(.stream_push); + } + + const jump_offset = try scope.emitJumpInst(.br_push); + _ = try scope.makeJump(.{ + .mode = .absolute, + .label_index = label_index, + .code_offset = jump_offset, + }); + + choice_list.appendAssumeCapacity(.{ + .label_index = label_index, + .start_expression = branch_expr_data.start_expr, + .inner_expression = branch_expr_data.inner_expr, + .option_expression = branch_expr_data.option_expr, + .block_stmt = branch_data.rhs, + }); + } + + try scope.emitSimpleInst(.br_table); + try scope.emitSimpleInst(.br_select_index); + try scope.emitSimpleInst(.br_dispatch); + + for (choice_list.items) |choice| { + scope.setLabel(choice.label_index); + + if (choice.start_expression) |expr_node| { + try checkStringLiteral(scope, expr_node); + try scope.emitSimpleInst(.stream_push); + } + if (choice.inner_expression) |expr_node| { + try checkStringLiteral(scope, expr_node); + try scope.emitSimpleInst(.stream_push); + } + + try scope.emitSimpleInst(.stream_flush); + if (choice.block_stmt) |block| { + try checkBlockStmt(scope, block); + } else { + try scope.emitSimpleInst(.exit); + } + } +} + fn checkVarDecl(scope: *Scope, decl_node: *const Ast.Node) !void { - const identifier_node = decl_node.data.bin.lhs orelse return error.Fucked; - const expr_node = decl_node.data.bin.rhs orelse return error.Fucked; + const identifier_node = decl_node.data.bin.lhs orelse return error.CompilerBug; + const expr_node = decl_node.data.bin.rhs orelse return error.CompilerBug; try checkExpr(scope, expr_node); switch (decl_node.tag) { @@ -461,6 +590,7 @@ fn checkStmt(scope: *Scope, stmt: *const Ast.Node) CheckError!void { .temp_decl => try checkVarDecl(scope, stmt), .assign_stmt => try checkAssignStmt(scope, stmt), .content_stmt => try checkContentStmt(scope, stmt), + .choice_stmt => try checkChoiceStmt(scope, stmt), .expr_stmt => try checkExprStmt(scope, stmt), else => return error.NotImplemented, } @@ -522,23 +652,18 @@ fn dumpStringsWithHex(astgen: *const AstGen) void { std.debug.print("[{d:04}] ", .{start}); for (s) |b| std.debug.print("{x:02} ", .{b}); - std.debug.print("00 {s}\n", .{s}); + std.debug.print("00: {s}\n", .{s}); start = end + 1; } } /// Perform code generation via tree-walk. pub fn generate(gpa: std.mem.Allocator, tree: *const Ast) !Story { - const root_node = tree.root orelse return error.Fucked; + const root_node = tree.root orelse return error.CompilerBug; var story: Story = .{ .allocator = gpa, .is_exited = false, .can_advance = false, - .gc_objects = .{}, - .globals = .empty, - .paths = .empty, - .stack = .empty, - .call_stack = .empty, }; var astgen: AstGen = .{ .gpa = gpa, diff --git a/src/Story.zig b/src/Story.zig index 4539e8d..8dbe0d1 100644 --- a/src/Story.zig +++ b/src/Story.zig @@ -12,12 +12,14 @@ allocator: std.mem.Allocator, dump_writer: ?*std.Io.Writer = null, is_exited: bool = false, can_advance: bool = false, -gc_objects: std.SinglyLinkedList = .{}, +choice_index: usize = 0, +current_choices: std.ArrayListUnmanaged(Choice) = .empty, globals: std.StringHashMapUnmanaged(?*Object) = .empty, paths: std.ArrayListUnmanaged(*Object) = .empty, stack: std.ArrayListUnmanaged(?*Object) = .empty, call_stack: std.ArrayListUnmanaged(CallFrame) = .empty, stack_max: usize = 128, +gc_objects: std.SinglyLinkedList = .{}, pub const CallFrame = struct { ip: usize, @@ -25,15 +27,30 @@ pub const CallFrame = struct { callee: *Object.ContentPath, }; +pub const Choice = struct { + text: std.ArrayListUnmanaged(u8), + dest_offset: u16, +}; + pub const Opcode = enum(u8) { + /// Exit the VM normally. exit, ret, + /// Pop a value off the stack, discarding it. pop, true, false, + /// Pop two values off the stack and calculate their sum. + /// The result will be pushed to the stack. add, + /// Pop two values off the stack and calculate their difference. + /// The result will be pushed to the stack. sub, + /// Pop two values off the stack and calculate their product. + /// The result will be pushed to the stack. mul, + /// Pop two values off the stack and calculate their quotient. + /// The result will be pushed to the stack. div, mod, neg, @@ -53,12 +70,16 @@ pub const Opcode = enum(u8) { store, load_global, store_global, - load_choice_id, - content, - line, - glue, - choice, - flush, + /// Pop a value off the stack and write it to the content stream. + stream_push, + stream_line, + stream_glue, + /// Flush the content stream to the story consumer. + stream_flush, + br_push, + br_table, + br_dispatch, + br_select_index, _, }; @@ -71,6 +92,7 @@ pub fn deinit(story: *Story) void { object.destroy(story); } + story.current_choices.deinit(gpa); story.globals.deinit(gpa); story.paths.deinit(gpa); story.stack.deinit(gpa); @@ -174,14 +196,17 @@ fn setGlobal(vm: *Story, key: *const Object.String, value: *Object) !void { return vm.globals.putAssumeCapacity(key_bytes, value); } -fn execute(vm: *Story, writer: *std.Io.Writer) !void { +fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { + const gpa = vm.allocator; defer { vm.can_advance = false; } - if (vm.isCallStackEmpty()) return; + if (vm.isCallStackEmpty()) return .empty; const frame = vm.currentFrame(); const code = std.mem.bytesAsSlice(Opcode, frame.callee.bytes); + var stream_writer = std.Io.Writer.Allocating.init(gpa); + defer stream_writer.deinit(); while (true) { if (vm.dump_writer) |w| { @@ -190,7 +215,7 @@ fn execute(vm: *Story, writer: *std.Io.Writer) !void { switch (code[frame.ip]) { .exit => { vm.is_exited = true; - return; + return .empty; }, .pop => { const object_top = vm.popStack(); @@ -224,16 +249,6 @@ fn execute(vm: *Story, writer: *std.Io.Writer) !void { } frame.ip += 1; }, - .content => { - const arg_object = vm.popStack(); - if (arg_object) |object| { - const string_object = try Object.String.fromObject(vm, object); - const string_bytes = string_object.bytes[0..string_object.length]; - try writer.writeAll(string_bytes); - } - - frame.ip += 1; - }, .load_const => { const index: u8 = @intFromEnum(code[frame.ip + 1]); const value = try vm.getConstant(frame, index); @@ -276,17 +291,53 @@ fn execute(vm: *Story, writer: *std.Io.Writer) !void { } frame.ip += 2; }, + .stream_push => { + const arg_object = vm.popStack(); + if (arg_object) |object| { + const string_object = try Object.String.fromObject(vm, object); + const string_bytes = string_object.bytes[0..string_object.length]; + try stream_writer.writer.writeAll(string_bytes); + } + + frame.ip += 1; + }, + .stream_flush => { + frame.ip += 1; + return stream_writer.toArrayList(); + }, + .br_push => { + const arg_offset = std.mem.bytesToValue(u16, code[frame.ip + 1 ..][0..2]); + + try vm.current_choices.append(gpa, .{ + .text = stream_writer.toArrayList(), + .dest_offset = std.mem.bigToNative(u16, arg_offset), + }); + frame.ip += 3; + }, + .br_table => { + // No-op currently. + frame.ip += 1; + }, + .br_select_index => { + vm.can_advance = false; + frame.ip += 1; + return .empty; + }, + .br_dispatch => { + const index = vm.choice_index; + const branch_dispatch = vm.current_choices.items[index]; + defer vm.current_choices.clearRetainingCapacity(); + + frame.ip = branch_dispatch.dest_offset; + }, else => return error.InvalidInstruction, } } } -pub fn advance(story: *Story) ![]const u8 { - const gpa = story.allocator; - var aw = std.Io.Writer.Allocating.init(gpa); - - try story.execute(&aw.writer); - return aw.toOwnedSlice(); +pub fn advance(story: *Story, gpa: std.mem.Allocator) ![]const u8 { + var content = try story.execute(); + return content.toOwnedSlice(gpa); } fn divert(vm: *Story, path_name: []const u8) !void { @@ -319,6 +370,12 @@ pub const LoadOptions = struct { use_color: bool = true, }; +pub fn selectChoiceIndex(story: *Story, index: usize) !void { + if (index >= story.current_choices.items.len) return error.InvalidChoice; + story.choice_index = index; + story.can_advance = true; +} + pub fn loadFromString( gpa: std.mem.Allocator, source_bytes: [:0]const u8, diff --git a/src/Story/Dumper.zig b/src/Story/Dumper.zig index 137c968..3f119d2 100644 --- a/src/Story/Dumper.zig +++ b/src/Story/Dumper.zig @@ -38,21 +38,33 @@ fn dumpGlobalInst( return offset + 2; } +const Jump = enum { + relative, + absolute, +}; + fn dumpJumpInst( d: Dumper, context: *const Object.ContentPath, offset: usize, op: Opcode, + mode: Jump, ) !usize { var jump: u16 = @as(u16, context.bytes[offset + 1]) << 8; jump |= context.bytes[offset + 2]; - try d.writer.print("{s} 0x{x:0>4} (0x{x:0>4} -> 0x{x:0>4})\n", .{ - @tagName(op), - jump, - offset, - offset + 3 + jump, - }); + switch (mode) { + .relative => try d.writer.print("{s} 0x{x:0>4} (0x{x:0>4} -> 0x{x:0>4})\n", .{ + @tagName(op), + jump, + offset, + offset + 3 + jump, + }), + .absolute => try d.writer.print("{s} 0x{x:0>4}\n", .{ + @tagName(op), + jump, + }), + } return offset + 3; } @@ -72,43 +84,41 @@ pub fn dumpInst( try d.writer.print("0x{x:0>4} | ", .{offset}); } switch (op) { - .exit, - .ret, - .pop, - .true, - .false, - .add, - .sub, - .mul, - .div, - .mod, - .neg, - .not, - .cmp_eq, - .cmp_lt, - .cmp_lte, - .cmp_gt, - .cmp_gte, - .flush, - .load_choice_id, - .content, - .choice, - .line, - .glue, - => return d.dumpSimpleInst(offset, op), - .load_const, - .load, - .store, - => return d.dumpByteInst(path, offset, op), - .load_global, - .store_global, - .call, - .divert, - => return d.dumpGlobalInst(path, offset, op), - .jmp, - .jmp_t, - .jmp_f, - => return d.dumpJumpInst(path, offset, op), + .exit => return d.dumpSimpleInst(offset, op), + .ret => return d.dumpSimpleInst(offset, op), + .pop => return d.dumpSimpleInst(offset, op), + .true => return d.dumpSimpleInst(offset, op), + .false => return d.dumpSimpleInst(offset, op), + .add => return d.dumpSimpleInst(offset, op), + .sub => return d.dumpSimpleInst(offset, op), + .mul => return d.dumpSimpleInst(offset, op), + .div => return d.dumpSimpleInst(offset, op), + .mod => return d.dumpSimpleInst(offset, op), + .neg => return d.dumpSimpleInst(offset, op), + .not => return d.dumpSimpleInst(offset, op), + .cmp_eq => return d.dumpSimpleInst(offset, op), + .cmp_lt => return d.dumpSimpleInst(offset, op), + .cmp_lte => return d.dumpSimpleInst(offset, op), + .cmp_gt => return d.dumpSimpleInst(offset, op), + .cmp_gte => return d.dumpSimpleInst(offset, op), + .load_const => return d.dumpByteInst(path, offset, op), + .load => return d.dumpByteInst(path, offset, op), + .store => return d.dumpByteInst(path, offset, op), + .load_global => return d.dumpGlobalInst(path, offset, op), + .store_global => return d.dumpGlobalInst(path, offset, op), + .call => return d.dumpGlobalInst(path, offset, op), + .divert => return d.dumpGlobalInst(path, offset, op), + .jmp => return d.dumpJumpInst(path, offset, op, .relative), + .jmp_t => return d.dumpJumpInst(path, offset, op, .relative), + .jmp_f => return d.dumpJumpInst(path, offset, op, .relative), + .stream_push => return d.dumpSimpleInst(offset, op), + .stream_flush => return d.dumpSimpleInst(offset, op), + .stream_line => return d.dumpSimpleInst(offset, op), + .stream_glue => return d.dumpSimpleInst(offset, op), + .br_push => return d.dumpJumpInst(path, offset, op, .absolute), + .br_table => return d.dumpSimpleInst(offset, op), + .br_dispatch => return d.dumpSimpleInst(offset, op), + .br_select_index => return d.dumpSimpleInst(offset, op), else => |code| { try d.writer.print("Unknown opcode 0x{x:0>4}\n", .{@intFromEnum(code)}); return offset + 1; diff --git a/src/main.zig b/src/main.zig index 5666f81..0017a9b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -73,6 +73,9 @@ fn mainArgs( }; }; + const stdin = std.fs.File.stdin(); + var stdin_reader = stdin.reader(&stdin_buffer); + const stdout = std.fs.File.stdout(); var stdout_writer = stdout.writer(&stdout_buffer); @@ -89,11 +92,26 @@ fn mainArgs( try story.dump(&stderr_writer.interface); if (compile_only) return; - while (story.can_advance) { - const content = try story.advance(); - defer gpa.free(content); + while (!story.is_exited and story.can_advance) { + while (story.can_advance) { + const content_text = try story.advance(gpa); + defer gpa.free(content_text); + std.debug.print("{s}\n", .{content_text}); + } + if (story.current_choices.items.len > 0) { + for (story.current_choices.items, 0..) |*choice, index| { + const choice_text = try choice.text.toOwnedSlice(gpa); + defer gpa.free(choice_text); + std.debug.print("[{d}]: {s}\n", .{ index + 1, choice_text }); + } + std.debug.print("> ", .{}); - std.debug.print("{s}\n", .{content}); + const input_line = try stdin_reader.interface.takeDelimiter('\n'); + if (input_line) |bytes| { + const choice_index = try std.fmt.parseUnsigned(usize, bytes, 10); + try story.selectChoiceIndex(if (choice_index == 0) 0 else choice_index - 1); + } + } } }