diff --git a/src/AstGen.zig b/src/AstGen.zig index 1860258..c93feba 100644 --- a/src/AstGen.zig +++ b/src/AstGen.zig @@ -280,6 +280,13 @@ const GenIr = struct { 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 { return .{ .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_lesser_expr => return binaryOp(gi, scope, node, .cmp_lt), .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_start_expr => unreachable, .choice_option_expr => unreachable, @@ -1171,7 +1178,9 @@ fn callExpr( const scratch_top = astgen.scratch.items.len; 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; 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 { + // TODO: Revisit this. const data = node.data.bin; 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 { const astgen = gi.astgen; 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(); defer child_scope.deinit(); - for (stmt_list) |inner_node| { - _ = switch (inner_node.tag) { - .var_decl => try varDecl(gi, &child_scope, inner_node), - .const_decl => try varDecl(gi, &child_scope, inner_node), - .temp_decl => try tempDecl(gi, &child_scope, inner_node), - .assign_stmt => try assignStmt(gi, &child_scope, inner_node), - .content_stmt => try contentStmt(gi, &child_scope, inner_node), - .choice_stmt => try choiceStmt(gi, &child_scope, inner_node), - .expr_stmt => try exprStmt(gi, &child_scope, inner_node), - .divert_stmt => try divertStmt(gi, &child_scope, inner_node), + for (stmt_list) |node| { + _ = switch (node.tag) { + .var_decl => try varDecl(gi, &child_scope, node), + .const_decl => try varDecl(gi, &child_scope, node), + .temp_decl => try tempDecl(gi, &child_scope, node), + .assign_stmt => try assignStmt(gi, &child_scope, node), + .content_stmt => try contentStmt(gi, &child_scope, node), + .choice_stmt => try choiceStmt(gi, &child_scope, node), + .expr_stmt => try exprStmt(gi, &child_scope, 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)), }; } - _ = 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 { @@ -1415,9 +1437,10 @@ fn stitchDeclInner( } } 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 { - _ = try decl_block.addUnaryNode(.implicit_ret, .none); + try blockInner(&decl_block, scope, &.{}); } 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); } -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 { const astgen = gi.astgen; diff --git a/src/Ir.zig b/src/Ir.zig index c6d96e6..678e07f 100644 --- a/src/Ir.zig +++ b/src/Ir.zig @@ -147,10 +147,16 @@ pub const Inst = struct { pub const Tag = enum { file, + /// Uses the `payload` union field. declaration, + /// Uses the `payload` union field. decl_var, + /// Uses the `payload` union field. decl_knot, + /// Uses the `payload` union field. decl_stitch, + /// Uses the `payload` union field. + decl_function, /// Uses the `str_tok` union field. decl_ref, alloc, @@ -199,6 +205,9 @@ pub const Inst = struct { content_push, content_flush, choice_br, + // Uses the `un` union field. + ret, + // Uses the `un` union field. implicit_ret, call, divert, @@ -264,6 +273,11 @@ pub const Inst = struct { body_len: u32, }; + pub const Function = struct { + /// Number of instructions for the function's body block. + body_len: u32, + }; + pub const Field = struct { lhs: Ref, field_name_start: NullTerminatedString, @@ -324,4 +338,56 @@ pub const Inst = struct { 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, + }; + } }; diff --git a/src/Sema.zig b/src/Sema.zig index 9096488..d93d392 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -29,6 +29,7 @@ pub const ValueInfo = union(enum) { variable: InternPool.Index, knot: InternPool.Index, stitch: InternPool.Index, + function: InternPool.Index, temp: u32, }; @@ -294,7 +295,11 @@ pub const Builder = struct { const local_index = try self.getOrPutConstantIndex(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); try self.addConstOp(.load_global, @intCast(local_index)); }, @@ -536,7 +541,7 @@ fn irDeclRef( const ident = try sema.lookupIdentifier(builder, ip_index, src_loc); if (inline_block) { switch (ident.tag) { - .knot, .stitch => unreachable, + .knot, .stitch, .function => unreachable, .var_const => return sema.resolveGlobalDecl(builder, ip_index, src_loc), .var_mut => return sema.fail( src_loc, @@ -548,6 +553,7 @@ fn irDeclRef( switch (ident.tag) { .knot => return .{ .knot = ip_index }, .stitch => return .{ .stitch = ip_index }, + .function => return .{ .function = ip_index }, .var_mut => 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)); }, .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", .{}), + .stitch => |_| return sema.fail(src, "could not assign to stitch", .{}), + .function => |_| return sema.fail(src, "could not assign to function", .{}), } 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 { try builder.addByteOp(.exit); } -fn irCall(_: *Sema, _: *Builder, _: Ir.Inst.Index) !ValueInfo { - return .none; +fn irCall( + 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( @@ -809,13 +870,8 @@ fn irDivert( const arg_end = sema.ir.extra[extra.end + i]; defer arg_start = arg_end; const arg_body = body[arg_start..arg_end]; - _ = try analyzeBodyInner(sema, builder, @ptrCast(arg_body), false); - // FIXME: hack - { - const last_inst: Ir.Inst.Index = @enumFromInt(arg_body[arg_body.len - 1]); - const val = sema.resolveInst(last_inst.toRef()); - try builder.ensureLoad(val); - } + const arg_value = try analyzeBodyInner(sema, builder, @ptrCast(arg_body), false); + if (arg_value != .none) try builder.ensureLoad(arg_value); } 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( sema: *Sema, builder: *Builder, @@ -894,6 +965,7 @@ fn analyzeBodyInner( .decl_var => unreachable, // never present inside block bodies .decl_knot => 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), .store => { try irStore(sema, builder, inst); @@ -919,6 +991,10 @@ fn analyzeBodyInner( .cmp_gt => try irBinaryOp(sema, builder, inst, .cmp_gt), .cmp_gte => try irBinaryOp(sema, builder, inst, .cmp_gte), .decl_ref => try irDeclRef(sema, builder, inst, inline_block), + .ret => { + try irRet(sema, builder, inst); + continue; + }, .implicit_ret => { try irImplicitRet(sema, builder, inst); continue; @@ -948,12 +1024,12 @@ fn analyzeBodyInner( try irSwitchBr(sema, builder, inst); continue; }, - .call => try irCall(sema, builder, inst), + .call => try irCall(sema, builder, inst, .direct), + .field_call => try irCall(sema, builder, inst, .field), .divert => { try irDivert(sema, builder, inst, .direct); continue; }, - .field_call => try irCall(sema, builder, inst), .field_divert => { try irDivert(sema, builder, inst, .field); continue; @@ -968,6 +1044,7 @@ fn analyzeBodyInner( return result; } +// TODO: No return allowed. pub fn analyzeStitch( sema: *Sema, builder: *Builder, @@ -979,6 +1056,19 @@ pub fn analyzeStitch( _ = 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( sema: *Sema, builder: *Builder, @@ -1088,7 +1178,6 @@ fn scanTopLevelDecl( .namespace = child_namespace, }; } - try sema.module.queueWorkItem(.{ .tag = .stitch, .decl_name = decl_name, @@ -1096,6 +1185,27 @@ fn scanTopLevelDecl( .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, } } diff --git a/src/Story.zig b/src/Story.zig index d78bbff..4c10da0 100644 --- a/src/Story.zig +++ b/src/Story.zig @@ -14,13 +14,16 @@ dump_writer: ?*std.Io.Writer = null, is_exited: bool = false, can_advance: bool = false, choice_index: usize = 0, +stack_top: usize = 0, +call_stack_top: usize = 0, current_choices: std.ArrayListUnmanaged(Choice) = .empty, code_chunks: std.ArrayListUnmanaged(*Object.Code) = .empty, -constants_pool: std.ArrayListUnmanaged(Value) = .empty, globals: std.StringHashMapUnmanaged(?Value) = .empty, -stack: std.ArrayListUnmanaged(?Value) = .empty, -call_stack: std.ArrayListUnmanaged(CallFrame) = .empty, -stack_max: usize = 128, +stack: []Value = &.{}, +call_stack: []CallFrame = &.{}, +/// Global constants pool. +constants_pool: []const Value = &.{}, +/// Linked list of all tracked runtime objects. gc_objects: std.SinglyLinkedList = .{}, // FIXME: This was a hack to keep string bytes alive. string_bytes: []const u8 = &.{}, @@ -28,6 +31,7 @@ string_bytes: []const u8 = &.{}, pub const default_knot_name: [:0]const u8 = "$__main__$"; pub const Value = union(enum) { + nil, bool: bool, int: i64, float: f64, @@ -35,6 +39,7 @@ pub const Value = union(enum) { pub fn tagBytes(v: Value) []const u8 { return switch (v) { + .nil => "Nil", .bool => "Bool", .int => "Int", .float => "Float", @@ -57,7 +62,7 @@ pub const Value = union(enum) { pub fn isTruthy(v: Value) bool { return switch (v) { - //.nil => false, + .nil => false, .bool => |b| b, .int => |i| i != 0, .float => |f| f != 0.0, @@ -133,6 +138,7 @@ pub const Value = union(enum) { pub fn eql(lhs: Value, rhs: Value) bool { return switch (lhs) { + .nil => rhs == .nil, .bool => |l| rhs == .bool and l == rhs.bool, .int => |l| switch (rhs) { .int => |r| l == r, @@ -183,15 +189,15 @@ pub const Value = union(enum) { pub fn negate(lhs: Value) !Value { switch (lhs) { - .bool => return error.TypeError, + .nil, .bool, .object => return error.TypeError, .int => |int| return .{ .int = -int }, .float => |float| return .{ .float = -float }, - .object => return error.TypeError, } } pub fn format(value: Value, writer: *std.Io.Writer) error{WriteFailed}!void { switch (value) { + .nil => try writer.writeAll(value.tagBytes()), .bool => |boolean| try writer.writeAll(if (boolean) "true" else "false"), .int => |int| try writer.print("{d}", .{int}), .float => |float| { @@ -222,9 +228,10 @@ pub const Value = union(enum) { }; pub const CallFrame = struct { + callee: *Object.Knot, + caller_top: usize, ip: usize, sp: usize, - callee: *Object.Knot, }; pub const Choice = struct { @@ -304,62 +311,52 @@ pub fn deinit(story: *Story) void { } story.current_choices.deinit(gpa); - story.constants_pool.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 { - return vm.call_stack.items.len == 0; + gpa.free(story.string_bytes); + gpa.free(story.constants_pool); + gpa.free(story.stack); + gpa.free(story.call_stack); } 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 { - const stack_top = vm.stack.items.len; - if (stack_top <= offset) return null; - - return vm.stack.items[stack_top - offset - 1]; + if (vm.stack_top <= offset) return null; + return vm.stack[vm.stack_top - offset - 1]; } fn pushStack(vm: *Story, value: Value) !void { - const gpa = vm.allocator; - const stack_top = vm.stack.items.len; - const max_stack_top = vm.stack_max; - if (stack_top >= max_stack_top) return error.StackOverflow; - - return vm.stack.append(gpa, value); + if (vm.stack_top >= vm.stack.len) return error.StackOverflow; + vm.stack[vm.stack_top] = value; + vm.stack_top += 1; } 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 { if (offset >= frame.callee.code.constants.len) return error.InvalidArgument; - 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 { - const stack_top = vm.stack.items.len; - const stack_offset = frame.sp + offset; - assert(stack_top > stack_offset); - - return vm.stack.items[stack_offset]; + assert(vm.stack_top > frame.sp + offset); + return vm.stack[frame.sp + offset]; } fn setLocal(vm: *Story, frame: *CallFrame, offset: u8, value: Value) void { - const stack_top = vm.stack.items.len; - const stack_offset = frame.sp + offset; - assert(stack_top > stack_offset); - - vm.stack.items[stack_offset] = value; + assert(vm.stack_top > frame.sp + offset); + vm.stack[frame.sp + offset] = value; } // 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) { const gpa = vm.allocator; + if (vm.call_stack_top == 0) return .empty; errdefer vm.can_advance = false; - if (vm.isCallStackEmpty()) return .empty; - var stream_writer = std.Io.Writer.Allocating.init(gpa); defer stream_writer.deinit(); + var frame = vm.currentFrame(); while (true) { - const frame = vm.currentFrame(); const code = std.mem.bytesAsSlice(Opcode, frame.callee.code.bytecode); if (vm.dump_writer) |w| { Dumper.trace(vm, w, frame) catch {}; @@ -419,14 +415,27 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { vm.can_advance = false; 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 => { - const value: Value = .{ .bool = true }; - try vm.pushStack(value); + try vm.pushStack(.{ .bool = true }); frame.ip += 1; }, .false => { - const value: Value = .{ .bool = false }; - try vm.pushStack(value); + try vm.pushStack(.{ .bool = false }); frame.ip += 1; }, .pop => { @@ -519,6 +528,43 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { 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 => { const index: u8 = @intFromEnum(code[frame.ip + 1]); const value = try vm.getConstant(frame, index); @@ -611,16 +657,6 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { 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 => { const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]); frame.ip += 2; @@ -667,38 +703,62 @@ pub fn getKnot(vm: *Story, name: []const u8) ?*Object.Knot { return knot; } -// TODO(Brett): Add arguments? -fn divertToKnot(vm: *Story, knot: *Object.Knot) !void { - const gpa = vm.allocator; - const stack_ptr = vm.stack.items.len - knot.code.args_count; - const stack_needed = knot.code.stack_size; +fn call(vm: *Story, knot: *Object.Knot) !void { + if (vm.call_stack_top >= vm.call_stack.len) + return error.CallStackOverflow; - try vm.stack.ensureUnusedCapacity(gpa, stack_needed); - try vm.call_stack.ensureUnusedCapacity(gpa, 1); + 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; - 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, .ip = 0, - .sp = stack_ptr, - }); - vm.stack.appendNTimesAssumeCapacity(null, stack_needed); - vm.can_advance = true; + .sp = sp, + .caller_top = caller_top, + }; + vm.call_stack_top += 1; } -fn divertToValue(vm: *Story, value: Value) !void { - switch (value) { - .object => |object| switch (object.tag) { - .knot => try divertToKnot(vm, @ptrCast(object)), - else => return error.TypeError, - }, - else => return error.TypeError, +// 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], + ); } -} -fn divert(vm: *Story, knot_name: []const u8) !void { - return if (getKnot(vm, knot_name)) |knot| { - return divertToKnot(vm, knot); - } else return error.InvalidPath; + 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, + }; } pub const LoadOptions = struct { @@ -746,15 +806,25 @@ pub fn loadFromString( 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 = .{ .allocator = gpa, .can_advance = false, .dump_writer = if (options.dump_trace) options.dump_writer else null, + .stack = eval_stack_ptr, + .call_stack = call_stack_ptr, }; errdefer story.deinit(); + try comp.setupStoryRuntime(gpa, &story); if (story.getKnot(Story.default_knot_name)) |knot| { - try story.divertToKnot(knot); + try story.divert(knot); story.can_advance = true; } return story; diff --git a/src/Story/Dumper.zig b/src/Story/Dumper.zig index da42c9f..02e809e 100644 --- a/src/Story/Dumper.zig +++ b/src/Story/Dumper.zig @@ -22,7 +22,6 @@ fn dumpByteInst( op: Opcode, ) !usize { const code = knot.code; - const constants_pool = &self.story.constants_pool; assert(code.bytecode.len > offset + 1); const arg = code.bytecode[offset + 1]; @@ -31,9 +30,8 @@ fn dumpByteInst( try w.writeAll(" ("); if (code.constants.len > arg) { const constant_index = code.constants[arg]; - if (constants_pool.items.len > constant_index) { - const global_constant = &constants_pool.items[constant_index]; - try self.dumpValue(w, global_constant); + if (self.story.constants_pool.len > constant_index) { + try self.dumpValue(w, &self.story.constants_pool[constant_index]); } else { try w.writeAll("invalid!"); } @@ -55,14 +53,11 @@ fn dumpGlobalInst( op: Opcode, ) !usize { const code = knot.code; - const constants_pool = &self.story.constants_pool; - assert(code.bytecode.len > offset + 1); const arg = code.bytecode[offset + 1]; assert(code.constants.len > arg); const constant_index = code.constants[arg]; - const global_constant = constants_pool.items[constant_index]; - switch (global_constant) { + switch (self.story.constants_pool[constant_index]) { .object => |object| switch (object.tag) { .string => { 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 { switch (value.*) { + .nil => try w.print( + "<{s}>", + .{value.tagBytes()}, + ), .bool => |_| try w.print( "<{s} value={f}>", .{ value.tagBytes(), value }, @@ -313,8 +312,8 @@ pub fn dump(story: *Story, writer: *std.Io.Writer) !void { try writer.writeAll("=== Constants ===\n"); var i: usize = 0; - while (i < story.constants_pool.items.len) : (i += 1) { - const global_constant = &story.constants_pool.items[i]; + while (i < story.constants_pool.len) : (i += 1) { + const global_constant = &story.constants_pool[i]; try story_dumper.dumpValue(writer, global_constant); 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 { - var dumper: Dumper = .{ .story = story }; - const stack = &story.stack; - const stack_top = story.stack.items.len; - +pub fn trace(vm: *Story, writer: *std.Io.Writer, frame: *Story.CallFrame) !void { + var dumper: Dumper = .{ .story = vm }; try writer.print("\tStack => stack_pointer={d}, objects=[", .{frame.sp}); - if (stack_top > 0) { - // FIXME: There has to be a better way to do this. - if (stack_top > 1) { - var i: usize = frame.sp; - 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"); - } + const window = vm.stack[frame.sp..vm.stack_top]; + for (window, 0..) |*slot, i| { + if (i > 0) try writer.writeAll(", "); + try dumper.dumpValue(writer, slot); } try writer.writeAll("]\n"); diff --git a/src/Story/Object.zig b/src/Story/Object.zig index e331e31..59f677a 100644 --- a/src/Story/Object.zig +++ b/src/Story/Object.zig @@ -95,7 +95,7 @@ pub const String = struct { const print_buffer_len = 64; var print_buffer: [print_buffer_len]u8 = undefined; switch (value) { - .bool, .int, .float => { + .nil, .bool, .int, .float => { const bytes = try std.fmt.bufPrint(&print_buffer, "{f}", .{value}); return .create(story, .{ .bytes = bytes }); }, diff --git a/src/Story/runtime_tests.zig b/src/Story/runtime_tests.zig index cf9dd0c..0a47b50 100644 --- a/src/Story/runtime_tests.zig +++ b/src/Story/runtime_tests.zig @@ -18,10 +18,18 @@ test "fixture - I002 (Fogg comforts Passepartout)" { try testRuntimeFixture("I002"); } +test "fixture - I004 (Print number as English)" { + try testRuntimeFixture("I004"); +} + test "fixture - I005 (Const variable)" { try testRuntimeFixture("I005"); } +test "fixture - I006 (Multiple constant references)" { + try testRuntimeFixture("I006"); +} + test "fixture - I007 (Set non existant variable)" { try testRuntimeFixture("I007"); } @@ -35,6 +43,10 @@ test "fixture - I011 (Temporaries at global scope)" { // try testRuntimeFixture("I012"); //} +test "fixture - I014 (Variable swap recurse)" { + try testRuntimeFixture("I014"); +} + test "fixture - I016 (Empty)" { try testRuntimeFixture("I016"); } @@ -59,10 +71,26 @@ test "fixture - I023 (Whitespace)" { 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)" { 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)" { try testRuntimeFixture("I042"); } @@ -75,18 +103,35 @@ test "fixture - I064 (Done stops thread)" { 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)" { try testRuntimeFixture("I078"); } +test "fixture - I117 (Factorial recursive)" { + try testRuntimeFixture("I117"); +} + test "fixture - I118 (Literal unary)" { try testRuntimeFixture("I118"); } +test "fixture - I119 (Basic string literals)" { + try testRuntimeFixture("I119"); +} + test "fixture - I121 (Arithmetic)" { try testRuntimeFixture("I121"); } +test "fixture - I124 (Evaluating ink functions from game 2)" { + try testRuntimeFixture("I124"); +} + test "fixture - I133 (Float printing precision)" { try testRuntimeFixture("I133"); } diff --git a/src/Story/testdata/I004/input.txt b/src/Story/testdata/I004/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I004/story.ink b/src/Story/testdata/I004/story.ink new file mode 100644 index 0000000..5d20aef --- /dev/null +++ b/src/Story/testdata/I004/story.ink @@ -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 + } + } +} diff --git a/src/Story/testdata/I004/transcript.txt b/src/Story/testdata/I004/transcript.txt new file mode 100644 index 0000000..e715bf2 --- /dev/null +++ b/src/Story/testdata/I004/transcript.txt @@ -0,0 +1 @@ +You have fifty-eight coins. diff --git a/src/Story/testdata/I006/input.txt b/src/Story/testdata/I006/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I006/story.ink b/src/Story/testdata/I006/story.ink new file mode 100644 index 0000000..48e21b3 --- /dev/null +++ b/src/Story/testdata/I006/story.ink @@ -0,0 +1,3 @@ +CONST CONST_STR = "ConstantString" +VAR varStr = CONST_STR +{varStr == CONST_STR:success} diff --git a/src/Story/testdata/I006/transcript.txt b/src/Story/testdata/I006/transcript.txt new file mode 100644 index 0000000..2e9ba47 --- /dev/null +++ b/src/Story/testdata/I006/transcript.txt @@ -0,0 +1 @@ +success diff --git a/src/Story/testdata/I014/input.txt b/src/Story/testdata/I014/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I014/story.ink b/src/Story/testdata/I014/story.ink new file mode 100644 index 0000000..8593db3 --- /dev/null +++ b/src/Story/testdata/I014/story.ink @@ -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 diff --git a/src/Story/testdata/I014/transcript.txt b/src/Story/testdata/I014/transcript.txt new file mode 100644 index 0000000..8d04f96 --- /dev/null +++ b/src/Story/testdata/I014/transcript.txt @@ -0,0 +1 @@ +1 2 diff --git a/src/Story/testdata/I033/input.txt b/src/Story/testdata/I033/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I033/story.ink b/src/Story/testdata/I033/story.ink new file mode 100644 index 0000000..f6a7994 --- /dev/null +++ b/src/Story/testdata/I033/story.ink @@ -0,0 +1,4 @@ +hello -> world +== world +world +-> END diff --git a/src/Story/testdata/I033/transcript.txt b/src/Story/testdata/I033/transcript.txt new file mode 100644 index 0000000..3b18e51 --- /dev/null +++ b/src/Story/testdata/I033/transcript.txt @@ -0,0 +1 @@ +hello world diff --git a/src/Story/testdata/I034/input.txt b/src/Story/testdata/I034/input.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/src/Story/testdata/I034/input.txt @@ -0,0 +1 @@ +1 diff --git a/src/Story/testdata/I034/story.ink b/src/Story/testdata/I034/story.ink new file mode 100644 index 0000000..d201214 --- /dev/null +++ b/src/Story/testdata/I034/story.ink @@ -0,0 +1,4 @@ +* hello -> world +== world +world +-> END diff --git a/src/Story/testdata/I034/transcript.txt b/src/Story/testdata/I034/transcript.txt new file mode 100644 index 0000000..b035082 --- /dev/null +++ b/src/Story/testdata/I034/transcript.txt @@ -0,0 +1,2 @@ +1: hello +?> hello world diff --git a/src/Story/testdata/I036/input.txt b/src/Story/testdata/I036/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I036/story.ink b/src/Story/testdata/I036/story.ink new file mode 100644 index 0000000..43e5e04 --- /dev/null +++ b/src/Story/testdata/I036/story.ink @@ -0,0 +1,9 @@ +A +~temp someTemp = string() +B +A +{string()} +B +=== function string() + ~ return "{3}" +} diff --git a/src/Story/testdata/I036/transcript.txt b/src/Story/testdata/I036/transcript.txt new file mode 100644 index 0000000..6c938a1 --- /dev/null +++ b/src/Story/testdata/I036/transcript.txt @@ -0,0 +1,5 @@ +A +B +A +3 +B diff --git a/src/Story/testdata/I037/input.txt b/src/Story/testdata/I037/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I037/story.ink b/src/Story/testdata/I037/story.ink new file mode 100644 index 0000000..7fbf2e0 --- /dev/null +++ b/src/Story/testdata/I037/story.ink @@ -0,0 +1,6 @@ +{isTrue(): + x +} +=== function isTrue() + X + ~ return true diff --git a/src/Story/testdata/I037/transcript.txt b/src/Story/testdata/I037/transcript.txt new file mode 100644 index 0000000..f34e05a --- /dev/null +++ b/src/Story/testdata/I037/transcript.txt @@ -0,0 +1,2 @@ +X +x diff --git a/src/Story/testdata/I044/input.txt b/src/Story/testdata/I044/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I044/story.ink b/src/Story/testdata/I044/story.ink new file mode 100644 index 0000000..fb91946 --- /dev/null +++ b/src/Story/testdata/I044/story.ink @@ -0,0 +1,7 @@ +A +{f():X} +C +=== function f() +{ true: + ~ return false +} diff --git a/src/Story/testdata/I044/transcript.txt b/src/Story/testdata/I044/transcript.txt new file mode 100644 index 0000000..8ec30d8 --- /dev/null +++ b/src/Story/testdata/I044/transcript.txt @@ -0,0 +1,2 @@ +A +C diff --git a/src/Story/testdata/I045/input.txt b/src/Story/testdata/I045/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I045/story.ink b/src/Story/testdata/I045/story.ink new file mode 100644 index 0000000..31853d9 --- /dev/null +++ b/src/Story/testdata/I045/story.ink @@ -0,0 +1,6 @@ +A {f():B} +X +=== function f() === +{true: + ~ return false +} diff --git a/src/Story/testdata/I045/transcript.txt b/src/Story/testdata/I045/transcript.txt new file mode 100644 index 0000000..2a8510f --- /dev/null +++ b/src/Story/testdata/I045/transcript.txt @@ -0,0 +1,2 @@ +A +X diff --git a/src/Story/testdata/I046/input.txt b/src/Story/testdata/I046/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I046/story.ink b/src/Story/testdata/I046/story.ink new file mode 100644 index 0000000..c259ea2 --- /dev/null +++ b/src/Story/testdata/I046/story.ink @@ -0,0 +1,7 @@ +A line. +{ f(): + Another line. +} +== function f == +{false:nothing} +~ return true diff --git a/src/Story/testdata/I046/transcript.txt b/src/Story/testdata/I046/transcript.txt new file mode 100644 index 0000000..e5c2c36 --- /dev/null +++ b/src/Story/testdata/I046/transcript.txt @@ -0,0 +1,2 @@ +A line. +Another line. diff --git a/src/Story/testdata/I047/input.txt b/src/Story/testdata/I047/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I047/story.ink b/src/Story/testdata/I047/story.ink new file mode 100644 index 0000000..f8e33a4 --- /dev/null +++ b/src/Story/testdata/I047/story.ink @@ -0,0 +1,6 @@ +I have {five()} eggs. +== function five == +{false: + Don't print this +} +five diff --git a/src/Story/testdata/I047/transcript.txt b/src/Story/testdata/I047/transcript.txt new file mode 100644 index 0000000..a11cb01 --- /dev/null +++ b/src/Story/testdata/I047/transcript.txt @@ -0,0 +1 @@ +I have five eggs. diff --git a/src/Story/testdata/I048/input.txt b/src/Story/testdata/I048/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I048/story.ink b/src/Story/testdata/I048/story.ink new file mode 100644 index 0000000..14e3955 --- /dev/null +++ b/src/Story/testdata/I048/story.ink @@ -0,0 +1,2 @@ +Some <> +content<> with glue. diff --git a/src/Story/testdata/I048/transcript.txt b/src/Story/testdata/I048/transcript.txt new file mode 100644 index 0000000..6d318e5 --- /dev/null +++ b/src/Story/testdata/I048/transcript.txt @@ -0,0 +1 @@ +Some content with glue. diff --git a/src/Story/testdata/I055/input.txt b/src/Story/testdata/I055/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I055/story.ink b/src/Story/testdata/I055/story.ink new file mode 100644 index 0000000..b2f890d --- /dev/null +++ b/src/Story/testdata/I055/story.ink @@ -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 diff --git a/src/Story/testdata/I055/transcript.txt b/src/Story/testdata/I055/transcript.txt new file mode 100644 index 0000000..cedf6ae --- /dev/null +++ b/src/Story/testdata/I055/transcript.txt @@ -0,0 +1 @@ +We hurried home to Savile Row as fast as we could. diff --git a/src/Story/testdata/I061/input.txt b/src/Story/testdata/I061/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I061/story.ink b/src/Story/testdata/I061/story.ink new file mode 100644 index 0000000..df224c2 --- /dev/null +++ b/src/Story/testdata/I061/story.ink @@ -0,0 +1,8 @@ +=== intro += top + { main: -> done } + -> END += main + -> top += done + -> END diff --git a/src/Story/testdata/I061/transcript.txt b/src/Story/testdata/I061/transcript.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I075/input.txt b/src/Story/testdata/I075/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I075/story.ink b/src/Story/testdata/I075/story.ink new file mode 100644 index 0000000..cb3876e --- /dev/null +++ b/src/Story/testdata/I075/story.ink @@ -0,0 +1,7 @@ +{RunAThing()} +== function RunAThing == +The first line. +The second line. +== SomewhereElse == +{"somewhere else"} +->END diff --git a/src/Story/testdata/I075/transcript.txt b/src/Story/testdata/I075/transcript.txt new file mode 100644 index 0000000..7f4264d --- /dev/null +++ b/src/Story/testdata/I075/transcript.txt @@ -0,0 +1,2 @@ +The first line. +The second line. diff --git a/src/Story/testdata/I076/input.txt b/src/Story/testdata/I076/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I076/story.ink b/src/Story/testdata/I076/story.ink new file mode 100644 index 0000000..b7243b4 --- /dev/null +++ b/src/Story/testdata/I076/story.ink @@ -0,0 +1,8 @@ + { six() + two() } + -> END +=== function six + ~ return four() + two() +=== function four + ~ return two() + two() +=== function two + ~ return 2 diff --git a/src/Story/testdata/I076/transcript.txt b/src/Story/testdata/I076/transcript.txt new file mode 100644 index 0000000..45a4fb7 --- /dev/null +++ b/src/Story/testdata/I076/transcript.txt @@ -0,0 +1 @@ +8 diff --git a/src/Story/testdata/I082/input.txt b/src/Story/testdata/I082/input.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/src/Story/testdata/I082/input.txt @@ -0,0 +1 @@ +1 diff --git a/src/Story/testdata/I082/story.ink b/src/Story/testdata/I082/story.ink new file mode 100644 index 0000000..4ae38c4 --- /dev/null +++ b/src/Story/testdata/I082/story.ink @@ -0,0 +1 @@ +* choice -> DONE diff --git a/src/Story/testdata/I082/transcript.txt b/src/Story/testdata/I082/transcript.txt new file mode 100644 index 0000000..abbdc10 --- /dev/null +++ b/src/Story/testdata/I082/transcript.txt @@ -0,0 +1,2 @@ +1: choice +?> choice diff --git a/src/Story/testdata/I085/input.txt b/src/Story/testdata/I085/input.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/src/Story/testdata/I085/input.txt @@ -0,0 +1 @@ +1 diff --git a/src/Story/testdata/I085/story.ink b/src/Story/testdata/I085/story.ink new file mode 100644 index 0000000..bb7cf74 --- /dev/null +++ b/src/Story/testdata/I085/story.ink @@ -0,0 +1,4 @@ +* 'Hello {name()}[, your name is {name()}.'],' I said, knowing full well that his name was {name()}. +-> DONE +== function name == +Joe diff --git a/src/Story/testdata/I085/transcript.txt b/src/Story/testdata/I085/transcript.txt new file mode 100644 index 0000000..e233540 --- /dev/null +++ b/src/Story/testdata/I085/transcript.txt @@ -0,0 +1,2 @@ +1: 'Hello Joe, your name is Joe.' +?> 'Hello Joe,' I said, knowing full well that his name was Joe. diff --git a/src/Story/testdata/I087/input.txt b/src/Story/testdata/I087/input.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/src/Story/testdata/I087/input.txt @@ -0,0 +1 @@ +1 diff --git a/src/Story/testdata/I087/story.ink b/src/Story/testdata/I087/story.ink new file mode 100644 index 0000000..17a1a3f --- /dev/null +++ b/src/Story/testdata/I087/story.ink @@ -0,0 +1,7 @@ +-> knot +== knot + * option text[]. {true: Conditional bit.} -> next + -> DONE +== next + Next. + -> DONE diff --git a/src/Story/testdata/I087/transcript.txt b/src/Story/testdata/I087/transcript.txt new file mode 100644 index 0000000..d96dd69 --- /dev/null +++ b/src/Story/testdata/I087/transcript.txt @@ -0,0 +1,2 @@ +1: option text +?> option text. Conditional bit. Next. diff --git a/src/Story/testdata/I094/input.txt b/src/Story/testdata/I094/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I094/story.ink b/src/Story/testdata/I094/story.ink new file mode 100644 index 0000000..b7a44c6 --- /dev/null +++ b/src/Story/testdata/I094/story.ink @@ -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 + } + } +} diff --git a/src/Story/testdata/I094/transcript.txt b/src/Story/testdata/I094/transcript.txt new file mode 100644 index 0000000..f95c21c --- /dev/null +++ b/src/Story/testdata/I094/transcript.txt @@ -0,0 +1,6 @@ +. four . +. fifteen . +. thirty-seven . +. one hundred and one . +. two hundred and twenty-two . +. one thousand two hundred and thirty-four . diff --git a/src/Story/testdata/I095/input.txt b/src/Story/testdata/I095/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I095/story.ink b/src/Story/testdata/I095/story.ink new file mode 100644 index 0000000..04bb48e --- /dev/null +++ b/src/Story/testdata/I095/story.ink @@ -0,0 +1,8 @@ +{true: + a +} <> b +{true: + a +} <> { true: + b +} diff --git a/src/Story/testdata/I095/transcript.txt b/src/Story/testdata/I095/transcript.txt new file mode 100644 index 0000000..1739755 --- /dev/null +++ b/src/Story/testdata/I095/transcript.txt @@ -0,0 +1,2 @@ +a b +a b diff --git a/src/Story/testdata/I097/input.txt b/src/Story/testdata/I097/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I097/story.ink b/src/Story/testdata/I097/story.ink new file mode 100644 index 0000000..b94e24f --- /dev/null +++ b/src/Story/testdata/I097/story.ink @@ -0,0 +1,7 @@ +~ func () +text 2 +~ temp tempVar = func () +text 2 +== function func () + text1 + ~ return true diff --git a/src/Story/testdata/I097/transcript.txt b/src/Story/testdata/I097/transcript.txt new file mode 100644 index 0000000..26236d2 --- /dev/null +++ b/src/Story/testdata/I097/transcript.txt @@ -0,0 +1,4 @@ +text1 +text 2 +text1 +text 2 diff --git a/src/Story/testdata/I112/input.txt b/src/Story/testdata/I112/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I112/story.ink b/src/Story/testdata/I112/story.ink new file mode 100644 index 0000000..695a162 --- /dev/null +++ b/src/Story/testdata/I112/story.ink @@ -0,0 +1,4 @@ +{ 1: + - 2: x + - 3: y +} diff --git a/src/Story/testdata/I112/transcript.txt b/src/Story/testdata/I112/transcript.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I113/input.txt b/src/Story/testdata/I113/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I113/story.ink b/src/Story/testdata/I113/story.ink new file mode 100644 index 0000000..adef282 --- /dev/null +++ b/src/Story/testdata/I113/story.ink @@ -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 +} diff --git a/src/Story/testdata/I113/transcript.txt b/src/Story/testdata/I113/transcript.txt new file mode 100644 index 0000000..dbfd555 --- /dev/null +++ b/src/Story/testdata/I113/transcript.txt @@ -0,0 +1,4 @@ +other +other +other +other diff --git a/src/Story/testdata/I115/input.txt b/src/Story/testdata/I115/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I115/story.ink b/src/Story/testdata/I115/story.ink new file mode 100644 index 0000000..ac8b815 --- /dev/null +++ b/src/Story/testdata/I115/story.ink @@ -0,0 +1,5 @@ +{ 3: + - 3: + - 4: + txt +} diff --git a/src/Story/testdata/I115/transcript.txt b/src/Story/testdata/I115/transcript.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I116/input.txt b/src/Story/testdata/I116/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I116/story.ink b/src/Story/testdata/I116/story.ink new file mode 100644 index 0000000..78845e1 --- /dev/null +++ b/src/Story/testdata/I116/story.ink @@ -0,0 +1,4 @@ +{ +- false: + beep +} diff --git a/src/Story/testdata/I116/transcript.txt b/src/Story/testdata/I116/transcript.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I117/input.txt b/src/Story/testdata/I117/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I117/story.ink b/src/Story/testdata/I117/story.ink new file mode 100644 index 0000000..aa6449e --- /dev/null +++ b/src/Story/testdata/I117/story.ink @@ -0,0 +1,7 @@ +{ factorial(5) } +== function factorial(n) == + { n == 1: + ~ return 1 + - else: + ~ return (n * factorial(n-1)) + } diff --git a/src/Story/testdata/I117/transcript.txt b/src/Story/testdata/I117/transcript.txt new file mode 100644 index 0000000..52bd8e4 --- /dev/null +++ b/src/Story/testdata/I117/transcript.txt @@ -0,0 +1 @@ +120 diff --git a/src/Story/testdata/I119/input.txt b/src/Story/testdata/I119/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I119/story.ink b/src/Story/testdata/I119/story.ink new file mode 100644 index 0000000..d8d52db --- /dev/null +++ b/src/Story/testdata/I119/story.ink @@ -0,0 +1,3 @@ +VAR x = "Hello world 1" +{x} +Hello {"world"} 2. diff --git a/src/Story/testdata/I119/transcript.txt b/src/Story/testdata/I119/transcript.txt new file mode 100644 index 0000000..37b8249 --- /dev/null +++ b/src/Story/testdata/I119/transcript.txt @@ -0,0 +1,2 @@ +Hello world 1 +Hello world 2. diff --git a/src/Story/testdata/I124/input.txt b/src/Story/testdata/I124/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/I124/story.ink b/src/Story/testdata/I124/story.ink new file mode 100644 index 0000000..c64599c --- /dev/null +++ b/src/Story/testdata/I124/story.ink @@ -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 diff --git a/src/Story/testdata/I124/transcript.txt b/src/Story/testdata/I124/transcript.txt new file mode 100644 index 0000000..4fcefbf --- /dev/null +++ b/src/Story/testdata/I124/transcript.txt @@ -0,0 +1,3 @@ +One +Two +Three diff --git a/src/Story/testdata/I127/input.txt b/src/Story/testdata/I127/input.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/src/Story/testdata/I127/input.txt @@ -0,0 +1 @@ +1 diff --git a/src/Story/testdata/I127/story.ink b/src/Story/testdata/I127/story.ink new file mode 100644 index 0000000..22dc405 --- /dev/null +++ b/src/Story/testdata/I127/story.ink @@ -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 diff --git a/src/Story/testdata/I127/transcript.txt b/src/Story/testdata/I127/transcript.txt new file mode 100644 index 0000000..fbe49ee --- /dev/null +++ b/src/Story/testdata/I127/transcript.txt @@ -0,0 +1,5 @@ +Hello world! +Hello world 2! + +1: choice +?> choice diff --git a/src/compile.zig b/src/compile.zig index cc80d51..7a54542 100644 --- a/src/compile.zig +++ b/src/compile.zig @@ -164,6 +164,7 @@ pub const InternPool = struct { } }; +// TODO: Revisit this. We might not need this at all. pub const WorkItem = struct { tag: Tag, next: ?*WorkItem = null, @@ -174,6 +175,7 @@ pub const WorkItem = struct { pub const Tag = enum { knot, stitch, + function, }; }; @@ -235,6 +237,7 @@ pub const Module = struct { pub const Tag = enum { knot, stitch, + function, var_mut, var_const, }; @@ -315,6 +318,16 @@ pub const Module = struct { .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 { assert(mod.errors.items.len == 0); 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| { const obj = try mod.makeValueFromInterned(story, value); - story.constants_pool.appendAssumeCapacity(obj); + constants_pool.appendAssumeCapacity(obj); } for (mod.globals.items) |global| { 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_obj: *Object.Knot = @ptrCast(parent_knot_value.?.object); 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; mod.ir.string_bytes = &.{}; } diff --git a/src/print_ir.zig b/src/print_ir.zig index 5fc3aa0..698dd22 100644 --- a/src/print_ir.zig +++ b/src/print_ir.zig @@ -293,7 +293,10 @@ pub const Writer = struct { .declaration => try self.writeDeclarationInst(w, inst), .decl_var => try self.writeVarDeclInst(w, inst), .decl_knot => try self.writeKnotDeclInst(w, inst), + // TODO: Revisit this. .decl_stitch => try self.writeVarDeclInst(w, inst), + // TODO: Revisit this. + .decl_function => try self.writeVarDeclInst(w, inst), .decl_ref => try self.writeStrTokInst(w, inst), .condbr => try self.writeCondbrInst(w, inst), .@"break" => try self.writeBreakInst(w, inst), @@ -323,6 +326,7 @@ pub const Writer = struct { .content_push => try self.writeUnaryInst(w, inst), .content_flush => 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), .call => try self.writeCallInst(w, inst, .direct), .divert => try self.writeCallInst(w, inst, .direct),