feat: function calls

This commit is contained in:
Brett Broadhurst 2026-03-29 15:52:34 -06:00
parent 11d99fba38
commit d08e753664
Failed to generate hash of commit
99 changed files with 881 additions and 148 deletions

View file

@ -280,6 +280,13 @@ const GenIr = struct {
self.instructions.items[self.instructions_top..]; self.instructions.items[self.instructions_top..];
} }
fn endsWithNoReturn(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.isNoReturn();
}
fn makeSubBlock(self: *GenIr) GenIr { fn makeSubBlock(self: *GenIr) GenIr {
return .{ return .{
.astgen = self.astgen, .astgen = self.astgen,
@ -730,7 +737,7 @@ fn expr(gi: *GenIr, scope: *Scope, optional_node: ?*const Ast.Node) InnerError!I
.logical_greater_or_equal_expr => return binaryOp(gi, scope, node, .cmp_gte), .logical_greater_or_equal_expr => return binaryOp(gi, scope, node, .cmp_gte),
.logical_lesser_expr => return binaryOp(gi, scope, node, .cmp_lt), .logical_lesser_expr => return binaryOp(gi, scope, node, .cmp_lt),
.logical_lesser_or_equal_expr => return binaryOp(gi, scope, node, .cmp_lte), .logical_lesser_or_equal_expr => return binaryOp(gi, scope, node, .cmp_lte),
.call_expr => unreachable, .call_expr => return callExpr(gi, scope, node, .call),
.choice_expr => unreachable, .choice_expr => unreachable,
.choice_start_expr => unreachable, .choice_start_expr => unreachable,
.choice_option_expr => unreachable, .choice_option_expr => unreachable,
@ -1171,7 +1178,9 @@ fn callExpr(
const scratch_top = astgen.scratch.items.len; const scratch_top = astgen.scratch.items.len;
defer astgen.scratch.shrinkRetainingCapacity(scratch_top); defer astgen.scratch.shrinkRetainingCapacity(scratch_top);
const arguments: ?[]*Ast.Node = if (args_node) |n| n.data.list.items.? else null; // FIXME: List nodes should not have optional slices.
// This hack is an abomination.
const arguments: ?[]*Ast.Node = if (args_node) |n| if (n.data.list.items) |items| items else null else null;
const args_count = if (arguments) |args| args.len else 0; const args_count = if (arguments) |args| args.len else 0;
try astgen.scratch.resize(gpa, scratch_top + args_count); try astgen.scratch.resize(gpa, scratch_top + args_count);
@ -1275,10 +1284,20 @@ fn divertExpr(gi: *GenIr, scope: *Scope, node: *const Ast.Node) !void {
} }
fn divertStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) !void { fn divertStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) !void {
// TODO: Revisit this.
const data = node.data.bin; const data = node.data.bin;
return divertExpr(gi, scope, data.lhs.?); return divertExpr(gi, scope, data.lhs.?);
} }
fn returnStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) !void {
// TODO: Revisit this.
const ret_arg = if (node.data.bin.lhs) |lhs| blk: {
const arg_inst = try expr(gi, scope, lhs);
break :blk arg_inst;
} else .none;
_ = try gi.addUnaryNode(.ret, ret_arg);
}
fn tempDecl(gi: *GenIr, scope: *Scope, decl_node: *const Ast.Node) !void { fn tempDecl(gi: *GenIr, scope: *Scope, decl_node: *const Ast.Node) !void {
const astgen = gi.astgen; const astgen = gi.astgen;
const identifier_node = decl_node.data.bin.lhs.?; const identifier_node = decl_node.data.bin.lhs.?;
@ -1327,20 +1346,23 @@ fn blockInner(gi: *GenIr, parent_scope: *Scope, stmt_list: []*Ast.Node) !void {
var child_scope = parent_scope.makeChild(); var child_scope = parent_scope.makeChild();
defer child_scope.deinit(); defer child_scope.deinit();
for (stmt_list) |inner_node| { for (stmt_list) |node| {
_ = switch (inner_node.tag) { _ = switch (node.tag) {
.var_decl => try varDecl(gi, &child_scope, inner_node), .var_decl => try varDecl(gi, &child_scope, node),
.const_decl => try varDecl(gi, &child_scope, inner_node), .const_decl => try varDecl(gi, &child_scope, node),
.temp_decl => try tempDecl(gi, &child_scope, inner_node), .temp_decl => try tempDecl(gi, &child_scope, node),
.assign_stmt => try assignStmt(gi, &child_scope, inner_node), .assign_stmt => try assignStmt(gi, &child_scope, node),
.content_stmt => try contentStmt(gi, &child_scope, inner_node), .content_stmt => try contentStmt(gi, &child_scope, node),
.choice_stmt => try choiceStmt(gi, &child_scope, inner_node), .choice_stmt => try choiceStmt(gi, &child_scope, node),
.expr_stmt => try exprStmt(gi, &child_scope, inner_node), .expr_stmt => try exprStmt(gi, &child_scope, node),
.divert_stmt => try divertStmt(gi, &child_scope, inner_node), .divert_stmt => try divertStmt(gi, &child_scope, node),
.return_stmt => try returnStmt(gi, &child_scope, node),
inline else => |e| @panic("Unexpected node: " ++ @tagName(e)), inline else => |e| @panic("Unexpected node: " ++ @tagName(e)),
}; };
} }
_ = try gi.addUnaryNode(.implicit_ret, .none); if (!gi.endsWithNoReturn()) {
_ = try gi.addUnaryNode(.implicit_ret, .none);
}
} }
fn blockStmt(block: *GenIr, scope: *Scope, stmt_node: *const Ast.Node) InnerError!void { fn blockStmt(block: *GenIr, scope: *Scope, stmt_node: *const Ast.Node) InnerError!void {
@ -1415,9 +1437,10 @@ fn stitchDeclInner(
} }
} }
if (body_node) |body| { if (body_node) |body| {
try blockStmt(&decl_block, scope, body); const body_list = body.data.list.items.?;
try blockInner(&decl_block, scope, body_list);
} else { } else {
_ = try decl_block.addUnaryNode(.implicit_ret, .none); try blockInner(&decl_block, scope, &.{});
} }
const decl_str = try astgen.strFromNode(identifier_node); const decl_str = try astgen.strFromNode(identifier_node);
@ -1440,7 +1463,65 @@ fn stitchDecl(gi: *GenIr, parent_scope: *Scope, decl_node: *const Ast.Node) Inne
return stitchDeclInner(gi, &decl_scope, decl_node, prototype_node.?, body_node); return stitchDeclInner(gi, &decl_scope, decl_node, prototype_node.?, body_node);
} }
fn functionDecl(_: *GenIr, _: *Scope, _: *const Ast.Node) InnerError!void {} fn functionDeclInner(
gi: *GenIr,
scope: *Scope,
node: *const Ast.Node,
prototype_node: *const Ast.Node,
body_node: ?*const Ast.Node,
) InnerError!void {
const astgen = gi.astgen;
const prototype_data = prototype_node.data.bin;
const identifier_node = prototype_data.lhs.?;
const decl_inst = try gi.addAsIndex(.{
.tag = .declaration,
.data = .{ .payload = undefined },
});
var decl_block = gi.makeSubBlock();
defer decl_block.unstack();
const stitch_inst = try decl_block.makePayloadNode(.decl_function);
if (prototype_data.rhs) |args_node| {
const args_list = args_node.data.list.items.?;
for (args_list) |arg| {
assert(arg.tag == .parameter_decl);
const arg_str = try astgen.strFromNode(arg);
const arg_inst = try decl_block.addStrTok(.param, arg_str.index, arg.loc.start);
// TODO: Maybe make decl accept a ref?
try scope.insert(arg_str.index, .{
.decl_node = arg,
.inst_index = arg_inst.toIndex().?,
});
}
}
if (body_node) |body| {
try blockStmt(&decl_block, scope, body);
} else {
_ = try decl_block.addUnaryNode(.implicit_ret, .none);
}
const decl_str = try astgen.strFromNode(identifier_node);
try setDeclStitchPayload(stitch_inst, &decl_block);
try setDeclaration(decl_inst, .{
.name = decl_str.index,
.value = stitch_inst,
.gi = gi,
.node = node,
});
}
fn functionDecl(gi: *GenIr, parent_scope: *Scope, decl_node: *const Ast.Node) InnerError!void {
const knot_data = decl_node.data.bin;
const prototype_node = knot_data.lhs;
const body_node = knot_data.rhs;
var decl_scope = parent_scope.makeChild();
defer decl_scope.deinit();
return functionDeclInner(gi, &decl_scope, decl_node, prototype_node.?, body_node);
}
fn knotDecl(gi: *GenIr, parent_scope: *Scope, decl_node: *const Ast.Node) InnerError!void { fn knotDecl(gi: *GenIr, parent_scope: *Scope, decl_node: *const Ast.Node) InnerError!void {
const astgen = gi.astgen; const astgen = gi.astgen;

View file

@ -147,10 +147,16 @@ pub const Inst = struct {
pub const Tag = enum { pub const Tag = enum {
file, file,
/// Uses the `payload` union field.
declaration, declaration,
/// Uses the `payload` union field.
decl_var, decl_var,
/// Uses the `payload` union field.
decl_knot, decl_knot,
/// Uses the `payload` union field.
decl_stitch, decl_stitch,
/// Uses the `payload` union field.
decl_function,
/// Uses the `str_tok` union field. /// Uses the `str_tok` union field.
decl_ref, decl_ref,
alloc, alloc,
@ -199,6 +205,9 @@ pub const Inst = struct {
content_push, content_push,
content_flush, content_flush,
choice_br, choice_br,
// Uses the `un` union field.
ret,
// Uses the `un` union field.
implicit_ret, implicit_ret,
call, call,
divert, divert,
@ -264,6 +273,11 @@ pub const Inst = struct {
body_len: u32, body_len: u32,
}; };
pub const Function = struct {
/// Number of instructions for the function's body block.
body_len: u32,
};
pub const Field = struct { pub const Field = struct {
lhs: Ref, lhs: Ref,
field_name_start: NullTerminatedString, field_name_start: NullTerminatedString,
@ -324,4 +338,56 @@ pub const Inst = struct {
byte_offset: u32, byte_offset: u32,
}; };
}; };
pub fn isNoReturn(inst: Inst) bool {
return switch (inst.tag) {
.file,
.declaration,
.decl_var,
.decl_knot,
.decl_stitch,
.decl_function,
.decl_ref,
.alloc,
.load,
.store,
.add,
.sub,
.mul,
.div,
.mod,
.neg,
.not,
.bool_and,
.bool_or,
.cmp_eq,
.cmp_neq,
.cmp_gt,
.cmp_gte,
.cmp_lt,
.cmp_lte,
.int,
.float,
.str,
.block,
.condbr,
.switch_br,
.content_push,
.content_flush,
.choice_br,
.call,
.divert,
.field_ptr,
.field_call,
.field_divert,
.param,
=> false,
.ret,
.implicit_ret,
.@"break",
.done,
.exit,
=> true,
};
}
}; };

View file

@ -29,6 +29,7 @@ pub const ValueInfo = union(enum) {
variable: InternPool.Index, variable: InternPool.Index,
knot: InternPool.Index, knot: InternPool.Index,
stitch: InternPool.Index, stitch: InternPool.Index,
function: InternPool.Index,
temp: u32, temp: u32,
}; };
@ -294,7 +295,11 @@ pub const Builder = struct {
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));
}, },
.variable, .knot, .stitch => |index| { .variable,
.knot,
.stitch,
.function,
=> |index| {
const local_index = try self.getOrPutConstantIndex(index); const local_index = try self.getOrPutConstantIndex(index);
try self.addConstOp(.load_global, @intCast(local_index)); try self.addConstOp(.load_global, @intCast(local_index));
}, },
@ -536,7 +541,7 @@ fn irDeclRef(
const ident = try sema.lookupIdentifier(builder, ip_index, src_loc); const ident = try sema.lookupIdentifier(builder, ip_index, src_loc);
if (inline_block) { if (inline_block) {
switch (ident.tag) { switch (ident.tag) {
.knot, .stitch => unreachable, .knot, .stitch, .function => unreachable,
.var_const => return sema.resolveGlobalDecl(builder, ip_index, src_loc), .var_const => return sema.resolveGlobalDecl(builder, ip_index, src_loc),
.var_mut => return sema.fail( .var_mut => return sema.fail(
src_loc, src_loc,
@ -548,6 +553,7 @@ fn irDeclRef(
switch (ident.tag) { switch (ident.tag) {
.knot => return .{ .knot = ip_index }, .knot => return .{ .knot = ip_index },
.stitch => return .{ .stitch = ip_index }, .stitch => return .{ .stitch = ip_index },
.function => return .{ .function = ip_index },
.var_mut => return .{ .variable = ip_index }, .var_mut => return .{ .variable = ip_index },
.var_const => return .{ .variable = ip_index }, .var_const => return .{ .variable = ip_index },
} }
@ -577,8 +583,9 @@ fn irStore(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void
try builder.addConstOp(.store_global, @intCast(local_index)); try builder.addConstOp(.store_global, @intCast(local_index));
}, },
.temp => |temp| try builder.addConstOp(.store, @intCast(temp)), .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", .{}), .knot => |_| return sema.fail(src, "could not assign to knot", .{}),
.stitch => |_| return sema.fail(src, "could not assign to stitch", .{}),
.function => |_| return sema.fail(src, "could not assign to function", .{}),
} }
try builder.addByteOp(.pop); try builder.addByteOp(.pop);
@ -757,12 +764,66 @@ fn irChoiceBr(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!vo
} }
} }
fn irRet(sema: *Sema, builder: *Builder, inst: Ir.Inst.Index) InnerError!void {
const data = sema.ir.instructions[@intFromEnum(inst)].data.un;
const value = sema.resolveInst(data.lhs);
if (value != .none) try builder.ensureLoad(value);
try builder.addByteOp(.ret);
}
fn irImplicitRet(_: *Sema, builder: *Builder, _: Ir.Inst.Index) InnerError!void { fn irImplicitRet(_: *Sema, builder: *Builder, _: Ir.Inst.Index) InnerError!void {
try builder.addByteOp(.exit); try builder.addByteOp(.exit);
} }
fn irCall(_: *Sema, _: *Builder, _: Ir.Inst.Index) !ValueInfo { fn irCall(
return .none; sema: *Sema,
builder: *Builder,
inst: Ir.Inst.Index,
comptime kind: enum { direct, field },
) !ValueInfo {
const ExtraType = switch (kind) {
.direct => Ir.Inst.Call,
.field => Ir.Inst.FieldCall,
};
const data = sema.ir.instructions[@intFromEnum(inst)].data.payload;
const extra = sema.ir.extraData(ExtraType, data.extra_index);
const body = sema.ir.extra[extra.end..];
const callee_src: SrcLoc = .{ .src_offset = data.src_offset };
switch (kind) {
.direct => {
const callee = sema.resolveInst(extra.data.callee);
_ = try analyzeCallTarget(sema, builder, callee_src, callee);
},
.field => {
const callee = sema.resolveInst(extra.data.obj_ptr);
const target = try analyzeCallTarget(sema, builder, callee_src, callee);
const ip_index = try sema.module.intern_pool.getOrPutStr(
sema.gpa,
extra.data.field_name_start,
);
const e = try sema.lookupInNamespace(target.namespace.?, ip_index, callee_src);
switch (e.tag) {
.function => {
const local_index = try builder.getOrPutConstantIndex(ip_index);
_ = try builder.addConstOp(.load_attr, @intCast(local_index));
},
else => return sema.fail(callee_src, "invalid call target", .{}),
}
},
}
const args_len = extra.data.args_len;
var arg_start: u32 = args_len;
var i: u32 = 0;
while (i < args_len) : (i += 1) {
const arg_end = sema.ir.extra[extra.end + i];
defer arg_start = arg_end;
const arg_body = body[arg_start..arg_end];
const arg_value = try analyzeBodyInner(sema, builder, @ptrCast(arg_body), false);
if (arg_value != .none) try builder.ensureLoad(arg_value);
}
try builder.addConstOp(.call, @intCast(args_len));
return .stack;
} }
fn irDivert( fn irDivert(
@ -809,13 +870,8 @@ fn irDivert(
const arg_end = sema.ir.extra[extra.end + i]; const arg_end = sema.ir.extra[extra.end + i];
defer arg_start = arg_end; defer arg_start = arg_end;
const arg_body = body[arg_start..arg_end]; const arg_body = body[arg_start..arg_end];
_ = try analyzeBodyInner(sema, builder, @ptrCast(arg_body), false); const arg_value = try analyzeBodyInner(sema, builder, @ptrCast(arg_body), false);
// FIXME: hack if (arg_value != .none) try builder.ensureLoad(arg_value);
{
const last_inst: Ir.Inst.Index = @enumFromInt(arg_body[arg_body.len - 1]);
const val = sema.resolveInst(last_inst.toRef());
try builder.ensureLoad(val);
}
} }
try builder.addConstOp(.divert, @intCast(args_len)); try builder.addConstOp(.divert, @intCast(args_len));
} }
@ -864,6 +920,21 @@ fn analyzeArithmeticArg(
} }
} }
fn analyzeCallTarget(
sema: *Sema,
builder: *Builder,
src: SrcLoc,
callee: ValueInfo,
) !Module.Namespace.Decl {
switch (callee) {
.function => |ip_index| {
try builder.ensureLoad(callee);
return sema.lookupIdentifier(builder, ip_index, src);
},
else => return sema.fail(src, "invalid call target", .{}),
}
}
fn analyzeDivertTarget( fn analyzeDivertTarget(
sema: *Sema, sema: *Sema,
builder: *Builder, builder: *Builder,
@ -894,6 +965,7 @@ fn analyzeBodyInner(
.decl_var => unreachable, // never present inside block bodies .decl_var => unreachable, // never present inside block bodies
.decl_knot => unreachable, // never present inside block bodies .decl_knot => unreachable, // never present inside block bodies
.decl_stitch => unreachable, // never present inside block bodies .decl_stitch => unreachable, // never present inside block bodies
.decl_function => unreachable, // never present inside block bodies
.alloc => try irAlloc(sema, builder, inst), .alloc => try irAlloc(sema, builder, inst),
.store => { .store => {
try irStore(sema, builder, inst); try irStore(sema, builder, inst);
@ -919,6 +991,10 @@ fn analyzeBodyInner(
.cmp_gt => try irBinaryOp(sema, builder, inst, .cmp_gt), .cmp_gt => try irBinaryOp(sema, builder, inst, .cmp_gt),
.cmp_gte => try irBinaryOp(sema, builder, inst, .cmp_gte), .cmp_gte => try irBinaryOp(sema, builder, inst, .cmp_gte),
.decl_ref => try irDeclRef(sema, builder, inst, inline_block), .decl_ref => try irDeclRef(sema, builder, inst, inline_block),
.ret => {
try irRet(sema, builder, inst);
continue;
},
.implicit_ret => { .implicit_ret => {
try irImplicitRet(sema, builder, inst); try irImplicitRet(sema, builder, inst);
continue; continue;
@ -948,12 +1024,12 @@ fn analyzeBodyInner(
try irSwitchBr(sema, builder, inst); try irSwitchBr(sema, builder, inst);
continue; continue;
}, },
.call => try irCall(sema, builder, inst), .call => try irCall(sema, builder, inst, .direct),
.field_call => try irCall(sema, builder, inst, .field),
.divert => { .divert => {
try irDivert(sema, builder, inst, .direct); try irDivert(sema, builder, inst, .direct);
continue; continue;
}, },
.field_call => try irCall(sema, builder, inst),
.field_divert => { .field_divert => {
try irDivert(sema, builder, inst, .field); try irDivert(sema, builder, inst, .field);
continue; continue;
@ -968,6 +1044,7 @@ fn analyzeBodyInner(
return result; return result;
} }
// TODO: No return allowed.
pub fn analyzeStitch( pub fn analyzeStitch(
sema: *Sema, sema: *Sema,
builder: *Builder, builder: *Builder,
@ -979,6 +1056,19 @@ pub fn analyzeStitch(
_ = try analyzeBodyInner(sema, builder, body, false); _ = try analyzeBodyInner(sema, builder, body, false);
} }
// TODO: No diverts allowed.
pub fn analyzeFunction(
sema: *Sema,
builder: *Builder,
inst: Ir.Inst.Index,
) !void {
const data = sema.ir.instructions[@intFromEnum(inst)].data.payload;
const extra = sema.ir.extraData(Ir.Inst.Function, data.extra_index);
const body = sema.ir.bodySlice(extra.end, extra.data.body_len);
_ = try analyzeBodyInner(sema, builder, body, false);
}
// TODO: No return allowed.
pub fn analyzeKnot( pub fn analyzeKnot(
sema: *Sema, sema: *Sema,
builder: *Builder, builder: *Builder,
@ -1088,7 +1178,6 @@ fn scanTopLevelDecl(
.namespace = child_namespace, .namespace = child_namespace,
}; };
} }
try sema.module.queueWorkItem(.{ try sema.module.queueWorkItem(.{
.tag = .stitch, .tag = .stitch,
.decl_name = decl_name, .decl_name = decl_name,
@ -1096,6 +1185,27 @@ fn scanTopLevelDecl(
.namespace = child_namespace, .namespace = child_namespace,
}); });
}, },
.decl_function => {
const child_namespace = try sema.module.createNamespace(namespace);
const gop = try namespace.decls.getOrPut(sema.arena, decl_name);
if (gop.found_existing) {
return sema.fail(src_loc, "duplicate identifier", .{});
} else {
gop.value_ptr.* = .{
.tag = .function,
.decl_inst = extra.value,
.args_count = 0,
.namespace = child_namespace,
};
}
try sema.module.queueWorkItem(.{
.tag = .function,
.decl_name = decl_name,
.inst_index = extra.value,
.namespace = child_namespace,
});
},
else => unreachable, else => unreachable,
} }
} }

View file

@ -14,13 +14,16 @@ dump_writer: ?*std.Io.Writer = null,
is_exited: bool = false, is_exited: bool = false,
can_advance: bool = false, can_advance: bool = false,
choice_index: usize = 0, choice_index: usize = 0,
stack_top: usize = 0,
call_stack_top: usize = 0,
current_choices: std.ArrayListUnmanaged(Choice) = .empty, current_choices: std.ArrayListUnmanaged(Choice) = .empty,
code_chunks: std.ArrayListUnmanaged(*Object.Code) = .empty, code_chunks: std.ArrayListUnmanaged(*Object.Code) = .empty,
constants_pool: std.ArrayListUnmanaged(Value) = .empty,
globals: std.StringHashMapUnmanaged(?Value) = .empty, globals: std.StringHashMapUnmanaged(?Value) = .empty,
stack: std.ArrayListUnmanaged(?Value) = .empty, stack: []Value = &.{},
call_stack: std.ArrayListUnmanaged(CallFrame) = .empty, call_stack: []CallFrame = &.{},
stack_max: usize = 128, /// Global constants pool.
constants_pool: []const Value = &.{},
/// Linked list of all tracked runtime objects.
gc_objects: std.SinglyLinkedList = .{}, gc_objects: std.SinglyLinkedList = .{},
// FIXME: This was a hack to keep string bytes alive. // FIXME: This was a hack to keep string bytes alive.
string_bytes: []const u8 = &.{}, string_bytes: []const u8 = &.{},
@ -28,6 +31,7 @@ string_bytes: []const u8 = &.{},
pub const default_knot_name: [:0]const u8 = "$__main__$"; pub const default_knot_name: [:0]const u8 = "$__main__$";
pub const Value = union(enum) { pub const Value = union(enum) {
nil,
bool: bool, bool: bool,
int: i64, int: i64,
float: f64, float: f64,
@ -35,6 +39,7 @@ pub const Value = union(enum) {
pub fn tagBytes(v: Value) []const u8 { pub fn tagBytes(v: Value) []const u8 {
return switch (v) { return switch (v) {
.nil => "Nil",
.bool => "Bool", .bool => "Bool",
.int => "Int", .int => "Int",
.float => "Float", .float => "Float",
@ -57,7 +62,7 @@ pub const Value = union(enum) {
pub fn isTruthy(v: Value) bool { pub fn isTruthy(v: Value) bool {
return switch (v) { return switch (v) {
//.nil => false, .nil => false,
.bool => |b| b, .bool => |b| b,
.int => |i| i != 0, .int => |i| i != 0,
.float => |f| f != 0.0, .float => |f| f != 0.0,
@ -133,6 +138,7 @@ pub const Value = union(enum) {
pub fn eql(lhs: Value, rhs: Value) bool { pub fn eql(lhs: Value, rhs: Value) bool {
return switch (lhs) { return switch (lhs) {
.nil => rhs == .nil,
.bool => |l| rhs == .bool and l == rhs.bool, .bool => |l| rhs == .bool and l == rhs.bool,
.int => |l| switch (rhs) { .int => |l| switch (rhs) {
.int => |r| l == r, .int => |r| l == r,
@ -183,15 +189,15 @@ pub const Value = union(enum) {
pub fn negate(lhs: Value) !Value { pub fn negate(lhs: Value) !Value {
switch (lhs) { switch (lhs) {
.bool => return error.TypeError, .nil, .bool, .object => return error.TypeError,
.int => |int| return .{ .int = -int }, .int => |int| return .{ .int = -int },
.float => |float| return .{ .float = -float }, .float => |float| return .{ .float = -float },
.object => return error.TypeError,
} }
} }
pub fn format(value: Value, writer: *std.Io.Writer) error{WriteFailed}!void { pub fn format(value: Value, writer: *std.Io.Writer) error{WriteFailed}!void {
switch (value) { switch (value) {
.nil => try writer.writeAll(value.tagBytes()),
.bool => |boolean| try writer.writeAll(if (boolean) "true" else "false"), .bool => |boolean| try writer.writeAll(if (boolean) "true" else "false"),
.int => |int| try writer.print("{d}", .{int}), .int => |int| try writer.print("{d}", .{int}),
.float => |float| { .float => |float| {
@ -222,9 +228,10 @@ pub const Value = union(enum) {
}; };
pub const CallFrame = struct { pub const CallFrame = struct {
callee: *Object.Knot,
caller_top: usize,
ip: usize, ip: usize,
sp: usize, sp: usize,
callee: *Object.Knot,
}; };
pub const Choice = struct { pub const Choice = struct {
@ -304,62 +311,52 @@ pub fn deinit(story: *Story) void {
} }
story.current_choices.deinit(gpa); story.current_choices.deinit(gpa);
story.constants_pool.deinit(gpa);
story.globals.deinit(gpa); story.globals.deinit(gpa);
story.stack.deinit(gpa);
story.call_stack.deinit(gpa);
gpa.free(story.string_bytes);
}
fn isCallStackEmpty(vm: *const Story) bool { gpa.free(story.string_bytes);
return vm.call_stack.items.len == 0; gpa.free(story.constants_pool);
gpa.free(story.stack);
gpa.free(story.call_stack);
} }
fn currentFrame(vm: *Story) *CallFrame { fn currentFrame(vm: *Story) *CallFrame {
return &vm.call_stack.items[vm.call_stack.items.len - 1]; assert(vm.call_stack_top > 0);
return &vm.call_stack[vm.call_stack_top - 1];
} }
fn peekStack(vm: *Story, offset: usize) ?Value { fn peekStack(vm: *Story, offset: usize) ?Value {
const stack_top = vm.stack.items.len; if (vm.stack_top <= offset) return null;
if (stack_top <= offset) return null; return vm.stack[vm.stack_top - offset - 1];
return vm.stack.items[stack_top - offset - 1];
} }
fn pushStack(vm: *Story, value: Value) !void { fn pushStack(vm: *Story, value: Value) !void {
const gpa = vm.allocator; if (vm.stack_top >= vm.stack.len) return error.StackOverflow;
const stack_top = vm.stack.items.len; vm.stack[vm.stack_top] = value;
const max_stack_top = vm.stack_max; vm.stack_top += 1;
if (stack_top >= max_stack_top) return error.StackOverflow;
return vm.stack.append(gpa, value);
} }
fn popStack(vm: *Story) ?Value { fn popStack(vm: *Story) ?Value {
return vm.stack.pop() orelse unreachable; if (vm.stack_top == 0) return null;
const stack_top = vm.stack_top;
vm.stack_top -= 1;
return vm.stack[stack_top];
} }
fn getConstant(story: *Story, frame: *CallFrame, offset: u8) !Value { fn getConstant(story: *Story, frame: *CallFrame, offset: u8) !Value {
if (offset >= frame.callee.code.constants.len) return error.InvalidArgument; if (offset >= frame.callee.code.constants.len) return error.InvalidArgument;
const constant_index = frame.callee.code.constants[offset]; const constant_index = frame.callee.code.constants[offset];
return story.constants_pool.items[constant_index]; return story.constants_pool[constant_index];
} }
fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?Value { fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?Value {
const stack_top = vm.stack.items.len; assert(vm.stack_top > frame.sp + offset);
const stack_offset = frame.sp + offset; return vm.stack[frame.sp + offset];
assert(stack_top > stack_offset);
return vm.stack.items[stack_offset];
} }
fn setLocal(vm: *Story, frame: *CallFrame, offset: u8, value: Value) void { fn setLocal(vm: *Story, frame: *CallFrame, offset: u8, value: Value) void {
const stack_top = vm.stack.items.len; assert(vm.stack_top > frame.sp + offset);
const stack_offset = frame.sp + offset; vm.stack[frame.sp + offset] = value;
assert(stack_top > stack_offset);
vm.stack.items[stack_offset] = value;
} }
// TODO: This should probably check the constants table first. // TODO: This should probably check the constants table first.
@ -396,15 +393,14 @@ 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;
if (vm.call_stack_top == 0) return .empty;
errdefer vm.can_advance = false; errdefer vm.can_advance = false;
if (vm.isCallStackEmpty()) return .empty;
var stream_writer = std.Io.Writer.Allocating.init(gpa); var stream_writer = std.Io.Writer.Allocating.init(gpa);
defer stream_writer.deinit(); defer stream_writer.deinit();
var frame = vm.currentFrame();
while (true) { while (true) {
const frame = vm.currentFrame();
const code = std.mem.bytesAsSlice(Opcode, frame.callee.code.bytecode); const code = std.mem.bytesAsSlice(Opcode, frame.callee.code.bytecode);
if (vm.dump_writer) |w| { if (vm.dump_writer) |w| {
Dumper.trace(vm, w, frame) catch {}; Dumper.trace(vm, w, frame) catch {};
@ -419,14 +415,27 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
vm.can_advance = false; vm.can_advance = false;
return .empty; return .empty;
}, },
.ret => {
const return_value = vm.stack[vm.stack_top - 1];
vm.call_stack_top -= 1;
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];
},
.true => { .true => {
const value: Value = .{ .bool = true }; try vm.pushStack(.{ .bool = true });
try vm.pushStack(value);
frame.ip += 1; frame.ip += 1;
}, },
.false => { .false => {
const value: Value = .{ .bool = false }; try vm.pushStack(.{ .bool = false });
try vm.pushStack(value);
frame.ip += 1; frame.ip += 1;
}, },
.pop => { .pop => {
@ -519,6 +528,43 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
return error.InvalidArgument; return error.InvalidArgument;
} }
}, },
.call => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
frame.ip += 2;
if (peekStack(vm, arg_offset)) |value| {
switch (value) {
.object => |object| switch (object.tag) {
.knot => try call(vm, @ptrCast(object)),
else => unreachable,
},
else => unreachable,
}
} else {
return error.InvalidArgument;
}
// Re-fetch we're now in the callee's frame
frame = &vm.call_stack[vm.call_stack_top - 1];
},
.divert => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
frame.ip += 2;
if (peekStack(vm, arg_offset)) |value| {
switch (value) {
.object => |object| switch (object.tag) {
.knot => try divert(vm, @ptrCast(object)),
else => unreachable,
},
else => unreachable,
}
} else {
return error.InvalidArgument;
}
frame = &vm.call_stack[vm.call_stack_top - 1];
},
.load_const => { .load_const => {
const index: u8 = @intFromEnum(code[frame.ip + 1]); const index: u8 = @intFromEnum(code[frame.ip + 1]);
const value = try vm.getConstant(frame, index); const value = try vm.getConstant(frame, index);
@ -611,16 +657,6 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
frame.ip = branch_dispatch.dest_offset; frame.ip = branch_dispatch.dest_offset;
}, },
.divert => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
frame.ip += 2;
if (peekStack(vm, arg_offset)) |value| {
try divertToValue(vm, value);
} else {
return error.InvalidArgument;
}
},
.load_attr => { .load_attr => {
const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]); const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]);
frame.ip += 2; frame.ip += 2;
@ -667,38 +703,62 @@ pub fn getKnot(vm: *Story, name: []const u8) ?*Object.Knot {
return knot; return knot;
} }
// TODO(Brett): Add arguments? fn call(vm: *Story, knot: *Object.Knot) !void {
fn divertToKnot(vm: *Story, knot: *Object.Knot) !void { if (vm.call_stack_top >= vm.call_stack.len)
const gpa = vm.allocator; return error.CallStackOverflow;
const stack_ptr = vm.stack.items.len - knot.code.args_count;
const stack_needed = knot.code.stack_size;
try vm.stack.ensureUnusedCapacity(gpa, stack_needed); const locals_count = knot.code.locals_count;
try vm.call_stack.ensureUnusedCapacity(gpa, 1); 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;
vm.call_stack.appendAssumeCapacity(.{ 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, .callee = knot,
.ip = 0, .ip = 0,
.sp = stack_ptr, .sp = sp,
}); .caller_top = caller_top,
vm.stack.appendNTimesAssumeCapacity(null, stack_needed); };
vm.can_advance = true; vm.call_stack_top += 1;
} }
fn divertToValue(vm: *Story, value: Value) !void { // Diverts are essentially tail calls.
switch (value) { fn divert(vm: *Story, knot: *Object.Knot) !void {
.object => |object| switch (object.tag) { const args_count = knot.code.args_count;
.knot => try divertToKnot(vm, @ptrCast(object)), const locals_count = knot.code.locals_count;
else => return error.TypeError, if (vm.call_stack_top == 0) return vm.call(knot);
},
else => return error.TypeError, 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],
);
} }
}
fn divert(vm: *Story, knot_name: []const u8) !void { const frame_top = sp + args_count + locals_count;
return if (getKnot(vm, knot_name)) |knot| { if (frame_top > vm.stack.len) return error.StackOverflow;
return divertToKnot(vm, knot); for (vm.stack[sp + args_count .. frame_top]) |*slot| slot.* = .nil;
} else return error.InvalidPath; vm.stack_top = frame_top;
current_frame.* = .{
.callee = knot,
.ip = 0,
.sp = sp,
.caller_top = caller_top,
};
} }
pub const LoadOptions = struct { pub const LoadOptions = struct {
@ -746,15 +806,25 @@ pub fn loadFromString(
return error.LoadFailed; return error.LoadFailed;
} }
const stack_size = 128;
const eval_stack_ptr = try gpa.alloc(Value, stack_size);
errdefer gpa.free(eval_stack_ptr);
const call_stack_ptr = try gpa.alloc(CallFrame, stack_size);
errdefer gpa.free(call_stack_ptr);
var story: Story = .{ var story: Story = .{
.allocator = gpa, .allocator = gpa,
.can_advance = false, .can_advance = false,
.dump_writer = if (options.dump_trace) options.dump_writer else null, .dump_writer = if (options.dump_trace) options.dump_writer else null,
.stack = eval_stack_ptr,
.call_stack = call_stack_ptr,
}; };
errdefer story.deinit(); errdefer story.deinit();
try comp.setupStoryRuntime(gpa, &story); try comp.setupStoryRuntime(gpa, &story);
if (story.getKnot(Story.default_knot_name)) |knot| { if (story.getKnot(Story.default_knot_name)) |knot| {
try story.divertToKnot(knot); try story.divert(knot);
story.can_advance = true; story.can_advance = true;
} }
return story; return story;

View file

@ -22,7 +22,6 @@ fn dumpByteInst(
op: Opcode, op: Opcode,
) !usize { ) !usize {
const code = knot.code; const code = knot.code;
const constants_pool = &self.story.constants_pool;
assert(code.bytecode.len > offset + 1); assert(code.bytecode.len > offset + 1);
const arg = code.bytecode[offset + 1]; const arg = code.bytecode[offset + 1];
@ -31,9 +30,8 @@ fn dumpByteInst(
try w.writeAll(" ("); try w.writeAll(" (");
if (code.constants.len > arg) { if (code.constants.len > arg) {
const constant_index = code.constants[arg]; const constant_index = code.constants[arg];
if (constants_pool.items.len > constant_index) { if (self.story.constants_pool.len > constant_index) {
const global_constant = &constants_pool.items[constant_index]; try self.dumpValue(w, &self.story.constants_pool[constant_index]);
try self.dumpValue(w, global_constant);
} else { } else {
try w.writeAll("invalid!"); try w.writeAll("invalid!");
} }
@ -55,14 +53,11 @@ fn dumpGlobalInst(
op: Opcode, op: Opcode,
) !usize { ) !usize {
const code = knot.code; const code = knot.code;
const constants_pool = &self.story.constants_pool;
assert(code.bytecode.len > offset + 1); assert(code.bytecode.len > offset + 1);
const arg = code.bytecode[offset + 1]; const arg = code.bytecode[offset + 1];
assert(code.constants.len > arg); assert(code.constants.len > arg);
const constant_index = code.constants[arg]; const constant_index = code.constants[arg];
const global_constant = constants_pool.items[constant_index]; switch (self.story.constants_pool[constant_index]) {
switch (global_constant) {
.object => |object| switch (object.tag) { .object => |object| switch (object.tag) {
.string => { .string => {
const global_name: *Object.String = @ptrCast(object); const global_name: *Object.String = @ptrCast(object);
@ -280,6 +275,10 @@ pub fn dumpKnot(self: *Dumper, w: *std.Io.Writer, knot: *const Object.Knot) !voi
pub fn dumpValue(_: Dumper, w: *std.Io.Writer, value: *const Value) !void { pub fn dumpValue(_: Dumper, w: *std.Io.Writer, value: *const Value) !void {
switch (value.*) { switch (value.*) {
.nil => try w.print(
"<{s}>",
.{value.tagBytes()},
),
.bool => |_| try w.print( .bool => |_| try w.print(
"<{s} value={f}>", "<{s} value={f}>",
.{ value.tagBytes(), value }, .{ value.tagBytes(), value },
@ -313,8 +312,8 @@ pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
try writer.writeAll("=== Constants ===\n"); try writer.writeAll("=== Constants ===\n");
var i: usize = 0; var i: usize = 0;
while (i < story.constants_pool.items.len) : (i += 1) { while (i < story.constants_pool.len) : (i += 1) {
const global_constant = &story.constants_pool.items[i]; const global_constant = &story.constants_pool[i];
try story_dumper.dumpValue(writer, global_constant); try story_dumper.dumpValue(writer, global_constant);
try writer.writeAll("\n"); try writer.writeAll("\n");
} }
@ -350,31 +349,14 @@ pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
} }
} }
pub fn trace(story: *Story, writer: *std.Io.Writer, frame: *Story.CallFrame) !void { pub fn trace(vm: *Story, writer: *std.Io.Writer, frame: *Story.CallFrame) !void {
var dumper: Dumper = .{ .story = story }; var dumper: Dumper = .{ .story = vm };
const stack = &story.stack;
const stack_top = story.stack.items.len;
try writer.print("\tStack => stack_pointer={d}, objects=[", .{frame.sp}); try writer.print("\tStack => stack_pointer={d}, objects=[", .{frame.sp});
if (stack_top > 0) { const window = vm.stack[frame.sp..vm.stack_top];
// FIXME: There has to be a better way to do this. for (window, 0..) |*slot, i| {
if (stack_top > 1) { if (i > 0) try writer.writeAll(", ");
var i: usize = frame.sp; try dumper.dumpValue(writer, slot);
while (i < stack.items.len - 1) : (i += 1) {
if (stack.items[i]) |*value| {
try dumper.dumpValue(writer, value);
} else {
try writer.writeAll("null");
}
try writer.writeAll(", ");
}
}
if (stack.items[stack.items.len - 1]) |*object| {
try dumper.dumpValue(writer, object);
} else {
try writer.writeAll("null");
}
} }
try writer.writeAll("]\n"); try writer.writeAll("]\n");

View file

@ -95,7 +95,7 @@ pub const String = struct {
const print_buffer_len = 64; const print_buffer_len = 64;
var print_buffer: [print_buffer_len]u8 = undefined; var print_buffer: [print_buffer_len]u8 = undefined;
switch (value) { switch (value) {
.bool, .int, .float => { .nil, .bool, .int, .float => {
const bytes = try std.fmt.bufPrint(&print_buffer, "{f}", .{value}); const bytes = try std.fmt.bufPrint(&print_buffer, "{f}", .{value});
return .create(story, .{ .bytes = bytes }); return .create(story, .{ .bytes = bytes });
}, },

View file

@ -18,10 +18,18 @@ test "fixture - I002 (Fogg comforts Passepartout)" {
try testRuntimeFixture("I002"); try testRuntimeFixture("I002");
} }
test "fixture - I004 (Print number as English)" {
try testRuntimeFixture("I004");
}
test "fixture - I005 (Const variable)" { test "fixture - I005 (Const variable)" {
try testRuntimeFixture("I005"); try testRuntimeFixture("I005");
} }
test "fixture - I006 (Multiple constant references)" {
try testRuntimeFixture("I006");
}
test "fixture - I007 (Set non existant variable)" { test "fixture - I007 (Set non existant variable)" {
try testRuntimeFixture("I007"); try testRuntimeFixture("I007");
} }
@ -35,6 +43,10 @@ test "fixture - I011 (Temporaries at global scope)" {
// try testRuntimeFixture("I012"); // try testRuntimeFixture("I012");
//} //}
test "fixture - I014 (Variable swap recurse)" {
try testRuntimeFixture("I014");
}
test "fixture - I016 (Empty)" { test "fixture - I016 (Empty)" {
try testRuntimeFixture("I016"); try testRuntimeFixture("I016");
} }
@ -59,10 +71,26 @@ test "fixture - I023 (Whitespace)" {
try testRuntimeFixture("I023"); try testRuntimeFixture("I023");
} }
test "fixture - I033 (Newline consistency, the first)" {
try testRuntimeFixture("I033");
}
test "fixture - I034 (Newline consistency, the second)" {
try testRuntimeFixture("I034");
}
test "fixture - I035 (Newline consistency, the third)" { test "fixture - I035 (Newline consistency, the third)" {
try testRuntimeFixture("I035"); try testRuntimeFixture("I035");
} }
test "fixture - I036 (Newlines with string eval)" {
try testRuntimeFixture("I036");
}
test "fixture - I037 (Newline at start of multiline conditional)" {
try testRuntimeFixture("I037");
}
test "fixture - I042 (Weave options)" { test "fixture - I042 (Weave options)" {
try testRuntimeFixture("I042"); try testRuntimeFixture("I042");
} }
@ -75,18 +103,35 @@ test "fixture - I064 (Done stops thread)" {
try testRuntimeFixture("I064"); try testRuntimeFixture("I064");
} }
// This one is named oddly...
test "fixture - I075 (Clean callstack reset on path choice)" {
try testRuntimeFixture("I075");
}
test "fixture - I078 (Choice with brackets only)" { test "fixture - I078 (Choice with brackets only)" {
try testRuntimeFixture("I078"); try testRuntimeFixture("I078");
} }
test "fixture - I117 (Factorial recursive)" {
try testRuntimeFixture("I117");
}
test "fixture - I118 (Literal unary)" { test "fixture - I118 (Literal unary)" {
try testRuntimeFixture("I118"); try testRuntimeFixture("I118");
} }
test "fixture - I119 (Basic string literals)" {
try testRuntimeFixture("I119");
}
test "fixture - I121 (Arithmetic)" { test "fixture - I121 (Arithmetic)" {
try testRuntimeFixture("I121"); try testRuntimeFixture("I121");
} }
test "fixture - I124 (Evaluating ink functions from game 2)" {
try testRuntimeFixture("I124");
}
test "fixture - I133 (Float printing precision)" { test "fixture - I133 (Float printing precision)" {
try testRuntimeFixture("I133"); try testRuntimeFixture("I133");
} }

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

53
src/Story/testdata/I004/story.ink vendored Normal file
View file

@ -0,0 +1,53 @@
You have {print_num(58)} coins.
=== function print_num(x)
{
- x >= 1000:
{print_num(x / 1000)} thousand { x mod 1000 > 0:{print_num(x mod 1000)}}
- x >= 100:
{print_num(x / 100)} hundred { x mod 100 > 0:and {print_num(x mod 100)}}
- x == 0:
zero
- else:
{ x >= 20:
{ x / 10:
- 2: twenty
- 3: thirty
- 4: forty
- 5: fifty
- 6: sixty
- 7: seventy
- 8: eighty
- 9: ninety
}
{ x mod 10 > 0:
<>-<>
}
}
{ x < 10 || x > 20:
{ x mod 10:
- 1: one
- 2: two
- 3: three
- 4: four
- 5: five
- 6: six
- 7: seven
- 8: eight
- 9: nine
}
- else:
{ x:
- 10: ten
- 11: eleven
- 12: twelve
- 13: thirteen
- 14: fourteen
- 15: fifteen
- 16: sixteen
- 17: seventeen
- 18: eighteen
- 19: nineteen
}
}
}

View file

@ -0,0 +1 @@
You have fifty-eight coins.

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

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

@ -0,0 +1,3 @@
CONST CONST_STR = "ConstantString"
VAR varStr = CONST_STR
{varStr == CONST_STR:success}

View file

@ -0,0 +1 @@
success

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

9
src/Story/testdata/I014/story.ink vendored Normal file
View file

@ -0,0 +1,9 @@
~ f(1, 1)
== function f(x, y) ==
{ x == 1 and y == 1:
~ x = 2
~ f(y, x)
- else:
{x} {y}
}
~ return

View file

@ -0,0 +1 @@
1 2

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

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

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

View file

@ -0,0 +1 @@
hello world

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

@ -0,0 +1 @@
1

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

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

View file

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

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

9
src/Story/testdata/I036/story.ink vendored Normal file
View file

@ -0,0 +1,9 @@
A
~temp someTemp = string()
B
A
{string()}
B
=== function string()
~ return "{3}"
}

View file

@ -0,0 +1,5 @@
A
B
A
3
B

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

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

@ -0,0 +1,6 @@
{isTrue():
x
}
=== function isTrue()
X
~ return true

View file

@ -0,0 +1,2 @@
X
x

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

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

@ -0,0 +1,7 @@
A
{f():X}
C
=== function f()
{ true:
~ return false
}

View file

@ -0,0 +1,2 @@
A
C

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

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

@ -0,0 +1,6 @@
A {f():B}
X
=== function f() ===
{true:
~ return false
}

View file

@ -0,0 +1,2 @@
A
X

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

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

@ -0,0 +1,7 @@
A line.
{ f():
Another line.
}
== function f ==
{false:nothing}
~ return true

View file

@ -0,0 +1,2 @@
A line.
Another line.

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

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

@ -0,0 +1,6 @@
I have {five()} eggs.
== function five ==
{false:
Don't print this
}
five

View file

@ -0,0 +1 @@
I have five eggs.

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

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

@ -0,0 +1,2 @@
Some <>
content<> with glue.

View file

@ -0,0 +1 @@
Some content with glue.

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

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

@ -0,0 +1,6 @@
-> hurry_home
=== hurry_home ===
We hurried home to Savile Row -> as_fast_as_we_could
=== as_fast_as_we_could ===
as fast as we could.
-> DONE

View file

@ -0,0 +1 @@
We hurried home to Savile Row as fast as we could.

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

8
src/Story/testdata/I061/story.ink vendored Normal file
View file

@ -0,0 +1,8 @@
=== intro
= top
{ main: -> done }
-> END
= main
-> top
= done
-> END

View file

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

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

@ -0,0 +1,7 @@
{RunAThing()}
== function RunAThing ==
The first line.
The second line.
== SomewhereElse ==
{"somewhere else"}
->END

View file

@ -0,0 +1,2 @@
The first line.
The second line.

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

8
src/Story/testdata/I076/story.ink vendored Normal file
View file

@ -0,0 +1,8 @@
{ six() + two() }
-> END
=== function six
~ return four() + two()
=== function four
~ return two() + two()
=== function two
~ return 2

View file

@ -0,0 +1 @@
8

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

@ -0,0 +1 @@
1

1
src/Story/testdata/I082/story.ink vendored Normal file
View file

@ -0,0 +1 @@
* choice -> DONE

View file

@ -0,0 +1,2 @@
1: choice
?> choice

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

@ -0,0 +1 @@
1

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

@ -0,0 +1,4 @@
* 'Hello {name()}[, your name is {name()}.'],' I said, knowing full well that his name was {name()}.
-> DONE
== function name ==
Joe

View file

@ -0,0 +1,2 @@
1: 'Hello Joe, your name is Joe.'
?> 'Hello Joe,' I said, knowing full well that his name was Joe.

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

@ -0,0 +1 @@
1

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

@ -0,0 +1,7 @@
-> knot
== knot
* option text[]. {true: Conditional bit.} -> next
-> DONE
== next
Next.
-> DONE

View file

@ -0,0 +1,2 @@
1: option text
?> option text. Conditional bit. Next.

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

55
src/Story/testdata/I094/story.ink vendored Normal file
View file

@ -0,0 +1,55 @@
. {print_num(4)} .
. {print_num(15)} .
. {print_num(37)} .
. {print_num(101)} .
. {print_num(222)} .
. {print_num(1234)} .
=== function print_num(x) ===
{
- x >= 1000:
{print_num(x / 1000)} thousand { x mod 1000 > 0:{print_num(x mod 1000)}}
- x >= 100:
{print_num(x / 100)} hundred { x mod 100 > 0:and {print_num(x mod 100)}}
- x == 0:
zero
- else:
{ x >= 20:
{ x / 10:
- 2: twenty
- 3: thirty
- 4: forty
- 5: fifty
- 6: sixty
- 7: seventy
- 8: eighty
- 9: ninety
}
{ x mod 10 > 0:<>-<>}
}
{ x < 10 || x > 20:
{ x mod 10:
- 1: one
- 2: two
- 3: three
- 4: four
- 5: five
- 6: six
- 7: seven
- 8: eight
- 9: nine
}
- else:
{ x:
- 10: ten
- 11: eleven
- 12: twelve
- 13: thirteen
- 14: fourteen
- 15: fifteen
- 16: sixteen
- 17: seventeen
- 18: eighteen
- 19: nineteen
}
}
}

View file

@ -0,0 +1,6 @@
. four .
. fifteen .
. thirty-seven .
. one hundred and one .
. two hundred and twenty-two .
. one thousand two hundred and thirty-four .

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

8
src/Story/testdata/I095/story.ink vendored Normal file
View file

@ -0,0 +1,8 @@
{true:
a
} <> b
{true:
a
} <> { true:
b
}

View file

@ -0,0 +1,2 @@
a b
a b

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

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

@ -0,0 +1,7 @@
~ func ()
text 2
~ temp tempVar = func ()
text 2
== function func ()
text1
~ return true

View file

@ -0,0 +1,4 @@
text1
text 2
text1
text 2

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

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

@ -0,0 +1,4 @@
{ 1:
- 2: x
- 3: y
}

View file

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

20
src/Story/testdata/I113/story.ink vendored Normal file
View file

@ -0,0 +1,20 @@
VAR x = 3
{
- x == 1: one
- x == 2: two
- else: other
}
{
- x == 1: one
- x == 2: two
- other
}
{ x == 4:
- The main clause
- else: other
}
{ x == 4:
The main clause
- else:
other
}

View file

@ -0,0 +1,4 @@
other
other
other
other

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

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

@ -0,0 +1,5 @@
{ 3:
- 3:
- 4:
txt
}

View file

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

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

@ -0,0 +1,4 @@
{
- false:
beep
}

View file

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

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

@ -0,0 +1,7 @@
{ factorial(5) }
== function factorial(n) ==
{ n == 1:
~ return 1
- else:
~ return (n * factorial(n-1))
}

View file

@ -0,0 +1 @@
120

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

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

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

View file

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

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

12
src/Story/testdata/I124/story.ink vendored Normal file
View file

@ -0,0 +1,12 @@
One
Two
Three
== function func1 ==
This is a function
~ return 5
== function func2 ==
This is a function without a return value
~ return
== function add(x,y) ==
x = {x}, y = {y}
~ return x + y

View file

@ -0,0 +1,3 @@
One
Two
Three

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

@ -0,0 +1 @@
1

10
src/Story/testdata/I127/story.ink vendored Normal file
View file

@ -0,0 +1,10 @@
VAR testVar = 5
VAR testVar2 = 10
Hello world!
~ testVar = 15
~ testVar2 = 100
Hello world 2!
* choice
~ testVar = 25
~ testVar2 = 200
-> END

View file

@ -0,0 +1,5 @@
Hello world!
Hello world 2!
1: choice
?> choice

View file

@ -164,6 +164,7 @@ pub const InternPool = struct {
} }
}; };
// TODO: Revisit this. We might not need this at all.
pub const WorkItem = struct { pub const WorkItem = struct {
tag: Tag, tag: Tag,
next: ?*WorkItem = null, next: ?*WorkItem = null,
@ -174,6 +175,7 @@ pub const WorkItem = struct {
pub const Tag = enum { pub const Tag = enum {
knot, knot,
stitch, stitch,
function,
}; };
}; };
@ -235,6 +237,7 @@ pub const Module = struct {
pub const Tag = enum { pub const Tag = enum {
knot, knot,
stitch, stitch,
function,
var_mut, var_mut,
var_const, var_const,
}; };
@ -315,6 +318,16 @@ pub const Module = struct {
.code_index = @enumFromInt(chunk_index), .code_index = @enumFromInt(chunk_index),
}); });
}, },
.function => {
try sema.analyzeFunction(&builder, work_unit.inst_index);
try builder.finalize();
try mod.stitches.append(gpa, .{
.knot_index = null,
.name_index = work_unit.decl_name,
.code_index = @enumFromInt(chunk_index),
});
},
} }
} }
} }
@ -412,11 +425,13 @@ pub const Module = struct {
pub fn setupStoryRuntime(mod: *Module, gpa: std.mem.Allocator, story: *Story) !void { pub fn setupStoryRuntime(mod: *Module, gpa: std.mem.Allocator, story: *Story) !void {
assert(mod.errors.items.len == 0); assert(mod.errors.items.len == 0);
const constants_len = mod.intern_pool.values.items.len; const constants_len = mod.intern_pool.values.items.len;
var constants_pool: std.ArrayListUnmanaged(Value) = .empty;
try constants_pool.ensureUnusedCapacity(gpa, constants_len);
defer constants_pool.deinit(gpa);
try story.constants_pool.ensureUnusedCapacity(gpa, constants_len);
for (mod.intern_pool.values.items) |value| { for (mod.intern_pool.values.items) |value| {
const obj = try mod.makeValueFromInterned(story, value); const obj = try mod.makeValueFromInterned(story, value);
story.constants_pool.appendAssumeCapacity(obj); constants_pool.appendAssumeCapacity(obj);
} }
for (mod.globals.items) |global| { for (mod.globals.items) |global| {
const key_bytes = mod.intern_pool.getStrBytes(mod.ir, global.key); const key_bytes = mod.intern_pool.getStrBytes(mod.ir, global.key);
@ -459,8 +474,13 @@ pub const Module = struct {
const parent_knot_value = story.globals.get(parent_knot_name).?; const parent_knot_value = story.globals.get(parent_knot_name).?;
const parent_knot_obj: *Object.Knot = @ptrCast(parent_knot_value.?.object); const parent_knot_obj: *Object.Knot = @ptrCast(parent_knot_value.?.object);
try parent_knot_obj.members.put(gpa, name_bytes, &stitch_obj.base); try parent_knot_obj.members.put(gpa, name_bytes, &stitch_obj.base);
} else {
const value: Value = .{ .object = &stitch_obj.base };
try story.globals.put(gpa, name_bytes, value);
} }
} }
story.constants_pool = try constants_pool.toOwnedSlice(gpa);
story.string_bytes = mod.ir.string_bytes; story.string_bytes = mod.ir.string_bytes;
mod.ir.string_bytes = &.{}; mod.ir.string_bytes = &.{};
} }

View file

@ -293,7 +293,10 @@ pub const Writer = struct {
.declaration => try self.writeDeclarationInst(w, inst), .declaration => try self.writeDeclarationInst(w, inst),
.decl_var => try self.writeVarDeclInst(w, inst), .decl_var => try self.writeVarDeclInst(w, inst),
.decl_knot => try self.writeKnotDeclInst(w, inst), .decl_knot => try self.writeKnotDeclInst(w, inst),
// TODO: Revisit this.
.decl_stitch => try self.writeVarDeclInst(w, inst), .decl_stitch => try self.writeVarDeclInst(w, inst),
// TODO: Revisit this.
.decl_function => try self.writeVarDeclInst(w, inst),
.decl_ref => try self.writeStrTokInst(w, inst), .decl_ref => try self.writeStrTokInst(w, inst),
.condbr => try self.writeCondbrInst(w, inst), .condbr => try self.writeCondbrInst(w, inst),
.@"break" => try self.writeBreakInst(w, inst), .@"break" => try self.writeBreakInst(w, inst),
@ -323,6 +326,7 @@ pub const Writer = struct {
.content_push => try self.writeUnaryInst(w, inst), .content_push => try self.writeUnaryInst(w, inst),
.content_flush => try self.writeUnaryInst(w, inst), .content_flush => try self.writeUnaryInst(w, inst),
.choice_br => try self.writeChoiceBrInst(w, inst), .choice_br => try self.writeChoiceBrInst(w, inst),
.ret => try self.writeUnaryInst(w, inst),
.implicit_ret => try self.writeUnaryInst(w, inst), .implicit_ret => try self.writeUnaryInst(w, inst),
.call => try self.writeCallInst(w, inst, .direct), .call => try self.writeCallInst(w, inst, .direct),
.divert => try self.writeCallInst(w, inst, .direct), .divert => try self.writeCallInst(w, inst, .direct),