feat: runtime content processing, supporting glue

This commit is contained in:
Brett Broadhurst 2026-03-30 16:47:05 -06:00
parent d325cdf965
commit f5eac49729
Failed to generate hash of commit
12 changed files with 387 additions and 286 deletions

View file

@ -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;

View file

@ -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,

View file

@ -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: {

View file

@ -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 => {

View file

@ -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 = "<STDIN>",
@ -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;
}

View file

@ -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),

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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),

View file

@ -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;