From fea8659b82ea1a785f933c58ba9d65ceb05a4094 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 1 Jan 2021 19:24:02 -0700 Subject: stage2: comptime function calls * Function calls that happen in a comptime scope get called at compile-time. We do this by putting the parameters in place as constant values and then running regular function analysis on the body. * Added `Scope.Block.dump()` for debugging purposes. * Fixed some code to call `identifierTokenString` rather than `tokenSlice`, making it work for `@""` syntax. * Implemented `Value.copy` for big integers. Follow-up issues to tackle: * Adding compile errors to the callsite instead of the callee Decl. * Proper error notes for "called from here". - Related: #7555 * Branch quotas. * ZIR support? --- src/Module.zig | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) (limited to 'src/Module.zig') diff --git a/src/Module.zig b/src/Module.zig index d1719059c4..29c19c09a0 100644 --- a/src/Module.zig +++ b/src/Module.zig @@ -268,6 +268,11 @@ pub const Decl = struct { } } + /// Asserts that the `Decl` is part of AST and not ZIRModule. + pub fn getFileScope(self: *Decl) *Scope.File { + return self.scope.cast(Scope.Container).?.file_scope; + } + fn removeDependant(self: *Decl, other: *Decl) void { self.dependants.removeAssertDiscard(other); } @@ -776,6 +781,11 @@ pub const Scope = struct { results: ArrayListUnmanaged(*Inst), block_inst: *Inst.Block, }; + + /// For debugging purposes. + pub fn dump(self: *Block, mod: Module) void { + zir.dumpBlock(mod, self); + } }; /// This is a temporary structure, references to it are valid only @@ -992,11 +1002,11 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { defer tracy.end(); const container_scope = decl.scope.cast(Scope.Container).?; - const tree = try self.getAstTree(container_scope); + const tree = try self.getAstTree(container_scope.file_scope); const ast_node = tree.root_node.decls()[decl.src_index]; switch (ast_node.tag) { .FnProto => { - const fn_proto = @fieldParentPtr(ast.Node.FnProto, "base", ast_node); + const fn_proto = ast_node.castTag(.FnProto).?; decl.analysis = .in_progress; @@ -1131,7 +1141,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { for (fn_proto.params()) |param, i| { const name_token = param.name_token.?; const src = tree.token_locs[name_token].start; - const param_name = tree.tokenSlice(name_token); // TODO: call identifierTokenString + const param_name = try self.identifierTokenString(&gen_scope.base, name_token); const arg = try gen_scope_arena.allocator.create(zir.Inst.Arg); arg.* = .{ .base = .{ @@ -1496,12 +1506,10 @@ fn getSrcModule(self: *Module, root_scope: *Scope.ZIRModule) !*zir.Module { } } -fn getAstTree(self: *Module, container_scope: *Scope.Container) !*ast.Tree { +pub fn getAstTree(self: *Module, root_scope: *Scope.File) !*ast.Tree { const tracy = trace(@src()); defer tracy.end(); - const root_scope = container_scope.file_scope; - switch (root_scope.status) { .never_loaded, .unloaded_success => { try self.failed_files.ensureCapacity(self.gpa, self.failed_files.items().len + 1); @@ -1549,7 +1557,7 @@ pub fn analyzeContainer(self: *Module, container_scope: *Scope.Container) !void // We may be analyzing it for the first time, or this may be // an incremental update. This code handles both cases. - const tree = try self.getAstTree(container_scope); + const tree = try self.getAstTree(container_scope.file_scope); const decls = tree.root_node.decls(); try self.comp.work_queue.ensureUnusedCapacity(decls.len); @@ -3427,3 +3435,23 @@ pub fn validateVarType(mod: *Module, scope: *Scope, src: usize, ty: Type) !void return mod.fail(scope, src, "variable of type '{}' must be const or comptime", .{ty}); } } + +/// Identifier token -> String (allocated in scope.arena()) +pub fn identifierTokenString(mod: *Module, scope: *Scope, token: ast.TokenIndex) InnerError![]const u8 { + const tree = scope.tree(); + + const ident_name = tree.tokenSlice(token); + if (mem.startsWith(u8, ident_name, "@")) { + const raw_string = ident_name[1..]; + var bad_index: usize = undefined; + return std.zig.parseStringLiteral(scope.arena(), raw_string, &bad_index) catch |err| switch (err) { + error.InvalidCharacter => { + const bad_byte = raw_string[bad_index]; + const src = tree.token_locs[token].start; + return mod.fail(scope, src + 1 + bad_index, "invalid string literal character: '{c}'\n", .{bad_byte}); + }, + else => |e| return e, + }; + } + return ident_name; +} -- cgit v1.2.3 From 9362f382ab7023592cc1d71044217b847b122406 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 2 Jan 2021 12:32:30 -0700 Subject: stage2: implement function call inlining in the frontend * remove the -Ddump-zir thing. that's handled through --verbose-ir * rework Fn to have an is_inline flag without requiring any more memory on the heap per function. * implement a rough first version of dumping typed zir (tzir) which is a lot more helpful for debugging than what we had before. We don't have a way to parse it though. * keep track of whether the inline-ness of a function changes because if it does we have to go update callsites. * add compile error for inline and export used together. inline function calls and comptime function calls are implemented the same way. A block instruction is set up to capture the result, and then a scope is set up that has a flag for is_comptime and some state if the scope is being inlined. when analyzing `ret` instructions, zig looks for inlining state in the scope, and if found, treats `ret` as a `break` instruction instead, with the target block being the one set up at the inline callsite. Follow-up items: * Complete out the debug TZIR dumping code. * Don't redundantly generate ZIR for each inline/comptime function call. Instead we should add a new state enum tag to Fn. * comptime and inlining branch quotas. * Add more test cases. --- build.zig | 2 - src/Compilation.zig | 14 +- src/Module.zig | 150 ++++++++++++----- src/codegen.zig | 10 +- src/codegen/c.zig | 2 +- src/codegen/wasm.zig | 2 +- src/config.zig.in | 1 - src/link/Elf.zig | 10 -- src/link/MachO/DebugSymbols.zig | 10 -- src/llvm_backend.zig | 2 +- src/zir.zig | 349 ++++++++++++++++++++++++++++------------ src/zir_sema.zig | 239 ++++++++++++++++++--------- test/stage2/zir.zig | 12 +- 13 files changed, 549 insertions(+), 254 deletions(-) (limited to 'src/Module.zig') diff --git a/build.zig b/build.zig index 2b0685d19e..f86b0d3bec 100644 --- a/build.zig +++ b/build.zig @@ -220,7 +220,6 @@ pub fn build(b: *Builder) !void { } const log_scopes = b.option([]const []const u8, "log", "Which log scopes to enable") orelse &[0][]const u8{}; - const zir_dumps = b.option([]const []const u8, "dump-zir", "Which functions to dump ZIR for before codegen") orelse &[0][]const u8{}; const opt_version_string = b.option([]const u8, "version-string", "Override Zig version string. Default is to find out with git."); const version = if (opt_version_string) |version| version else v: { @@ -277,7 +276,6 @@ pub fn build(b: *Builder) !void { exe.addBuildOption(std.SemanticVersion, "semver", semver); exe.addBuildOption([]const []const u8, "log_scopes", log_scopes); - exe.addBuildOption([]const []const u8, "zir_dumps", zir_dumps); exe.addBuildOption(bool, "enable_tracy", tracy != null); exe.addBuildOption(bool, "is_stage1", is_stage1); if (tracy) |tracy_path| { diff --git a/src/Compilation.zig b/src/Compilation.zig index de115b9b40..a6f39a3154 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -1459,10 +1459,10 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor const module = self.bin_file.options.module.?; if (decl.typed_value.most_recent.typed_value.val.castTag(.function)) |payload| { const func = payload.data; - switch (func.analysis) { + switch (func.bits.state) { .queued => module.analyzeFnBody(decl, func) catch |err| switch (err) { error.AnalysisFail => { - assert(func.analysis != .in_progress); + assert(func.bits.state != .in_progress); continue; }, error.OutOfMemory => return error.OutOfMemory, @@ -1471,12 +1471,16 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor .sema_failure, .dependency_failure => continue, .success => {}, } - // Here we tack on additional allocations to the Decl's arena. The allocations are - // lifetime annotations in the ZIR. + // Here we tack on additional allocations to the Decl's arena. The allocations + // are lifetime annotations in the ZIR. var decl_arena = decl.typed_value.most_recent.arena.?.promote(module.gpa); defer decl.typed_value.most_recent.arena.?.* = decl_arena.state; log.debug("analyze liveness of {s}\n", .{decl.name}); - try liveness.analyze(module.gpa, &decl_arena.allocator, func.analysis.success); + try liveness.analyze(module.gpa, &decl_arena.allocator, func.data.body); + + if (self.verbose_ir) { + func.dump(module.*); + } } assert(decl.typed_value.most_recent.typed_value.ty.hasCodeGenBits()); diff --git a/src/Module.zig b/src/Module.zig index 29c19c09a0..db76ecd5db 100644 --- a/src/Module.zig +++ b/src/Module.zig @@ -286,23 +286,40 @@ pub const Decl = struct { /// Extern functions do not have this data structure; they are represented by /// the `Decl` only, with a `Value` tag of `extern_fn`. pub const Fn = struct { - /// This memory owned by the Decl's TypedValue.Managed arena allocator. - analysis: union(enum) { + bits: packed struct { + /// Get and set this field via `analysis` and `setAnalysis`. + state: Analysis.Tag, + /// We carry this state into `Fn` instead of leaving it in the AST so that + /// analysis of function calls can happen even on functions whose AST has + /// been unloaded from memory. + is_inline: bool, + unused_bits: u4 = 0, + }, + /// Get and set this data via `analysis` and `setAnalysis`. + data: union { + none: void, + zir: *ZIR, + body: Body, + }, + owner_decl: *Decl, + + pub const Analysis = union(Tag) { queued: *ZIR, in_progress, - /// There will be a corresponding ErrorMsg in Module.failed_decls sema_failure, - /// This Fn might be OK but it depends on another Decl which did not successfully complete - /// semantic analysis. dependency_failure, success: Body, - }, - owner_decl: *Decl, - /// This memory is temporary and points to stack memory for the duration - /// of Fn analysis. - pub const Analysis = struct { - inner_block: Scope.Block, + pub const Tag = enum(u3) { + queued, + in_progress, + /// There will be a corresponding ErrorMsg in Module.failed_decls + sema_failure, + /// This Fn might be OK but it depends on another Decl which did not + /// successfully complete semantic analysis. + dependency_failure, + success, + }; }; /// Contains un-analyzed ZIR instructions generated from Zig source AST. @@ -311,22 +328,37 @@ pub const Fn = struct { arena: std.heap.ArenaAllocator.State, }; - /// For debugging purposes. - pub fn dump(self: *Fn, mod: Module) void { - std.debug.print("Module.Function(name={s}) ", .{self.owner_decl.name}); - switch (self.analysis) { - .queued => { - std.debug.print("queued\n", .{}); + pub fn analysis(self: Fn) Analysis { + return switch (self.bits.state) { + .queued => .{ .queued = self.data.zir }, + .success => .{ .success = self.data.body }, + .in_progress => .in_progress, + .sema_failure => .sema_failure, + .dependency_failure => .dependency_failure, + }; + } + + pub fn setAnalysis(self: *Fn, anal: Analysis) void { + switch (anal) { + .queued => |zir_ptr| { + self.bits.state = .queued; + self.data = .{ .zir = zir_ptr }; }, - .in_progress => { - std.debug.print("in_progress\n", .{}); + .success => |body| { + self.bits.state = .success; + self.data = .{ .body = body }; }, - else => { - std.debug.print("\n", .{}); - zir.dumpFn(mod, self); + .in_progress, .sema_failure, .dependency_failure => { + self.bits.state = anal; + self.data = .{ .none = {} }; }, } } + + /// For debugging purposes. + pub fn dump(self: *Fn, mod: Module) void { + zir.dumpFn(mod, self); + } }; pub const Var = struct { @@ -773,13 +805,33 @@ pub const Scope = struct { instructions: ArrayListUnmanaged(*Inst), /// Points to the arena allocator of DeclAnalysis arena: *Allocator, - label: ?Label = null, + label: Label = Label.none, is_comptime: bool, - pub const Label = struct { - zir_block: *zir.Inst.Block, - results: ArrayListUnmanaged(*Inst), - block_inst: *Inst.Block, + pub const Label = union(enum) { + none, + /// This `Block` maps a block ZIR instruction to the corresponding + /// TZIR instruction for break instruction analysis. + breaking: struct { + zir_block: *zir.Inst.Block, + merges: Merges, + }, + /// This `Block` indicates that an inline function call is happening + /// and return instructions should be analyzed as a break instruction + /// to this TZIR block instruction. + inlining: struct { + /// We use this to count from 0 so that arg instructions know + /// which parameter index they are, without having to store + /// a parameter index with each arg instruction. + param_index: usize, + casted_args: []*Inst, + merges: Merges, + }, + + pub const Merges = struct { + results: ArrayListUnmanaged(*Inst), + block_inst: *Inst.Block, + }; }; /// For debugging purposes. @@ -1189,8 +1241,21 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { break :blk fn_zir; }; + const is_inline = blk: { + if (fn_proto.getExternExportInlineToken()) |maybe_inline_token| { + if (tree.token_ids[maybe_inline_token] == .Keyword_inline) { + break :blk true; + } + } + break :blk false; + }; + new_func.* = .{ - .analysis = .{ .queued = fn_zir }, + .bits = .{ + .state = .queued, + .is_inline = is_inline, + }, + .data = .{ .zir = fn_zir }, .owner_decl = decl, }; fn_payload.* = .{ @@ -1199,11 +1264,16 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { }; var prev_type_has_bits = false; + var prev_is_inline = false; var type_changed = true; if (decl.typedValueManaged()) |tvm| { prev_type_has_bits = tvm.typed_value.ty.hasCodeGenBits(); type_changed = !tvm.typed_value.ty.eql(fn_type); + if (tvm.typed_value.val.castTag(.function)) |payload| { + const prev_func = payload.data; + prev_is_inline = prev_func.bits.is_inline; + } tvm.deinit(self.gpa); } @@ -1221,18 +1291,26 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { decl.analysis = .complete; decl.generation = self.generation; - if (fn_type.hasCodeGenBits()) { + if (!is_inline and fn_type.hasCodeGenBits()) { // We don't fully codegen the decl until later, but we do need to reserve a global // offset table index for it. This allows us to codegen decls out of dependency order, // increasing how many computations can be done in parallel. try self.comp.bin_file.allocateDeclIndexes(decl); try self.comp.work_queue.writeItem(.{ .codegen_decl = decl }); - } else if (prev_type_has_bits) { + } else if (!prev_is_inline and prev_type_has_bits) { self.comp.bin_file.freeDecl(decl); } if (fn_proto.getExternExportInlineToken()) |maybe_export_token| { if (tree.token_ids[maybe_export_token] == .Keyword_export) { + if (is_inline) { + return self.failTok( + &block_scope.base, + maybe_export_token, + "export of inline function", + .{}, + ); + } const export_src = tree.token_locs[maybe_export_token].start; const name_loc = tree.token_locs[fn_proto.getNameToken().?]; const name = tree.tokenSliceLoc(name_loc); @@ -1240,7 +1318,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { try self.analyzeExport(&block_scope.base, export_src, name, decl); } } - return type_changed; + return type_changed or is_inline != prev_is_inline; }, .VarDecl => { const var_decl = @fieldParentPtr(ast.Node.VarDecl, "base", ast_node); @@ -1824,15 +1902,15 @@ pub fn analyzeFnBody(self: *Module, decl: *Decl, func: *Fn) !void { }; defer inner_block.instructions.deinit(self.gpa); - const fn_zir = func.analysis.queued; + const fn_zir = func.data.zir; defer fn_zir.arena.promote(self.gpa).deinit(); - func.analysis = .{ .in_progress = {} }; + func.setAnalysis(.in_progress); log.debug("set {s} to in_progress\n", .{decl.name}); try zir_sema.analyzeBody(self, &inner_block.base, fn_zir.body); const instructions = try arena.allocator.dupe(*Inst, inner_block.instructions.items); - func.analysis = .{ .success = .{ .instructions = instructions } }; + func.setAnalysis(.{ .success = .{ .instructions = instructions } }); log.debug("set {s} to success\n", .{decl.name}); } @@ -2329,7 +2407,7 @@ pub fn analyzeDeclRef(self: *Module, scope: *Scope, src: usize, decl: *Decl) Inn self.ensureDeclAnalyzed(decl) catch |err| { if (scope.cast(Scope.Block)) |block| { if (block.func) |func| { - func.analysis = .dependency_failure; + func.setAnalysis(.dependency_failure); } else { block.decl.analysis = .dependency_failure; } @@ -3029,7 +3107,7 @@ fn failWithOwnedErrorMsg(self: *Module, scope: *Scope, src: usize, err_msg: *Com .block => { const block = scope.cast(Scope.Block).?; if (block.func) |func| { - func.analysis = .sema_failure; + func.setAnalysis(.sema_failure); } else { block.decl.analysis = .sema_failure; block.decl.generation = self.generation; diff --git a/src/codegen.zig b/src/codegen.zig index 6530b687e5..588c3dec4c 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -532,7 +532,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type { self.code.items.len += 4; try self.dbgSetPrologueEnd(); - try self.genBody(self.mod_fn.analysis.success); + try self.genBody(self.mod_fn.data.body); const stack_end = self.max_end_stack; if (stack_end > math.maxInt(i32)) @@ -576,7 +576,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type { }); } else { try self.dbgSetPrologueEnd(); - try self.genBody(self.mod_fn.analysis.success); + try self.genBody(self.mod_fn.data.body); try self.dbgSetEpilogueBegin(); } }, @@ -593,7 +593,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type { try self.dbgSetPrologueEnd(); - try self.genBody(self.mod_fn.analysis.success); + try self.genBody(self.mod_fn.data.body); // Backpatch stack offset const stack_end = self.max_end_stack; @@ -638,13 +638,13 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type { writeInt(u32, try self.code.addManyAsArray(4), Instruction.pop(.al, .{ .fp, .pc }).toU32()); } else { try self.dbgSetPrologueEnd(); - try self.genBody(self.mod_fn.analysis.success); + try self.genBody(self.mod_fn.data.body); try self.dbgSetEpilogueBegin(); } }, else => { try self.dbgSetPrologueEnd(); - try self.genBody(self.mod_fn.analysis.success); + try self.genBody(self.mod_fn.data.body); try self.dbgSetEpilogueBegin(); }, } diff --git a/src/codegen/c.zig b/src/codegen/c.zig index 684a03eb79..712d663af0 100644 --- a/src/codegen/c.zig +++ b/src/codegen/c.zig @@ -275,7 +275,7 @@ pub fn generate(file: *C, module: *Module, decl: *Decl) !void { try writer.writeAll(" {"); const func: *Module.Fn = func_payload.data; - const instructions = func.analysis.success.instructions; + const instructions = func.data.body.instructions; if (instructions.len > 0) { try writer.writeAll("\n"); for (instructions) |inst| { diff --git a/src/codegen/wasm.zig b/src/codegen/wasm.zig index c7ad59f5d1..1eb4f5bc29 100644 --- a/src/codegen/wasm.zig +++ b/src/codegen/wasm.zig @@ -63,7 +63,7 @@ pub fn genCode(buf: *ArrayList(u8), decl: *Decl) !void { // TODO: check for and handle death of instructions const tv = decl.typed_value.most_recent.typed_value; const mod_fn = tv.val.castTag(.function).?.data; - for (mod_fn.analysis.success.instructions) |inst| try genInst(buf, decl, inst); + for (mod_fn.data.body.instructions) |inst| try genInst(buf, decl, inst); // Write 'end' opcode try writer.writeByte(0x0B); diff --git a/src/config.zig.in b/src/config.zig.in index 9d16cf3824..0dbd3f3c91 100644 --- a/src/config.zig.in +++ b/src/config.zig.in @@ -2,7 +2,6 @@ pub const have_llvm = true; pub const version: [:0]const u8 = "@ZIG_VERSION@"; pub const semver = try @import("std").SemanticVersion.parse(version); pub const log_scopes: []const []const u8 = &[_][]const u8{}; -pub const zir_dumps: []const []const u8 = &[_][]const u8{}; pub const enable_tracy = false; pub const is_stage1 = true; pub const skip_non_native = false; diff --git a/src/link/Elf.zig b/src/link/Elf.zig index 116d7c9859..d74236f8c1 100644 --- a/src/link/Elf.zig +++ b/src/link/Elf.zig @@ -2178,16 +2178,6 @@ pub fn updateDecl(self: *Elf, module: *Module, decl: *Module.Decl) !void { else => false, }; if (is_fn) { - const zir_dumps = if (std.builtin.is_test) &[0][]const u8{} else build_options.zir_dumps; - if (zir_dumps.len != 0) { - for (zir_dumps) |fn_name| { - if (mem.eql(u8, mem.spanZ(decl.name), fn_name)) { - std.debug.print("\n{s}\n", .{decl.name}); - typed_value.val.castTag(.function).?.data.dump(module.*); - } - } - } - // For functions we need to add a prologue to the debug line program. try dbg_line_buffer.ensureCapacity(26); diff --git a/src/link/MachO/DebugSymbols.zig b/src/link/MachO/DebugSymbols.zig index c70fcc5825..11f87d5495 100644 --- a/src/link/MachO/DebugSymbols.zig +++ b/src/link/MachO/DebugSymbols.zig @@ -936,16 +936,6 @@ pub fn initDeclDebugBuffers( const typed_value = decl.typed_value.most_recent.typed_value; switch (typed_value.ty.zigTypeTag()) { .Fn => { - const zir_dumps = if (std.builtin.is_test) &[0][]const u8{} else build_options.zir_dumps; - if (zir_dumps.len != 0) { - for (zir_dumps) |fn_name| { - if (mem.eql(u8, mem.spanZ(decl.name), fn_name)) { - std.debug.print("\n{}\n", .{decl.name}); - typed_value.val.cast(Value.Payload.Function).?.func.dump(module.*); - } - } - } - // For functions we need to add a prologue to the debug line program. try dbg_line_buffer.ensureCapacity(26); diff --git a/src/llvm_backend.zig b/src/llvm_backend.zig index 51d1a0840e..5814aa7e7e 100644 --- a/src/llvm_backend.zig +++ b/src/llvm_backend.zig @@ -294,7 +294,7 @@ pub const LLVMIRModule = struct { const entry_block = llvm_func.appendBasicBlock("Entry"); self.builder.positionBuilderAtEnd(entry_block); - const instructions = func.analysis.success.instructions; + const instructions = func.data.body.instructions; for (instructions) |inst| { switch (inst.tag) { .breakpoint => try self.genBreakpoint(inst.castTag(.breakpoint).?), diff --git a/src/zir.zig b/src/zir.zig index 043c54faf0..64b74f24d9 100644 --- a/src/zir.zig +++ b/src/zir.zig @@ -793,7 +793,9 @@ pub const Inst = struct { fn_type: *Inst, body: Module.Body, }, - kw_args: struct {}, + kw_args: struct { + is_inline: bool = false, + }, }; pub const FnType = struct { @@ -1847,83 +1849,258 @@ pub fn emit(allocator: *Allocator, old_module: *IrModule) !Module { /// For debugging purposes, prints a function representation to stderr. pub fn dumpFn(old_module: IrModule, module_fn: *IrModule.Fn) void { const allocator = old_module.gpa; - var ctx: EmitZIR = .{ + var ctx: DumpTzir = .{ .allocator = allocator, - .decls = .{}, .arena = std.heap.ArenaAllocator.init(allocator), .old_module = &old_module, - .next_auto_name = 0, - .names = std.StringArrayHashMap(void).init(allocator), - .primitive_table = std.AutoHashMap(Inst.Primitive.Builtin, *Decl).init(allocator), - .indent = 0, - .block_table = std.AutoHashMap(*ir.Inst.Block, *Inst.Block).init(allocator), - .loop_table = std.AutoHashMap(*ir.Inst.Loop, *Inst.Loop).init(allocator), - .metadata = std.AutoHashMap(*Inst, Module.MetaData).init(allocator), - .body_metadata = std.AutoHashMap(*Module.Body, Module.BodyMetaData).init(allocator), + .module_fn = module_fn, + .indent = 2, + .inst_table = DumpTzir.InstTable.init(allocator), + .partial_inst_table = DumpTzir.InstTable.init(allocator), + .const_table = DumpTzir.InstTable.init(allocator), }; - defer ctx.metadata.deinit(); - defer ctx.body_metadata.deinit(); - defer ctx.block_table.deinit(); - defer ctx.loop_table.deinit(); - defer ctx.decls.deinit(allocator); - defer ctx.names.deinit(); - defer ctx.primitive_table.deinit(); + defer ctx.inst_table.deinit(); + defer ctx.partial_inst_table.deinit(); + defer ctx.const_table.deinit(); defer ctx.arena.deinit(); - const fn_ty = module_fn.owner_decl.typed_value.most_recent.typed_value.ty; - _ = ctx.emitFn(module_fn, 0, fn_ty) catch |err| { - std.debug.print("unable to dump function: {s}\n", .{@errorName(err)}); - return; - }; - var module = Module{ - .decls = ctx.decls.items, - .arena = ctx.arena, - .metadata = ctx.metadata, - .body_metadata = ctx.body_metadata, - }; - - module.dump(); + switch (module_fn.analysis()) { + .queued => std.debug.print("(queued)", .{}), + .in_progress => std.debug.print("(in_progress)", .{}), + .sema_failure => std.debug.print("(sema_failure)", .{}), + .dependency_failure => std.debug.print("(dependency_failure)", .{}), + .success => |body| { + ctx.dump(body, std.io.getStdErr().writer()) catch @panic("failed to dump TZIR"); + }, + } } -/// For debugging purposes, prints a function representation to stderr. -pub fn dumpBlock(old_module: IrModule, module_block: *IrModule.Scope.Block) void { - const allocator = old_module.gpa; - var ctx: EmitZIR = .{ - .allocator = allocator, - .decls = .{}, - .arena = std.heap.ArenaAllocator.init(allocator), - .old_module = &old_module, - .next_auto_name = 0, - .names = std.StringArrayHashMap(void).init(allocator), - .primitive_table = std.AutoHashMap(Inst.Primitive.Builtin, *Decl).init(allocator), - .indent = 0, - .block_table = std.AutoHashMap(*ir.Inst.Block, *Inst.Block).init(allocator), - .loop_table = std.AutoHashMap(*ir.Inst.Loop, *Inst.Loop).init(allocator), - .metadata = std.AutoHashMap(*Inst, Module.MetaData).init(allocator), - .body_metadata = std.AutoHashMap(*Module.Body, Module.BodyMetaData).init(allocator), - }; - defer ctx.metadata.deinit(); - defer ctx.body_metadata.deinit(); - defer ctx.block_table.deinit(); - defer ctx.loop_table.deinit(); - defer ctx.decls.deinit(allocator); - defer ctx.names.deinit(); - defer ctx.primitive_table.deinit(); - defer ctx.arena.deinit(); +const DumpTzir = struct { + allocator: *Allocator, + arena: std.heap.ArenaAllocator, + old_module: *const IrModule, + module_fn: *IrModule.Fn, + indent: usize, + inst_table: InstTable, + partial_inst_table: InstTable, + const_table: InstTable, + next_index: usize = 0, + next_partial_index: usize = 0, + next_const_index: usize = 0, + + const InstTable = std.AutoArrayHashMap(*ir.Inst, usize); + + fn dump(dtz: *DumpTzir, body: ir.Body, writer: std.fs.File.Writer) !void { + // First pass to pre-populate the table so that we can show even invalid references. + // Must iterate the same order we iterate the second time. + // We also look for constants and put them in the const_table. + for (body.instructions) |inst| { + try dtz.inst_table.put(inst, dtz.next_index); + dtz.next_index += 1; + switch (inst.tag) { + .alloc, + .retvoid, + .unreach, + .breakpoint, + .dbg_stmt, + => {}, - _ = ctx.emitBlock(module_block, 0) catch |err| { - std.debug.print("unable to dump function: {}\n", .{err}); - return; - }; - var module = Module{ - .decls = ctx.decls.items, - .arena = ctx.arena, - .metadata = ctx.metadata, - .body_metadata = ctx.body_metadata, - }; + .ref, + .ret, + .bitcast, + .not, + .isnonnull, + .isnull, + .iserr, + .ptrtoint, + .floatcast, + .intcast, + .load, + .unwrap_optional, + .wrap_optional, + => { + const un_op = inst.cast(ir.Inst.UnOp).?; + try dtz.findConst(un_op.operand); + }, - module.dump(); -} + .add, + .sub, + .cmp_lt, + .cmp_lte, + .cmp_eq, + .cmp_gte, + .cmp_gt, + .cmp_neq, + .store, + .booland, + .boolor, + .bitand, + .bitor, + .xor, + => { + const bin_op = inst.cast(ir.Inst.BinOp).?; + try dtz.findConst(bin_op.lhs); + try dtz.findConst(bin_op.rhs); + }, + + .arg => {}, + + // TODO fill out this debug printing + .assembly, + .block, + .br, + .brvoid, + .call, + .condbr, + .constant, + .loop, + .varptr, + .switchbr, + => {}, + } + } + + std.debug.print("Module.Function(name={s}):\n", .{dtz.module_fn.owner_decl.name}); + + for (dtz.const_table.items()) |entry| { + const constant = entry.key.castTag(.constant).?; + try writer.print(" @{d}: {} = {};\n", .{ + entry.value, constant.base.ty, constant.val, + }); + } + + return dtz.dumpBody(body, writer); + } + + fn dumpBody(dtz: *DumpTzir, body: ir.Body, writer: std.fs.File.Writer) !void { + for (body.instructions) |inst| { + const my_index = dtz.next_partial_index; + try dtz.partial_inst_table.put(inst, my_index); + dtz.next_partial_index += 1; + + try writer.writeByteNTimes(' ', dtz.indent); + try writer.print("%{d}: {} = {s}(", .{ + my_index, inst.ty, @tagName(inst.tag), + }); + switch (inst.tag) { + .alloc, + .retvoid, + .unreach, + .breakpoint, + .dbg_stmt, + => try writer.writeAll(")\n"), + + .ref, + .ret, + .bitcast, + .not, + .isnonnull, + .isnull, + .iserr, + .ptrtoint, + .floatcast, + .intcast, + .load, + .unwrap_optional, + .wrap_optional, + => { + const un_op = inst.cast(ir.Inst.UnOp).?; + if (dtz.partial_inst_table.get(un_op.operand)) |operand_index| { + try writer.print("%{d})\n", .{operand_index}); + } else if (dtz.const_table.get(un_op.operand)) |operand_index| { + try writer.print("@{d})\n", .{operand_index}); + } else if (dtz.inst_table.get(un_op.operand)) |operand_index| { + try writer.print("%{d}) // Instruction does not dominate all uses!\n", .{ + operand_index, + }); + } else { + try writer.writeAll("!BADREF!)\n"); + } + }, + + .add, + .sub, + .cmp_lt, + .cmp_lte, + .cmp_eq, + .cmp_gte, + .cmp_gt, + .cmp_neq, + .store, + .booland, + .boolor, + .bitand, + .bitor, + .xor, + => { + var lhs_kinky: ?usize = null; + var rhs_kinky: ?usize = null; + + const bin_op = inst.cast(ir.Inst.BinOp).?; + if (dtz.partial_inst_table.get(bin_op.lhs)) |operand_index| { + try writer.print("%{d}, ", .{operand_index}); + } else if (dtz.const_table.get(bin_op.lhs)) |operand_index| { + try writer.print("@{d}, ", .{operand_index}); + } else if (dtz.inst_table.get(bin_op.lhs)) |operand_index| { + lhs_kinky = operand_index; + try writer.print("%{d}, ", .{operand_index}); + } else { + try writer.writeAll("!BADREF!, "); + } + if (dtz.partial_inst_table.get(bin_op.rhs)) |operand_index| { + try writer.print("%{d}", .{operand_index}); + } else if (dtz.const_table.get(bin_op.rhs)) |operand_index| { + try writer.print("@{d}", .{operand_index}); + } else if (dtz.inst_table.get(bin_op.rhs)) |operand_index| { + rhs_kinky = operand_index; + try writer.print("%{d}", .{operand_index}); + } else { + try writer.writeAll("!BADREF!"); + } + if (lhs_kinky != null or rhs_kinky != null) { + try writer.writeAll(") // Instruction does not dominate all uses!"); + if (lhs_kinky) |lhs| { + try writer.print(" %{d}", .{lhs}); + } + if (rhs_kinky) |rhs| { + try writer.print(" %{d}", .{rhs}); + } + try writer.writeAll("\n"); + } else { + try writer.writeAll(")\n"); + } + }, + + .arg => { + const arg = inst.castTag(.arg).?; + try writer.print("{s})\n", .{arg.name}); + }, + + // TODO fill out this debug printing + .assembly, + .block, + .br, + .brvoid, + .call, + .condbr, + .constant, + .loop, + .varptr, + .switchbr, + => { + try writer.writeAll("!TODO!)\n"); + }, + } + } + } + + fn findConst(dtz: *DumpTzir, operand: *ir.Inst) !void { + if (operand.tag == .constant) { + try dtz.const_table.put(operand, dtz.next_const_index); + dtz.next_const_index += 1; + } + } +}; const EmitZIR = struct { allocator: *Allocator, @@ -2105,36 +2282,6 @@ const EmitZIR = struct { return &declref_inst.base; } - fn emitBlock(self: *EmitZIR, module_block: *IrModule.Scope.Block, src: usize) Allocator.Error!*Decl { - var inst_table = std.AutoHashMap(*ir.Inst, *Inst).init(self.allocator); - defer inst_table.deinit(); - - var instructions = std.ArrayList(*Inst).init(self.allocator); - defer instructions.deinit(); - - const body: ir.Body = .{ .instructions = module_block.instructions.items }; - try self.emitBody(body, &inst_table, &instructions); - - const fn_type = try self.emitType(src, Type.initTag(.void)); - - const arena_instrs = try self.arena.allocator.alloc(*Inst, instructions.items.len); - mem.copy(*Inst, arena_instrs, instructions.items); - - const fn_inst = try self.arena.allocator.create(Inst.Fn); - fn_inst.* = .{ - .base = .{ - .src = src, - .tag = Inst.Fn.base_tag, - }, - .positionals = .{ - .fn_type = fn_type.inst, - .body = .{ .instructions = arena_instrs }, - }, - .kw_args = .{}, - }; - return self.emitUnnamedDecl(&fn_inst.base); - } - fn emitFn(self: *EmitZIR, module_fn: *IrModule.Fn, src: usize, ty: Type) Allocator.Error!*Decl { var inst_table = std.AutoHashMap(*ir.Inst, *Inst).init(self.allocator); defer inst_table.deinit(); @@ -2142,7 +2289,7 @@ const EmitZIR = struct { var instructions = std.ArrayList(*Inst).init(self.allocator); defer instructions.deinit(); - switch (module_fn.analysis) { + switch (module_fn.analysis()) { .queued => unreachable, .in_progress => unreachable, .success => |body| { @@ -2224,7 +2371,9 @@ const EmitZIR = struct { .fn_type = fn_type.inst, .body = .{ .instructions = arena_instrs }, }, - .kw_args = .{}, + .kw_args = .{ + .is_inline = module_fn.bits.is_inline, + }, }; return self.emitUnnamedDecl(&fn_inst.base); } diff --git a/src/zir_sema.zig b/src/zir_sema.zig index 96a19f20f4..9365996bb6 100644 --- a/src/zir_sema.zig +++ b/src/zir_sema.zig @@ -577,7 +577,15 @@ fn analyzeInstCompileError(mod: *Module, scope: *Scope, inst: *zir.Inst.UnOp) In } fn analyzeInstArg(mod: *Module, scope: *Scope, inst: *zir.Inst.Arg) InnerError!*Inst { - const b = try mod.requireRuntimeBlock(scope, inst.base.src); + const b = try mod.requireFunctionBlock(scope, inst.base.src); + switch (b.label) { + .none, .breaking => {}, + .inlining => |*inlining| { + const param_index = inlining.param_index; + inlining.param_index += 1; + return inlining.casted_args[param_index]; + }, + } const fn_ty = b.func.?.owner_decl.typed_value.most_recent.typed_value.ty; const param_index = b.instructions.items.len; const param_count = fn_ty.fnParamLen(); @@ -636,7 +644,7 @@ fn analyzeInstBlockFlat(mod: *Module, scope: *Scope, inst: *zir.Inst.Block, is_c .decl = parent_block.decl, .instructions = .{}, .arena = parent_block.arena, - .label = null, + .label = .none, .is_comptime = parent_block.is_comptime or is_comptime, }; defer child_block.instructions.deinit(mod.gpa); @@ -674,41 +682,56 @@ fn analyzeInstBlock(mod: *Module, scope: *Scope, inst: *zir.Inst.Block, is_compt .decl = parent_block.decl, .instructions = .{}, .arena = parent_block.arena, - // TODO @as here is working around a stage1 miscompilation bug :( - .label = @as(?Scope.Block.Label, Scope.Block.Label{ - .zir_block = inst, - .results = .{}, - .block_inst = block_inst, - }), + .label = Scope.Block.Label{ + .breaking = .{ + .zir_block = inst, + .merges = .{ + .results = .{}, + .block_inst = block_inst, + }, + }, + }, .is_comptime = is_comptime or parent_block.is_comptime, }; - const label = &child_block.label.?; + const merges = &child_block.label.breaking.merges; defer child_block.instructions.deinit(mod.gpa); - defer label.results.deinit(mod.gpa); + defer merges.results.deinit(mod.gpa); try analyzeBody(mod, &child_block.base, inst.positionals.body); + return analyzeBlockBody(mod, scope, &child_block, merges); +} + +fn analyzeBlockBody( + mod: *Module, + scope: *Scope, + child_block: *Scope.Block, + merges: *Scope.Block.Label.Merges, +) InnerError!*Inst { + const parent_block = scope.cast(Scope.Block).?; + // Blocks must terminate with noreturn instruction. assert(child_block.instructions.items.len != 0); assert(child_block.instructions.items[child_block.instructions.items.len - 1].ty.isNoReturn()); - if (label.results.items.len == 0) { - // No need for a block instruction. We can put the new instructions directly into the parent block. + if (merges.results.items.len == 0) { + // No need for a block instruction. We can put the new instructions + // directly into the parent block. const copied_instructions = try parent_block.arena.dupe(*Inst, child_block.instructions.items); try parent_block.instructions.appendSlice(mod.gpa, copied_instructions); return copied_instructions[copied_instructions.len - 1]; } - if (label.results.items.len == 1) { + if (merges.results.items.len == 1) { const last_inst_index = child_block.instructions.items.len - 1; const last_inst = child_block.instructions.items[last_inst_index]; if (last_inst.breakBlock()) |br_block| { - if (br_block == block_inst) { + if (br_block == merges.block_inst) { // No need for a block instruction. We can put the new instructions directly into the parent block. // Here we omit the break instruction. const copied_instructions = try parent_block.arena.dupe(*Inst, child_block.instructions.items[0..last_inst_index]); try parent_block.instructions.appendSlice(mod.gpa, copied_instructions); - return label.results.items[0]; + return merges.results.items[0]; } } } @@ -717,10 +740,10 @@ fn analyzeInstBlock(mod: *Module, scope: *Scope, inst: *zir.Inst.Block, is_compt // Need to set the type and emit the Block instruction. This allows machine code generation // to emit a jump instruction to after the block when it encounters the break. - try parent_block.instructions.append(mod.gpa, &block_inst.base); - block_inst.base.ty = try mod.resolvePeerTypes(scope, label.results.items); - block_inst.body = .{ .instructions = try parent_block.arena.dupe(*Inst, child_block.instructions.items) }; - return &block_inst.base; + try parent_block.instructions.append(mod.gpa, &merges.block_inst.base); + merges.block_inst.base.ty = try mod.resolvePeerTypes(scope, merges.results.items); + merges.block_inst.body = .{ .instructions = try parent_block.arena.dupe(*Inst, child_block.instructions.items) }; + return &merges.block_inst.base; } fn analyzeInstBreakpoint(mod: *Module, scope: *Scope, inst: *zir.Inst.NoOp) InnerError!*Inst { @@ -829,14 +852,32 @@ fn analyzeInstCall(mod: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerError const ret_type = func.ty.fnReturnType(); const b = try mod.requireFunctionBlock(scope, inst.base.src); - if (b.is_comptime) { - const fn_val = try mod.resolveConstValue(scope, func); - const module_fn = switch (fn_val.tag()) { - .function => fn_val.castTag(.function).?.data, - .extern_fn => return mod.fail(scope, inst.base.src, "comptime call of extern function", .{}), + const is_comptime_call = b.is_comptime or inst.kw_args.modifier == .compile_time; + const is_inline_call = is_comptime_call or inst.kw_args.modifier == .always_inline or blk: { + // This logic will get simplified by + // https://github.com/ziglang/zig/issues/6429 + if (try mod.resolveDefinedValue(scope, func)) |func_val| { + const module_fn = switch (func_val.tag()) { + .function => func_val.castTag(.function).?.data, + else => break :blk false, + }; + break :blk module_fn.bits.is_inline; + } + break :blk false; + }; + if (is_inline_call) { + const func_val = try mod.resolveConstValue(scope, func); + const module_fn = switch (func_val.tag()) { + .function => func_val.castTag(.function).?.data, + .extern_fn => return mod.fail(scope, inst.base.src, "{s} call of extern function", .{ + @as([]const u8, if (is_comptime_call) "comptime" else "inline"), + }), else => unreachable, }; const callee_decl = module_fn.owner_decl; + // TODO: De-duplicate this with the code in Module.zig that generates + // ZIR for the same function and re-use the same ZIR for runtime function + // generation and for inline/comptime calls. const callee_file_scope = callee_decl.getFileScope(); const tree = mod.getAstTree(callee_file_scope) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, @@ -859,23 +900,31 @@ fn analyzeInstCall(mod: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerError }; defer gen_scope.instructions.deinit(mod.gpa); - // Add a const instruction for each parameter. + // We need an instruction for each parameter, and they must be first in the body. + try gen_scope.instructions.resize(mod.gpa, fn_proto.params_len); var params_scope = &gen_scope.base; for (fn_proto.params()) |param, i| { const name_token = param.name_token.?; const src = tree.token_locs[name_token].start; const param_name = try mod.identifierTokenString(scope, name_token); - const arg_val = try mod.resolveConstValue(scope, casted_args[i]); - const arg = try astgen.addZIRInstConst(mod, params_scope, src, .{ - .ty = casted_args[i].ty, - .val = arg_val, - }); + const arg = try call_arena.allocator.create(zir.Inst.Arg); + arg.* = .{ + .base = .{ + .tag = .arg, + .src = src, + }, + .positionals = .{ + .name = param_name, + }, + .kw_args = .{}, + }; + gen_scope.instructions.items[i] = &arg.base; const sub_scope = try call_arena.allocator.create(Scope.LocalVal); sub_scope.* = .{ .parent = params_scope, .gen_zir = &gen_scope, .name = param_name, - .inst = arg, + .inst = &arg.base, }; params_scope = &sub_scope.base; } @@ -896,42 +945,52 @@ fn analyzeInstCall(mod: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerError zir.dumpZir(mod.gpa, "fn_body_callee", callee_decl.name, gen_scope.instructions.items) catch {}; } - // Analyze the ZIR. - var inner_block: Scope.Block = .{ + // Analyze the ZIR. The same ZIR gets analyzed into a runtime function + // or an inlined call depending on what union tag the `label` field is + // set to in the `Scope.Block`. + // This block instruction will be used to capture the return value from the + // inlined function. + const block_inst = try scope.arena().create(Inst.Block); + block_inst.* = .{ + .base = .{ + .tag = Inst.Block.base_tag, + .ty = ret_type, + .src = inst.base.src, + }, + .body = undefined, + }; + var child_block: Scope.Block = .{ .parent = null, .func = module_fn, - .decl = callee_decl, + // Note that we pass the caller's Decl, not the callee. This causes + // compile errors to be attached (correctly) to the caller's Decl. + .decl = scope.decl().?, .instructions = .{}, - .arena = &call_arena.allocator, - .is_comptime = true, + .arena = scope.arena(), + .label = Scope.Block.Label{ + .inlining = .{ + .param_index = 0, + .casted_args = casted_args, + .merges = .{ + .results = .{}, + .block_inst = block_inst, + }, + }, + }, + .is_comptime = is_comptime_call, }; - defer inner_block.instructions.deinit(mod.gpa); + const merges = &child_block.label.inlining.merges; + + defer child_block.instructions.deinit(mod.gpa); + defer merges.results.deinit(mod.gpa); - // TODO make sure compile errors that happen from this analyzeBody are reported correctly - // and attach to the caller Decl not the callee. - try analyzeBody(mod, &inner_block.base, .{ + // This will have return instructions analyzed as break instructions to + // the block_inst above. + try analyzeBody(mod, &child_block.base, .{ .instructions = gen_scope.instructions.items, }); - if (mod.comp.verbose_ir) { - inner_block.dump(mod.*); - } - - assert(inner_block.instructions.items.len == 1); - const only_inst = inner_block.instructions.items[0]; - switch (only_inst.tag) { - .ret => { - const ret_inst = only_inst.castTag(.ret).?; - const operand = ret_inst.operand; - const callee_arena = scope.arena(); - return mod.constInst(scope, inst.base.src, .{ - .ty = try operand.ty.copy(callee_arena), - .val = try operand.value().?.copy(callee_arena), - }); - }, - .retvoid => return mod.constVoid(scope, inst.base.src), - else => unreachable, - } + return analyzeBlockBody(mod, scope, &child_block, merges); } return mod.addCall(b, inst.base.src, ret_type, func, casted_args); @@ -954,7 +1013,11 @@ fn analyzeInstFn(mod: *Module, scope: *Scope, fn_inst: *zir.Inst.Fn) InnerError! }; const new_func = try scope.arena().create(Module.Fn); new_func.* = .{ - .analysis = .{ .queued = fn_zir }, + .bits = .{ + .state = .queued, + .is_inline = fn_inst.kw_args.is_inline, + }, + .data = .{ .zir = fn_zir }, .owner_decl = scope.decl().?, }; return mod.constInst(scope, fn_inst.base.src, .{ @@ -2020,21 +2083,41 @@ fn analyzeInstUnreachable( fn analyzeInstRet(mod: *Module, scope: *Scope, inst: *zir.Inst.UnOp) InnerError!*Inst { const operand = try resolveInst(mod, scope, inst.positionals.operand); const b = try mod.requireFunctionBlock(scope, inst.base.src); - return mod.addUnOp(b, inst.base.src, Type.initTag(.noreturn), .ret, operand); + + switch (b.label) { + .inlining => |*inlining| { + // We are inlining a function call; rewrite the `ret` as a `break`. + try inlining.merges.results.append(mod.gpa, operand); + return mod.addBr(b, inst.base.src, inlining.merges.block_inst, operand); + }, + .none, .breaking => { + return mod.addUnOp(b, inst.base.src, Type.initTag(.noreturn), .ret, operand); + }, + } } fn analyzeInstRetVoid(mod: *Module, scope: *Scope, inst: *zir.Inst.NoOp) InnerError!*Inst { const b = try mod.requireFunctionBlock(scope, inst.base.src); - if (b.func) |func| { - // Need to emit a compile error if returning void is not allowed. - const void_inst = try mod.constVoid(scope, inst.base.src); - const fn_ty = func.owner_decl.typed_value.most_recent.typed_value.ty; - const casted_void = try mod.coerce(scope, fn_ty.fnReturnType(), void_inst); - if (casted_void.ty.zigTypeTag() != .Void) { - return mod.addUnOp(b, inst.base.src, Type.initTag(.noreturn), .ret, casted_void); - } + switch (b.label) { + .inlining => |*inlining| { + // We are inlining a function call; rewrite the `retvoid` as a `breakvoid`. + const void_inst = try mod.constVoid(scope, inst.base.src); + try inlining.merges.results.append(mod.gpa, void_inst); + return mod.addBr(b, inst.base.src, inlining.merges.block_inst, void_inst); + }, + .none, .breaking => { + if (b.func) |func| { + // Need to emit a compile error if returning void is not allowed. + const void_inst = try mod.constVoid(scope, inst.base.src); + const fn_ty = func.owner_decl.typed_value.most_recent.typed_value.ty; + const casted_void = try mod.coerce(scope, fn_ty.fnReturnType(), void_inst); + if (casted_void.ty.zigTypeTag() != .Void) { + return mod.addUnOp(b, inst.base.src, Type.initTag(.noreturn), .ret, casted_void); + } + } + return mod.addNoOp(b, inst.base.src, Type.initTag(.noreturn), .retvoid); + }, } - return mod.addNoOp(b, inst.base.src, Type.initTag(.noreturn), .retvoid); } fn floatOpAllowed(tag: zir.Inst.Tag) bool { @@ -2054,12 +2137,16 @@ fn analyzeBreak( ) InnerError!*Inst { var opt_block = scope.cast(Scope.Block); while (opt_block) |block| { - if (block.label) |*label| { - if (label.zir_block == zir_block) { - try label.results.append(mod.gpa, operand); - const b = try mod.requireRuntimeBlock(scope, src); - return mod.addBr(b, src, label.block_inst, operand); - } + switch (block.label) { + .none => {}, + .breaking => |*label| { + if (label.zir_block == zir_block) { + try label.merges.results.append(mod.gpa, operand); + const b = try mod.requireFunctionBlock(scope, src); + return mod.addBr(b, src, label.merges.block_inst, operand); + } + }, + .inlining => unreachable, // Invalid `break` ZIR inside inline function call. } opt_block = block.parent; } else unreachable; diff --git a/test/stage2/zir.zig b/test/stage2/zir.zig index da4038e792..c29e636cd4 100644 --- a/test/stage2/zir.zig +++ b/test/stage2/zir.zig @@ -30,7 +30,7 @@ pub fn addCases(ctx: *TestContext) !void { \\@unnamed$7 = fntype([], @void, cc=C) \\@entry = fn(@unnamed$7, { \\ %0 = returnvoid() ; deaths=0b1000000000000000 - \\}) + \\}, is_inline=0) \\ ); ctx.transformZIR("elemptr, add, cmp, condbr, return, breakpoint", linux_x64, @@ -78,7 +78,7 @@ pub fn addCases(ctx: *TestContext) !void { \\@unnamed$6 = fntype([], @void, cc=C) \\@entry = fn(@unnamed$6, { \\ %0 = returnvoid() ; deaths=0b1000000000000000 - \\}) + \\}, is_inline=0) \\@entry__anon_1 = str("2\x08\x01\n") \\@9 = declref("9__anon_0") \\@9__anon_0 = str("entry") @@ -123,17 +123,17 @@ pub fn addCases(ctx: *TestContext) !void { \\@entry = fn(@unnamed$7, { \\ %0 = call(@a, [], modifier=auto) ; deaths=0b1000000000000001 \\ %1 = returnvoid() ; deaths=0b1000000000000000 - \\}) + \\}, is_inline=0) \\@unnamed$9 = fntype([], @void, cc=C) \\@a = fn(@unnamed$9, { \\ %0 = call(@b, [], modifier=auto) ; deaths=0b1000000000000001 \\ %1 = returnvoid() ; deaths=0b1000000000000000 - \\}) + \\}, is_inline=0) \\@unnamed$11 = fntype([], @void, cc=C) \\@b = fn(@unnamed$11, { \\ %0 = call(@a, [], modifier=auto) ; deaths=0b1000000000000001 \\ %1 = returnvoid() ; deaths=0b1000000000000000 - \\}) + \\}, is_inline=0) \\ ); // Now we introduce a compile error @@ -203,7 +203,7 @@ pub fn addCases(ctx: *TestContext) !void { \\@unnamed$7 = fntype([], @void, cc=C) \\@entry = fn(@unnamed$7, { \\ %0 = returnvoid() ; deaths=0b1000000000000000 - \\}) + \\}, is_inline=0) \\ ); } -- cgit v1.2.3 From 006e7f68056af62ae7713d7ef228841d11874735 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 2 Jan 2021 13:40:23 -0700 Subject: stage2: re-use ZIR for comptime and inline calls Instead of freeing ZIR after semantic analysis, we keep it around so that it can be used for comptime calls, inline calls, and generic function calls. ZIR memory is now managed by the Decl arena. Debug dump() functions are conditionally compiled; only available in Debug builds of the compiler. Add a test for an inline function call. --- src/Compilation.zig | 9 ++-- src/Module.zig | 136 +++++++++++++++------------------------------------ src/codegen.zig | 10 ++-- src/codegen/c.zig | 2 +- src/codegen/wasm.zig | 2 +- src/llvm_backend.zig | 2 +- src/zir.zig | 17 ++++--- src/zir_sema.zig | 99 ++----------------------------------- test/stage2/test.zig | 25 ++++++++++ 9 files changed, 93 insertions(+), 209 deletions(-) (limited to 'src/Module.zig') diff --git a/src/Compilation.zig b/src/Compilation.zig index a6f39a3154..9a06aee561 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -1459,15 +1459,16 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor const module = self.bin_file.options.module.?; if (decl.typed_value.most_recent.typed_value.val.castTag(.function)) |payload| { const func = payload.data; - switch (func.bits.state) { + switch (func.state) { .queued => module.analyzeFnBody(decl, func) catch |err| switch (err) { error.AnalysisFail => { - assert(func.bits.state != .in_progress); + assert(func.state != .in_progress); continue; }, error.OutOfMemory => return error.OutOfMemory, }, .in_progress => unreachable, + .inline_only => unreachable, // don't queue work for this .sema_failure, .dependency_failure => continue, .success => {}, } @@ -1476,9 +1477,9 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor var decl_arena = decl.typed_value.most_recent.arena.?.promote(module.gpa); defer decl.typed_value.most_recent.arena.?.* = decl_arena.state; log.debug("analyze liveness of {s}\n", .{decl.name}); - try liveness.analyze(module.gpa, &decl_arena.allocator, func.data.body); + try liveness.analyze(module.gpa, &decl_arena.allocator, func.body); - if (self.verbose_ir) { + if (std.builtin.mode == .Debug and self.verbose_ir) { func.dump(module.*); } } diff --git a/src/Module.zig b/src/Module.zig index db76ecd5db..be6ca0df63 100644 --- a/src/Module.zig +++ b/src/Module.zig @@ -286,75 +286,29 @@ pub const Decl = struct { /// Extern functions do not have this data structure; they are represented by /// the `Decl` only, with a `Value` tag of `extern_fn`. pub const Fn = struct { - bits: packed struct { - /// Get and set this field via `analysis` and `setAnalysis`. - state: Analysis.Tag, - /// We carry this state into `Fn` instead of leaving it in the AST so that - /// analysis of function calls can happen even on functions whose AST has - /// been unloaded from memory. - is_inline: bool, - unused_bits: u4 = 0, - }, - /// Get and set this data via `analysis` and `setAnalysis`. - data: union { - none: void, - zir: *ZIR, - body: Body, - }, owner_decl: *Decl, - - pub const Analysis = union(Tag) { - queued: *ZIR, + /// Contains un-analyzed ZIR instructions generated from Zig source AST. + /// Even after we finish analysis, the ZIR is kept in memory, so that + /// comptime and inline function calls can happen. + zir: zir.Module.Body, + /// undefined unless analysis state is `success`. + body: Body, + state: Analysis, + + pub const Analysis = enum { + queued, + /// This function intentionally only has ZIR generated because it is marked + /// inline, which means no runtime version of the function will be generated. + inline_only, in_progress, + /// There will be a corresponding ErrorMsg in Module.failed_decls sema_failure, + /// This Fn might be OK but it depends on another Decl which did not + /// successfully complete semantic analysis. dependency_failure, - success: Body, - - pub const Tag = enum(u3) { - queued, - in_progress, - /// There will be a corresponding ErrorMsg in Module.failed_decls - sema_failure, - /// This Fn might be OK but it depends on another Decl which did not - /// successfully complete semantic analysis. - dependency_failure, - success, - }; + success, }; - /// Contains un-analyzed ZIR instructions generated from Zig source AST. - pub const ZIR = struct { - body: zir.Module.Body, - arena: std.heap.ArenaAllocator.State, - }; - - pub fn analysis(self: Fn) Analysis { - return switch (self.bits.state) { - .queued => .{ .queued = self.data.zir }, - .success => .{ .success = self.data.body }, - .in_progress => .in_progress, - .sema_failure => .sema_failure, - .dependency_failure => .dependency_failure, - }; - } - - pub fn setAnalysis(self: *Fn, anal: Analysis) void { - switch (anal) { - .queued => |zir_ptr| { - self.bits.state = .queued; - self.data = .{ .zir = zir_ptr }; - }, - .success => |body| { - self.bits.state = .success; - self.data = .{ .body = body }; - }, - .in_progress, .sema_failure, .dependency_failure => { - self.bits.state = anal; - self.data = .{ .none = {} }; - }, - } - } - /// For debugging purposes. pub fn dump(self: *Fn, mod: Module) void { zir.dumpFn(mod, self); @@ -1124,7 +1078,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { .param_types = param_types, }, .{}); - if (self.comp.verbose_ir) { + if (std.builtin.mode == .Debug and self.comp.verbose_ir) { zir.dumpZir(self.gpa, "fn_type", decl.name, fn_type_scope.instructions.items) catch {}; } @@ -1175,14 +1129,11 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { const new_func = try decl_arena.allocator.create(Fn); const fn_payload = try decl_arena.allocator.create(Value.Payload.Function); - const fn_zir = blk: { - // This scope's arena memory is discarded after the ZIR generation - // pass completes, and semantic analysis of it completes. - var gen_scope_arena = std.heap.ArenaAllocator.init(self.gpa); - errdefer gen_scope_arena.deinit(); + const fn_zir: zir.Module.Body = blk: { + // We put the ZIR inside the Decl arena. var gen_scope: Scope.GenZIR = .{ .decl = decl, - .arena = &gen_scope_arena.allocator, + .arena = &decl_arena.allocator, .parent = decl.scope, }; defer gen_scope.instructions.deinit(self.gpa); @@ -1194,7 +1145,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { const name_token = param.name_token.?; const src = tree.token_locs[name_token].start; const param_name = try self.identifierTokenString(&gen_scope.base, name_token); - const arg = try gen_scope_arena.allocator.create(zir.Inst.Arg); + const arg = try decl_arena.allocator.create(zir.Inst.Arg); arg.* = .{ .base = .{ .tag = .arg, @@ -1206,7 +1157,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { .kw_args = .{}, }; gen_scope.instructions.items[i] = &arg.base; - const sub_scope = try gen_scope_arena.allocator.create(Scope.LocalVal); + const sub_scope = try decl_arena.allocator.create(Scope.LocalVal); sub_scope.* = .{ .parent = params_scope, .gen_zir = &gen_scope, @@ -1227,18 +1178,13 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { _ = try astgen.addZIRNoOp(self, &gen_scope.base, src, .returnvoid); } - if (self.comp.verbose_ir) { + if (std.builtin.mode == .Debug and self.comp.verbose_ir) { zir.dumpZir(self.gpa, "fn_body", decl.name, gen_scope.instructions.items) catch {}; } - const fn_zir = try gen_scope_arena.allocator.create(Fn.ZIR); - fn_zir.* = .{ - .body = .{ - .instructions = try gen_scope.arena.dupe(*zir.Inst, gen_scope.instructions.items), - }, - .arena = gen_scope_arena.state, + break :blk .{ + .instructions = try gen_scope.arena.dupe(*zir.Inst, gen_scope.instructions.items), }; - break :blk fn_zir; }; const is_inline = blk: { @@ -1249,13 +1195,12 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { } break :blk false; }; + const anal_state = ([2]Fn.Analysis{ .queued, .inline_only })[@boolToInt(is_inline)]; new_func.* = .{ - .bits = .{ - .state = .queued, - .is_inline = is_inline, - }, - .data = .{ .zir = fn_zir }, + .state = anal_state, + .zir = fn_zir, + .body = undefined, .owner_decl = decl, }; fn_payload.* = .{ @@ -1272,7 +1217,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { type_changed = !tvm.typed_value.ty.eql(fn_type); if (tvm.typed_value.val.castTag(.function)) |payload| { const prev_func = payload.data; - prev_is_inline = prev_func.bits.is_inline; + prev_is_inline = prev_func.state == .inline_only; } tvm.deinit(self.gpa); @@ -1391,7 +1336,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { const src = tree.token_locs[init_node.firstToken()].start; const init_inst = try astgen.expr(self, &gen_scope.base, init_result_loc, init_node); - if (self.comp.verbose_ir) { + if (std.builtin.mode == .Debug and self.comp.verbose_ir) { zir.dumpZir(self.gpa, "var_init", decl.name, gen_scope.instructions.items) catch {}; } @@ -1435,7 +1380,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { .val = Value.initTag(.type_type), }); const var_type = try astgen.expr(self, &type_scope.base, .{ .ty = type_type }, type_node); - if (self.comp.verbose_ir) { + if (std.builtin.mode == .Debug and self.comp.verbose_ir) { zir.dumpZir(self.gpa, "var_type", decl.name, type_scope.instructions.items) catch {}; } @@ -1511,7 +1456,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { defer gen_scope.instructions.deinit(self.gpa); _ = try astgen.comptimeExpr(self, &gen_scope.base, .none, comptime_decl.expr); - if (self.comp.verbose_ir) { + if (std.builtin.mode == .Debug and self.comp.verbose_ir) { zir.dumpZir(self.gpa, "comptime_block", decl.name, gen_scope.instructions.items) catch {}; } @@ -1902,15 +1847,14 @@ pub fn analyzeFnBody(self: *Module, decl: *Decl, func: *Fn) !void { }; defer inner_block.instructions.deinit(self.gpa); - const fn_zir = func.data.zir; - defer fn_zir.arena.promote(self.gpa).deinit(); - func.setAnalysis(.in_progress); + func.state = .in_progress; log.debug("set {s} to in_progress\n", .{decl.name}); - try zir_sema.analyzeBody(self, &inner_block.base, fn_zir.body); + try zir_sema.analyzeBody(self, &inner_block.base, func.zir); const instructions = try arena.allocator.dupe(*Inst, inner_block.instructions.items); - func.setAnalysis(.{ .success = .{ .instructions = instructions } }); + func.state = .success; + func.body = .{ .instructions = instructions }; log.debug("set {s} to success\n", .{decl.name}); } @@ -2407,7 +2351,7 @@ pub fn analyzeDeclRef(self: *Module, scope: *Scope, src: usize, decl: *Decl) Inn self.ensureDeclAnalyzed(decl) catch |err| { if (scope.cast(Scope.Block)) |block| { if (block.func) |func| { - func.setAnalysis(.dependency_failure); + func.state = .dependency_failure; } else { block.decl.analysis = .dependency_failure; } @@ -3107,7 +3051,7 @@ fn failWithOwnedErrorMsg(self: *Module, scope: *Scope, src: usize, err_msg: *Com .block => { const block = scope.cast(Scope.Block).?; if (block.func) |func| { - func.setAnalysis(.sema_failure); + func.state = .sema_failure; } else { block.decl.analysis = .sema_failure; block.decl.generation = self.generation; diff --git a/src/codegen.zig b/src/codegen.zig index 588c3dec4c..58be73a31c 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -532,7 +532,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type { self.code.items.len += 4; try self.dbgSetPrologueEnd(); - try self.genBody(self.mod_fn.data.body); + try self.genBody(self.mod_fn.body); const stack_end = self.max_end_stack; if (stack_end > math.maxInt(i32)) @@ -576,7 +576,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type { }); } else { try self.dbgSetPrologueEnd(); - try self.genBody(self.mod_fn.data.body); + try self.genBody(self.mod_fn.body); try self.dbgSetEpilogueBegin(); } }, @@ -593,7 +593,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type { try self.dbgSetPrologueEnd(); - try self.genBody(self.mod_fn.data.body); + try self.genBody(self.mod_fn.body); // Backpatch stack offset const stack_end = self.max_end_stack; @@ -638,13 +638,13 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type { writeInt(u32, try self.code.addManyAsArray(4), Instruction.pop(.al, .{ .fp, .pc }).toU32()); } else { try self.dbgSetPrologueEnd(); - try self.genBody(self.mod_fn.data.body); + try self.genBody(self.mod_fn.body); try self.dbgSetEpilogueBegin(); } }, else => { try self.dbgSetPrologueEnd(); - try self.genBody(self.mod_fn.data.body); + try self.genBody(self.mod_fn.body); try self.dbgSetEpilogueBegin(); }, } diff --git a/src/codegen/c.zig b/src/codegen/c.zig index 712d663af0..1a89e22d48 100644 --- a/src/codegen/c.zig +++ b/src/codegen/c.zig @@ -275,7 +275,7 @@ pub fn generate(file: *C, module: *Module, decl: *Decl) !void { try writer.writeAll(" {"); const func: *Module.Fn = func_payload.data; - const instructions = func.data.body.instructions; + const instructions = func.body.instructions; if (instructions.len > 0) { try writer.writeAll("\n"); for (instructions) |inst| { diff --git a/src/codegen/wasm.zig b/src/codegen/wasm.zig index 1eb4f5bc29..036243dcca 100644 --- a/src/codegen/wasm.zig +++ b/src/codegen/wasm.zig @@ -63,7 +63,7 @@ pub fn genCode(buf: *ArrayList(u8), decl: *Decl) !void { // TODO: check for and handle death of instructions const tv = decl.typed_value.most_recent.typed_value; const mod_fn = tv.val.castTag(.function).?.data; - for (mod_fn.data.body.instructions) |inst| try genInst(buf, decl, inst); + for (mod_fn.body.instructions) |inst| try genInst(buf, decl, inst); // Write 'end' opcode try writer.writeByte(0x0B); diff --git a/src/llvm_backend.zig b/src/llvm_backend.zig index 5814aa7e7e..97406797b6 100644 --- a/src/llvm_backend.zig +++ b/src/llvm_backend.zig @@ -294,7 +294,7 @@ pub const LLVMIRModule = struct { const entry_block = llvm_func.appendBasicBlock("Entry"); self.builder.positionBuilderAtEnd(entry_block); - const instructions = func.data.body.instructions; + const instructions = func.body.instructions; for (instructions) |inst| { switch (inst.tag) { .breakpoint => try self.genBreakpoint(inst.castTag(.breakpoint).?), diff --git a/src/zir.zig b/src/zir.zig index 64b74f24d9..56ddee919c 100644 --- a/src/zir.zig +++ b/src/zir.zig @@ -1864,13 +1864,15 @@ pub fn dumpFn(old_module: IrModule, module_fn: *IrModule.Fn) void { defer ctx.const_table.deinit(); defer ctx.arena.deinit(); - switch (module_fn.analysis()) { + switch (module_fn.state) { .queued => std.debug.print("(queued)", .{}), + .inline_only => std.debug.print("(inline_only)", .{}), .in_progress => std.debug.print("(in_progress)", .{}), .sema_failure => std.debug.print("(sema_failure)", .{}), .dependency_failure => std.debug.print("(dependency_failure)", .{}), - .success => |body| { - ctx.dump(body, std.io.getStdErr().writer()) catch @panic("failed to dump TZIR"); + .success => { + const writer = std.io.getStdErr().writer(); + ctx.dump(module_fn.body, writer) catch @panic("failed to dump TZIR"); }, } } @@ -2289,11 +2291,12 @@ const EmitZIR = struct { var instructions = std.ArrayList(*Inst).init(self.allocator); defer instructions.deinit(); - switch (module_fn.analysis()) { + switch (module_fn.state) { .queued => unreachable, .in_progress => unreachable, - .success => |body| { - try self.emitBody(body, &inst_table, &instructions); + .inline_only => unreachable, + .success => { + try self.emitBody(module_fn.body, &inst_table, &instructions); }, .sema_failure => { const err_msg = self.old_module.failed_decls.get(module_fn.owner_decl).?; @@ -2372,7 +2375,7 @@ const EmitZIR = struct { .body = .{ .instructions = arena_instrs }, }, .kw_args = .{ - .is_inline = module_fn.bits.is_inline, + .is_inline = module_fn.state == .inline_only, }, }; return self.emitUnnamedDecl(&fn_inst.base); diff --git a/src/zir_sema.zig b/src/zir_sema.zig index 9365996bb6..e8d995dd5e 100644 --- a/src/zir_sema.zig +++ b/src/zir_sema.zig @@ -25,8 +25,6 @@ const trace = @import("tracy.zig").trace; const Scope = Module.Scope; const InnerError = Module.InnerError; const Decl = Module.Decl; -const astgen = @import("astgen.zig"); -const ast = std.zig.ast; pub fn analyzeInst(mod: *Module, scope: *Scope, old_inst: *zir.Inst) InnerError!*Inst { switch (old_inst.tag) { @@ -861,7 +859,7 @@ fn analyzeInstCall(mod: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerError .function => func_val.castTag(.function).?.data, else => break :blk false, }; - break :blk module_fn.bits.is_inline; + break :blk module_fn.state == .inline_only; } break :blk false; }; @@ -874,76 +872,6 @@ fn analyzeInstCall(mod: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerError }), else => unreachable, }; - const callee_decl = module_fn.owner_decl; - // TODO: De-duplicate this with the code in Module.zig that generates - // ZIR for the same function and re-use the same ZIR for runtime function - // generation and for inline/comptime calls. - const callee_file_scope = callee_decl.getFileScope(); - const tree = mod.getAstTree(callee_file_scope) catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, - error.AnalysisFail => return error.AnalysisFail, - // TODO: make sure this gets retried and not cached - else => return mod.fail(scope, inst.base.src, "failed to load {s}: {s}", .{ - callee_file_scope.sub_file_path, @errorName(err), - }), - }; - const ast_node = tree.root_node.decls()[callee_decl.src_index]; - const fn_proto = ast_node.castTag(.FnProto).?; - - var call_arena = std.heap.ArenaAllocator.init(mod.gpa); - defer call_arena.deinit(); - - var gen_scope: Scope.GenZIR = .{ - .decl = callee_decl, - .arena = &call_arena.allocator, - .parent = callee_decl.scope, - }; - defer gen_scope.instructions.deinit(mod.gpa); - - // We need an instruction for each parameter, and they must be first in the body. - try gen_scope.instructions.resize(mod.gpa, fn_proto.params_len); - var params_scope = &gen_scope.base; - for (fn_proto.params()) |param, i| { - const name_token = param.name_token.?; - const src = tree.token_locs[name_token].start; - const param_name = try mod.identifierTokenString(scope, name_token); - const arg = try call_arena.allocator.create(zir.Inst.Arg); - arg.* = .{ - .base = .{ - .tag = .arg, - .src = src, - }, - .positionals = .{ - .name = param_name, - }, - .kw_args = .{}, - }; - gen_scope.instructions.items[i] = &arg.base; - const sub_scope = try call_arena.allocator.create(Scope.LocalVal); - sub_scope.* = .{ - .parent = params_scope, - .gen_zir = &gen_scope, - .name = param_name, - .inst = &arg.base, - }; - params_scope = &sub_scope.base; - } - - const body_node = fn_proto.getBodyNode().?; // We handle extern functions above. - const body_block = body_node.cast(ast.Node.Block).?; - - try astgen.blockExpr(mod, params_scope, body_block); - - if (gen_scope.instructions.items.len == 0 or - !gen_scope.instructions.items[gen_scope.instructions.items.len - 1].tag.isNoReturn()) - { - const src = tree.token_locs[body_block.rbrace].start; - _ = try astgen.addZIRNoOp(mod, &gen_scope.base, src, .returnvoid); - } - - if (mod.comp.verbose_ir) { - zir.dumpZir(mod.gpa, "fn_body_callee", callee_decl.name, gen_scope.instructions.items) catch {}; - } // Analyze the ZIR. The same ZIR gets analyzed into a runtime function // or an inlined call depending on what union tag the `label` field is @@ -986,9 +914,7 @@ fn analyzeInstCall(mod: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerError // This will have return instructions analyzed as break instructions to // the block_inst above. - try analyzeBody(mod, &child_block.base, .{ - .instructions = gen_scope.instructions.items, - }); + try analyzeBody(mod, &child_block.base, module_fn.zir); return analyzeBlockBody(mod, scope, &child_block, merges); } @@ -998,26 +924,11 @@ fn analyzeInstCall(mod: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerError fn analyzeInstFn(mod: *Module, scope: *Scope, fn_inst: *zir.Inst.Fn) InnerError!*Inst { const fn_type = try resolveType(mod, scope, fn_inst.positionals.fn_type); - const fn_zir = blk: { - var fn_arena = std.heap.ArenaAllocator.init(mod.gpa); - errdefer fn_arena.deinit(); - - const fn_zir = try scope.arena().create(Module.Fn.ZIR); - fn_zir.* = .{ - .body = .{ - .instructions = fn_inst.positionals.body.instructions, - }, - .arena = fn_arena.state, - }; - break :blk fn_zir; - }; const new_func = try scope.arena().create(Module.Fn); new_func.* = .{ - .bits = .{ - .state = .queued, - .is_inline = fn_inst.kw_args.is_inline, - }, - .data = .{ .zir = fn_zir }, + .state = if (fn_inst.kw_args.is_inline) .inline_only else .queued, + .zir = fn_inst.positionals.body, + .body = undefined, .owner_decl = scope.decl().?, }; return mod.constInst(scope, fn_inst.base.src, .{ diff --git a/test/stage2/test.zig b/test/stage2/test.zig index 18c54b367e..79f5c3a73e 100644 --- a/test/stage2/test.zig +++ b/test/stage2/test.zig @@ -342,6 +342,7 @@ pub fn addCases(ctx: *TestContext) !void { , "", ); + // comptime function call case.addCompareOutput( \\export fn _start() noreturn { \\ exit(); @@ -365,6 +366,30 @@ pub fn addCases(ctx: *TestContext) !void { , "", ); + // Inline function call + case.addCompareOutput( + \\export fn _start() noreturn { + \\ var x: usize = 3; + \\ const y = add(1, 2, x); + \\ exit(y - 6); + \\} + \\ + \\inline fn add(a: usize, b: usize, c: usize) usize { + \\ return a + b + c; + \\} + \\ + \\fn exit(code: usize) noreturn { + \\ asm volatile ("syscall" + \\ : + \\ : [number] "{rax}" (231), + \\ [arg1] "{rdi}" (code) + \\ : "rcx", "r11", "memory" + \\ ); + \\ unreachable; + \\} + , + "", + ); } { -- cgit v1.2.3 From 50a530196ca4e91b387f9937475dd8891edb3f4f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 2 Jan 2021 14:28:03 -0700 Subject: stage2: fix handling compile error in inline fn call * scopes properly inherit inlining information * compile errors of inline function calls are properly attached to the caller rather than the callee. - added a test case for this * --watch still opens a repl if compile errors happen. --- src/Module.zig | 71 +++++++++++++++++------------ src/main.zig | 2 +- src/zir_sema.zig | 125 ++++++++++++++++++++++++--------------------------- test/stage2/test.zig | 51 +++++++++++++++++++++ 4 files changed, 154 insertions(+), 95 deletions(-) (limited to 'src/Module.zig') diff --git a/src/Module.zig b/src/Module.zig index be6ca0df63..8acc485079 100644 --- a/src/Module.zig +++ b/src/Module.zig @@ -759,33 +759,33 @@ pub const Scope = struct { instructions: ArrayListUnmanaged(*Inst), /// Points to the arena allocator of DeclAnalysis arena: *Allocator, - label: Label = Label.none, + label: ?Label = null, + inlining: ?Inlining, is_comptime: bool, - pub const Label = union(enum) { - none, - /// This `Block` maps a block ZIR instruction to the corresponding - /// TZIR instruction for break instruction analysis. - breaking: struct { - zir_block: *zir.Inst.Block, - merges: Merges, - }, - /// This `Block` indicates that an inline function call is happening - /// and return instructions should be analyzed as a break instruction - /// to this TZIR block instruction. - inlining: struct { - /// We use this to count from 0 so that arg instructions know - /// which parameter index they are, without having to store - /// a parameter index with each arg instruction. - param_index: usize, - casted_args: []*Inst, - merges: Merges, - }, + /// This `Block` maps a block ZIR instruction to the corresponding + /// TZIR instruction for break instruction analysis. + pub const Label = struct { + zir_block: *zir.Inst.Block, + merges: Merges, + }; - pub const Merges = struct { - results: ArrayListUnmanaged(*Inst), - block_inst: *Inst.Block, - }; + /// This `Block` indicates that an inline function call is happening + /// and return instructions should be analyzed as a break instruction + /// to this TZIR block instruction. + pub const Inlining = struct { + caller: ?*Fn, + /// We use this to count from 0 so that arg instructions know + /// which parameter index they are, without having to store + /// a parameter index with each arg instruction. + param_index: usize, + casted_args: []*Inst, + merges: Merges, + }; + + pub const Merges = struct { + results: ArrayListUnmanaged(*Inst), + block_inst: *Inst.Block, }; /// For debugging purposes. @@ -1093,6 +1093,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { .decl = decl, .instructions = .{}, .arena = &decl_arena.allocator, + .inlining = null, .is_comptime = false, }; defer block_scope.instructions.deinit(self.gpa); @@ -1281,6 +1282,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { .decl = decl, .instructions = .{}, .arena = &decl_arena.allocator, + .inlining = null, .is_comptime = true, }; defer block_scope.instructions.deinit(self.gpa); @@ -1346,6 +1348,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { .decl = decl, .instructions = .{}, .arena = &gen_scope_arena.allocator, + .inlining = null, .is_comptime = true, }; defer inner_block.instructions.deinit(self.gpa); @@ -1466,6 +1469,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { .decl = decl, .instructions = .{}, .arena = &analysis_arena.allocator, + .inlining = null, .is_comptime = true, }; defer block_scope.instructions.deinit(self.gpa); @@ -1843,6 +1847,7 @@ pub fn analyzeFnBody(self: *Module, decl: *Decl, func: *Fn) !void { .decl = decl, .instructions = .{}, .arena = &arena.allocator, + .inlining = null, .is_comptime = false, }; defer inner_block.instructions.deinit(self.gpa); @@ -3050,11 +3055,20 @@ fn failWithOwnedErrorMsg(self: *Module, scope: *Scope, src: usize, err_msg: *Com }, .block => { const block = scope.cast(Scope.Block).?; - if (block.func) |func| { - func.state = .sema_failure; + if (block.inlining) |*inlining| { + if (inlining.caller) |func| { + func.state = .sema_failure; + } else { + block.decl.analysis = .sema_failure; + block.decl.generation = self.generation; + } } else { - block.decl.analysis = .sema_failure; - block.decl.generation = self.generation; + if (block.func) |func| { + func.state = .sema_failure; + } else { + block.decl.analysis = .sema_failure; + block.decl.generation = self.generation; + } } self.failed_decls.putAssumeCapacityNoClobber(block.decl, err_msg); }, @@ -3414,6 +3428,7 @@ pub fn addSafetyCheck(mod: *Module, parent_block: *Scope.Block, ok: *Inst, panic .decl = parent_block.decl, .instructions = .{}, .arena = parent_block.arena, + .inlining = parent_block.inlining, .is_comptime = parent_block.is_comptime, }; defer fail_block.instructions.deinit(mod.gpa); diff --git a/src/main.zig b/src/main.zig index b1243badff..7b0e3fad7f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1818,7 +1818,7 @@ fn buildOutputType( }; updateModule(gpa, comp, zir_out_path, hook) catch |err| switch (err) { - error.SemanticAnalyzeFail => process.exit(1), + error.SemanticAnalyzeFail => if (!watch) process.exit(1), else => |e| return e, }; try comp.makeBinFileExecutable(); diff --git a/src/zir_sema.zig b/src/zir_sema.zig index e8d995dd5e..62d4de10e0 100644 --- a/src/zir_sema.zig +++ b/src/zir_sema.zig @@ -576,13 +576,10 @@ fn analyzeInstCompileError(mod: *Module, scope: *Scope, inst: *zir.Inst.UnOp) In fn analyzeInstArg(mod: *Module, scope: *Scope, inst: *zir.Inst.Arg) InnerError!*Inst { const b = try mod.requireFunctionBlock(scope, inst.base.src); - switch (b.label) { - .none, .breaking => {}, - .inlining => |*inlining| { - const param_index = inlining.param_index; - inlining.param_index += 1; - return inlining.casted_args[param_index]; - }, + if (b.inlining) |*inlining| { + const param_index = inlining.param_index; + inlining.param_index += 1; + return inlining.casted_args[param_index]; } const fn_ty = b.func.?.owner_decl.typed_value.most_recent.typed_value.ty; const param_index = b.instructions.items.len; @@ -620,6 +617,7 @@ fn analyzeInstLoop(mod: *Module, scope: *Scope, inst: *zir.Inst.Loop) InnerError .decl = parent_block.decl, .instructions = .{}, .arena = parent_block.arena, + .inlining = parent_block.inlining, .is_comptime = parent_block.is_comptime, }; defer child_block.instructions.deinit(mod.gpa); @@ -642,7 +640,8 @@ fn analyzeInstBlockFlat(mod: *Module, scope: *Scope, inst: *zir.Inst.Block, is_c .decl = parent_block.decl, .instructions = .{}, .arena = parent_block.arena, - .label = .none, + .label = null, + .inlining = parent_block.inlining, .is_comptime = parent_block.is_comptime or is_comptime, }; defer child_block.instructions.deinit(mod.gpa); @@ -680,18 +679,18 @@ fn analyzeInstBlock(mod: *Module, scope: *Scope, inst: *zir.Inst.Block, is_compt .decl = parent_block.decl, .instructions = .{}, .arena = parent_block.arena, - .label = Scope.Block.Label{ - .breaking = .{ - .zir_block = inst, - .merges = .{ - .results = .{}, - .block_inst = block_inst, - }, + // TODO @as here is working around a stage1 miscompilation bug :( + .label = @as(?Scope.Block.Label, Scope.Block.Label{ + .zir_block = inst, + .merges = .{ + .results = .{}, + .block_inst = block_inst, }, - }, + }), + .inlining = parent_block.inlining, .is_comptime = is_comptime or parent_block.is_comptime, }; - const merges = &child_block.label.breaking.merges; + const merges = &child_block.label.?.merges; defer child_block.instructions.deinit(mod.gpa); defer merges.results.deinit(mod.gpa); @@ -705,7 +704,7 @@ fn analyzeBlockBody( mod: *Module, scope: *Scope, child_block: *Scope.Block, - merges: *Scope.Block.Label.Merges, + merges: *Scope.Block.Merges, ) InnerError!*Inst { const parent_block = scope.cast(Scope.Block).?; @@ -895,19 +894,20 @@ fn analyzeInstCall(mod: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerError .decl = scope.decl().?, .instructions = .{}, .arena = scope.arena(), - .label = Scope.Block.Label{ - .inlining = .{ - .param_index = 0, - .casted_args = casted_args, - .merges = .{ - .results = .{}, - .block_inst = block_inst, - }, + .label = null, + // TODO @as here is working around a stage1 miscompilation bug :( + .inlining = @as(?Scope.Block.Inlining, Scope.Block.Inlining{ + .caller = b.func, + .param_index = 0, + .casted_args = casted_args, + .merges = .{ + .results = .{}, + .block_inst = block_inst, }, - }, + }), .is_comptime = is_comptime_call, }; - const merges = &child_block.label.inlining.merges; + const merges = &child_block.inlining.?.merges; defer child_block.instructions.deinit(mod.gpa); defer merges.results.deinit(mod.gpa); @@ -1416,6 +1416,7 @@ fn analyzeInstSwitchBr(mod: *Module, scope: *Scope, inst: *zir.Inst.SwitchBr) In .decl = parent_block.decl, .instructions = .{}, .arena = parent_block.arena, + .inlining = parent_block.inlining, .is_comptime = parent_block.is_comptime, }; defer case_block.instructions.deinit(mod.gpa); @@ -1955,6 +1956,7 @@ fn analyzeInstCondBr(mod: *Module, scope: *Scope, inst: *zir.Inst.CondBr) InnerE .decl = parent_block.decl, .instructions = .{}, .arena = parent_block.arena, + .inlining = parent_block.inlining, .is_comptime = parent_block.is_comptime, }; defer true_block.instructions.deinit(mod.gpa); @@ -1966,6 +1968,7 @@ fn analyzeInstCondBr(mod: *Module, scope: *Scope, inst: *zir.Inst.CondBr) InnerE .decl = parent_block.decl, .instructions = .{}, .arena = parent_block.arena, + .inlining = parent_block.inlining, .is_comptime = parent_block.is_comptime, }; defer false_block.instructions.deinit(mod.gpa); @@ -1995,40 +1998,34 @@ fn analyzeInstRet(mod: *Module, scope: *Scope, inst: *zir.Inst.UnOp) InnerError! const operand = try resolveInst(mod, scope, inst.positionals.operand); const b = try mod.requireFunctionBlock(scope, inst.base.src); - switch (b.label) { - .inlining => |*inlining| { - // We are inlining a function call; rewrite the `ret` as a `break`. - try inlining.merges.results.append(mod.gpa, operand); - return mod.addBr(b, inst.base.src, inlining.merges.block_inst, operand); - }, - .none, .breaking => { - return mod.addUnOp(b, inst.base.src, Type.initTag(.noreturn), .ret, operand); - }, + if (b.inlining) |*inlining| { + // We are inlining a function call; rewrite the `ret` as a `break`. + try inlining.merges.results.append(mod.gpa, operand); + return mod.addBr(b, inst.base.src, inlining.merges.block_inst, operand); } + + return mod.addUnOp(b, inst.base.src, Type.initTag(.noreturn), .ret, operand); } fn analyzeInstRetVoid(mod: *Module, scope: *Scope, inst: *zir.Inst.NoOp) InnerError!*Inst { const b = try mod.requireFunctionBlock(scope, inst.base.src); - switch (b.label) { - .inlining => |*inlining| { - // We are inlining a function call; rewrite the `retvoid` as a `breakvoid`. - const void_inst = try mod.constVoid(scope, inst.base.src); - try inlining.merges.results.append(mod.gpa, void_inst); - return mod.addBr(b, inst.base.src, inlining.merges.block_inst, void_inst); - }, - .none, .breaking => { - if (b.func) |func| { - // Need to emit a compile error if returning void is not allowed. - const void_inst = try mod.constVoid(scope, inst.base.src); - const fn_ty = func.owner_decl.typed_value.most_recent.typed_value.ty; - const casted_void = try mod.coerce(scope, fn_ty.fnReturnType(), void_inst); - if (casted_void.ty.zigTypeTag() != .Void) { - return mod.addUnOp(b, inst.base.src, Type.initTag(.noreturn), .ret, casted_void); - } - } - return mod.addNoOp(b, inst.base.src, Type.initTag(.noreturn), .retvoid); - }, + if (b.inlining) |*inlining| { + // We are inlining a function call; rewrite the `retvoid` as a `breakvoid`. + const void_inst = try mod.constVoid(scope, inst.base.src); + try inlining.merges.results.append(mod.gpa, void_inst); + return mod.addBr(b, inst.base.src, inlining.merges.block_inst, void_inst); + } + + if (b.func) |func| { + // Need to emit a compile error if returning void is not allowed. + const void_inst = try mod.constVoid(scope, inst.base.src); + const fn_ty = func.owner_decl.typed_value.most_recent.typed_value.ty; + const casted_void = try mod.coerce(scope, fn_ty.fnReturnType(), void_inst); + if (casted_void.ty.zigTypeTag() != .Void) { + return mod.addUnOp(b, inst.base.src, Type.initTag(.noreturn), .ret, casted_void); + } } + return mod.addNoOp(b, inst.base.src, Type.initTag(.noreturn), .retvoid); } fn floatOpAllowed(tag: zir.Inst.Tag) bool { @@ -2048,16 +2045,12 @@ fn analyzeBreak( ) InnerError!*Inst { var opt_block = scope.cast(Scope.Block); while (opt_block) |block| { - switch (block.label) { - .none => {}, - .breaking => |*label| { - if (label.zir_block == zir_block) { - try label.merges.results.append(mod.gpa, operand); - const b = try mod.requireFunctionBlock(scope, src); - return mod.addBr(b, src, label.merges.block_inst, operand); - } - }, - .inlining => unreachable, // Invalid `break` ZIR inside inline function call. + if (block.label) |*label| { + if (label.zir_block == zir_block) { + try label.merges.results.append(mod.gpa, operand); + const b = try mod.requireFunctionBlock(scope, src); + return mod.addBr(b, src, label.merges.block_inst, operand); + } } opt_block = block.parent; } else unreachable; diff --git a/test/stage2/test.zig b/test/stage2/test.zig index 79f5c3a73e..8d3fac4760 100644 --- a/test/stage2/test.zig +++ b/test/stage2/test.zig @@ -1379,4 +1379,55 @@ pub fn addCases(ctx: *TestContext) !void { \\} , &[_][]const u8{":2:9: error: variable of type '@Type(.Null)' must be const or comptime"}); } + + { + var case = ctx.exe("compile error in inline fn call fixed", linux_x64); + case.addError( + \\export fn _start() noreturn { + \\ var x: usize = 3; + \\ const y = add(10, 2, x); + \\ exit(y - 6); + \\} + \\ + \\inline fn add(a: usize, b: usize, c: usize) usize { + \\ if (a == 10) @compileError("bad"); + \\ return a + b + c; + \\} + \\ + \\fn exit(code: usize) noreturn { + \\ asm volatile ("syscall" + \\ : + \\ : [number] "{rax}" (231), + \\ [arg1] "{rdi}" (code) + \\ : "rcx", "r11", "memory" + \\ ); + \\ unreachable; + \\} + , &[_][]const u8{":8:18: error: bad"}); + + case.addCompareOutput( + \\export fn _start() noreturn { + \\ var x: usize = 3; + \\ const y = add(1, 2, x); + \\ exit(y - 6); + \\} + \\ + \\inline fn add(a: usize, b: usize, c: usize) usize { + \\ if (a == 10) @compileError("bad"); + \\ return a + b + c; + \\} + \\ + \\fn exit(code: usize) noreturn { + \\ asm volatile ("syscall" + \\ : + \\ : [number] "{rax}" (231), + \\ [arg1] "{rdi}" (code) + \\ : "rcx", "r11", "memory" + \\ ); + \\ unreachable; + \\} + , + "", + ); + } } -- cgit v1.2.3 From 654832253a7857e78aab85e28ed09fb16b632dd2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 2 Jan 2021 22:42:07 -0700 Subject: stage2: support recursive inline/comptime functions zir.Inst no longer has an `analyzed_inst` field. This is previously how we mapped ZIR to their TZIR counterparts, however with the way inline and comptime function calls work, we can potentially have the same ZIR structure being analyzed by multiple different analyses, such as during a recursive inline function call. This would cause the `analyzed_inst` field to become clobbered. So instead, we use a table to map the instructions to their semantically analyzed counterparts. This will help with multi-threaded compilation as well. Scope.Block.Inlining is split into 2 different layers of "sharedness". The first layer is shared by the whole inline/comptime function call stack. It contains the callsite where something is being inlined and the branch count/quota. The second layer is different per function call but shared by all the blocks within the function being inlined. Add support for debug dumping br and brvoid TZIR instructions. Remove the "unreachable code" error. It was happening even for this case: ```zig if (comptime_condition) return; bar(); // error: unreachable code ``` We will need smarter logic for when it is legal to emit this compile error. Remove the ZIR test cases. These are redundant with other higher level Zig source tests we have, and maintaining support for ZIRModule as a first-class top level abstraction is getting in the way of clean compiler design for the main use case. We will have ZIR/TZIR based test cases someday to help with testing optimization passes and ZIR to TZIR analysis, but as is, these test cases are not accomplishing that, and they are getting in the way. --- src/Module.zig | 63 ++++++++-- src/zir.zig | 77 ++++++++++++- src/zir_sema.zig | 139 +++++++++++----------- test/stage2/test.zig | 48 +++++++- test/stage2/zir.zig | 316 --------------------------------------------------- 5 files changed, 244 insertions(+), 399 deletions(-) delete mode 100644 test/stage2/zir.zig (limited to 'src/Module.zig') diff --git a/src/Module.zig b/src/Module.zig index 8acc485079..24ea48043b 100644 --- a/src/Module.zig +++ b/src/Module.zig @@ -752,17 +752,22 @@ pub const Scope = struct { /// during semantic analysis of the block. pub const Block = struct { pub const base_tag: Tag = .block; + base: Scope = Scope{ .tag = base_tag }, parent: ?*Block, + /// Maps ZIR to TZIR. Shared to sub-blocks. + inst_table: *InstTable, func: ?*Fn, decl: *Decl, instructions: ArrayListUnmanaged(*Inst), /// Points to the arena allocator of DeclAnalysis arena: *Allocator, label: ?Label = null, - inlining: ?Inlining, + inlining: ?*Inlining, is_comptime: bool, + pub const InstTable = std.AutoHashMap(*zir.Inst, *Inst); + /// This `Block` maps a block ZIR instruction to the corresponding /// TZIR instruction for break instruction analysis. pub const Label = struct { @@ -773,14 +778,23 @@ pub const Scope = struct { /// This `Block` indicates that an inline function call is happening /// and return instructions should be analyzed as a break instruction /// to this TZIR block instruction. + /// It is shared among all the blocks in an inline or comptime called + /// function. pub const Inlining = struct { - caller: ?*Fn, + /// Shared state among the entire inline/comptime call stack. + shared: *Shared, /// We use this to count from 0 so that arg instructions know /// which parameter index they are, without having to store /// a parameter index with each arg instruction. param_index: usize, casted_args: []*Inst, merges: Merges, + + pub const Shared = struct { + caller: ?*Fn, + branch_count: u64, + branch_quota: u64, + }; }; pub const Merges = struct { @@ -1087,8 +1101,12 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { errdefer decl_arena.deinit(); const decl_arena_state = try decl_arena.allocator.create(std.heap.ArenaAllocator.State); + var inst_table = Scope.Block.InstTable.init(self.gpa); + defer inst_table.deinit(); + var block_scope: Scope.Block = .{ .parent = null, + .inst_table = &inst_table, .func = null, .decl = decl, .instructions = .{}, @@ -1276,8 +1294,12 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { errdefer decl_arena.deinit(); const decl_arena_state = try decl_arena.allocator.create(std.heap.ArenaAllocator.State); + var decl_inst_table = Scope.Block.InstTable.init(self.gpa); + defer decl_inst_table.deinit(); + var block_scope: Scope.Block = .{ .parent = null, + .inst_table = &decl_inst_table, .func = null, .decl = decl, .instructions = .{}, @@ -1342,8 +1364,12 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { zir.dumpZir(self.gpa, "var_init", decl.name, gen_scope.instructions.items) catch {}; } + var var_inst_table = Scope.Block.InstTable.init(self.gpa); + defer var_inst_table.deinit(); + var inner_block: Scope.Block = .{ .parent = null, + .inst_table = &var_inst_table, .func = null, .decl = decl, .instructions = .{}, @@ -1352,10 +1378,12 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { .is_comptime = true, }; defer inner_block.instructions.deinit(self.gpa); - try zir_sema.analyzeBody(self, &inner_block.base, .{ .instructions = gen_scope.instructions.items }); + try zir_sema.analyzeBody(self, &inner_block, .{ + .instructions = gen_scope.instructions.items, + }); // The result location guarantees the type coercion. - const analyzed_init_inst = init_inst.analyzed_inst.?; + const analyzed_init_inst = var_inst_table.get(init_inst).?; // The is_comptime in the Scope.Block guarantees the result is comptime-known. const val = analyzed_init_inst.value().?; @@ -1463,8 +1491,12 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { zir.dumpZir(self.gpa, "comptime_block", decl.name, gen_scope.instructions.items) catch {}; } + var inst_table = Scope.Block.InstTable.init(self.gpa); + defer inst_table.deinit(); + var block_scope: Scope.Block = .{ .parent = null, + .inst_table = &inst_table, .func = null, .decl = decl, .instructions = .{}, @@ -1474,7 +1506,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool { }; defer block_scope.instructions.deinit(self.gpa); - _ = try zir_sema.analyzeBody(self, &block_scope.base, .{ + _ = try zir_sema.analyzeBody(self, &block_scope, .{ .instructions = gen_scope.instructions.items, }); @@ -1841,8 +1873,11 @@ pub fn analyzeFnBody(self: *Module, decl: *Decl, func: *Fn) !void { // Use the Decl's arena for function memory. var arena = decl.typed_value.most_recent.arena.?.promote(self.gpa); defer decl.typed_value.most_recent.arena.?.* = arena.state; + var inst_table = Scope.Block.InstTable.init(self.gpa); + defer inst_table.deinit(); var inner_block: Scope.Block = .{ .parent = null, + .inst_table = &inst_table, .func = func, .decl = decl, .instructions = .{}, @@ -1855,7 +1890,7 @@ pub fn analyzeFnBody(self: *Module, decl: *Decl, func: *Fn) !void { func.state = .in_progress; log.debug("set {s} to in_progress\n", .{decl.name}); - try zir_sema.analyzeBody(self, &inner_block.base, func.zir); + try zir_sema.analyzeBody(self, &inner_block, func.zir); const instructions = try arena.allocator.dupe(*Inst, inner_block.instructions.items); func.state = .success; @@ -3055,8 +3090,8 @@ fn failWithOwnedErrorMsg(self: *Module, scope: *Scope, src: usize, err_msg: *Com }, .block => { const block = scope.cast(Scope.Block).?; - if (block.inlining) |*inlining| { - if (inlining.caller) |func| { + if (block.inlining) |inlining| { + if (inlining.shared.caller) |func| { func.state = .sema_failure; } else { block.decl.analysis = .sema_failure; @@ -3424,6 +3459,7 @@ pub fn addSafetyCheck(mod: *Module, parent_block: *Scope.Block, ok: *Inst, panic var fail_block: Scope.Block = .{ .parent = parent_block, + .inst_table = parent_block.inst_table, .func = parent_block.func, .decl = parent_block.decl, .instructions = .{}, @@ -3492,3 +3528,14 @@ pub fn identifierTokenString(mod: *Module, scope: *Scope, token: ast.TokenIndex) } return ident_name; } + +pub fn emitBackwardBranch(mod: *Module, block: *Scope.Block, src: usize) !void { + const shared = block.inlining.?.shared; + shared.branch_count += 1; + if (shared.branch_count > shared.branch_quota) { + // TODO show the "called from here" stack + return mod.fail(&block.base, src, "evaluation exceeded {d} backwards branches", .{ + shared.branch_quota, + }); + } +} diff --git a/src/zir.zig b/src/zir.zig index 56ddee919c..3fd2ac7c80 100644 --- a/src/zir.zig +++ b/src/zir.zig @@ -25,12 +25,13 @@ pub const Decl = struct { /// These are instructions that correspond to the ZIR text format. See `ir.Inst` for /// in-memory, analyzed instructions with types and values. +/// We use a table to map these instruction to their respective semantically analyzed +/// instructions because it is possible to have multiple analyses on the same ZIR +/// happening at the same time. pub const Inst = struct { tag: Tag, /// Byte offset into the source. src: usize, - /// Pre-allocated field for mapping ZIR text instructions to post-analysis instructions. - analyzed_inst: ?*ir.Inst = null, /// These names are used directly as the instruction names in the text format. pub const Tag = enum { @@ -1947,11 +1948,20 @@ const DumpTzir = struct { .arg => {}, + .br => { + const br = inst.castTag(.br).?; + try dtz.findConst(&br.block.base); + try dtz.findConst(br.operand); + }, + + .brvoid => { + const brvoid = inst.castTag(.brvoid).?; + try dtz.findConst(&brvoid.block.base); + }, + // TODO fill out this debug printing .assembly, .block, - .br, - .brvoid, .call, .condbr, .constant, @@ -2078,11 +2088,66 @@ const DumpTzir = struct { try writer.print("{s})\n", .{arg.name}); }, + .br => { + const br = inst.castTag(.br).?; + + var lhs_kinky: ?usize = null; + var rhs_kinky: ?usize = null; + + if (dtz.partial_inst_table.get(&br.block.base)) |operand_index| { + try writer.print("%{d}, ", .{operand_index}); + } else if (dtz.const_table.get(&br.block.base)) |operand_index| { + try writer.print("@{d}, ", .{operand_index}); + } else if (dtz.inst_table.get(&br.block.base)) |operand_index| { + lhs_kinky = operand_index; + try writer.print("%{d}, ", .{operand_index}); + } else { + try writer.writeAll("!BADREF!, "); + } + + if (dtz.partial_inst_table.get(br.operand)) |operand_index| { + try writer.print("%{d}", .{operand_index}); + } else if (dtz.const_table.get(br.operand)) |operand_index| { + try writer.print("@{d}", .{operand_index}); + } else if (dtz.inst_table.get(br.operand)) |operand_index| { + rhs_kinky = operand_index; + try writer.print("%{d}", .{operand_index}); + } else { + try writer.writeAll("!BADREF!"); + } + + if (lhs_kinky != null or rhs_kinky != null) { + try writer.writeAll(") // Instruction does not dominate all uses!"); + if (lhs_kinky) |lhs| { + try writer.print(" %{d}", .{lhs}); + } + if (rhs_kinky) |rhs| { + try writer.print(" %{d}", .{rhs}); + } + try writer.writeAll("\n"); + } else { + try writer.writeAll(")\n"); + } + }, + + .brvoid => { + const brvoid = inst.castTag(.brvoid).?; + if (dtz.partial_inst_table.get(&brvoid.block.base)) |operand_index| { + try writer.print("%{d})\n", .{operand_index}); + } else if (dtz.const_table.get(&brvoid.block.base)) |operand_index| { + try writer.print("@{d})\n", .{operand_index}); + } else if (dtz.inst_table.get(&brvoid.block.base)) |operand_index| { + try writer.print("%{d}) // Instruction does not dominate all uses!\n", .{ + operand_index, + }); + } else { + try writer.writeAll("!BADREF!)\n"); + } + }, + // TODO fill out this debug printing .assembly, .block, - .br, - .brvoid, .call, .condbr, .constant, diff --git a/src/zir_sema.zig b/src/zir_sema.zig index 62d4de10e0..a5627933e1 100644 --- a/src/zir_sema.zig +++ b/src/zir_sema.zig @@ -159,16 +159,11 @@ pub fn analyzeInst(mod: *Module, scope: *Scope, old_inst: *zir.Inst) InnerError! } } -pub fn analyzeBody(mod: *Module, scope: *Scope, body: zir.Module.Body) !void { - for (body.instructions) |src_inst, i| { - const analyzed_inst = try analyzeInst(mod, scope, src_inst); - src_inst.analyzed_inst = analyzed_inst; +pub fn analyzeBody(mod: *Module, block: *Scope.Block, body: zir.Module.Body) !void { + for (body.instructions) |src_inst| { + const analyzed_inst = try analyzeInst(mod, &block.base, src_inst); + try block.inst_table.putNoClobber(src_inst, analyzed_inst); if (analyzed_inst.ty.zigTypeTag() == .NoReturn) { - for (body.instructions[i..]) |unreachable_inst| { - if (unreachable_inst.castTag(.dbg_stmt)) |dbg_stmt| { - return mod.fail(scope, dbg_stmt.base.src, "unreachable code", .{}); - } - } break; } } @@ -180,8 +175,8 @@ pub fn analyzeBodyValueAsType( zir_result_inst: *zir.Inst, body: zir.Module.Body, ) !Type { - try analyzeBody(mod, &block_scope.base, body); - const result_inst = zir_result_inst.analyzed_inst.?; + try analyzeBody(mod, block_scope, body); + const result_inst = block_scope.inst_table.get(zir_result_inst).?; const val = try mod.resolveConstValue(&block_scope.base, result_inst); return val.toType(block_scope.base.arena()); } @@ -264,30 +259,9 @@ fn resolveCompleteZirDecl(mod: *Module, scope: *Scope, src_decl: *zir.Decl) Inne return decl; } -/// TODO Look into removing this function. The body is only needed for .zir files, not .zig files. -pub fn resolveInst(mod: *Module, scope: *Scope, old_inst: *zir.Inst) InnerError!*Inst { - if (old_inst.analyzed_inst) |inst| return inst; - - // If this assert trips, the instruction that was referenced did not get properly - // analyzed before it was referenced. - const zir_module = scope.namespace().cast(Scope.ZIRModule).?; - const entry = if (old_inst.cast(zir.Inst.DeclVal)) |declval| blk: { - const decl_name = declval.positionals.name; - const entry = zir_module.contents.module.findDecl(decl_name) orelse - return mod.fail(scope, old_inst.src, "decl '{s}' not found", .{decl_name}); - break :blk entry; - } else blk: { - // If this assert trips, the instruction that was referenced did not get - // properly analyzed by a previous instruction analysis before it was - // referenced by the current one. - break :blk zir_module.contents.module.findInstDecl(old_inst).?; - }; - const decl = try resolveCompleteZirDecl(mod, scope, entry.decl); - const decl_ref = try mod.analyzeDeclRef(scope, old_inst.src, decl); - // Note: it would be tempting here to store the result into old_inst.analyzed_inst field, - // but this would prevent the analyzeDeclRef from happening, which is needed to properly - // detect Decl dependencies and dependency failures on updates. - return mod.analyzeDeref(scope, old_inst.src, decl_ref, old_inst.src); +pub fn resolveInst(mod: *Module, scope: *Scope, zir_inst: *zir.Inst) InnerError!*Inst { + const block = scope.cast(Scope.Block).?; + return block.inst_table.get(zir_inst).?; // Instruction does not dominate all uses! } fn resolveConstString(mod: *Module, scope: *Scope, old_inst: *zir.Inst) ![]u8 { @@ -576,7 +550,7 @@ fn analyzeInstCompileError(mod: *Module, scope: *Scope, inst: *zir.Inst.UnOp) In fn analyzeInstArg(mod: *Module, scope: *Scope, inst: *zir.Inst.Arg) InnerError!*Inst { const b = try mod.requireFunctionBlock(scope, inst.base.src); - if (b.inlining) |*inlining| { + if (b.inlining) |inlining| { const param_index = inlining.param_index; inlining.param_index += 1; return inlining.casted_args[param_index]; @@ -613,6 +587,7 @@ fn analyzeInstLoop(mod: *Module, scope: *Scope, inst: *zir.Inst.Loop) InnerError var child_block: Scope.Block = .{ .parent = parent_block, + .inst_table = parent_block.inst_table, .func = parent_block.func, .decl = parent_block.decl, .instructions = .{}, @@ -622,7 +597,7 @@ fn analyzeInstLoop(mod: *Module, scope: *Scope, inst: *zir.Inst.Loop) InnerError }; defer child_block.instructions.deinit(mod.gpa); - try analyzeBody(mod, &child_block.base, inst.positionals.body); + try analyzeBody(mod, &child_block, inst.positionals.body); // Loop repetition is implied so the last instruction may or may not be a noreturn instruction. @@ -636,6 +611,7 @@ fn analyzeInstBlockFlat(mod: *Module, scope: *Scope, inst: *zir.Inst.Block, is_c var child_block: Scope.Block = .{ .parent = parent_block, + .inst_table = parent_block.inst_table, .func = parent_block.func, .decl = parent_block.decl, .instructions = .{}, @@ -646,7 +622,7 @@ fn analyzeInstBlockFlat(mod: *Module, scope: *Scope, inst: *zir.Inst.Block, is_c }; defer child_block.instructions.deinit(mod.gpa); - try analyzeBody(mod, &child_block.base, inst.positionals.body); + try analyzeBody(mod, &child_block, inst.positionals.body); try parent_block.instructions.appendSlice(mod.gpa, child_block.instructions.items); @@ -675,6 +651,7 @@ fn analyzeInstBlock(mod: *Module, scope: *Scope, inst: *zir.Inst.Block, is_compt var child_block: Scope.Block = .{ .parent = parent_block, + .inst_table = parent_block.inst_table, .func = parent_block.func, .decl = parent_block.decl, .instructions = .{}, @@ -695,7 +672,7 @@ fn analyzeInstBlock(mod: *Module, scope: *Scope, inst: *zir.Inst.Block, is_compt defer child_block.instructions.deinit(mod.gpa); defer merges.results.deinit(mod.gpa); - try analyzeBody(mod, &child_block.base, inst.positionals.body); + try analyzeBody(mod, &child_block, inst.positionals.body); return analyzeBlockBody(mod, scope, &child_block, merges); } @@ -886,8 +863,30 @@ fn analyzeInstCall(mod: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerError }, .body = undefined, }; + // If this is the top of the inline/comptime call stack, we use this data. + // Otherwise we pass on the shared data from the parent scope. + var shared_inlining = Scope.Block.Inlining.Shared{ + .branch_count = 0, + .branch_quota = 1000, + .caller = b.func, + }; + // This one is shared among sub-blocks within the same callee, but not + // shared among the entire inline/comptime call stack. + var inlining = Scope.Block.Inlining{ + .shared = if (b.inlining) |inlining| inlining.shared else &shared_inlining, + .param_index = 0, + .casted_args = casted_args, + .merges = .{ + .results = .{}, + .block_inst = block_inst, + }, + }; + var inst_table = Scope.Block.InstTable.init(mod.gpa); + defer inst_table.deinit(); + var child_block: Scope.Block = .{ .parent = null, + .inst_table = &inst_table, .func = module_fn, // Note that we pass the caller's Decl, not the callee. This causes // compile errors to be attached (correctly) to the caller's Decl. @@ -895,16 +894,7 @@ fn analyzeInstCall(mod: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerError .instructions = .{}, .arena = scope.arena(), .label = null, - // TODO @as here is working around a stage1 miscompilation bug :( - .inlining = @as(?Scope.Block.Inlining, Scope.Block.Inlining{ - .caller = b.func, - .param_index = 0, - .casted_args = casted_args, - .merges = .{ - .results = .{}, - .block_inst = block_inst, - }, - }), + .inlining = &inlining, .is_comptime = is_comptime_call, }; const merges = &child_block.inlining.?.merges; @@ -912,11 +902,19 @@ fn analyzeInstCall(mod: *Module, scope: *Scope, inst: *zir.Inst.Call) InnerError defer child_block.instructions.deinit(mod.gpa); defer merges.results.deinit(mod.gpa); + try mod.emitBackwardBranch(&child_block, inst.base.src); + // This will have return instructions analyzed as break instructions to // the block_inst above. - try analyzeBody(mod, &child_block.base, module_fn.zir); + try analyzeBody(mod, &child_block, module_fn.zir); - return analyzeBlockBody(mod, scope, &child_block, merges); + const result = try analyzeBlockBody(mod, scope, &child_block, merges); + if (result.castTag(.constant)) |constant| { + log.debug("inline call resulted in {}", .{constant.val}); + } else { + log.debug("inline call resulted in {}", .{result}); + } + return result; } return mod.addCall(b, inst.base.src, ret_type, func, casted_args); @@ -1393,17 +1391,17 @@ fn analyzeInstSwitchBr(mod: *Module, scope: *Scope, inst: *zir.Inst.SwitchBr) In const item = try mod.resolveConstValue(scope, casted); if (target_val.eql(item)) { - try analyzeBody(mod, scope, case.body); + try analyzeBody(mod, scope.cast(Scope.Block).?, case.body); return mod.constNoReturn(scope, inst.base.src); } } - try analyzeBody(mod, scope, inst.positionals.else_body); + try analyzeBody(mod, scope.cast(Scope.Block).?, inst.positionals.else_body); return mod.constNoReturn(scope, inst.base.src); } if (inst.positionals.cases.len == 0) { // no cases just analyze else_branch - try analyzeBody(mod, scope, inst.positionals.else_body); + try analyzeBody(mod, scope.cast(Scope.Block).?, inst.positionals.else_body); return mod.constNoReturn(scope, inst.base.src); } @@ -1412,6 +1410,7 @@ fn analyzeInstSwitchBr(mod: *Module, scope: *Scope, inst: *zir.Inst.SwitchBr) In var case_block: Scope.Block = .{ .parent = parent_block, + .inst_table = parent_block.inst_table, .func = parent_block.func, .decl = parent_block.decl, .instructions = .{}, @@ -1429,7 +1428,7 @@ fn analyzeInstSwitchBr(mod: *Module, scope: *Scope, inst: *zir.Inst.SwitchBr) In const casted = try mod.coerce(scope, target.ty, resolved); const item = try mod.resolveConstValue(scope, casted); - try analyzeBody(mod, &case_block.base, case.body); + try analyzeBody(mod, &case_block, case.body); cases[i] = .{ .item = item, @@ -1438,7 +1437,7 @@ fn analyzeInstSwitchBr(mod: *Module, scope: *Scope, inst: *zir.Inst.SwitchBr) In } case_block.instructions.items.len = 0; - try analyzeBody(mod, &case_block.base, inst.positionals.else_body); + try analyzeBody(mod, &case_block, inst.positionals.else_body); const else_body: ir.Body = .{ .instructions = try parent_block.arena.dupe(*Inst, case_block.instructions.items), @@ -1756,24 +1755,26 @@ fn analyzeInstComptimeOp(mod: *Module, scope: *Scope, res_type: Type, inst: *zir } const is_int = res_type.isInt() or res_type.zigTypeTag() == .ComptimeInt; - const value = try switch (inst.base.tag) { + const value = switch (inst.base.tag) { .add => blk: { const val = if (is_int) - Module.intAdd(scope.arena(), lhs_val, rhs_val) + try Module.intAdd(scope.arena(), lhs_val, rhs_val) else - mod.floatAdd(scope, res_type, inst.base.src, lhs_val, rhs_val); + try mod.floatAdd(scope, res_type, inst.base.src, lhs_val, rhs_val); break :blk val; }, .sub => blk: { const val = if (is_int) - Module.intSub(scope.arena(), lhs_val, rhs_val) + try Module.intSub(scope.arena(), lhs_val, rhs_val) else - mod.floatSub(scope, res_type, inst.base.src, lhs_val, rhs_val); + try mod.floatSub(scope, res_type, inst.base.src, lhs_val, rhs_val); break :blk val; }, else => return mod.fail(scope, inst.base.src, "TODO Implement arithmetic operand '{s}'", .{@tagName(inst.base.tag)}), }; + log.debug("{s}({}, {}) result: {}", .{ @tagName(inst.base.tag), lhs_val, rhs_val, value }); + return mod.constInst(scope, inst.base.src, .{ .ty = res_type, .val = value, @@ -1942,16 +1943,17 @@ fn analyzeInstCondBr(mod: *Module, scope: *Scope, inst: *zir.Inst.CondBr) InnerE const uncasted_cond = try resolveInst(mod, scope, inst.positionals.condition); const cond = try mod.coerce(scope, Type.initTag(.bool), uncasted_cond); + const parent_block = scope.cast(Scope.Block).?; + if (try mod.resolveDefinedValue(scope, cond)) |cond_val| { const body = if (cond_val.toBool()) &inst.positionals.then_body else &inst.positionals.else_body; - try analyzeBody(mod, scope, body.*); + try analyzeBody(mod, parent_block, body.*); return mod.constNoReturn(scope, inst.base.src); } - const parent_block = try mod.requireRuntimeBlock(scope, inst.base.src); - var true_block: Scope.Block = .{ .parent = parent_block, + .inst_table = parent_block.inst_table, .func = parent_block.func, .decl = parent_block.decl, .instructions = .{}, @@ -1960,10 +1962,11 @@ fn analyzeInstCondBr(mod: *Module, scope: *Scope, inst: *zir.Inst.CondBr) InnerE .is_comptime = parent_block.is_comptime, }; defer true_block.instructions.deinit(mod.gpa); - try analyzeBody(mod, &true_block.base, inst.positionals.then_body); + try analyzeBody(mod, &true_block, inst.positionals.then_body); var false_block: Scope.Block = .{ .parent = parent_block, + .inst_table = parent_block.inst_table, .func = parent_block.func, .decl = parent_block.decl, .instructions = .{}, @@ -1972,7 +1975,7 @@ fn analyzeInstCondBr(mod: *Module, scope: *Scope, inst: *zir.Inst.CondBr) InnerE .is_comptime = parent_block.is_comptime, }; defer false_block.instructions.deinit(mod.gpa); - try analyzeBody(mod, &false_block.base, inst.positionals.else_body); + try analyzeBody(mod, &false_block, inst.positionals.else_body); const then_body: ir.Body = .{ .instructions = try scope.arena().dupe(*Inst, true_block.instructions.items) }; const else_body: ir.Body = .{ .instructions = try scope.arena().dupe(*Inst, false_block.instructions.items) }; @@ -1998,7 +2001,7 @@ fn analyzeInstRet(mod: *Module, scope: *Scope, inst: *zir.Inst.UnOp) InnerError! const operand = try resolveInst(mod, scope, inst.positionals.operand); const b = try mod.requireFunctionBlock(scope, inst.base.src); - if (b.inlining) |*inlining| { + if (b.inlining) |inlining| { // We are inlining a function call; rewrite the `ret` as a `break`. try inlining.merges.results.append(mod.gpa, operand); return mod.addBr(b, inst.base.src, inlining.merges.block_inst, operand); @@ -2009,7 +2012,7 @@ fn analyzeInstRet(mod: *Module, scope: *Scope, inst: *zir.Inst.UnOp) InnerError! fn analyzeInstRetVoid(mod: *Module, scope: *Scope, inst: *zir.Inst.NoOp) InnerError!*Inst { const b = try mod.requireFunctionBlock(scope, inst.base.src); - if (b.inlining) |*inlining| { + if (b.inlining) |inlining| { // We are inlining a function call; rewrite the `retvoid` as a `breakvoid`. const void_inst = try mod.constVoid(scope, inst.base.src); try inlining.merges.results.append(mod.gpa, void_inst); diff --git a/test/stage2/test.zig b/test/stage2/test.zig index 8d3fac4760..9a74e9ee09 100644 --- a/test/stage2/test.zig +++ b/test/stage2/test.zig @@ -27,7 +27,6 @@ const wasi = std.zig.CrossTarget{ }; pub fn addCases(ctx: *TestContext) !void { - try @import("zir.zig").addCases(ctx); try @import("cbe.zig").addCases(ctx); try @import("spu-ii.zig").addCases(ctx); try @import("arm.zig").addCases(ctx); @@ -1430,4 +1429,51 @@ pub fn addCases(ctx: *TestContext) !void { "", ); } + { + var case = ctx.exe("recursive inline function", linux_x64); + case.addCompareOutput( + \\export fn _start() noreturn { + \\ const y = fibonacci(7); + \\ exit(y - 21); + \\} + \\ + \\inline fn fibonacci(n: usize) usize { + \\ if (n <= 2) return n; + \\ return fibonacci(n - 2) + fibonacci(n - 1); + \\} + \\ + \\fn exit(code: usize) noreturn { + \\ asm volatile ("syscall" + \\ : + \\ : [number] "{rax}" (231), + \\ [arg1] "{rdi}" (code) + \\ : "rcx", "r11", "memory" + \\ ); + \\ unreachable; + \\} + , + "", + ); + case.addError( + \\export fn _start() noreturn { + \\ const y = fibonacci(999); + \\ exit(y - 21); + \\} + \\ + \\inline fn fibonacci(n: usize) usize { + \\ if (n <= 2) return n; + \\ return fibonacci(n - 2) + fibonacci(n - 1); + \\} + \\ + \\fn exit(code: usize) noreturn { + \\ asm volatile ("syscall" + \\ : + \\ : [number] "{rax}" (231), + \\ [arg1] "{rdi}" (code) + \\ : "rcx", "r11", "memory" + \\ ); + \\ unreachable; + \\} + , &[_][]const u8{":8:10: error: evaluation exceeded 1000 backwards branches"}); + } } diff --git a/test/stage2/zir.zig b/test/stage2/zir.zig deleted file mode 100644 index c29e636cd4..0000000000 --- a/test/stage2/zir.zig +++ /dev/null @@ -1,316 +0,0 @@ -const std = @import("std"); -const TestContext = @import("../../src/test.zig").TestContext; -// self-hosted does not yet support PE executable files / COFF object files -// or mach-o files. So we do the ZIR transform test cases cross compiling for -// x86_64-linux. -const linux_x64 = std.zig.CrossTarget{ - .cpu_arch = .x86_64, - .os_tag = .linux, -}; - -pub fn addCases(ctx: *TestContext) !void { - ctx.transformZIR("referencing decls which appear later in the file", linux_x64, - \\@void = primitive(void) - \\@fnty = fntype([], @void, cc=C) - \\ - \\@9 = str("entry") - \\@11 = export(@9, "entry") - \\ - \\@entry = fn(@fnty, { - \\ %11 = returnvoid() - \\}) - , - \\@void = primitive(void) - \\@fnty = fntype([], @void, cc=C) - \\@9 = declref("9__anon_0") - \\@9__anon_0 = str("entry") - \\@unnamed$4 = str("entry") - \\@unnamed$5 = export(@unnamed$4, "entry") - \\@11 = primitive(void_value) - \\@unnamed$7 = fntype([], @void, cc=C) - \\@entry = fn(@unnamed$7, { - \\ %0 = returnvoid() ; deaths=0b1000000000000000 - \\}, is_inline=0) - \\ - ); - ctx.transformZIR("elemptr, add, cmp, condbr, return, breakpoint", linux_x64, - \\@void = primitive(void) - \\@usize = primitive(usize) - \\@fnty = fntype([], @void, cc=C) - \\@0 = int(0) - \\@1 = int(1) - \\@2 = int(2) - \\@3 = int(3) - \\ - \\@entry = fn(@fnty, { - \\ %a = str("\x32\x08\x01\x0a") - \\ %a_ref = ref(%a) - \\ %eptr0 = elemptr(%a_ref, @0) - \\ %eptr1 = elemptr(%a_ref, @1) - \\ %eptr2 = elemptr(%a_ref, @2) - \\ %eptr3 = elemptr(%a_ref, @3) - \\ %v0 = deref(%eptr0) - \\ %v1 = deref(%eptr1) - \\ %v2 = deref(%eptr2) - \\ %v3 = deref(%eptr3) - \\ %x0 = add(%v0, %v1) - \\ %x1 = add(%v2, %v3) - \\ %result = add(%x0, %x1) - \\ - \\ %expected = int(69) - \\ %ok = cmp_eq(%result, %expected) - \\ %10 = condbr(%ok, { - \\ %11 = returnvoid() - \\ }, { - \\ %12 = breakpoint() - \\ }) - \\}) - \\ - \\@9 = str("entry") - \\@11 = export(@9, "entry") - , - \\@void = primitive(void) - \\@fnty = fntype([], @void, cc=C) - \\@0 = int(0) - \\@1 = int(1) - \\@2 = int(2) - \\@3 = int(3) - \\@unnamed$6 = fntype([], @void, cc=C) - \\@entry = fn(@unnamed$6, { - \\ %0 = returnvoid() ; deaths=0b1000000000000000 - \\}, is_inline=0) - \\@entry__anon_1 = str("2\x08\x01\n") - \\@9 = declref("9__anon_0") - \\@9__anon_0 = str("entry") - \\@unnamed$11 = str("entry") - \\@unnamed$12 = export(@unnamed$11, "entry") - \\@11 = primitive(void_value) - \\ - ); - - { - var case = ctx.objZIR("reference cycle with compile error in the cycle", linux_x64); - case.addTransform( - \\@void = primitive(void) - \\@fnty = fntype([], @void, cc=C) - \\ - \\@9 = str("entry") - \\@11 = export(@9, "entry") - \\ - \\@entry = fn(@fnty, { - \\ %0 = call(@a, []) - \\ %1 = returnvoid() - \\}) - \\ - \\@a = fn(@fnty, { - \\ %0 = call(@b, []) - \\ %1 = returnvoid() - \\}) - \\ - \\@b = fn(@fnty, { - \\ %0 = call(@a, []) - \\ %1 = returnvoid() - \\}) - , - \\@void = primitive(void) - \\@fnty = fntype([], @void, cc=C) - \\@9 = declref("9__anon_0") - \\@9__anon_0 = str("entry") - \\@unnamed$4 = str("entry") - \\@unnamed$5 = export(@unnamed$4, "entry") - \\@11 = primitive(void_value) - \\@unnamed$7 = fntype([], @void, cc=C) - \\@entry = fn(@unnamed$7, { - \\ %0 = call(@a, [], modifier=auto) ; deaths=0b1000000000000001 - \\ %1 = returnvoid() ; deaths=0b1000000000000000 - \\}, is_inline=0) - \\@unnamed$9 = fntype([], @void, cc=C) - \\@a = fn(@unnamed$9, { - \\ %0 = call(@b, [], modifier=auto) ; deaths=0b1000000000000001 - \\ %1 = returnvoid() ; deaths=0b1000000000000000 - \\}, is_inline=0) - \\@unnamed$11 = fntype([], @void, cc=C) - \\@b = fn(@unnamed$11, { - \\ %0 = call(@a, [], modifier=auto) ; deaths=0b1000000000000001 - \\ %1 = returnvoid() ; deaths=0b1000000000000000 - \\}, is_inline=0) - \\ - ); - // Now we introduce a compile error - case.addError( - \\@void = primitive(void) - \\@fnty = fntype([], @void, cc=C) - \\ - \\@9 = str("entry") - \\@11 = export(@9, "entry") - \\ - \\@entry = fn(@fnty, { - \\ %0 = call(@a, []) - \\ %1 = returnvoid() - \\}) - \\ - \\@a = fn(@fnty, { - \\ %0 = call(@c, []) - \\ %1 = returnvoid() - \\}) - \\ - \\@b = str("message") - \\ - \\@c = fn(@fnty, { - \\ %9 = compileerror(@b) - \\ %0 = call(@a, []) - \\ %1 = returnvoid() - \\}) - , - &[_][]const u8{ - ":20:21: error: message", - }, - ); - // Now we remove the call to `a`. `a` and `b` form a cycle, but no entry points are - // referencing either of them. This tests that the cycle is detected, and the error - // goes away. - case.addTransform( - \\@void = primitive(void) - \\@fnty = fntype([], @void, cc=C) - \\ - \\@9 = str("entry") - \\@11 = export(@9, "entry") - \\ - \\@entry = fn(@fnty, { - \\ %0 = returnvoid() - \\}) - \\ - \\@a = fn(@fnty, { - \\ %0 = call(@c, []) - \\ %1 = returnvoid() - \\}) - \\ - \\@b = str("message") - \\ - \\@c = fn(@fnty, { - \\ %9 = compileerror(@b) - \\ %0 = call(@a, []) - \\ %1 = returnvoid() - \\}) - , - \\@void = primitive(void) - \\@fnty = fntype([], @void, cc=C) - \\@9 = declref("9__anon_3") - \\@9__anon_3 = str("entry") - \\@unnamed$4 = str("entry") - \\@unnamed$5 = export(@unnamed$4, "entry") - \\@11 = primitive(void_value) - \\@unnamed$7 = fntype([], @void, cc=C) - \\@entry = fn(@unnamed$7, { - \\ %0 = returnvoid() ; deaths=0b1000000000000000 - \\}, is_inline=0) - \\ - ); - } - - if (std.Target.current.os.tag != .linux or - std.Target.current.cpu.arch != .x86_64) - { - // TODO implement self-hosted PE (.exe file) linking - // TODO implement more ZIR so we don't depend on x86_64-linux - return; - } - - ctx.compareOutputZIR("hello world ZIR", - \\@noreturn = primitive(noreturn) - \\@void = primitive(void) - \\@usize = primitive(usize) - \\@0 = int(0) - \\@1 = int(1) - \\@2 = int(2) - \\@3 = int(3) - \\ - \\@msg = str("Hello, world!\n") - \\ - \\@start_fnty = fntype([], @noreturn, cc=Naked) - \\@start = fn(@start_fnty, { - \\ %SYS_exit_group = int(231) - \\ %exit_code = as(@usize, @0) - \\ - \\ %syscall = str("syscall") - \\ %sysoutreg = str("={rax}") - \\ %rax = str("{rax}") - \\ %rdi = str("{rdi}") - \\ %rcx = str("rcx") - \\ %rdx = str("{rdx}") - \\ %rsi = str("{rsi}") - \\ %r11 = str("r11") - \\ %memory = str("memory") - \\ - \\ %SYS_write = as(@usize, @1) - \\ %STDOUT_FILENO = as(@usize, @1) - \\ - \\ %msg_addr = ptrtoint(@msg) - \\ - \\ %len_name = str("len") - \\ %msg_len_ptr = fieldptr(@msg, %len_name) - \\ %msg_len = deref(%msg_len_ptr) - \\ %rc_write = asm(%syscall, @usize, - \\ volatile=1, - \\ output=%sysoutreg, - \\ inputs=[%rax, %rdi, %rsi, %rdx], - \\ clobbers=[%rcx, %r11, %memory], - \\ args=[%SYS_write, %STDOUT_FILENO, %msg_addr, %msg_len]) - \\ - \\ %rc_exit = asm(%syscall, @usize, - \\ volatile=1, - \\ output=%sysoutreg, - \\ inputs=[%rax, %rdi], - \\ clobbers=[%rcx, %r11, %memory], - \\ args=[%SYS_exit_group, %exit_code]) - \\ - \\ %99 = unreachable() - \\}); - \\ - \\@9 = str("_start") - \\@11 = export(@9, "start") - , - \\Hello, world! - \\ - ); - - ctx.compareOutputZIR("function call with no args no return value", - \\@noreturn = primitive(noreturn) - \\@void = primitive(void) - \\@usize = primitive(usize) - \\@0 = int(0) - \\@1 = int(1) - \\@2 = int(2) - \\@3 = int(3) - \\ - \\@exit0_fnty = fntype([], @noreturn) - \\@exit0 = fn(@exit0_fnty, { - \\ %SYS_exit_group = int(231) - \\ %exit_code = as(@usize, @0) - \\ - \\ %syscall = str("syscall") - \\ %sysoutreg = str("={rax}") - \\ %rax = str("{rax}") - \\ %rdi = str("{rdi}") - \\ %rcx = str("rcx") - \\ %r11 = str("r11") - \\ %memory = str("memory") - \\ - \\ %rc = asm(%syscall, @usize, - \\ volatile=1, - \\ output=%sysoutreg, - \\ inputs=[%rax, %rdi], - \\ clobbers=[%rcx, %r11, %memory], - \\ args=[%SYS_exit_group, %exit_code]) - \\ - \\ %99 = unreachable() - \\}); - \\ - \\@start_fnty = fntype([], @noreturn, cc=Naked) - \\@start = fn(@start_fnty, { - \\ %0 = call(@exit0, []) - \\}) - \\@9 = str("_start") - \\@11 = export(@9, "start") - , ""); -} -- cgit v1.2.3