feat: runtime content processing, supporting glue
This commit is contained in:
parent
d325cdf965
commit
f5eac49729
12 changed files with 387 additions and 286 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
17
src/Sema.zig
17
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 => {
|
||||
|
|
|
|||
521
src/Story.zig
521
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 = "<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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
29
src/main.zig
29
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue