diff --git a/src/AstGen.zig b/src/AstGen.zig index d709833..38c45b4 100644 --- a/src/AstGen.zig +++ b/src/AstGen.zig @@ -285,6 +285,13 @@ const GenIr = struct { return last_inst.isNoReturn(); } + fn endsWithContent(self: *GenIr) bool { + if (self.isEmpty()) return false; + const last_inst_index = self.instructions.items[self.instructions.items.len - 1]; + const last_inst = self.astgen.instructions.items[@intFromEnum(last_inst_index)]; + return last_inst.tag == .content_push; + } + fn makeSubBlock(self: *GenIr) GenIr { return .{ .astgen = self.astgen, @@ -807,7 +814,7 @@ fn inlineIfStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir. if (data.rhs) |rhs| { // TODO: Revisit this. This isn't quite correct. switch (rhs.tag) { - .content => _ = try contentExpr(&then_block, scope, rhs), + .content => _ = try content(&then_block, scope, rhs), inline else => |tag| @panic("Unexpected node type: " ++ @tagName(tag)), } } @@ -1051,8 +1058,11 @@ fn switchStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.In return switch_br.toRef(); } -fn contentExpr(block: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref { +fn content(block: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref { const data = node.data.content; + if (data.leading_glue) { + _ = try block.addUnaryNode(.content_glue, .none); + } for (data.items) |child_node| { switch (child_node.tag) { .string_literal => { @@ -1070,13 +1080,17 @@ fn contentExpr(block: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!I else => unreachable, } } + if (data.trailing_glue) { + _ = try block.addUnaryNode(.content_glue, .none); + } else if (block.endsWithContent()) { + _ = try block.addUnaryNode(.content_line, .none); + } return .none; } -fn contentStmt(gen: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref { - const expr_node = node.data.bin.lhs.?; - const expr_ref = try contentExpr(gen, scope, expr_node); - return gen.addUnaryNode(.content_flush, expr_ref); +fn contentStmt(block: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void { + const data = node.data.bin; + _ = try content(block, scope, data.lhs.?); } fn assignStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void { @@ -1133,6 +1147,9 @@ fn choiceStmt( if (branch_data.rhs) |branch_body| { _ = try blockStmt(&sub_block, scope, branch_body); } + if (!sub_block.endsWithNoReturn()) { + _ = try sub_block.addUnaryNode(.implicit_ret, .none); + } const body = sub_block.instructionsSlice(); const case_extra_len = @typeInfo(Ir.Inst.SwitchBr.Case).@"struct".fields.len + body.len; diff --git a/src/Ir.zig b/src/Ir.zig index 678e07f..a3fe6ef 100644 --- a/src/Ir.zig +++ b/src/Ir.zig @@ -202,8 +202,10 @@ pub const Inst = struct { condbr, @"break", switch_br, + /// Uses the `un` union field. content_push, - content_flush, + content_line, + content_glue, choice_br, // Uses the `un` union field. ret, @@ -373,7 +375,8 @@ pub const Inst = struct { .condbr, .switch_br, .content_push, - .content_flush, + .content_line, + .content_glue, .choice_br, .call, .divert, diff --git a/src/Parse.zig b/src/Parse.zig index c555a87..a9f04d7 100644 --- a/src/Parse.zig +++ b/src/Parse.zig @@ -923,13 +923,12 @@ fn parseLbraceExpr(p: *Parse) Error!?*Ast.Node { .end = rbrace_token.loc.end, }, lhs, null); } - _ = try p.expectToken(.colon, true); + _ = try p.expectToken(.colon, false); if (p.checkToken(.newline)) { _ = p.nextToken(); return parseConditional(p, lbrace_token, lhs); } else { - _ = p.nextToken(); return parseInlineIf(p, lbrace_token, lhs); } } @@ -977,19 +976,22 @@ fn parseContent(p: *Parse, options: ContentOptions) Error!?*Ast.Node { leading_glue = true; _ = p.nextToken(); } - while (true) { + loop: while (true) { const node: ?*Ast.Node = switch (p.token.tag) { .eof, .newline, .left_arrow, .right_brace => break, .left_brace => try parseLbraceExpr(p), .right_arrow => try parseDivertStmt(p), .glue => blk: { - const next_token = p.nextToken(); - switch (next_token.tag) { - .eof, .newline, .right_brace => { - trailing_glue = true; - break; - }, - else => break :blk null, + while (true) { + _ = p.nextToken(); + switch (p.token.tag) { + .whitespace => continue, + .eof, .newline, .right_brace => { + trailing_glue = true; + break :loop; + }, + else => break :blk null, + } } }, else => |tag| blk: { diff --git a/src/Sema.zig b/src/Sema.zig index 76a6489..913a3ee 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -697,10 +697,6 @@ fn irContentPush(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError try builder.addByteOp(.stream_push); } -fn irContentFlush(_: *Sema, builder: *Builder, _: Ir.Inst.Index) InnerError!void { - try builder.addByteOp(.stream_flush); -} - fn irChoiceBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void { const gpa = sema.gpa; const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; @@ -716,6 +712,8 @@ fn irChoiceBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!vo const case_label = try builder.addLabel(); branch_labels.appendAssumeCapacity(case_label); + try builder.addByteOp(.stream_mark); + switch (case_extra.data.operand_1) { .none => {}, else => |content| { @@ -762,8 +760,7 @@ fn irChoiceBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!vo try builder.addByteOp(.stream_push); }, } - - try builder.addByteOp(.stream_flush); + try builder.addByteOp(.stream_line); _ = try analyzeBodyInner(sema, builder, body_slice, false); } } @@ -1018,8 +1015,12 @@ fn analyzeBodyInner( try irContentPush(sema, builder, inst); continue; }, - .content_flush => { - try irContentFlush(sema, builder, inst); + .content_line => { + try builder.addByteOp(.stream_line); + continue; + }, + .content_glue => { + try builder.addByteOp(.stream_glue); continue; }, .choice_br => { diff --git a/src/Story.zig b/src/Story.zig index e66e08b..cff8700 100644 --- a/src/Story.zig +++ b/src/Story.zig @@ -9,27 +9,102 @@ const Dumper = @import("Story/Dumper.zig"); const assert = std.debug.assert; const Story = @This(); -allocator: std.mem.Allocator, -dump_writer: ?*std.Io.Writer = null, +gpa: std.mem.Allocator, +arena: std.heap.ArenaAllocator, + +// Flags is_exited: bool = false, can_advance: bool = false, -choice_index: usize = 0, + stack_top: usize = 0, call_stack_top: usize = 0, +output_marker: usize = 0, +choice_selected: ?Choice = null, +output_buffer: std.ArrayListUnmanaged(OutputCommand) = .empty, +output_scratch: std.ArrayListUnmanaged(u8) = .empty, current_choices: std.ArrayListUnmanaged(Choice) = .empty, -code_chunks: std.ArrayListUnmanaged(*Object.Code) = .empty, globals: std.StringHashMapUnmanaged(Value) = .empty, stack: []Value = &.{}, call_stack: []CallFrame = &.{}, -/// Global constants pool. -constants_pool: []const Value = &.{}, +code_chunks: std.ArrayListUnmanaged(*Object.Code) = .empty, /// Linked list of all tracked runtime objects. gc_objects: std.SinglyLinkedList = .{}, +/// Global constants pool. +constants_pool: []const Value = &.{}, // FIXME: This was a hack to keep string bytes alive. string_bytes: []const u8 = &.{}, +dump_writer: ?*std.Io.Writer = null, pub const default_knot_name: [:0]const u8 = "$__main__$"; +pub const Opcode = enum(u8) { + /// Exit the VM normally. + exit, + done, + ret, + /// Pop a value off the stack, discarding it. + pop, + /// Push an object representing the boolean value of "true" to the stack. + true, + /// Push an object representing the boolean value of "false" to the stack. + 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, + not, + cmp_eq, + cmp_neq, + cmp_lt, + cmp_gt, + cmp_lte, + cmp_gte, + /// Jump unconditionally to the target address. + jmp, + /// Jump conditionally to the target address if the boolean value at the + /// top of the stack is true. + jmp_t, + /// Jump conditionally to the target address if the boolean value at the + /// top of the stack is false. + jmp_f, + call, + divert, + load_const, + load, + store, + load_global, + store_global, + load_attr, + store_attr, + /// Pop a value off the stack and write it to the content stream. + stream_push, + stream_line, + stream_glue, + stream_mark, + br_push, + br_table, + br_dispatch, + br_select_index, + _, +}; + +pub const CallFrame = struct { + callee: *Object.Knot, + caller_top: usize, + ip: usize, + sp: usize, +}; + pub const Value = union(enum) { nil, bool: bool, @@ -237,82 +312,19 @@ pub const Value = union(enum) { } }; -pub const CallFrame = struct { - callee: *Object.Knot, - caller_top: usize, - ip: usize, - sp: usize, +pub const OutputCommand = union(enum) { + line, + glue, + value: Value, }; pub const Choice = struct { - text: std.ArrayListUnmanaged(u8), + content: []const u8, dest_offset: u16, }; -pub const Opcode = enum(u8) { - /// Exit the VM normally. - exit, - done, - ret, - /// Pop a value off the stack, discarding it. - pop, - /// Push an object representing the boolean value of "true" to the stack. - true, - /// Push an object representing the boolean value of "false" to the stack. - 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, - not, - cmp_eq, - cmp_neq, - cmp_lt, - cmp_gt, - cmp_lte, - cmp_gte, - /// Jump unconditionally to the target address. - jmp, - /// Jump conditionally to the target address if the boolean value at the - /// top of the stack is true. - jmp_t, - /// Jump conditionally to the target address if the boolean value at the - /// top of the stack is false. - jmp_f, - call, - divert, - load_const, - load, - store, - load_global, - store_global, - load_attr, - store_attr, - /// 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, - _, -}; - pub fn deinit(story: *Story) void { - const gpa = story.allocator; + const gpa = story.gpa; var next = story.gc_objects.first; while (next) |node| { const object: *Object = @alignCast(@fieldParentPtr("node", node)); @@ -320,13 +332,17 @@ pub fn deinit(story: *Story) void { object.destroy(story); } - story.current_choices.deinit(gpa); + story.arena.deinit(); story.globals.deinit(gpa); + story.current_choices.deinit(gpa); + story.output_scratch.deinit(gpa); + story.output_buffer.deinit(gpa); gpa.free(story.string_bytes); gpa.free(story.constants_pool); gpa.free(story.stack); gpa.free(story.call_stack); + story.* = undefined; } fn currentFrame(vm: *Story) *CallFrame { @@ -350,7 +366,7 @@ fn popStack(vm: *Story) ?Value { const stack_top = vm.stack_top; vm.stack_top -= 1; - return vm.stack[stack_top]; + return vm.stack[stack_top - 1]; } fn getConstant(story: *Story, frame: *CallFrame, offset: u8) !Value { @@ -398,13 +414,98 @@ fn setGlobal(vm: *Story, key: Value, value: Value) !void { } } -fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { - const gpa = vm.allocator; - if (vm.call_stack_top == 0) return .empty; - errdefer vm.can_advance = false; +pub fn getKnot(vm: *Story, name: []const u8) ?*Object.Knot { + const knot: ?*Object.Knot = blk: { + if (vm.globals.get(name)) |value| { + // TODO: Do a check here. + break :blk @ptrCast(value.object); + } + break :blk null; + }; + return knot; +} - var stream_writer = std.Io.Writer.Allocating.init(gpa); - defer stream_writer.deinit(); +fn call(vm: *Story, knot: *Object.Knot) !void { + if (vm.call_stack_top >= vm.call_stack.len) + return error.CallStackOverflow; + + const locals_count = knot.code.locals_count; + const args_count = knot.code.args_count; + const sp = vm.stack_top - args_count; + const caller_top = if (vm.call_stack_top == 0) + sp + else + sp - 1; + + const frame_top = sp + args_count + locals_count; + if (frame_top > vm.stack.len) return error.StackOverflow; + for (vm.stack[sp + args_count .. frame_top]) |*slot| slot.* = .nil; + + vm.stack_top = frame_top; + vm.call_stack[vm.call_stack_top] = .{ + .callee = knot, + .ip = 0, + .sp = sp, + .caller_top = caller_top, + }; + vm.call_stack_top += 1; +} + +// Diverts are essentially tail calls. +fn divert(vm: *Story, knot: *Object.Knot) !void { + const args_count = knot.code.args_count; + const locals_count = knot.code.locals_count; + + if (!vm.can_advance) + vm.can_advance = true; + + if (vm.call_stack_top == 0) + return vm.call(knot); + + const args_start = vm.stack_top - args_count; + const current_frame = &vm.call_stack[vm.call_stack_top - 1]; + const sp = current_frame.sp; + const caller_top = current_frame.caller_top; + + if (args_count > 0) { + std.mem.copyForwards( + Value, + vm.stack[sp .. sp + args_count], + vm.stack[args_start .. args_start + args_count], + ); + } + + const frame_top = sp + args_count + locals_count; + if (frame_top > vm.stack.len) return error.StackOverflow; + + for (vm.stack[sp + args_count .. frame_top]) |*slot| slot.* = .nil; + vm.stack_top = frame_top; + + current_frame.* = .{ + .callee = knot, + .ip = 0, + .sp = sp, + .caller_top = caller_top, + }; +} + +fn readAddress(code: []const Story.Opcode, offset: usize) u16 { + const arg_offset = std.mem.bytesToValue(u16, code[offset + 1 ..][0..2]); + return std.mem.bigToNative(u16, arg_offset); +} + +const StepSignal = union(enum) { + exit, + done, + choices_ready, +}; + +fn step(vm: *Story) !StepSignal { + assert(vm.call_stack_top > 0); + assert(vm.can_advance == true); + + const gpa = vm.gpa; + const arena = vm.arena.allocator(); var frame = vm.currentFrame(); while (true) { @@ -413,15 +514,8 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { Dumper.trace(vm, w, frame) catch {}; } switch (code[frame.ip]) { - .exit => { - vm.is_exited = true; - vm.can_advance = false; - return .empty; - }, - .done => { - vm.can_advance = false; - return .empty; - }, + .exit => return .exit, + .done => return .done, .ret => { const return_value = vm.stack[vm.stack_top - 1]; @@ -429,14 +523,16 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { const completed_frame = vm.call_stack[vm.call_stack_top]; vm.stack_top = completed_frame.caller_top; - vm.stack[vm.stack_top] = return_value; vm.stack_top += 1; if (vm.call_stack_top == 0) return error.UnexpectedReturn; - frame = &vm.call_stack[vm.call_stack_top - 1]; }, + .pop => { + if (vm.popStack()) |_| {} else return error.InvalidArgument; + frame.ip += 1; + }, .true => { try vm.pushStack(.{ .bool = true }); frame.ip += 1; @@ -445,10 +541,6 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { try vm.pushStack(.{ .bool = false }); frame.ip += 1; }, - .pop => { - if (vm.popStack()) |_| {} else return error.InvalidArgument; - frame.ip += 1; - }, .add => { const lhs = vm.peekStack(1) orelse return error.Bugged; const rhs = vm.peekStack(0) orelse return error.Bugged; @@ -551,7 +643,6 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { return error.InvalidArgument; } - // Re-fetch — we're now in the callee's frame frame = &vm.call_stack[vm.call_stack_top - 1]; }, .divert => { @@ -593,6 +684,27 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { } frame.ip += 2; }, + .load_attr => { + const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]); + + if (peekStack(vm, 0)) |value| { + const knot_object: *Object.Knot = @ptrCast(value.object); + const arg_value = try vm.getConstant(frame, arg_offset); + const knot_attr: *Object.String = @ptrCast(arg_value.object); + + _ = popStack(vm); + + if (knot_object.members.get(knot_attr.toSlice())) |attr_object| { + try vm.pushStack(.{ .object = attr_object }); + } else { + return error.InvalidArgument; + } + } else { + return error.InvalidArgument; + } + frame.ip += 2; + }, + // TODO: store_attr .load_global => { const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]); const global_name = try vm.getConstant(frame, arg_offset); @@ -614,72 +726,48 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { frame.ip += 2; }, .stream_push => { - // FIXME: This should be more strict. - // Its not because theres a bug in when these instructions are - // emitted. - if (vm.peekStack(0)) |arg| { - const str_object = try Object.String.fromValue(vm, arg); - try stream_writer.writer.writeAll(str_object.toSlice()); - _ = vm.popStack(); - } //else { - //return error.InvalidArgument; - //} - + // TODO: Make this more strict. + if (popStack(vm)) |value| { + try vm.output_buffer.append(gpa, .{ .value = value }); + } frame.ip += 1; }, - .stream_flush => { + .stream_line => { + try vm.output_buffer.append(gpa, .line); + frame.ip += 1; + }, + .stream_glue => { + try vm.output_buffer.append(gpa, .glue); + frame.ip += 1; + }, + .stream_mark => { + vm.output_marker = vm.output_buffer.items.len; frame.ip += 1; - - // FIXME: This is a bit of a hack, but we have to deal with this right now. - const buffered = stream_writer.writer.buffered(); - if (buffered.len == 0) continue; - return stream_writer.toArrayList(); }, .br_push => { + const marker = vm.output_marker; const arg_offset = readAddress(code, frame.ip); + const output_stream = vm.output_buffer.items[marker..]; + const choice_display = try resolveOutputStream(vm, arena, output_stream); + defer vm.output_buffer.shrinkRetainingCapacity(marker); try vm.current_choices.append(gpa, .{ - .text = stream_writer.toArrayList(), + .content = choice_display, .dest_offset = arg_offset, }); + frame.ip += 3; }, .br_table => { frame.ip += 1; }, .br_select_index => { - vm.can_advance = false; frame.ip += 1; - return .empty; + return .choices_ready; }, .br_dispatch => { - const index = vm.choice_index; - const branch_dispatch = vm.current_choices.items[index]; - defer { - for (vm.current_choices.items) |*choice| { - choice.text.deinit(gpa); - } - vm.current_choices.clearRetainingCapacity(); - } - - frame.ip = branch_dispatch.dest_offset; - }, - .load_attr => { - const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]); - frame.ip += 2; - - if (peekStack(vm, 0)) |value| { - const knot_object: *Object.Knot = @ptrCast(value.object); - const arg_value = try vm.getConstant(frame, arg_offset); - const knot_attr: *Object.String = @ptrCast(arg_value.object); - - _ = popStack(vm); - - if (knot_object.members.get(knot_attr.toSlice())) |attr_object| { - try vm.pushStack(.{ .object = attr_object }); - } else { - return error.InvalidArgument; - } + if (vm.choice_selected) |choice| { + frame.ip = choice.dest_offset; } else { return error.InvalidArgument; } @@ -689,83 +777,68 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { } } -fn readAddress(code: []const Story.Opcode, offset: usize) u16 { - const arg_offset = std.mem.bytesToValue(u16, code[offset + 1 ..][0..2]); - return std.mem.bigToNative(u16, arg_offset); -} +fn resolveOutputStream( + story: *Story, + gpa: std.mem.Allocator, + stream: []const OutputCommand, +) ![]const u8 { + var pending_newline = false; + var result: std.ArrayListUnmanaged(u8) = .empty; + defer result.deinit(gpa); -pub fn advance(story: *Story, gpa: std.mem.Allocator) ![]const u8 { - var content = try story.execute(); - return content.toOwnedSlice(gpa); -} - -pub fn getKnot(vm: *Story, name: []const u8) ?*Object.Knot { - const knot: ?*Object.Knot = blk: { - if (vm.globals.get(name)) |value| { - // TODO: Do a check here. - break :blk @ptrCast(value.object); + for (stream) |cmd| { + switch (cmd) { + .value => |value| { + if (pending_newline) { + pending_newline = false; + try result.append(gpa, '\n'); + } + const str = try Object.String.fromValue(story, value); + try result.appendSlice(gpa, str.toSlice()); + }, + .line => { + if (result.items.len > 0) + pending_newline = true; + }, + .glue => pending_newline = false, } - break :blk null; - }; - return knot; -} - -fn call(vm: *Story, knot: *Object.Knot) !void { - if (vm.call_stack_top >= vm.call_stack.len) - return error.CallStackOverflow; - - const locals_count = knot.code.locals_count; - const args_count = knot.code.args_count; - const sp = vm.stack_top - args_count; - const caller_top = if (vm.call_stack_top == 0) - sp - else - sp - 1; - - const frame_top = sp + args_count + locals_count; - if (frame_top > vm.stack.len) return error.StackOverflow; - for (vm.stack[sp + args_count .. frame_top]) |*slot| slot.* = .nil; - - vm.stack_top = frame_top; - vm.call_stack[vm.call_stack_top] = .{ - .callee = knot, - .ip = 0, - .sp = sp, - .caller_top = caller_top, - }; - vm.call_stack_top += 1; -} - -// Diverts are essentially tail calls. -fn divert(vm: *Story, knot: *Object.Knot) !void { - const args_count = knot.code.args_count; - const locals_count = knot.code.locals_count; - if (vm.call_stack_top == 0) return vm.call(knot); - - const args_start = vm.stack_top - args_count; - const current_frame = &vm.call_stack[vm.call_stack_top - 1]; - const sp = current_frame.sp; - const caller_top = current_frame.caller_top; - - if (args_count > 0) { - std.mem.copyForwards( - Value, - vm.stack[sp .. sp + args_count], - vm.stack[args_start .. args_start + args_count], - ); } + return result.toOwnedSlice(gpa); +} - const frame_top = sp + args_count + locals_count; - if (frame_top > vm.stack.len) return error.StackOverflow; - for (vm.stack[sp + args_count .. frame_top]) |*slot| slot.* = .nil; - vm.stack_top = frame_top; +pub fn advance(story: *Story) !?[]const u8 { + const arena = story.arena.allocator(); + const output_buffer = &story.output_buffer; + const output_scratch = &story.output_scratch; - current_frame.* = .{ - .callee = knot, - .ip = 0, - .sp = sp, - .caller_top = caller_top, - }; + output_buffer.clearRetainingCapacity(); + output_scratch.clearRetainingCapacity(); + + if (!story.can_advance) return null; + while (story.can_advance) { + const signal = try story.step(); + switch (signal) { + .exit => { + story.is_exited = true; + break; + }, + .done, .choices_ready => break, + } + } + story.can_advance = false; + return try resolveOutputStream(story, arena, output_buffer.items[0..]); +} + +pub fn selectChoiceIndex(story: *Story, index: usize) !void { + const choices = &story.current_choices; + if (index >= choices.items.len) + return error.InvalidChoice; + + story.choice_selected = choices.items[index]; + story.can_advance = true; + story.output_marker = 0; + story.current_choices.clearRetainingCapacity(); + _ = story.arena.reset(.retain_capacity); } pub const LoadOptions = struct { @@ -777,12 +850,6 @@ pub const LoadOptions = struct { dump_trace: bool = false, }; -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 dump(story: *Story, writer: *std.Io.Writer) !void { return Dumper.dump(story, writer); } @@ -796,6 +863,7 @@ pub fn loadFromString( defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + var comp = try Module.compile(gpa, arena, .{ .source_bytes = source_bytes, .filename = "", @@ -813,6 +881,7 @@ pub fn loadFromString( return error.LoadFailed; } + // TODO: Make this configureable. const stack_size = 128; const eval_stack_ptr = try gpa.alloc(Value, stack_size); errdefer gpa.free(eval_stack_ptr); @@ -821,7 +890,8 @@ pub fn loadFromString( errdefer gpa.free(call_stack_ptr); var story: Story = .{ - .allocator = gpa, + .gpa = gpa, + .arena = .init(gpa), .can_advance = false, .dump_writer = if (options.dump_trace) options.dump_writer else null, .stack = eval_stack_ptr, @@ -832,7 +902,6 @@ pub fn loadFromString( try comp.setupStoryRuntime(gpa, &story); if (story.getKnot(Story.default_knot_name)) |knot| { try story.divert(knot); - story.can_advance = true; } return story; } diff --git a/src/Story/Dumper.zig b/src/Story/Dumper.zig index 0f97b82..564d034 100644 --- a/src/Story/Dumper.zig +++ b/src/Story/Dumper.zig @@ -154,9 +154,9 @@ pub fn dumpInst( .load_attr => return self.dumpGlobalInst(w, knot, offset, op), .store_attr => return self.dumpGlobalInst(w, knot, offset, op), .stream_push => return self.dumpSimpleInst(w, offset, op), - .stream_flush => return self.dumpSimpleInst(w, offset, op), .stream_line => return self.dumpSimpleInst(w, offset, op), .stream_glue => return self.dumpSimpleInst(w, offset, op), + .stream_mark => return self.dumpSimpleInst(w, offset, op), .br_push => return self.dumpJumpInst(w, knot, offset, op, .absolute), .br_table => return self.dumpSimpleInst(w, offset, op), .br_dispatch => return self.dumpSimpleInst(w, offset, op), diff --git a/src/Story/Object.zig b/src/Story/Object.zig index bb54df8..72d1099 100644 --- a/src/Story/Object.zig +++ b/src/Story/Object.zig @@ -71,7 +71,7 @@ pub const String = struct { }; pub fn create(story: *Story, options: Options) error{OutOfMemory}!*Object.String { - const gpa = story.allocator; + const gpa = story.gpa; const alloc_len = @sizeOf(Type) + options.bytes.len + 1; const raw = try gpa.alignedAlloc(u8, .of(Type), alloc_len); const object: *Type = @ptrCast(raw); @@ -94,7 +94,7 @@ pub const String = struct { } pub fn destroy(obj: *String, story: *Story) void { - const gpa = story.allocator; + const gpa = story.gpa; const alloc_len = @sizeOf(Type) + obj.length + 1; const base: [*]align(@alignOf(Type)) u8 = @ptrCast(obj); gpa.free(base[0..alloc_len]); @@ -120,7 +120,7 @@ pub const String = struct { } pub fn concat(story: *Story, lhs: *String, rhs: *String) !*Object.String { - const gpa = story.allocator; + const gpa = story.gpa; const length = lhs.length + rhs.length; const bytes = try gpa.alloc(u8, length + 1); defer gpa.free(bytes); @@ -158,7 +158,7 @@ pub const Code = struct { const Type = Code; pub fn create(story: *Story, options: Options) error{OutOfMemory}!*Object.Code { - const gpa = story.allocator; + const gpa = story.gpa; const raw = try gpa.alignedAlloc(u8, .of(Type), @sizeOf(Type)); const obj: *Type = @ptrCast(raw); @@ -176,7 +176,7 @@ pub const Code = struct { } pub fn destroy(obj: *Code, story: *Story) void { - const gpa = story.allocator; + const gpa = story.gpa; gpa.free(obj.constants); gpa.free(obj.bytecode); @@ -201,7 +201,7 @@ pub const Knot = struct { const Type = Knot; pub fn create(story: *Story, options: Options) error{OutOfMemory}!*Object.Knot { - const gpa = story.allocator; + const gpa = story.gpa; const raw = try gpa.alignedAlloc(u8, .of(Type), @sizeOf(Type)); const obj: *Type = @ptrCast(raw); @@ -219,7 +219,7 @@ pub const Knot = struct { } pub fn destroy(obj: *Knot, story: *Story) void { - const gpa = story.allocator; + const gpa = story.gpa; obj.members.deinit(gpa); const alloc_len = @sizeOf(Type); diff --git a/src/Story/runtime_tests.zig b/src/Story/runtime_tests.zig index c7746ba..2d0c866 100644 --- a/src/Story/runtime_tests.zig +++ b/src/Story/runtime_tests.zig @@ -183,29 +183,24 @@ fn testRunner(gpa: std.mem.Allocator, source_bytes: [:0]const u8, options: Optio defer story.deinit(); while (!story.is_exited and story.can_advance) { - while (story.can_advance) { - const content_text = try story.advance(gpa); - defer gpa.free(content_text); - if (content_text.len != 0) { - try io_w.print("{s}\n", .{content_text}); - } + while (try story.advance()) |content| { + try io_w.print("{s}\n", .{content}); } if (story.current_choices.items.len > 0) { for (story.current_choices.items, 0..) |*choice, index| { - try io_w.print("{d}: {s}\n", .{ index + 1, choice.text.items }); + try io_w.print("{d}: {s}\n", .{ index + 1, choice.content }); } try io_w.print("?> ", .{}); const input_line = try io_r.takeDelimiter('\n'); if (input_line) |bytes| { - const parsed_choice_index = try std.fmt.parseUnsigned(usize, bytes, 10); - const choice_index = if (parsed_choice_index == 0) 0 else parsed_choice_index - 1; // TODO: Seems like Ink proof wants to check the option text, not the actually // rendered text. //const result_text = story.current_choices.items[choice_index]; //try io_w.print("{s}\n", .{result_text.text.items}); - try story.selectChoiceIndex(choice_index); + const index = try std.fmt.parseUnsigned(usize, bytes, 10); + try story.selectChoiceIndex(if (index > 0) index - 1 else index); } } } diff --git a/src/compile.zig b/src/compile.zig index 7460495..a079bfb 100644 --- a/src/compile.zig +++ b/src/compile.zig @@ -543,6 +543,7 @@ pub const Module = struct { pub fn deinit(mod: *Module) void { const gpa = mod.gpa; + mod.tree.deinit(gpa); mod.ir.deinit(gpa); mod.intern_pool.deinit(gpa); mod.globals.deinit(gpa); diff --git a/src/main.zig b/src/main.zig index 1ce11d1..27cb5ca 100644 --- a/src/main.zig +++ b/src/main.zig @@ -108,28 +108,29 @@ fn mainArgs( return if (!compile_only) run(gpa, &story); } -fn run(gpa: std.mem.Allocator, story: *Story) !void { +fn run(_: std.mem.Allocator, story: *Story) !void { const stdin = std.fs.File.stdin(); var stdin_reader = stdin.reader(&stdin_buffer); + const stdout = std.fs.File.stdin(); + var stdout_writer = stdout.writer(&stdout_buffer); + const reader = &stdin_reader.interface; + const writer = &stdout_writer.interface; 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}); + while (try story.advance()) |content| { + try writer.print("{s}\n", .{content}); + try writer.flush(); } 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 }); + for (story.current_choices.items, 0..) |choice, index| { + try writer.print("[{d}]: {s}\n", .{ index + 1, choice.content }); } - std.debug.print("> ", .{}); + try writer.writeAll("> "); + try writer.flush(); - 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); + if (try reader.takeDelimiter('\n')) |bytes| { + const index = try std.fmt.parseUnsigned(usize, bytes, 10); + try story.selectChoiceIndex(if (index > 0) index - 1 else index); } } } diff --git a/src/print_ir.zig b/src/print_ir.zig index 698dd22..8e74107 100644 --- a/src/print_ir.zig +++ b/src/print_ir.zig @@ -324,7 +324,8 @@ pub const Writer = struct { .float => try self.writeFloatInst(w, inst), .str => try self.writeStringInst(w, inst), .content_push => try self.writeUnaryInst(w, inst), - .content_flush => try self.writeUnaryInst(w, inst), + .content_line => try self.writeUnaryInst(w, inst), + .content_glue => try self.writeUnaryInst(w, inst), .choice_br => try self.writeChoiceBrInst(w, inst), .ret => try self.writeUnaryInst(w, inst), .implicit_ret => try self.writeUnaryInst(w, inst), diff --git a/src/tokenizer.zig b/src/tokenizer.zig index e09612d..588f8b5 100644 --- a/src/tokenizer.zig +++ b/src/tokenizer.zig @@ -119,6 +119,7 @@ pub const Tokenizer = struct { slash, equal, bang, + pipe, less_than, greater_than, word, @@ -224,7 +225,10 @@ pub const Tokenizer = struct { }, '|' => { self.index += 1; - result.tag = .pipe; + switch (grammar) { + .content => result.tag = .pipe, + .expression => continue :state .pipe, + } }, '(' => { self.index += 1; @@ -280,6 +284,13 @@ pub const Tokenizer = struct { } }, }, + .pipe => switch (self.buffer[self.index]) { + '|' => { + self.index += 1; + result.tag = .pipe_pipe; + }, + else => result.tag = .pipe, + }, .minus => switch (self.buffer[self.index]) { '>' => { self.index += 1;