diff --git a/src/AstGen.zig b/src/AstGen.zig index 0be2315..af51193 100644 --- a/src/AstGen.zig +++ b/src/AstGen.zig @@ -15,6 +15,7 @@ instructions: std.ArrayListUnmanaged(Ir.Inst) = .empty, globals: std.ArrayListUnmanaged(Ir.Global) = .empty, global_ref_table: std.AutoHashMapUnmanaged(Ir.NullTerminatedString, usize) = .empty, extra: std.ArrayListUnmanaged(u32) = .empty, +scratch: std.ArrayListUnmanaged(u32) = .empty, errors: std.ArrayListUnmanaged(Ast.Error) = .empty, pub const InnerError = error{ @@ -32,38 +33,46 @@ pub fn deinit(astgen: *AstGen) void { astgen.global_ref_table.deinit(gpa); astgen.instructions.deinit(gpa); astgen.extra.deinit(gpa); + astgen.scratch.deinit(gpa); astgen.errors.deinit(gpa); } const Scope = struct { parent: ?*Scope, astgen: *AstGen, + namespace_prefix: Ir.NullTerminatedString, decls: std.AutoHashMapUnmanaged(Ir.NullTerminatedString, Decl), - pub const Decl = struct { + const Decl = struct { decl_node: *const Ast.Node, inst_index: Ir.Inst.Index, }; - pub fn deinit(self: *Scope) void { + fn deinit(self: *Scope) void { const gpa = self.astgen.gpa; self.decls.deinit(gpa); } - pub fn makeChild(parent_scope: *Scope) Scope { + fn makeChild(parent_scope: *Scope) Scope { return .{ .parent = parent_scope, .astgen = parent_scope.astgen, + .namespace_prefix = parent_scope.namespace_prefix, .decls = .empty, }; } - pub fn insert(self: *Scope, ref: Ir.NullTerminatedString, decl: Decl) !void { + fn setNamespacePrefix(scope: *Scope, relative: Ir.NullTerminatedString) !void { + const astgen = scope.astgen; + scope.namespace_prefix = try astgen.qualifiedString(scope.namespace_prefix, relative); + } + + fn insert(self: *Scope, ref: Ir.NullTerminatedString, decl: Decl) !void { const gpa = self.astgen.gpa; return self.decls.put(gpa, ref, decl); } - pub fn lookup(self: *Scope, ref: Ir.NullTerminatedString) ?Decl { + fn lookup(self: *Scope, ref: Ir.NullTerminatedString) ?Decl { var current_scope: ?*Scope = self; while (current_scope) |scope| : (current_scope = scope.parent) { const result = scope.decls.get(ref); @@ -149,6 +158,12 @@ const GenIr = struct { } }); } + fn addStr(gi: *GenIr, tag: Ir.Inst.Tag, str: Ir.NullTerminatedString) !Ir.Inst.Ref { + return add(gi, .{ .tag = tag, .data = .{ + .string = .{ .start = str }, + } }); + } + fn addUnaryNode(gi: *GenIr, tag: Ir.Inst.Tag, arg: Ir.Inst.Ref) !Ir.Inst.Ref { return add(gi, .{ .tag = tag, .data = .{ .un = .{ .lhs = arg }, @@ -174,20 +189,21 @@ const GenIr = struct { } }); } - fn makePayloadNode(gi: *GenIr, tag: Ir.Inst.Tag) !Ir.Inst.Index { + fn addPayloadNode(gi: *GenIr, tag: Ir.Inst.Tag, extra: anytype) !Ir.Inst.Ref { const gpa = gi.astgen.gpa; - const inst_index: Ir.Inst.Index = @enumFromInt(gi.astgen.instructions.items.len); - try gi.astgen.instructions.append(gpa, .{ - .tag = tag, - .data = .{ - .payload = .{ .payload_index = undefined }, - }, - }); - return inst_index; - } + try gi.instructions.ensureUnusedCapacity(gpa, 1); + try gi.astgen.instructions.ensureUnusedCapacity(gpa, 1); - fn makeDeclaration(gi: *GenIr) !Ir.Inst.Index { - return makePayloadNode(gi, .declaration); + const payload_index = try gi.astgen.addExtra(extra); + const new_index: Ir.Inst.Index = @enumFromInt(gi.astgen.instructions.items.len); + gi.astgen.instructions.appendAssumeCapacity(.{ + .tag = tag, + .data = .{ .payload = .{ + .payload_index = payload_index, + } }, + }); + gi.instructions.appendAssumeCapacity(new_index); + return new_index.toRef(); } fn makeBlockInst(gi: *GenIr, tag: Ir.Inst.Tag) !Ir.Inst.Index { @@ -208,14 +224,20 @@ const GenIr = struct { const extra_len = @typeInfo(Ir.Inst.Knot).@"struct".fields.len + body.len; try self.astgen.extra.ensureUnusedCapacity(gpa, extra_len); - const knot_node = try makePayloadNode(self, .decl_knot); - const inst_data = &self.astgen.instructions.items[@intFromEnum(knot_node)].data; + const new_index: Ir.Inst.Index = @enumFromInt(self.astgen.instructions.items.len); + try self.astgen.instructions.append(gpa, .{ + .tag = .decl_knot, + .data = .{ + .payload = .{ .payload_index = undefined }, + }, + }); + const inst_data = &self.astgen.instructions.items[@intFromEnum(new_index)].data; inst_data.payload.payload_index = self.astgen.addExtraAssumeCapacity( Ir.Inst.Knot{ .body_len = @intCast(body.len) }, ); self.astgen.appendBlockBody(body); - return knot_node; + return new_index; } fn addVar(self: *GenIr) !Ir.Inst.Index { @@ -304,6 +326,51 @@ const GenIr = struct { } }; +/// Splat an IR data struct into the `extra` array. +fn addExtra(astgen: *AstGen, extra: anytype) !u32 { + const fields = std.meta.fields(@TypeOf(extra)); + try astgen.extra.ensureUnusedCapacity(astgen.gpa, fields.len); + return addExtraAssumeCapacity(astgen, extra); +} + +/// Splat an IR data struct into the `extra` array. +fn addExtraAssumeCapacity(astgen: *AstGen, extra: anytype) u32 { + const fields = std.meta.fields(@TypeOf(extra)); + const extra_index: u32 = @intCast(astgen.extra.items.len); + astgen.extra.items.len += fields.len; + setExtra(astgen, extra_index, extra); + return extra_index; +} + +fn setExtra(astgen: *AstGen, index: usize, extra: anytype) void { + const fields = std.meta.fields(@TypeOf(extra)); + var i = index; + inline for (fields) |field| { + astgen.extra.items[i] = switch (field.type) { + u32 => @field(extra, field.name), + Ir.Inst.Index => @intFromEnum(@field(extra, field.name)), + Ir.Inst.Ref => @intFromEnum(@field(extra, field.name)), + Ir.NullTerminatedString => @intFromEnum(@field(extra, field.name)), + else => @compileError("bad field type"), + }; + i += 1; + } +} + +fn appendBlockBody(astgen: *AstGen, body: []const Ir.Inst.Index) void { + return appendBlockBodyArrayList(astgen, &astgen.extra, body); +} + +fn appendBlockBodyArrayList( + _: *AstGen, + list: *std.ArrayListUnmanaged(u32), + body: []const Ir.Inst.Index, +) void { + for (body) |inst_index| { + list.appendAssumeCapacity(@intFromEnum(inst_index)); + } +} + fn setDeclaration( decl_index: Ir.Inst.Index, args: struct { @@ -311,11 +378,11 @@ fn setDeclaration( tag: Ir.Global.Tag, ref: Ir.Inst.Index, decl_node: *const Ast.Node, - body_gi: *GenIr, + body_block: *GenIr, is_constant: bool = true, }, ) !void { - const astgen = args.body_gi.astgen; + const astgen = args.body_block.astgen; const gpa = astgen.gpa; const extra_len = @typeInfo(Ir.Inst.Declaration).@"struct".fields.len; const global_index = astgen.globals.items.len; @@ -324,22 +391,22 @@ fn setDeclaration( try astgen.globals.ensureUnusedCapacity(gpa, 1); try astgen.global_ref_table.ensureUnusedCapacity(gpa, 1); + if (astgen.global_ref_table.get(args.name)) |_| { + return astgen.fail(.redefined_identifier, args.decl_node); + } + const inst_data = &astgen.instructions.items[@intFromEnum(decl_index)].data; inst_data.payload.payload_index = astgen.addExtraAssumeCapacity( Ir.Inst.Declaration{ .name = args.name, .value = args.ref }, ); - if (astgen.global_ref_table.get(args.name)) |_| { - return astgen.fail(.redefined_identifier, args.decl_node); - } - astgen.globals.appendAssumeCapacity(.{ .tag = args.tag, .name = args.name, .is_constant = args.is_constant, }); astgen.global_ref_table.putAssumeCapacity(args.name, global_index); - args.body_gi.unstack(); + args.body_block.unstack(); } fn setCondBrPayload( @@ -372,29 +439,6 @@ fn setCondBrPayload( astgen.appendBlockBody(else_body); } -fn addExtraAssumeCapacity(astgen: *AstGen, extra: anytype) u32 { - const fields = std.meta.fields(@TypeOf(extra)); - const extra_index: u32 = @intCast(astgen.extra.items.len); - astgen.extra.items.len += fields.len; - setExtra(astgen, extra_index, extra); - return extra_index; -} - -fn setExtra(astgen: *AstGen, index: usize, extra: anytype) void { - const fields = std.meta.fields(@TypeOf(extra)); - var i = index; - inline for (fields) |field| { - astgen.extra.items[i] = switch (field.type) { - u32 => @field(extra, field.name), - Ir.Inst.Index => @intFromEnum(@field(extra, field.name)), - Ir.Inst.Ref => @intFromEnum(@field(extra, field.name)), - Ir.NullTerminatedString => @intFromEnum(@field(extra, field.name)), - else => @compileError("bad field type"), - }; - i += 1; - } -} - fn fail( self: *AstGen, tag: Ast.Error.Tag, @@ -413,12 +457,6 @@ fn fail( return error.SemanticError; } -fn appendBlockBody(self: *AstGen, body: []const Ir.Inst.Index) void { - for (body) |inst_index| { - self.extra.appendAssumeCapacity(@intFromEnum(inst_index)); - } -} - fn sliceFromNode(astgen: *const AstGen, node: *const Ast.Node) []const u8 { assert(node.loc.start <= node.loc.end); const source_bytes = astgen.tree.source; @@ -427,8 +465,8 @@ fn sliceFromNode(astgen: *const AstGen, node: *const Ast.Node) []const u8 { fn stringFromBytes(astgen: *AstGen, bytes: []const u8) error{OutOfMemory}!Ir.NullTerminatedString { const gpa = astgen.gpa; - const str_index: u32 = @intCast(astgen.string_bytes.items.len); const string_bytes = &astgen.string_bytes; + const str_index: u32 = @intCast(string_bytes.items.len); try string_bytes.appendSlice(gpa, bytes); const key: []const u8 = string_bytes.items[str_index..]; @@ -453,6 +491,46 @@ fn stringFromNode(astgen: *AstGen, node: *const Ast.Node) !Ir.NullTerminatedStri return astgen.stringFromBytes(name_bytes); } +fn nullTerminatedString(astgen: *AstGen, str: Ir.NullTerminatedString) [:0]const u8 { + const slice = astgen.string_bytes.items[@intFromEnum(str)..]; + return slice[0..std.mem.indexOfScalar(u8, slice, 0).? :0]; +} + +fn qualifiedString( + astgen: *AstGen, + prefix: Ir.NullTerminatedString, + relative: Ir.NullTerminatedString, +) !Ir.NullTerminatedString { + const gpa = astgen.gpa; + const string_bytes = &astgen.string_bytes; + const string_table = &astgen.string_table; + const str_index: u32 = @intCast(string_bytes.items.len); + + switch (prefix) { + .empty => return relative, + else => |prev| { + try string_bytes.appendSlice(gpa, nullTerminatedString(astgen, prev)); + try string_bytes.append(gpa, '.'); + try string_bytes.appendSlice(gpa, nullTerminatedString(astgen, relative)); + + const key: []const u8 = string_bytes.items[str_index..]; + const gop = try string_table.getOrPutContextAdapted(gpa, key, StringIndexAdapter{ + .bytes = string_bytes, + }, StringIndexContext{ + .bytes = string_bytes, + }); + if (gop.found_existing) { + string_bytes.shrinkRetainingCapacity(str_index); + return @enumFromInt(gop.key_ptr.*); + } else { + gop.key_ptr.* = str_index; + try string_bytes.append(gpa, 0); + return @enumFromInt(str_index); + } + }, + } +} + fn unaryOp( gi: *GenIr, scope: *Scope, @@ -565,7 +643,7 @@ fn expr(gi: *GenIr, scope: *Scope, optional_expr: ?*const Ast.Node) InnerError!I .choice_option_expr => unreachable, .choice_inner_expr => unreachable, .divert_expr => unreachable, - .selector_expr => unreachable, + .selector_expr => return fieldAccess(gi, scope, expr_node), .assign_stmt => unreachable, .block_stmt => unreachable, .content_stmt => unreachable, @@ -825,7 +903,10 @@ fn contentExpr(block: *GenIr, scope: *Scope, expr_node: *const Ast.Node) InnerEr const result = try stringLiteral(block, child_node); _ = try block.addUnaryNode(.content_push, result); }, - .inline_logic_expr => _ = try inlineLogicExpr(block, scope, child_node), + .inline_logic_expr => { + const result = try inlineLogicExpr(block, scope, child_node); + _ = try block.addUnaryNode(.content_push, result); + }, .if_stmt => _ = try ifStmt(block, scope, child_node), .multi_if_stmt => _ = try multiIfStmt(block, scope, child_node), .switch_stmt => _ = try switchStmt(block, scope, child_node), @@ -929,6 +1010,169 @@ fn choiceStmt( astgen.extra.appendSliceAssumeCapacity(case_indexes.items[0..]); } +const Callee = union(enum) { + field: struct { + obj_ptr: Ir.Inst.Ref, + /// Offset into `string_bytes`. + field_name_start: Ir.NullTerminatedString, + }, + direct: Ir.Inst.Ref, +}; + +fn fieldAccess(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref { + assert(node.tag == .selector_expr); + const data = node.data.bin; + + assert(data.rhs.?.tag == .identifier); + const field_str = try gi.astgen.stringFromNode(data.rhs.?); + const lhs = try expr(gi, scope, data.lhs.?); + + return gi.addPayloadNode(.field_ptr, Ir.Inst.Field{ + .lhs = lhs, + .field_name_start = field_str, + }); +} + +/// calleeExpr generates the function part of a call expression (f in f(x)), but +/// *not* the callee argument for the call. Its purpose is to distinguish +/// between standard calls and method call syntax `a.b()`. Thus, if the lhs +/// is a field access, we return using the `field` union field; +/// otherwise, we use the `direct` union field. +fn calleeExpr(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Callee { + switch (node.tag) { + .selector_expr => { + const data = node.data.bin; + const call_target = data.rhs.?; + assert(call_target.tag == .identifier); + + const field_str = try gi.astgen.stringFromNode(call_target); + const lhs = try expr(gi, scope, data.lhs.?); + return .{ + .field = .{ .obj_ptr = lhs, .field_name_start = field_str }, + }; + }, + .identifier => { + return .{ .direct = try expr(gi, scope, node) }; + }, + else => unreachable, + } +} + +fn callExpr( + gi: *GenIr, + scope: *Scope, + node: *const Ast.Node, + comptime call: enum { divert, call }, +) !Ir.Inst.Ref { + const astgen = gi.astgen; + const gpa = astgen.gpa; + const data = node.data.bin; + const callee_node = data.lhs.?; + const args_node = data.rhs; + const callee = try calleeExpr(gi, scope, callee_node); + + const scratch_top = astgen.scratch.items.len; + defer astgen.scratch.shrinkRetainingCapacity(scratch_top); + const args_count = if (args_node) |n| n.data.list.items.?.len else 0; + try astgen.scratch.resize(gpa, scratch_top + args_count); + var scratch_index = scratch_top; + + if (args_node) |n| { + const args_list = n.data.list.items.?; + for (args_list) |arg| { + var arg_block = gi.makeSubBlock(); + defer arg_block.unstack(); + + _ = try expr(&arg_block, scope, arg); + + const body = arg_block.instructionsSlice(); + try astgen.scratch.ensureUnusedCapacity(gpa, body.len); + appendBlockBodyArrayList(astgen, &astgen.scratch, body); + + astgen.scratch.items[scratch_index] = @intCast(astgen.scratch.items.len - scratch_top); + scratch_index += 1; + } + } + switch (callee) { + .direct => |callee_obj| { + const payload_index = try addExtra(astgen, Ir.Inst.Call{ + .callee = callee_obj, + .args_len = @intCast(args_count), + }); + if (args_count != 0) { + try astgen.extra.appendSlice(gpa, astgen.scratch.items[scratch_top..]); + } + return gi.add(.{ + .tag = if (call == .divert) .divert else .call, + .data = .{ .payload = .{ + .payload_index = payload_index, + } }, + }); + }, + .field => |callee_field| { + const payload_index = try addExtra(astgen, Ir.Inst.FieldCall{ + .obj_ptr = callee_field.obj_ptr, + .field_name_start = callee_field.field_name_start, + .args_len = @intCast(args_count), + }); + if (args_count != 0) { + try astgen.extra.appendSlice(gpa, astgen.scratch.items[scratch_top..]); + } + return gi.add(.{ + .tag = if (call == .divert) .field_divert else .field_call, + .data = .{ .payload = .{ + .payload_index = payload_index, + } }, + }); + }, + } +} + +fn divertExpr(gi: *GenIr, scope: *Scope, node: *const Ast.Node) !void { + // FIXME: The AST should always have an args list for these nodes. + const lhs = node.data.bin.lhs.?; + switch (lhs.tag) { + .identifier => { + const callee = try calleeExpr(gi, scope, lhs); + switch (callee) { + .direct => |callee_obj| { + const payload_index = try addExtra(gi.astgen, Ir.Inst.Call{ + .callee = callee_obj, + .args_len = @intCast(0), + }); + _ = try gi.add(.{ + .tag = .divert, + .data = .{ .payload = .{ + .payload_index = payload_index, + } }, + }); + }, + .field => |callee_field| { + const payload_index = try addExtra(gi.astgen, Ir.Inst.FieldCall{ + .obj_ptr = callee_field.obj_ptr, + .field_name_start = callee_field.field_name_start, + .args_len = @intCast(0), + }); + _ = try gi.add(.{ + .tag = .field_divert, + .data = .{ .payload = .{ + .payload_index = payload_index, + } }, + }); + }, + } + }, + else => { + _ = try callExpr(gi, scope, lhs, .divert); + }, + } +} + +fn divertStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) !void { + const data = node.data.bin; + return divertExpr(gi, scope, data.lhs.?); +} + fn tempDecl(gi: *GenIr, scope: *Scope, decl_node: *const Ast.Node) !void { const identifier_node = decl_node.data.bin.lhs.?; const expr_node = decl_node.data.bin.rhs.?; @@ -950,23 +1194,26 @@ fn tempDecl(gi: *GenIr, scope: *Scope, decl_node: *const Ast.Node) !void { fn varDecl(gi: *GenIr, scope: *Scope, decl_node: *const Ast.Node) !void { const astgen = gi.astgen; - const gpa = astgen.gpa; const identifier_node = decl_node.data.bin.lhs.?; const expr_node = decl_node.data.bin.rhs.?; - const decl_inst = try gi.makeDeclaration(); - try gi.instructions.append(gpa, decl_inst); + const decl_inst = try gi.add(.{ + .tag = .declaration, + .data = .{ .payload = .{ + .payload_index = undefined, + } }, + }); var decl_block = gi.makeSubBlock(); defer decl_block.unstack(); _ = try expr(&decl_block, scope, expr_node); const var_inst = try decl_block.addVar(); - try setDeclaration(decl_inst, .{ + try setDeclaration(decl_inst.toIndex().?, .{ .tag = .variable, .name = try astgen.stringFromNode(identifier_node), .ref = var_inst, .decl_node = decl_node, - .body_gi = &decl_block, + .body_block = &decl_block, .is_constant = decl_node.tag == .const_decl, }); } @@ -984,9 +1231,11 @@ fn blockInner(gi: *GenIr, parent_scope: *Scope, stmt_list: []*Ast.Node) !void { .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), else => unreachable, }; } + _ = try gi.addUnaryNode(.implicit_ret, .none); } fn blockStmt(block: *GenIr, scope: *Scope, stmt_node: *const Ast.Node) InnerError!void { @@ -995,74 +1244,143 @@ fn blockStmt(block: *GenIr, scope: *Scope, stmt_node: *const Ast.Node) InnerErro try blockInner(block, scope, block_stmts); } -const main_knot_name: [:0]const u8 = "$__main__$"; - fn defaultBlock( gi: *GenIr, scope: *Scope, body_node: *const Ast.Node, ) InnerError!void { const astgen = gi.astgen; - const gpa = astgen.gpa; - const decl_inst = try gi.makeDeclaration(); - try gi.instructions.append(gpa, decl_inst); + const decl_inst = try gi.addAsIndex(.{ + .tag = .declaration, + .data = .{ .payload = .{ + .payload_index = undefined, + } }, + }); var decl_scope = gi.makeSubBlock(); defer decl_scope.unstack(); // TODO: Make sure that this value is concrete to omit check. - const block_stmts = body_node.data.list.items orelse unreachable; + const block_stmts = body_node.data.list.items.?; try blockInner(&decl_scope, scope, block_stmts); const knot_inst = try decl_scope.addKnot(); try setDeclaration(decl_inst, .{ .tag = .knot, - .decl_node = body_node, - .name = try astgen.stringFromBytes("$__main__$"), + .name = try astgen.stringFromBytes(Story.default_knot_name), .ref = knot_inst, - .body_gi = &decl_scope, + .decl_node = body_node, + .body_block = &decl_scope, }); } -fn stitchDecl(_: *GenIr, _: *Scope, _: *const Ast.Node) InnerError!void {} +fn stitchDeclInner( + gi: *GenIr, + scope: *Scope, + decl_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 = .{ + .payload_index = undefined, + } }, + }); + + var decl_block = gi.makeSubBlock(); + defer decl_block.unstack(); + + 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.stringFromNode(arg); + const arg_inst = try decl_block.addStr(.param, arg_str); + + // TODO: Maybe make decl accept a ref? + try scope.insert(arg_str, .{ + .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 knot_inst = try decl_block.addKnot(); + const name_str = try astgen.stringFromNode(identifier_node); + try setDeclaration(decl_inst, .{ + .tag = .knot, + .name = try astgen.qualifiedString(scope.namespace_prefix, name_str), + .ref = knot_inst, + .decl_node = decl_node, + .body_block = &decl_block, + }); +} + +fn stitchDecl(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 stitchDeclInner(gi, &decl_scope, decl_node, prototype_node, body_node); +} fn functionDecl(_: *GenIr, _: *Scope, _: *const Ast.Node) InnerError!void {} -fn knotDecl(gen: *GenIr, scope: *Scope, decl_node: *const Ast.Node) InnerError!void { - const prototype_node = decl_node.data.knot_decl.prototype; - const nested_decls_list = decl_node.data.knot_decl.children orelse return; - const identifier_node = prototype_node.data.bin.lhs orelse unreachable; - const ident_ref = try gen.astgen.stringFromNode(identifier_node); - const knot_symbol = scope.lookup(ident_ref) orelse unreachable; - const knot_scope = knot_symbol.knot.decl_scope; +fn knotDecl(gi: *GenIr, parent_scope: *Scope, decl_node: *const Ast.Node) InnerError!void { + const knot_data = decl_node.data.knot_decl; + const prototype_node = knot_data.prototype; + const identifier_node = prototype_node.data.bin.lhs.?; + const nested_decls_list = knot_data.children.?; - var block_gen = gen.makeSubBlock(); - defer block_gen.deinit(); + var decl_scope = parent_scope.makeChild(); + defer decl_scope.deinit(); var start_index: usize = 0; const first_child = nested_decls_list[0]; if (first_child.tag == .block_stmt) { - try blockStmt(&block_gen, knot_scope, first_child); - if (nested_decls_list.len > 1) start_index += 1 else return; + try stitchDeclInner(gi, &decl_scope, decl_node, prototype_node, first_child); + start_index += 1; + } else { + try stitchDeclInner(gi, &decl_scope, decl_node, prototype_node, null); } + + const name_str = try gi.astgen.stringFromNode(identifier_node); + try decl_scope.setNamespacePrefix(name_str); + for (nested_decls_list[start_index..]) |nested_decl_node| { - switch (decl_node.tag) { - .stitch_decl => try stitchDecl(gen, knot_scope, nested_decl_node), - .function_decl => try functionDecl(gen, knot_scope, nested_decl_node), + switch (nested_decl_node.tag) { + .stitch_decl => try stitchDecl(gi, &decl_scope, nested_decl_node), + .function_decl => try functionDecl(gi, &decl_scope, nested_decl_node), else => unreachable, } } } -fn file(root_gi: *GenIr, scope: *Scope, file_node: *const Ast.Node) InnerError!void { - const astgen = root_gi.astgen; - const gpa = astgen.gpa; - const file_inst = try root_gi.makePayloadNode(.file); - try root_gi.instructions.append(gpa, file_inst); +fn file(gi: *GenIr, scope: *Scope, file_node: *const Ast.Node) InnerError!void { + const file_inst = try gi.addAsIndex(.{ + .tag = .file, + .data = .{ + .payload = .{ + .payload_index = undefined, + }, + }, + }); var start_index: usize = 0; - var file_scope = root_gi.makeSubBlock(); + var file_scope = gi.makeSubBlock(); defer file_scope.unstack(); + // TODO: Make sure this is non-nullable. const nested_decls_list = file_node.data.list.items orelse return; if (nested_decls_list.len == 0) return; @@ -1077,9 +1395,9 @@ fn file(root_gi: *GenIr, scope: *Scope, file_node: *const Ast.Node) InnerError!v } for (nested_decls_list[start_index..]) |child_node| { switch (child_node.tag) { - //.knot_decl => try knotDecl(gi, scope, child_node), - //.stitch_decl => try stitchDecl(gi, scope, child_node), - //.function_decl => try functionDecl(gi, scope, child_node), + .knot_decl => try knotDecl(gi, scope, child_node), + .stitch_decl => try stitchDecl(gi, scope, child_node), + .function_decl => try functionDecl(gi, scope, child_node), else => unreachable, } } @@ -1103,6 +1421,7 @@ pub fn generate(gpa: std.mem.Allocator, tree: *const Ast) !Ir { var file_scope: Scope = .{ .parent = null, .decls = .empty, + .namespace_prefix = .empty, .astgen = &astgen, }; var gen: GenIr = .{ diff --git a/src/Ir.zig b/src/Ir.zig index e46be51..1cbc7fd 100644 --- a/src/Ir.zig +++ b/src/Ir.zig @@ -77,6 +77,12 @@ pub const Inst = struct { content_flush, choice_br, implicit_ret, + call, + divert, + field_ptr, + field_call, + field_divert, + param, }; pub const Data = union { @@ -116,6 +122,11 @@ pub const Inst = struct { body_len: u32, }; + pub const Field = struct { + lhs: Ref, + field_name_start: NullTerminatedString, + }; + pub const Block = struct { body_len: u32, }; @@ -151,6 +162,17 @@ pub const Inst = struct { body_len: u32, }; }; + + pub const Call = struct { + args_len: u32, + callee: Ref, + }; + + pub const FieldCall = struct { + args_len: u32, + obj_ptr: Ref, + field_name_start: NullTerminatedString, + }; }; pub const Global = struct { @@ -186,6 +208,7 @@ pub fn deinit(ir: *Ir, gpa: std.mem.Allocator) void { const Render = struct { gpa: std.mem.Allocator, prefix: Prefix, + code: Ir, writer: *std.Io.Writer, pub const Error = error{ @@ -259,6 +282,7 @@ const Render = struct { fn renderBodyInner(r: *Render, ir: Ir, body_list: []const Inst.Index) Error!void { const io_w = r.writer; + try r.prefix.writeIndent(io_w); try io_w.writeAll("{\n"); { const old_len = try r.prefix.pushChildPrefix(r.gpa); @@ -413,6 +437,88 @@ const Render = struct { return io_w.writeAll(")"); } + fn writeTag(_: *Render, w: *std.Io.Writer, inst: Ir.Inst) Error!void { + return w.print("{s}", .{@tagName(inst.tag)}); + } + + fn writeIndent(self: *Render, w: *std.Io.Writer) Error!void { + return self.prefix.writeIndent(w); + } + + fn writeString(self: *Render, w: *std.Io.Writer, str: Ir.NullTerminatedString) Error!void { + return w.print("\"{s}\"", .{self.code.nullTerminatedString(str)}); + } + + fn writeFieldPtr(self: *Render, w: *std.Io.Writer, inst: Ir.Inst) Error!void { + const data = inst.data.payload; + const extra = self.code.extraData(Inst.Field, data.payload_index).data; + + try self.writeTag(w, inst); + try w.writeAll("("); + try self.renderInstRef(extra.lhs); + try w.writeAll(", "); + try self.writeString(w, extra.field_name_start); + return w.writeAll(")"); + } + + fn writeCall( + self: *Render, + w: *std.Io.Writer, + inst: Ir.Inst, + comptime kind: enum { + field, + direct, + }, + ) Error!void { + const ExtraType = switch (kind) { + .direct => Ir.Inst.Call, + .field => Ir.Inst.FieldCall, + }; + const data = inst.data.payload; + const extra = self.code.extraData(ExtraType, data.payload_index); + const args_len = extra.data.args_len; + const body = self.code.extra[extra.end..]; + + try self.writeTag(w, inst); + try w.writeAll("("); + + switch (kind) { + .direct => try self.renderInstRef(extra.data.callee), + .field => { + try self.renderInstRef(extra.data.obj_ptr); + try w.writeAll(", "); + try self.writeString(w, extra.data.field_name_start); + }, + } + try w.writeAll(", "); + try w.writeAll("[\n"); + + var arg_start: u32 = args_len; + var i: u32 = 0; + while (i < args_len) : (i += 1) { + const old_len = try self.prefix.pushChildPrefix(self.gpa); + defer self.prefix.restore(old_len); + + const arg_end = self.code.extra[extra.end + i]; + defer arg_start = arg_end; + const arg_body = body[arg_start..arg_end]; + try self.renderBodyInner(self.code, @ptrCast(arg_body)); + try w.writeAll(",\n"); + } + + try self.writeIndent(w); + try w.writeAll("]"); + return w.writeAll(")"); + } + + fn writeParam(self: *Render, w: *std.Io.Writer, inst: Ir.Inst) Error!void { + const data = inst.data.string.start; + try self.writeTag(w, inst); + try w.writeAll("("); + try self.writeString(w, data); + return w.writeAll(")"); + } + fn renderInst(r: *Render, ir: Ir, index: Inst.Index) Error!void { const io_w = r.writer; const inst_index: u32 = @intFromEnum(index); @@ -461,14 +567,25 @@ const Render = struct { .content_flush => try r.renderUnary(inst), .choice_br => try r.renderChoiceBr(ir, inst), .implicit_ret => try r.renderUnary(inst), + .call => try r.writeCall(r.writer, inst, .direct), + .divert => try r.writeCall(r.writer, inst, .direct), + .field_call => try r.writeCall(r.writer, inst, .field), + .field_divert => try r.writeCall(r.writer, inst, .field), + .field_ptr => try r.writeFieldPtr(r.writer, inst), + .param => try r.writeParam(r.writer, inst), } try io_w.writeAll("\n"); } }; pub fn render(ir: Ir, gpa: std.mem.Allocator, writer: *std.Io.Writer) !void { - std.debug.assert(ir.instructions.len > 0); - var r = Render{ .gpa = gpa, .writer = writer, .prefix = .{} }; + assert(ir.instructions.len > 0); + var r = Render{ + .gpa = gpa, + .writer = writer, + .prefix = .{}, + .code = ir, + }; defer r.prefix.deinit(gpa); try r.renderInst(ir, @enumFromInt(0)); diff --git a/src/Sema.zig b/src/Sema.zig index 2cea5ac..d8c9ed4 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -19,9 +19,9 @@ const InnerError = error{ }; const Ref = union(enum) { + none, bool_true, bool_false, - none, index: u32, constant: u32, global: u32, @@ -64,11 +64,11 @@ fn addGlobal(sema: *Sema, name: Ir.NullTerminatedString) !Ref { fn getGlobal(sema: *Sema, name: Ir.NullTerminatedString) !Ref { const interned = try sema.getConstant(.{ .string = name }); for (sema.ir.globals) |global| { - if (name == global.name) { + if (global.name == name) { return .{ .global = interned.constant }; } } - return sema.fail("unknown global variable"); + return fail(sema, "unknown global variable"); } fn irInteger(sema: *Sema, inst: Ir.Inst.Index) InnerError!Ref { @@ -352,9 +352,7 @@ fn irDeclKnot( .sema = sema, .knot = &knot, }; - defer chunk.fixups.deinit(gpa); - defer chunk.labels.deinit(gpa); - defer chunk.inst_map.deinit(gpa); + defer chunk.deinit(gpa); const body = sema.ir.bodySlice(extra.end, extra.data.body_len); try blockBodyInner(sema, &chunk, body); @@ -374,6 +372,63 @@ fn irDeclaration(sema: *Sema, parent_chunk: ?*Chunk, inst: Ir.Inst.Index) !void } } +fn irCall(_: *Sema, _: *Chunk, _: Ir.Inst.Index) !Ref { + return .none; +} + +fn irDivert( + sema: *Sema, + chunk: *Chunk, + inst: Ir.Inst.Index, + comptime kind: enum { direct, field }, +) !void { + 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.payload_index); + const args_len = extra.data.args_len; + const body = sema.ir.extra[extra.end..]; + + const callee = switch (kind) { + .direct => chunk.resolveInst(extra.data.callee), + .field => chunk.resolveInst(extra.data.obj_ptr), + }; + _ = try chunk.doLoad(callee); + + 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]; + try blockBodyInner(sema, chunk, @ptrCast(arg_body)); + // FIXME: hack + { + const last_inst: Ir.Inst.Index = @enumFromInt(arg_body[arg_body.len - 1]); + const val = chunk.resolveInst(last_inst.toRef()); + _ = try chunk.doLoad(val); + } + } + _ = try chunk.addConstOp(.divert, @intCast(args_len)); +} + +fn irFieldPtr(_: *Sema, _: *Chunk, _: Ir.Inst.Index) !Ref { + return .none; +} + +// TODO: Check for duplicate parameters. +fn irParam(_: *Sema, chunk: *Chunk, _: Ir.Inst.Index) !Ref { + //const data = sema.ir.instructions[@intFromEnum(inst)].data.string; + const local_index = chunk.knot.stack_size; + // TODO: Add constraints on how many temporaries we can have. + // max(u8) or max(u16) are most likey appropriate. + chunk.knot.arity += 1; + chunk.knot.stack_size += 1; + return .{ .local = local_index }; +} + fn blockBodyInner(sema: *Sema, chunk: *Chunk, body: []const Ir.Inst.Index) InnerError!void { const gpa = sema.gpa; @@ -433,6 +488,18 @@ fn blockBodyInner(sema: *Sema, chunk: *Chunk, body: []const Ir.Inst.Index) Inner continue; }, .implicit_ret => try irImplicitRet(sema, chunk, inst), + .call => try irCall(sema, chunk, inst), + .divert => { + try irDivert(sema, chunk, inst, .direct); + continue; + }, + .field_call => try irCall(sema, chunk, inst), + .field_divert => { + try irDivert(sema, chunk, inst, .field); + continue; + }, + .field_ptr => try irFieldPtr(sema, chunk, inst), + .param => try irParam(sema, chunk, inst), }; try chunk.inst_map.put(gpa, inst, ref); } @@ -473,6 +540,12 @@ const Chunk = struct { code_offset: u32, }; + fn deinit(chunk: *Chunk, gpa: std.mem.Allocator) void { + chunk.fixups.deinit(gpa); + chunk.labels.deinit(gpa); + chunk.inst_map.deinit(gpa); + } + fn addByteOp(chunk: *Chunk, op: Story.Opcode) error{OutOfMemory}!Ref { const gpa = chunk.sema.gpa; const bytecode = &chunk.knot.bytecode; @@ -627,13 +700,13 @@ pub const CompiledStory = struct { pub fn buildRuntime( self: *CompiledStory, gpa: std.mem.Allocator, - ir: Ir, + ir: *Ir, story: *Story, ) !void { + const globals_len = self.globals.len + self.knots.len; const constants_pool = &story.constants_pool; try constants_pool.ensureUnusedCapacity(gpa, self.constants.len); - try story.paths.ensureUnusedCapacity(gpa, self.knots.len); - try story.globals.ensureUnusedCapacity(gpa, @intCast(self.globals.len)); + try story.globals.ensureUnusedCapacity(gpa, @intCast(globals_len)); for (self.constants) |constant| { switch (constant) { @@ -656,15 +729,18 @@ pub const CompiledStory = struct { story.globals.putAssumeCapacity(name_bytes, null); } for (self.knots) |*knot| { + const knot_name = ir.nullTerminatedString(knot.name); const runtime_chunk: *Object.ContentPath = try .create(story, .{ - .name = try .create(story, ir.nullTerminatedString(knot.name)), + .name = try .create(story, knot_name), .arity = @intCast(knot.arity), .locals_count = @intCast(knot.stack_size - knot.arity), .const_pool = try knot.constants.toOwnedSlice(gpa), .bytes = try knot.bytecode.toOwnedSlice(gpa), }); - story.paths.appendAssumeCapacity(&runtime_chunk.base); + story.globals.putAssumeCapacity(knot_name, &runtime_chunk.base); } + story.string_bytes = ir.string_bytes; + ir.string_bytes = &.{}; } }; diff --git a/src/Story.zig b/src/Story.zig index 5a6e12f..e563383 100644 --- a/src/Story.zig +++ b/src/Story.zig @@ -17,11 +17,14 @@ choice_index: usize = 0, current_choices: std.ArrayListUnmanaged(Choice) = .empty, constants_pool: std.ArrayListUnmanaged(*Object) = .empty, globals: std.StringHashMapUnmanaged(?*Object) = .empty, -paths: std.ArrayListUnmanaged(*Object) = .empty, stack: std.ArrayListUnmanaged(?*Object) = .empty, call_stack: std.ArrayListUnmanaged(CallFrame) = .empty, stack_max: usize = 128, gc_objects: std.SinglyLinkedList = .{}, +// FIXME: This was a hack to keep string bytes alive. +string_bytes: []const u8 = &.{}, + +pub const default_knot_name: [:0]const u8 = "$__main__$"; pub const CallFrame = struct { ip: usize, @@ -104,9 +107,9 @@ pub fn deinit(story: *Story) void { story.current_choices.deinit(gpa); story.constants_pool.deinit(gpa); story.globals.deinit(gpa); - story.paths.deinit(gpa); story.stack.deinit(gpa); story.call_stack.deinit(gpa); + gpa.free(story.string_bytes); } pub fn dump(story: *Story, writer: *std.Io.Writer) !void { @@ -129,8 +132,14 @@ pub fn dump(story: *Story, writer: *std.Io.Writer) !void { try writer.writeAll("\n"); try writer.writeAll("=== Knots ===\n"); - for (story.paths.items) |path_object| { - try story_dumper.dump(@ptrCast(path_object)); + var knots_iter = story.globals.iterator(); + while (knots_iter.next()) |entry| { + if (entry.value_ptr.*) |global| { + switch (global.tag) { + .content_path => try story_dumper.dump(@ptrCast(global)), + else => {}, + } + } } } @@ -141,16 +150,18 @@ pub fn trace(story: *Story, writer: *std.Io.Writer, frame: *CallFrame) !void { const stack = &story.stack; const stack_top = story.stack.items.len; if (stack_top > 0) { - const last_slot = stack.items[stack.items.len - 1]; - for (stack.items[frame.sp .. stack.items.len - 1]) |slot| { - if (slot) |object| { - try story_dumper.dumpObject(object); - } else { - try writer.writeAll("null"); + // FIXME: There has to be a better way to do this. + if (stack_top > 1) { + for (stack.items[frame.sp .. stack.items.len - 1]) |slot| { + if (slot) |object| { + try story_dumper.dumpObject(object); + } else { + try writer.writeAll("null"); + } + try writer.writeAll(", "); } - try writer.writeAll(", "); } - if (last_slot) |object| { + if (stack.items[stack.items.len - 1]) |object| { try story_dumper.dumpObject(object); } else { try writer.writeAll("null"); @@ -198,7 +209,7 @@ fn getConstant(story: *Story, frame: *CallFrame, offset: u8) !*Object { } fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?*Object { - const stack_top = vm.stack.capacity; + const stack_top = vm.stack.items.len; const stack_offset = frame.sp + offset; assert(stack_top > stack_offset); @@ -206,19 +217,21 @@ fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?*Object { } fn setLocal(vm: *Story, frame: *CallFrame, offset: u8, value: *Object) void { - const stack_top = vm.stack.capacity; + const stack_top = vm.stack.items.len; const stack_offset = frame.sp + offset; assert(stack_top > stack_offset); vm.stack.items[stack_offset] = value; } +// TODO: This should probably check the constants table first. fn getGlobal(vm: *Story, key: *const Object.String) !*Object { const key_bytes = key.bytes[0..key.length]; const val = vm.globals.get(key_bytes) orelse return error.InvalidVariable; return val orelse unreachable; } +// TODO: This should probably check the constants table first. fn setGlobal(vm: *Story, key: *const Object.String, value: *Object) !void { const key_bytes = key.bytes[0..key.length]; return vm.globals.putAssumeCapacity(key_bytes, value); @@ -231,12 +244,12 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) { } if (vm.isCallStackEmpty()) return .empty; - const frame = vm.currentFrame(); - const code = std.mem.bytesAsSlice(Opcode, frame.callee.bytes); var stream_writer = std.Io.Writer.Allocating.init(gpa); defer stream_writer.deinit(); while (true) { + const frame = vm.currentFrame(); + const code = std.mem.bytesAsSlice(Opcode, frame.callee.bytes); if (vm.dump_writer) |w| { vm.trace(w, frame) catch {}; } @@ -447,6 +460,16 @@ 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)) |knot| { + try divertToKnot(vm, @ptrCast(knot)); + } else { + return error.InvalidArgument; + } + }, else => return error.InvalidInstruction, } } @@ -462,27 +485,37 @@ pub fn advance(story: *Story, gpa: std.mem.Allocator) ![]const u8 { return content.toOwnedSlice(gpa); } -fn divert(vm: *Story, path_name: []const u8) !void { - const gpa = vm.allocator; - const path_object: ?*Object.ContentPath = blk: { - for (vm.paths.items) |object| { - const current_path: *Object.ContentPath = @ptrCast(object); - const current_name = current_path.name; - // TODO(Brett): We probably should create a method for doing this. - const name_bytes = current_name.bytes[0..current_name.length]; - if (std.mem.eql(u8, name_bytes, path_name)) break :blk current_path; +pub fn getKnot(vm: *Story, name: []const u8) ?*Object.ContentPath { + const knot: ?*Object.ContentPath = blk: { + if (vm.globals.get(name)) |object| { + break :blk @ptrCast(object); } break :blk null; }; - if (path_object) |path| { - // TODO(Brett): Add arguments? - const stack_needed = path.arity + path.locals_count; - const stack_ptr = vm.stack.items.len; - try vm.stack.ensureUnusedCapacity(gpa, stack_needed); - try vm.call_stack.ensureUnusedCapacity(gpa, 1); + return knot; +} - vm.stack.appendNTimesAssumeCapacity(null, stack_needed); - vm.call_stack.appendAssumeCapacity(.{ .ip = 0, .sp = stack_ptr, .callee = path }); +// TODO(Brett): Add arguments? +fn divertToKnot(vm: *Story, knot: *Object.ContentPath) !void { + const gpa = vm.allocator; + const stack_ptr = vm.stack.items.len - knot.arity; + const stack_needed = knot.locals_count; + + try vm.stack.ensureUnusedCapacity(gpa, stack_needed); + try vm.call_stack.ensureUnusedCapacity(gpa, 1); + + vm.call_stack.appendAssumeCapacity(.{ + .callee = knot, + .ip = 0, + .sp = stack_ptr, + }); + vm.stack.appendNTimesAssumeCapacity(null, stack_needed); + vm.can_advance = true; +} + +fn divert(vm: *Story, knot_name: []const u8) !void { + return if (getKnot(vm, knot_name)) |knot| { + return divertToKnot(vm, knot); } else return error.InvalidPath; } @@ -535,7 +568,6 @@ pub fn loadFromString( try options.stderr_writer.flush(); return error.CompilationFailed; } - if (options.dump_ir) { if (options.dump_writer) |w| { try w.writeAll("=== Semantic IR ===\n"); @@ -552,8 +584,13 @@ pub fn loadFromString( .can_advance = false, .dump_writer = options.dump_writer, }; - try compiled.buildRuntime(gpa, sem_ir, &story); - try story.divert("$__main__$"); - story.can_advance = true; + errdefer story.deinit(); + + try compiled.buildRuntime(gpa, &sem_ir, &story); + + if (story.getKnot(Story.default_knot_name)) |knot| { + try story.divertToKnot(knot); + story.can_advance = true; + } return story; } diff --git a/src/Story/Dumper.zig b/src/Story/Dumper.zig index ffb7771..eda4d7e 100644 --- a/src/Story/Dumper.zig +++ b/src/Story/Dumper.zig @@ -110,8 +110,8 @@ pub fn dumpInst( .store => return d.dumpByteInst(path, offset, op), .load_global => return d.dumpGlobalInst(path, offset, op), .store_global => return d.dumpGlobalInst(path, offset, op), - .call => return d.dumpGlobalInst(path, offset, op), - .divert => return d.dumpGlobalInst(path, offset, op), + .call => return d.dumpByteInst(path, offset, op), + .divert => return d.dumpByteInst(path, offset, op), .jmp => return d.dumpJumpInst(path, offset, op, .relative), .jmp_t => return d.dumpJumpInst(path, offset, op, .relative), .jmp_f => return d.dumpJumpInst(path, offset, op, .relative), @@ -143,6 +143,7 @@ pub fn dump(d: Dumper, path: *const Object.ContentPath) !void { var index: usize = 0; while (index < path.bytes.len) { index = try d.dumpInst(path, index, false); + try d.writer.flush(); } return d.writer.flush(); }