fix: content behavior bugs

This commit is contained in:
Brett Broadhurst 2026-03-29 06:21:53 -06:00
parent 2260ccda25
commit 11d99fba38
Failed to generate hash of commit
59 changed files with 243 additions and 31 deletions

View file

@ -1224,7 +1224,34 @@ fn divertExpr(gi: *GenIr, scope: *Scope, node: *const Ast.Node) !void {
// FIXME: Oh God, the AST is completely fucked for this. // FIXME: Oh God, the AST is completely fucked for this.
const lhs = node.data.bin.lhs.?; const lhs = node.data.bin.lhs.?;
switch (lhs.tag) { switch (lhs.tag) {
.identifier, .selector_expr => { .identifier => {
// TODO: Revisit this
const str_slice = gi.astgen.tree.nodeSlice(lhs);
if (std.mem.eql(u8, str_slice, "DONE")) {
_ = try gi.addUnaryNode(.done, .none);
return;
} else if (std.mem.eql(u8, str_slice, "END")) {
_ = try gi.addUnaryNode(.exit, .none);
return;
}
const callee = try calleeExpr(gi, scope, lhs);
switch (callee) {
.direct => |callee_obj| {
_ = try gi.addPayloadNode(.divert, lhs, Ir.Inst.Call{
.callee = callee_obj,
.args_len = 0,
});
},
.field => |callee_field| {
_ = try gi.addPayloadNode(.field_divert, lhs, Ir.Inst.FieldCall{
.obj_ptr = callee_field.obj_ptr,
.field_name_start = callee_field.field_name_start,
.args_len = 0,
});
},
}
},
.selector_expr => {
const callee = try calleeExpr(gi, scope, lhs); const callee = try calleeExpr(gi, scope, lhs);
switch (callee) { switch (callee) {
.direct => |callee_obj| { .direct => |callee_obj| {
@ -1310,7 +1337,7 @@ fn blockInner(gi: *GenIr, parent_scope: *Scope, stmt_list: []*Ast.Node) !void {
.choice_stmt => try choiceStmt(gi, &child_scope, inner_node), .choice_stmt => try choiceStmt(gi, &child_scope, inner_node),
.expr_stmt => try exprStmt(gi, &child_scope, inner_node), .expr_stmt => try exprStmt(gi, &child_scope, inner_node),
.divert_stmt => try divertStmt(gi, &child_scope, inner_node), .divert_stmt => try divertStmt(gi, &child_scope, inner_node),
else => unreachable, inline else => |e| @panic("Unexpected node: " ++ @tagName(e)),
}; };
} }
_ = try gi.addUnaryNode(.implicit_ret, .none); _ = try gi.addUnaryNode(.implicit_ret, .none);

View file

@ -206,6 +206,8 @@ pub const Inst = struct {
field_call, field_call,
field_divert, field_divert,
param, param,
done,
exit,
}; };
pub const Data = union { pub const Data = union {

View file

@ -24,6 +24,7 @@ const InnerError = error{
pub const ValueInfo = union(enum) { pub const ValueInfo = union(enum) {
none, none,
stack,
value: InternPool.Index, value: InternPool.Index,
variable: InternPool.Index, variable: InternPool.Index,
knot: InternPool.Index, knot: InternPool.Index,
@ -288,6 +289,7 @@ pub const Builder = struct {
fn ensureLoad(self: *Builder, info: ValueInfo) InnerError!void { fn ensureLoad(self: *Builder, info: ValueInfo) InnerError!void {
switch (info) { switch (info) {
.none => unreachable, // caller should never load .none .none => unreachable, // caller should never load .none
.stack => {},
.value => |index| { .value => |index| {
const local_index = try self.getOrPutConstantIndex(index); const local_index = try self.getOrPutConstantIndex(index);
try self.addConstOp(.load_const, @intCast(local_index)); try self.addConstOp(.load_const, @intCast(local_index));
@ -451,7 +453,7 @@ fn irUnaryOp(
try builder.ensureLoad(lhs); try builder.ensureLoad(lhs);
try builder.addByteOp(op); try builder.addByteOp(op);
return .none; return .stack;
} }
fn irBinaryOp( fn irBinaryOp(
@ -485,7 +487,7 @@ fn irBinaryOp(
try builder.ensureLoad(lhs); try builder.ensureLoad(lhs);
try builder.ensureLoad(rhs); try builder.ensureLoad(rhs);
try builder.addByteOp(op); try builder.addByteOp(op);
return .none; return .stack;
} }
fn irLogicalOp( fn irLogicalOp(
@ -567,8 +569,16 @@ fn irStore(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void
try builder.ensureLoad(rhs); try builder.ensureLoad(rhs);
switch (lhs) { switch (lhs) {
.none => unreachable,
.stack => {},
.value => |_| return sema.fail(src, "could not assign to constant value", .{}), .value => |_| return sema.fail(src, "could not assign to constant value", .{}),
else => unreachable, .variable => |index| {
const local_index = try builder.getOrPutConstantIndex(index);
try builder.addConstOp(.store_global, @intCast(local_index));
},
.temp => |temp| try builder.addConstOp(.store, @intCast(temp)),
.stitch => |_| return sema.fail(src, "could not assign to stitch", .{}),
.knot => |_| return sema.fail(src, "could not assign to knot", .{}),
} }
try builder.addByteOp(.pop); try builder.addByteOp(.pop);
@ -577,8 +587,10 @@ fn irStore(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void
fn irLoad(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!ValueInfo { fn irLoad(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!ValueInfo {
const data = sema.ir.instructions[@intFromEnum(inst)].data.un; const data = sema.ir.instructions[@intFromEnum(inst)].data.un;
const lhs = sema.resolveInst(data.lhs); const lhs = sema.resolveInst(data.lhs);
if (lhs == .value) return lhs;
try builder.ensureLoad(lhs); try builder.ensureLoad(lhs);
return .none; return .stack;
} }
fn irCondBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!ValueInfo { fn irCondBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!ValueInfo {
@ -670,9 +682,8 @@ fn irSwitchBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!vo
fn irContentPush(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void { fn irContentPush(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void {
const data = sema.ir.instructions[@intFromEnum(inst)].data.un; const data = sema.ir.instructions[@intFromEnum(inst)].data.un;
const lhs = sema.resolveInst(data.lhs); const lhs = sema.resolveInst(data.lhs);
if (lhs != .none) { if (lhs == .none) return error.AnalysisFail;
try builder.ensureLoad(lhs); if (lhs != .stack) try builder.ensureLoad(lhs);
}
try builder.addByteOp(.stream_push); try builder.addByteOp(.stream_push);
} }
@ -820,6 +831,16 @@ fn irParam(_: *Sema, builder: *Builder, _: Ir.Inst.Index) !ValueInfo {
return .{ .temp = builder.addParameter() }; return .{ .temp = builder.addParameter() };
} }
fn irDone(_: *Sema, builder: *Builder, _: Ir.Inst.Index) !ValueInfo {
try builder.addByteOp(.done);
return .none;
}
fn irExit(_: *Sema, builder: *Builder, _: Ir.Inst.Index) !ValueInfo {
try builder.addByteOp(.exit);
return .none;
}
fn analyzeArithmeticArg( fn analyzeArithmeticArg(
sema: *Sema, sema: *Sema,
builder: *Builder, builder: *Builder,
@ -939,6 +960,8 @@ fn analyzeBodyInner(
}, },
.field_ptr => try irFieldPtr(sema, builder, inst), .field_ptr => try irFieldPtr(sema, builder, inst),
.param => try irParam(sema, builder, inst), .param => try irParam(sema, builder, inst),
.done => try irDone(sema, builder, inst),
.exit => try irExit(sema, builder, inst),
}; };
try sema.inst_map.put(sema.gpa, inst, result); try sema.inst_map.put(sema.gpa, inst, result);
} }

View file

@ -235,6 +235,7 @@ pub const Choice = struct {
pub const Opcode = enum(u8) { pub const Opcode = enum(u8) {
/// Exit the VM normally. /// Exit the VM normally.
exit, exit,
done,
ret, ret,
/// Pop a value off the stack, discarding it. /// Pop a value off the stack, discarding it.
pop, pop,
@ -395,9 +396,8 @@ fn setGlobal(vm: *Story, key: Value, value: Value) !void {
fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
const gpa = vm.allocator; const gpa = vm.allocator;
errdefer { errdefer vm.can_advance = false;
vm.can_advance = false;
}
if (vm.isCallStackEmpty()) return .empty; if (vm.isCallStackEmpty()) return .empty;
var stream_writer = std.Io.Writer.Allocating.init(gpa); var stream_writer = std.Io.Writer.Allocating.init(gpa);
@ -415,6 +415,10 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
vm.can_advance = false; vm.can_advance = false;
return .empty; return .empty;
}, },
.done => {
vm.can_advance = false;
return .empty;
},
.true => { .true => {
const value: Value = .{ .bool = true }; const value: Value = .{ .bool = true };
try vm.pushStack(value); try vm.pushStack(value);
@ -588,7 +592,6 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
frame.ip += 3; frame.ip += 3;
}, },
.br_table => { .br_table => {
// No-op currently.
frame.ip += 1; frame.ip += 1;
}, },
.br_select_index => { .br_select_index => {
@ -599,7 +602,12 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
.br_dispatch => { .br_dispatch => {
const index = vm.choice_index; const index = vm.choice_index;
const branch_dispatch = vm.current_choices.items[index]; const branch_dispatch = vm.current_choices.items[index];
defer vm.current_choices.clearRetainingCapacity(); defer {
for (vm.current_choices.items) |*choice| {
choice.text.deinit(gpa);
}
vm.current_choices.clearRetainingCapacity();
}
frame.ip = branch_dispatch.dest_offset; frame.ip = branch_dispatch.dest_offset;
}, },

View file

@ -129,6 +129,7 @@ pub fn dumpInst(
} }
switch (op) { switch (op) {
.exit => return self.dumpSimpleInst(w, offset, op), .exit => return self.dumpSimpleInst(w, offset, op),
.done => return self.dumpSimpleInst(w, offset, op),
.ret => return self.dumpSimpleInst(w, offset, op), .ret => return self.dumpSimpleInst(w, offset, op),
.pop => return self.dumpSimpleInst(w, offset, op), .pop => return self.dumpSimpleInst(w, offset, op),
.true => return self.dumpSimpleInst(w, offset, op), .true => return self.dumpSimpleInst(w, offset, op),

View file

@ -2,14 +2,6 @@ const std = @import("std");
const fatal = std.process.fatal; const fatal = std.process.fatal;
const ink = @import("../root.zig"); const ink = @import("../root.zig");
test "fixture - hello world" {
try testRuntimeFixture("hello-world");
}
test "fixture - monsieur-fogg" {
try testRuntimeFixture("monsieur-fogg");
}
test "fixture - variable arithmetic" { test "fixture - variable arithmetic" {
try testRuntimeFixture("variable-arithmetic"); try testRuntimeFixture("variable-arithmetic");
} }
@ -18,6 +10,75 @@ test "fixture - constant folding" {
try testRuntimeFixture("constant-folding"); try testRuntimeFixture("constant-folding");
} }
test "fixture - I001 (Minimal story)" {
try testRuntimeFixture("I001");
}
test "fixture - I002 (Fogg comforts Passepartout)" {
try testRuntimeFixture("I002");
}
test "fixture - I005 (Const variable)" {
try testRuntimeFixture("I005");
}
test "fixture - I007 (Set non existant variable)" {
try testRuntimeFixture("I007");
}
test "fixture - I011 (Temporaries at global scope)" {
try testRuntimeFixture("I011");
}
// TODO: This needs gather points.
//test "fixture - I012 (Variable declaration in conditional)" {
// try testRuntimeFixture("I012");
//}
test "fixture - I016 (Empty)" {
try testRuntimeFixture("I016");
}
test "fixture - I017 (End)" {
try testRuntimeFixture("I017");
}
test "fixture - I018 (End, the return of the end)" {
try testRuntimeFixture("I018");
}
test "fixture - I019 (End of content)" {
try testRuntimeFixture("I019");
}
test "fixture - I021 (Identifiers can start with numbers)" {
try testRuntimeFixture("I021");
}
test "fixture - I023 (Whitespace)" {
try testRuntimeFixture("I023");
}
test "fixture - I035 (Newline consistency, the third)" {
try testRuntimeFixture("I035");
}
test "fixture - I042 (Weave options)" {
try testRuntimeFixture("I042");
}
test "fixture - I051 (String constants)" {
try testRuntimeFixture("I051");
}
test "fixture - I064 (Done stops thread)" {
try testRuntimeFixture("I064");
}
test "fixture - I078 (Choice with brackets only)" {
try testRuntimeFixture("I078");
}
test "fixture - I118 (Literal unary)" { test "fixture - I118 (Literal unary)" {
try testRuntimeFixture("I118"); try testRuntimeFixture("I118");
} }
@ -62,16 +123,20 @@ fn testRunner(gpa: std.mem.Allocator, source_bytes: [:0]const u8, options: Optio
} }
if (story.current_choices.items.len > 0) { if (story.current_choices.items.len > 0) {
for (story.current_choices.items, 0..) |*choice, index| { for (story.current_choices.items, 0..) |*choice, index| {
const choice_text = try choice.text.toOwnedSlice(gpa); try io_w.print("{d}: {s}\n", .{ index + 1, choice.text.items });
defer gpa.free(choice_text);
try io_w.print("{d}: {s}\n", .{ index + 1, choice_text });
} }
try io_w.print("?> ", .{}); try io_w.print("?> ", .{});
const input_line = try io_r.takeDelimiter('\n'); const input_line = try io_r.takeDelimiter('\n');
if (input_line) |bytes| { if (input_line) |bytes| {
const choice_index = try std.fmt.parseUnsigned(usize, bytes, 10); const parsed_choice_index = try std.fmt.parseUnsigned(usize, bytes, 10);
try story.selectChoiceIndex(if (choice_index == 0) 0 else choice_index - 1); 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);
} }
} }
} }

0
src/Story/testdata/I005/input.txt vendored Normal file
View file

3
src/Story/testdata/I005/story.ink vendored Normal file
View file

@ -0,0 +1,3 @@
VAR x = c
CONST c = 5
{x}

View file

@ -0,0 +1 @@
5

0
src/Story/testdata/I007/input.txt vendored Normal file
View file

2
src/Story/testdata/I007/story.ink vendored Normal file
View file

@ -0,0 +1,2 @@
VAR x = "world"
Hello {x}.

View file

@ -0,0 +1 @@
Hello world.

0
src/Story/testdata/I011/input.txt vendored Normal file
View file

3
src/Story/testdata/I011/story.ink vendored Normal file
View file

@ -0,0 +1,3 @@
VAR x = 5
~ temp y = 4
{x}{y}

View file

@ -0,0 +1 @@
54

0
src/Story/testdata/I012/input.txt vendored Normal file
View file

5
src/Story/testdata/I012/story.ink vendored Normal file
View file

@ -0,0 +1,5 @@
VAR x = 0
{true:
- ~ x = 5
}
{x}

View file

@ -0,0 +1 @@
5

0
src/Story/testdata/I016/input.txt vendored Normal file
View file

0
src/Story/testdata/I016/story.ink vendored Normal file
View file

View file

0
src/Story/testdata/I017/input.txt vendored Normal file
View file

4
src/Story/testdata/I017/story.ink vendored Normal file
View file

@ -0,0 +1,4 @@
hello
-> END
world
-> END

View file

@ -0,0 +1 @@
hello

0
src/Story/testdata/I018/input.txt vendored Normal file
View file

6
src/Story/testdata/I018/story.ink vendored Normal file
View file

@ -0,0 +1,6 @@
-> test
== test ==
hello
-> END
world
-> END

View file

@ -0,0 +1 @@
hello

0
src/Story/testdata/I019/input.txt vendored Normal file
View file

3
src/Story/testdata/I019/story.ink vendored Normal file
View file

@ -0,0 +1,3 @@
== test ==
Content
-> END

View file

0
src/Story/testdata/I021/input.txt vendored Normal file
View file

7
src/Story/testdata/I021/story.ink vendored Normal file
View file

@ -0,0 +1,7 @@
-> 2tests
== 2tests ==
~ temp 512x2 = 512 * 2
~ temp 512x2p2 = 512x2 + 2
512x2 = {512x2}
512x2p2 = {512x2p2}
-> DONE

View file

@ -0,0 +1,2 @@
512x2 = 1024
512x2p2 = 1026

0
src/Story/testdata/I023/input.txt vendored Normal file
View file

7
src/Story/testdata/I023/story.ink vendored Normal file
View file

@ -0,0 +1,7 @@
-> firstKnot
=== firstKnot
Hello!
-> anotherKnot
=== anotherKnot
World.
-> END

View file

@ -0,0 +1,2 @@
Hello!
World.

1
src/Story/testdata/I035/input.txt vendored Normal file
View file

@ -0,0 +1 @@
1

5
src/Story/testdata/I035/story.ink vendored Normal file
View file

@ -0,0 +1,5 @@
* hello
-> world
== world
world
-> END

View file

@ -0,0 +1,3 @@
1: hello
?> hello
world

2
src/Story/testdata/I042/input.txt vendored Normal file
View file

@ -0,0 +1,2 @@
1
1

4
src/Story/testdata/I042/story.ink vendored Normal file
View file

@ -0,0 +1,4 @@
-> test
=== test
* Hello[.], world.
-> END

View file

@ -0,0 +1,2 @@
1: Hello.
?> Hello, world.

0
src/Story/testdata/I051/input.txt vendored Normal file
View file

3
src/Story/testdata/I051/story.ink vendored Normal file
View file

@ -0,0 +1,3 @@
{x}
VAR x = kX
CONST kX = "hi"

View file

@ -0,0 +1 @@
hi

0
src/Story/testdata/I064/input.txt vendored Normal file
View file

2
src/Story/testdata/I064/story.ink vendored Normal file
View file

@ -0,0 +1,2 @@
-> DONE
This content is inaccessible.

View file

1
src/Story/testdata/I078/input.txt vendored Normal file
View file

@ -0,0 +1 @@
1

2
src/Story/testdata/I078/story.ink vendored Normal file
View file

@ -0,0 +1,2 @@
* [Option]
Text

View file

@ -0,0 +1,2 @@
1: Option
?> Text

View file

@ -153,9 +153,14 @@ pub const InternPool = struct {
} }
pub fn deinit(ip: *InternPool, gpa: std.mem.Allocator) void { pub fn deinit(ip: *InternPool, gpa: std.mem.Allocator) void {
for (ip.code_chunks.items) |chunk| {
chunk.constants.deinit(gpa);
chunk.bytecode.deinit(gpa);
}
ip.values.deinit(gpa); ip.values.deinit(gpa);
ip.values_map.deinit(gpa); ip.values_map.deinit(gpa);
ip.code_chunks.deinit(gpa); ip.code_chunks.deinit(gpa);
ip.* = undefined;
} }
}; };
@ -179,11 +184,11 @@ pub const Module = struct {
arena: std.mem.Allocator, arena: std.mem.Allocator,
tree: Ast, tree: Ast,
ir: Ir, ir: Ir,
intern_pool: InternPool = .{},
globals: std.ArrayListUnmanaged(Global) = .empty, globals: std.ArrayListUnmanaged(Global) = .empty,
knots: std.ArrayListUnmanaged(Knot) = .empty, knots: std.ArrayListUnmanaged(Knot) = .empty,
stitches: std.ArrayListUnmanaged(Stitch) = .empty, stitches: std.ArrayListUnmanaged(Stitch) = .empty,
errors: std.ArrayListUnmanaged(Error) = .empty, errors: std.ArrayListUnmanaged(Error) = .empty,
intern_pool: InternPool = .{},
work_queue: WorkQueue = .{}, work_queue: WorkQueue = .{},
pub const Global = struct { pub const Global = struct {
@ -277,9 +282,12 @@ pub const Module = struct {
while (mod.work_queue.pop()) |work_unit| { while (mod.work_queue.pop()) |work_unit| {
const chunk_index = mod.intern_pool.code_chunks.items.len; const chunk_index = mod.intern_pool.code_chunks.items.len;
const code_chunk = try mod.createCodeChunk();
try mod.intern_pool.code_chunks.append(gpa, code_chunk);
var builder: Sema.Builder = .{ var builder: Sema.Builder = .{
.sema = &sema, .sema = &sema,
.code = try mod.createCodeChunk(), .code = code_chunk,
.namespace = work_unit.namespace, .namespace = work_unit.namespace,
}; };
defer builder.deinit(gpa); defer builder.deinit(gpa);
@ -292,7 +300,6 @@ pub const Module = struct {
try builder.finalize(); try builder.finalize();
knot_index = @enumFromInt(mod.knots.items.len); knot_index = @enumFromInt(mod.knots.items.len);
try mod.intern_pool.code_chunks.append(gpa, builder.code);
try mod.knots.append(gpa, .{ try mod.knots.append(gpa, .{
.name_index = work_unit.decl_name, .name_index = work_unit.decl_name,
.code_index = @enumFromInt(chunk_index), .code_index = @enumFromInt(chunk_index),
@ -302,7 +309,6 @@ pub const Module = struct {
try sema.analyzeStitch(&builder, work_unit.inst_index); try sema.analyzeStitch(&builder, work_unit.inst_index);
try builder.finalize(); try builder.finalize();
try mod.intern_pool.code_chunks.append(gpa, builder.code);
try mod.stitches.append(gpa, .{ try mod.stitches.append(gpa, .{
.knot_index = knot_index, .knot_index = knot_index,
.name_index = work_unit.decl_name, .name_index = work_unit.decl_name,

View file

@ -330,6 +330,8 @@ pub const Writer = struct {
.field_divert => try self.writeCallInst(w, inst, .field), .field_divert => try self.writeCallInst(w, inst, .field),
.field_ptr => try self.writeFieldPtrInst(w, inst), .field_ptr => try self.writeFieldPtrInst(w, inst),
.param => try self.writeStrTokInst(w, inst), .param => try self.writeStrTokInst(w, inst),
.done => try self.writeUnaryInst(w, inst),
.exit => try self.writeUnaryInst(w, inst),
} }
try w.writeAll(")"); try w.writeAll(")");
try w.writeAll("\n"); try w.writeAll("\n");