Commit b1b7708cc8
Changed files (2)
src-self-hosted
src-self-hosted/main.zig
@@ -487,7 +487,9 @@ fn buildOutputType(
}
fn updateModule(gpa: *Allocator, module: *Module, zir_out_path: ?[]const u8) !void {
+ var timer = try std.time.Timer.start();
try module.update();
+ const update_nanos = timer.read();
var errors = try module.getAllErrorsAlloc();
defer errors.deinit(module.allocator);
@@ -501,6 +503,8 @@ fn updateModule(gpa: *Allocator, module: *Module, zir_out_path: ?[]const u8) !vo
full_err_msg.msg,
});
}
+ } else {
+ std.debug.print("Update completed in {} ms\n", .{update_nanos / std.time.ns_per_ms});
}
if (zir_out_path) |zop| {
src-self-hosted/Module.zig
@@ -74,9 +74,9 @@ const DeclTable = std.HashMap(Scope.NameHash, *Decl, Scope.name_hash_hash, Scope
const WorkItem = union(enum) {
/// Write the machine code for a Decl to the output file.
codegen_decl: *Decl,
- /// Decl has been determined to be outdated; perform semantic analysis again.
- re_analyze_decl: *Decl,
/// The Decl needs to be analyzed and possibly export itself.
+ /// It may have already be analyzed, or it may have been determined
+ /// to be outdated; in this case perform semantic analysis again.
analyze_decl: *Decl,
};
@@ -403,6 +403,17 @@ pub const Scope = struct {
}
}
+ /// Asserts the scope is a namespace Scope and removes the Decl from the namespace.
+ pub fn removeDecl(base: *Scope, child: *Decl) void {
+ switch (base.tag) {
+ .file => return @fieldParentPtr(File, "base", base).removeDecl(child),
+ .zir_module => return @fieldParentPtr(ZIRModule, "base", base).removeDecl(child),
+ .block => unreachable,
+ .gen_zir => unreachable,
+ .decl => unreachable,
+ }
+ }
+
/// Asserts the scope is a File or ZIRModule and deinitializes it, then deallocates it.
pub fn destroy(base: *Scope, allocator: *Allocator) void {
switch (base.tag) {
@@ -462,6 +473,9 @@ pub const Scope = struct {
loaded_success,
},
+ /// Direct children of the file.
+ decls: ArrayListUnmanaged(*Decl),
+
pub fn unload(self: *File, allocator: *Allocator) void {
switch (self.status) {
.never_loaded,
@@ -484,10 +498,20 @@ pub const Scope = struct {
}
pub fn deinit(self: *File, allocator: *Allocator) void {
+ self.decls.deinit(allocator);
self.unload(allocator);
self.* = undefined;
}
+ pub fn removeDecl(self: *File, child: *Decl) void {
+ for (self.decls.items) |item, i| {
+ if (item == child) {
+ _ = self.decls.swapRemove(i);
+ return;
+ }
+ }
+ }
+
pub fn dumpSrc(self: *File, src: usize) void {
const loc = std.zig.findLineColumn(self.source.bytes, src);
std.debug.warn("{}:{}:{}\n", .{ self.sub_file_path, loc.line + 1, loc.column + 1 });
@@ -540,6 +564,11 @@ pub const Scope = struct {
loaded_success,
},
+ /// Even though .zir files only have 1 module, this set is still needed
+ /// because of anonymous Decls, which can exist in the global set, but
+ /// not this one.
+ decls: ArrayListUnmanaged(*Decl),
+
pub fn unload(self: *ZIRModule, allocator: *Allocator) void {
switch (self.status) {
.never_loaded,
@@ -569,10 +598,20 @@ pub const Scope = struct {
}
pub fn deinit(self: *ZIRModule, allocator: *Allocator) void {
+ self.decls.deinit(allocator);
self.unload(allocator);
self.* = undefined;
}
+ pub fn removeDecl(self: *ZIRModule, child: *Decl) void {
+ for (self.decls.items) |item, i| {
+ if (item == child) {
+ _ = self.decls.swapRemove(i);
+ return;
+ }
+ }
+ }
+
pub fn dumpSrc(self: *ZIRModule, src: usize) void {
const loc = std.zig.findLineColumn(self.source.bytes, src);
std.debug.warn("{}:{}:{}\n", .{ self.sub_file_path, loc.line + 1, loc.column + 1 });
@@ -700,6 +739,7 @@ pub fn init(gpa: *Allocator, options: InitOptions) !Module {
.source = .{ .unloaded = {} },
.contents = .{ .not_available = {} },
.status = .never_loaded,
+ .decls = .{},
};
break :blk &root_scope.base;
} else if (mem.endsWith(u8, options.root_pkg.root_src_path, ".zir")) {
@@ -709,6 +749,7 @@ pub fn init(gpa: *Allocator, options: InitOptions) !Module {
.source = .{ .unloaded = {} },
.contents = .{ .not_available = {} },
.status = .never_loaded,
+ .decls = .{},
};
break :blk &root_scope.base;
} else {
@@ -828,13 +869,14 @@ pub fn update(self: *Module) !void {
try self.performAllTheWork();
// Process the deletion set.
- while (self.deletion_set.popOrNull()) |decl| {
+ for (self.deletion_set.items) |decl| {
if (decl.dependants.items.len != 0) {
decl.deletion_flag = false;
continue;
}
try self.deleteDecl(decl);
}
+ self.deletion_set.shrink(self.allocator, 0);
self.link_error_flags = self.bin_file.error_flags;
@@ -969,49 +1011,6 @@ pub fn performAllTheWork(self: *Module) error{OutOfMemory}!void {
};
},
},
- .re_analyze_decl => |decl| switch (decl.analysis) {
- .unreferenced => unreachable,
- .in_progress => unreachable,
-
- .sema_failure,
- .codegen_failure,
- .dependency_failure,
- .complete,
- .codegen_failure_retryable,
- .sema_failure_retryable,
- => continue,
-
- .outdated => {
- if (decl.scope.cast(Scope.File)) |file_scope| {
- @panic("TODO re_analyze_decl for .zig files");
- } else if (decl.scope.cast(Scope.ZIRModule)) |zir_scope| {
- const zir_module = self.getSrcModule(zir_scope) catch |err| switch (err) {
- error.OutOfMemory => return error.OutOfMemory,
- else => {
- try self.failed_decls.ensureCapacity(self.failed_decls.size + 1);
- self.failed_decls.putAssumeCapacityNoClobber(decl, try ErrorMsg.create(
- self.allocator,
- decl.src(),
- "unable to load source file '{}': {}",
- .{ zir_scope.sub_file_path, @errorName(err) },
- ));
- decl.analysis = .codegen_failure_retryable;
- continue;
- },
- };
- const decl_name = mem.spanZ(decl.name);
- // We already detected deletions, so we know this will be found.
- const src_decl_and_index = zir_module.findDecl(decl_name).?;
- decl.src_index = src_decl_and_index.index;
- self.reAnalyzeDecl(decl, src_decl_and_index.decl.inst) catch |err| switch (err) {
- error.OutOfMemory => return error.OutOfMemory,
- error.AnalysisFail => continue,
- };
- } else {
- unreachable;
- }
- },
- },
.analyze_decl => |decl| {
self.ensureDeclAnalyzed(decl) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
@@ -1022,9 +1021,12 @@ pub fn performAllTheWork(self: *Module) error{OutOfMemory}!void {
}
fn ensureDeclAnalyzed(self: *Module, decl: *Decl) InnerError!void {
- switch (decl.analysis) {
+ const tracy = trace(@src());
+ defer tracy.end();
+
+ const subsequent_analysis = switch (decl.analysis) {
+ .complete => return,
.in_progress => unreachable,
- .outdated => unreachable,
.sema_failure,
.sema_failure_retryable,
@@ -1033,29 +1035,73 @@ fn ensureDeclAnalyzed(self: *Module, decl: *Decl) InnerError!void {
.codegen_failure_retryable,
=> return error.AnalysisFail,
- .complete => return,
+ .outdated => blk: {
+ //std.debug.warn("re-analyzing {}\n", .{decl.name});
+
+ // The exports this Decl performs will be re-discovered, so we remove them here
+ // prior to re-analysis.
+ self.deleteDeclExports(decl);
+ // Dependencies will be re-discovered, so we remove them here prior to re-analysis.
+ for (decl.dependencies.items) |dep| {
+ dep.removeDependant(decl);
+ if (dep.dependants.items.len == 0) {
+ // We don't perform a deletion here, because this Decl or another one
+ // may end up referencing it before the update is complete.
+ assert(!dep.deletion_flag);
+ dep.deletion_flag = true;
+ try self.deletion_set.append(self.allocator, dep);
+ }
+ }
+ decl.dependencies.shrink(self.allocator, 0);
- .unreferenced => {
- self.astGenAndAnalyzeDecl(decl) catch |err| switch (err) {
- error.OutOfMemory => return error.OutOfMemory,
- error.AnalysisFail => return error.AnalysisFail,
- else => {
- try self.failed_decls.ensureCapacity(self.failed_decls.size + 1);
- self.failed_decls.putAssumeCapacityNoClobber(decl, try ErrorMsg.create(
- self.allocator,
- decl.src(),
- "unable to analyze: {}",
- .{@errorName(err)},
- ));
- decl.analysis = .sema_failure_retryable;
- return error.AnalysisFail;
- },
- };
+ break :blk true;
+ },
+
+ .unreferenced => false,
+ };
+
+ const type_changed = self.astGenAndAnalyzeDecl(decl) catch |err| switch (err) {
+ error.OutOfMemory => return error.OutOfMemory,
+ error.AnalysisFail => return error.AnalysisFail,
+ else => {
+ try self.failed_decls.ensureCapacity(self.failed_decls.size + 1);
+ self.failed_decls.putAssumeCapacityNoClobber(decl, try ErrorMsg.create(
+ self.allocator,
+ decl.src(),
+ "unable to analyze: {}",
+ .{@errorName(err)},
+ ));
+ decl.analysis = .sema_failure_retryable;
+ return error.AnalysisFail;
},
+ };
+
+ if (subsequent_analysis) {
+ // We may need to chase the dependants and re-analyze them.
+ // However, if the decl is a function, and the type is the same, we do not need to.
+ if (type_changed or decl.typed_value.most_recent.typed_value.val.tag() != .function) {
+ for (decl.dependants.items) |dep| {
+ switch (dep.analysis) {
+ .unreferenced => unreachable,
+ .in_progress => unreachable,
+ .outdated => continue, // already queued for update
+
+ .dependency_failure,
+ .sema_failure,
+ .sema_failure_retryable,
+ .codegen_failure,
+ .codegen_failure_retryable,
+ .complete,
+ => if (dep.generation != self.generation) {
+ try self.markOutdatedDecl(dep);
+ },
+ }
+ }
+ }
}
}
-fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !void {
+fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !bool {
const tracy = trace(@src());
defer tracy.end();
@@ -1170,6 +1216,16 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !void {
};
fn_payload.* = .{ .func = new_func };
+ var prev_type_has_bits = 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);
+
+ tvm.deinit(self.allocator);
+ }
+
decl_arena_state.* = decl_arena.state;
decl.typed_value = .{
.most_recent = .{
@@ -1183,11 +1239,15 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !void {
decl.analysis = .complete;
decl.generation = self.generation;
- // 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.bin_file.allocateDeclIndexes(decl);
- try self.work_queue.writeItem(.{ .codegen_decl = decl });
+ if (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.bin_file.allocateDeclIndexes(decl);
+ try self.work_queue.writeItem(.{ .codegen_decl = decl });
+ } else if (prev_type_has_bits) {
+ self.bin_file.freeDecl(decl);
+ }
if (fn_proto.extern_export_inline_token) |maybe_export_token| {
if (tree.token_ids[maybe_export_token] == .Keyword_export) {
@@ -1198,6 +1258,7 @@ fn astGenAndAnalyzeDecl(self: *Module, decl: *Decl) !void {
try self.analyzeExport(&block_scope.base, export_src, name, decl);
}
}
+ return type_changed;
},
.VarDecl => @panic("TODO var decl"),
.Comptime => @panic("TODO comptime decl"),
@@ -1602,40 +1663,63 @@ fn getAstTree(self: *Module, root_scope: *Scope.File) !*ast.Tree {
}
fn analyzeRootSrcFile(self: *Module, root_scope: *Scope.File) !void {
- switch (root_scope.status) {
- .never_loaded => {
- const tree = try self.getAstTree(root_scope);
- const decls = tree.root_node.decls();
-
- try self.work_queue.ensureUnusedCapacity(decls.len);
-
- for (decls) |src_decl, decl_i| {
- if (src_decl.cast(ast.Node.FnProto)) |fn_proto| {
- // We will create a Decl for it regardless of analysis status.
- const name_tok = fn_proto.name_token orelse
- @panic("TODO handle missing function name in the parser");
- const name_loc = tree.token_locs[name_tok];
- const name = tree.tokenSliceLoc(name_loc);
- const name_hash = root_scope.fullyQualifiedNameHash(name);
- const contents_hash = std.zig.hashSrc(tree.getNodeSource(src_decl));
- const new_decl = try self.createNewDecl(&root_scope.base, name, decl_i, name_hash, contents_hash);
- if (fn_proto.extern_export_inline_token) |maybe_export_token| {
- if (tree.token_ids[maybe_export_token] == .Keyword_export) {
- self.work_queue.writeItemAssumeCapacity(.{ .analyze_decl = new_decl });
- }
+ // 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(root_scope);
+ const decls = tree.root_node.decls();
+
+ try self.work_queue.ensureUnusedCapacity(decls.len);
+ try root_scope.decls.ensureCapacity(self.allocator, decls.len);
+
+ // Keep track of the decls that we expect to see in this file so that
+ // we know which ones have been deleted.
+ var deleted_decls = std.AutoHashMap(*Decl, void).init(self.allocator);
+ defer deleted_decls.deinit();
+ try deleted_decls.ensureCapacity(root_scope.decls.items.len);
+ for (root_scope.decls.items) |file_decl| {
+ deleted_decls.putAssumeCapacityNoClobber(file_decl, {});
+ }
+
+ for (decls) |src_decl, decl_i| {
+ if (src_decl.cast(ast.Node.FnProto)) |fn_proto| {
+ // We will create a Decl for it regardless of analysis status.
+ const name_tok = fn_proto.name_token orelse
+ @panic("TODO handle missing function name in the parser");
+ const name_loc = tree.token_locs[name_tok];
+ const name = tree.tokenSliceLoc(name_loc);
+ const name_hash = root_scope.fullyQualifiedNameHash(name);
+ const contents_hash = std.zig.hashSrc(tree.getNodeSource(src_decl));
+ if (self.decl_table.get(name_hash)) |kv| {
+ const decl = kv.value;
+ // Update the AST Node index of the decl, even if its contents are unchanged, it may
+ // have been re-ordered.
+ decl.src_index = decl_i;
+ deleted_decls.removeAssertDiscard(decl);
+ if (!srcHashEql(decl.contents_hash, contents_hash)) {
+ try self.markOutdatedDecl(decl);
+ decl.contents_hash = contents_hash;
+ }
+ } else {
+ const new_decl = try self.createNewDecl(&root_scope.base, name, decl_i, name_hash, contents_hash);
+ root_scope.decls.appendAssumeCapacity(new_decl);
+ if (fn_proto.extern_export_inline_token) |maybe_export_token| {
+ if (tree.token_ids[maybe_export_token] == .Keyword_export) {
+ self.work_queue.writeItemAssumeCapacity(.{ .analyze_decl = new_decl });
}
}
- // TODO also look for global variable declarations
- // TODO also look for comptime blocks and exported globals
}
- },
-
- .unloaded_parse_failure,
- .unloaded_success,
- .loaded_success,
- => {
- @panic("TODO process update");
- },
+ }
+ // TODO also look for global variable declarations
+ // TODO also look for comptime blocks and exported globals
+ }
+ {
+ // Handle explicitly deleted decls from the source code. Not to be confused
+ // with when we delete decls because they are no longer referenced.
+ var it = deleted_decls.iterator();
+ while (it.next()) |kv| {
+ //std.debug.warn("noticed '{}' deleted from source\n", .{kv.key.name});
+ try self.deleteDecl(kv.key);
+ }
}
}
@@ -1684,7 +1768,7 @@ fn analyzeRootZIRModule(self: *Module, root_scope: *Scope.ZIRModule) !void {
const decl = kv.value;
deleted_decls.removeAssertDiscard(decl);
//std.debug.warn("'{}' contents: '{}'\n", .{ src_decl.name, src_decl.contents });
- if (!mem.eql(u8, &src_decl.contents_hash, &decl.contents_hash)) {
+ if (!srcHashEql(src_decl.contents_hash, decl.contents_hash)) {
try self.markOutdatedDecl(decl);
decl.contents_hash = src_decl.contents_hash;
}
@@ -1711,6 +1795,10 @@ fn analyzeRootZIRModule(self: *Module, root_scope: *Scope.ZIRModule) !void {
fn deleteDecl(self: *Module, decl: *Decl) !void {
try self.deletion_set.ensureCapacity(self.allocator, self.deletion_set.items.len + decl.dependencies.items.len);
+ // Remove from the namespace it resides in. In the case of an anonymous Decl it will
+ // not be present in the set, and this does nothing.
+ decl.scope.removeDecl(decl);
+
//std.debug.warn("deleting decl '{}'\n", .{decl.name});
const name_hash = decl.fullyQualifiedNameHash();
self.decl_table.removeAssertDiscard(name_hash);
@@ -1799,110 +1887,9 @@ fn analyzeFnBody(self: *Module, decl: *Decl, func: *Fn) !void {
//std.debug.warn("set {} to success\n", .{decl.name});
}
-fn reAnalyzeDecl(self: *Module, decl: *Decl, old_inst: *zir.Inst) InnerError!void {
- switch (decl.analysis) {
- .unreferenced => unreachable,
- .in_progress => unreachable,
- .dependency_failure,
- .sema_failure,
- .sema_failure_retryable,
- .codegen_failure,
- .codegen_failure_retryable,
- .complete,
- => return,
-
- .outdated => {}, // Decl re-analysis
- }
- //std.debug.warn("re-analyzing {}\n", .{decl.name});
-
- // The exports this Decl performs will be re-discovered, so we remove them here
- // prior to re-analysis.
- self.deleteDeclExports(decl);
- // Dependencies will be re-discovered, so we remove them here prior to re-analysis.
- for (decl.dependencies.items) |dep| {
- dep.removeDependant(decl);
- if (dep.dependants.items.len == 0) {
- // We don't perform a deletion here, because this Decl or another one
- // may end up referencing it before the update is complete.
- assert(!dep.deletion_flag);
- dep.deletion_flag = true;
- try self.deletion_set.append(self.allocator, dep);
- }
- }
- decl.dependencies.shrink(self.allocator, 0);
- var decl_scope: Scope.DeclAnalysis = .{
- .decl = decl,
- .arena = std.heap.ArenaAllocator.init(self.allocator),
- };
- errdefer decl_scope.arena.deinit();
-
- const typed_value = self.analyzeConstInst(&decl_scope.base, old_inst) catch |err| switch (err) {
- error.OutOfMemory => return error.OutOfMemory,
- error.AnalysisFail => {
- switch (decl.analysis) {
- .in_progress => decl.analysis = .dependency_failure,
- else => {},
- }
- decl.generation = self.generation;
- return error.AnalysisFail;
- },
- };
- const arena_state = try decl_scope.arena.allocator.create(std.heap.ArenaAllocator.State);
- arena_state.* = decl_scope.arena.state;
-
- var prev_type_has_bits = 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(typed_value.ty);
-
- tvm.deinit(self.allocator);
- }
- decl.typed_value = .{
- .most_recent = .{
- .typed_value = typed_value,
- .arena = arena_state,
- },
- };
- decl.analysis = .complete;
- decl.generation = self.generation;
- if (typed_value.ty.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.bin_file.allocateDeclIndexes(decl);
- try self.work_queue.writeItem(.{ .codegen_decl = decl });
- } else if (prev_type_has_bits) {
- self.bin_file.freeDecl(decl);
- }
-
- // If the decl is a function, and the type is the same, we do not need
- // to chase the dependants.
- if (type_changed or typed_value.val.tag() != .function) {
- for (decl.dependants.items) |dep| {
- switch (dep.analysis) {
- .unreferenced => unreachable,
- .in_progress => unreachable,
- .outdated => continue, // already queued for update
-
- .dependency_failure,
- .sema_failure,
- .sema_failure_retryable,
- .codegen_failure,
- .codegen_failure_retryable,
- .complete,
- => if (dep.generation != self.generation) {
- try self.markOutdatedDecl(dep);
- },
- }
- }
- }
-}
-
fn markOutdatedDecl(self: *Module, decl: *Decl) !void {
//std.debug.warn("mark {} outdated\n", .{decl.name});
- try self.work_queue.writeItem(.{ .re_analyze_decl = decl });
+ try self.work_queue.writeItem(.{ .analyze_decl = decl });
if (self.failed_decls.remove(decl)) |entry| {
entry.value.destroy(self.allocator);
}
@@ -2381,11 +2368,10 @@ fn createAnonymousDecl(
decl_arena: *std.heap.ArenaAllocator,
typed_value: TypedValue,
) !*Decl {
- var name_buf: [32]u8 = undefined;
const name_index = self.getNextAnonNameIndex();
- const name = std.fmt.bufPrint(&name_buf, "unnamed_{}", .{name_index}) catch unreachable;
- const name_hash = scope.namespace().fullyQualifiedNameHash(name);
const scope_decl = scope.decl().?;
+ const name = try std.fmt.allocPrint(self.allocator, "{}${}", .{ scope_decl.name, name_index });
+ const name_hash = scope.namespace().fullyQualifiedNameHash(name);
const src_hash: std.zig.SrcHash = undefined;
const new_decl = try self.createNewDecl(scope, name, scope_decl.src_index, name_hash, src_hash);
const decl_arena_state = try decl_arena.allocator.create(std.heap.ArenaAllocator.State);
@@ -3360,3 +3346,7 @@ pub const ErrorMsg = struct {
self.* = undefined;
}
};
+
+fn srcHashEql(a: std.zig.SrcHash, b: std.zig.SrcHash) bool {
+ return @bitCast(u128, a) == @bitCast(u128, b);
+}