stage2: rework the C backend

* std.ArrayList gains `moveToUnmanaged` and dead code
   `ArrayListUnmanaged.appendWrite` is deleted.
 * emit_h state is attached to Module rather than Compilation.
 * remove the implementation of emit-h because it did not properly
   integrate with incremental compilation. I will re-implement it
   in a follow-up commit.
 * Compilation: use the .codegen_failure tag rather than
   .dependency_failure tag for when `bin_file.updateDecl` fails.

C backend:
 * Use a CValue tagged union instead of strings for C values.
 * Cleanly separate state into Object and DeclGen:
   - Object is present only when generating a .c file
   - DeclGen is present for both generating a .c and .h
 * Move some functions into their respective Object/DeclGen namespace.
 * Forward decls are managed by the incremental compilation frontend; C
   backend no longer renders function signatures based on callsites.
   For simplicity, all functions always get forward decls.
 * Constants are managed by the incremental compilation frontend. C
   backend no longer has a "constants" section.
 * Participate in incremental compilation. Each Decl gets an ArrayList
   for its generated C code and it is updated when the Decl is updated.
   During flush(), all these are joined together in the output file.
 * The new CValue tagged union is used to clean up using of assigning to
   locals without an additional pointer local.
 * Fix bug with bitcast of non-pointers making the memcpy destination
   immutable.
This commit is contained in:
Andrew Kelley 2021-01-05 11:08:34 -07:00
parent 9360e5887c
commit 7b8cede61f
9 changed files with 661 additions and 613 deletions

View File

@ -100,10 +100,20 @@ pub fn ArrayListAligned(comptime T: type, comptime alignment: ?u29) type {
/// Initializes an ArrayListUnmanaged with the `items` and `capacity` fields
/// of this ArrayList. This ArrayList retains ownership of underlying memory.
/// Deprecated: use `moveToUnmanaged` which has different semantics.
pub fn toUnmanaged(self: Self) ArrayListAlignedUnmanaged(T, alignment) {
return .{ .items = self.items, .capacity = self.capacity };
}
/// Initializes an ArrayListUnmanaged with the `items` and `capacity` fields
/// of this ArrayList. Empties this ArrayList.
pub fn moveToUnmanaged(self: *Self) ArrayListAlignedUnmanaged(T, alignment) {
const allocator = self.allocator;
const result = .{ .items = self.items, .capacity = self.capacity };
self.* = init(allocator);
return result;
}
/// The caller owns the returned memory. Empties this ArrayList.
pub fn toOwnedSlice(self: *Self) Slice {
const allocator = self.allocator;
@ -551,14 +561,6 @@ pub fn ArrayListAlignedUnmanaged(comptime T: type, comptime alignment: ?u29) typ
mem.copy(T, self.items[oldlen..], items);
}
/// Same as `append` except it returns the number of bytes written, which is always the same
/// as `m.len`. The purpose of this function existing is to match `std.io.OutStream` API.
/// This function may be called only when `T` is `u8`.
fn appendWrite(self: *Self, allocator: *Allocator, m: []const u8) !usize {
try self.appendSlice(allocator, m);
return m.len;
}
/// Append a value to the list `n` times.
/// Allocates more memory as necessary.
pub fn appendNTimes(self: *Self, allocator: *Allocator, value: T, n: usize) !void {
@ -1129,13 +1131,13 @@ test "std.ArrayList/ArrayListUnmanaged: ArrayList(T) of struct T" {
}
}
test "std.ArrayList(u8) implements outStream" {
test "std.ArrayList(u8) implements writer" {
var buffer = ArrayList(u8).init(std.testing.allocator);
defer buffer.deinit();
const x: i32 = 42;
const y: i32 = 1234;
try buffer.outStream().print("x: {}\ny: {}\n", .{ x, y });
try buffer.writer().print("x: {}\ny: {}\n", .{ x, y });
testing.expectEqualSlices(u8, "x: 42\ny: 1234\n", buffer.items);
}

View File

@ -138,8 +138,6 @@ emit_llvm_ir: ?EmitLoc,
emit_analysis: ?EmitLoc,
emit_docs: ?EmitLoc,
c_header: ?c_link.Header,
work_queue_wait_group: WaitGroup,
pub const InnerError = Module.InnerError;
@ -866,9 +864,13 @@ pub fn create(gpa: *Allocator, options: InitOptions) !*Compilation {
.root_pkg = root_pkg,
.root_scope = root_scope,
.zig_cache_artifact_directory = zig_cache_artifact_directory,
.emit_h = options.emit_h,
};
break :blk module;
} else null;
} else blk: {
if (options.emit_h != null) return error.NoZigModuleForCHeader;
break :blk null;
};
errdefer if (module) |zm| zm.deinit();
const error_return_tracing = !strip and switch (options.optimize_mode) {
@ -996,7 +998,6 @@ pub fn create(gpa: *Allocator, options: InitOptions) !*Compilation {
.local_cache_directory = options.local_cache_directory,
.global_cache_directory = options.global_cache_directory,
.bin_file = bin_file,
.c_header = if (!use_llvm and options.emit_h != null) c_link.Header.init(gpa, options.emit_h) else null,
.emit_asm = options.emit_asm,
.emit_llvm_ir = options.emit_llvm_ir,
.emit_analysis = options.emit_analysis,
@ -1218,10 +1219,6 @@ pub fn destroy(self: *Compilation) void {
}
self.failed_c_objects.deinit(gpa);
if (self.c_header) |*header| {
header.deinit();
}
self.cache_parent.manifest_dir.close();
if (self.owned_link_dir) |*dir| dir.close();
@ -1325,20 +1322,6 @@ pub fn update(self: *Compilation) !void {
module.root_scope.unload(self.gpa);
}
}
// If we've chosen to emit a C header, flush the header to the disk.
if (self.c_header) |header| {
const header_path = header.emit_loc.?;
// If a directory has been provided, write the header there. Otherwise, just write it to the
// cache directory.
const header_dir = if (header_path.directory) |dir|
dir.handle
else
self.local_cache_directory.handle;
const header_file = try header_dir.createFile(header_path.basename, .{});
defer header_file.close();
try header.flush(header_file.writer());
}
}
/// Having the file open for writing is problematic as far as executing the
@ -1497,7 +1480,7 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor
switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.AnalysisFail => {
decl.analysis = .dependency_failure;
decl.analysis = .codegen_failure;
},
else => {
try module.failed_decls.ensureCapacity(module.gpa, module.failed_decls.items().len + 1);
@ -1512,25 +1495,6 @@ pub fn performAllTheWork(self: *Compilation) error{ TimerUnsupported, OutOfMemor
}
return;
};
if (self.c_header) |*header| {
c_codegen.generateHeader(self, module, header, decl) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.AnalysisFail => {
decl.analysis = .dependency_failure;
},
else => {
try module.failed_decls.ensureCapacity(module.gpa, module.failed_decls.items().len + 1);
module.failed_decls.putAssumeCapacityNoClobber(decl, try ErrorMsg.create(
module.gpa,
decl.src(),
"unable to generate C header: {s}",
.{@errorName(err)},
));
decl.analysis = .codegen_failure_retryable;
},
};
}
},
},
.analyze_decl => |decl| {
@ -2998,9 +2962,9 @@ fn updateStage1Module(comp: *Compilation, main_progress_node: *std.Progress.Node
man.hash.add(comp.bin_file.options.function_sections);
man.hash.add(comp.bin_file.options.is_test);
man.hash.add(comp.bin_file.options.emit != null);
man.hash.add(comp.c_header != null);
if (comp.c_header) |header| {
man.hash.addEmitLoc(header.emit_loc.?);
man.hash.add(mod.emit_h != null);
if (mod.emit_h) |emit_h| {
man.hash.addEmitLoc(emit_h);
}
man.hash.addOptionalEmitLoc(comp.emit_asm);
man.hash.addOptionalEmitLoc(comp.emit_llvm_ir);
@ -3105,10 +3069,10 @@ fn updateStage1Module(comp: *Compilation, main_progress_node: *std.Progress.Node
});
break :blk try directory.join(arena, &[_][]const u8{bin_basename});
} else "";
if (comp.c_header != null) {
if (comp.emit_h != null) {
log.warn("-femit-h is not available in the stage1 backend; no .h file will be produced", .{});
}
const emit_h_path = try stage1LocPath(arena, if (comp.c_header) |header| header.emit_loc else null, directory);
const emit_h_path = try stage1LocPath(arena, mod.emit_h, directory);
const emit_asm_path = try stage1LocPath(arena, comp.emit_asm, directory);
const emit_llvm_ir_path = try stage1LocPath(arena, comp.emit_llvm_ir, directory);
const emit_analysis_path = try stage1LocPath(arena, comp.emit_analysis, directory);

View File

@ -94,6 +94,8 @@ stage1_flags: packed struct {
reserved: u2 = 0,
} = .{},
emit_h: ?Compilation.EmitLoc,
pub const Export = struct {
options: std.builtin.ExportOptions,
/// Byte offset into the file that contains the export directive.
@ -1943,14 +1945,14 @@ fn allocateNewDecl(
.coff => .{ .coff = link.File.Coff.TextBlock.empty },
.elf => .{ .elf = link.File.Elf.TextBlock.empty },
.macho => .{ .macho = link.File.MachO.TextBlock.empty },
.c => .{ .c = {} },
.c => .{ .c = link.File.C.DeclBlock.empty },
.wasm => .{ .wasm = {} },
},
.fn_link = switch (self.comp.bin_file.tag) {
.coff => .{ .coff = {} },
.elf => .{ .elf = link.File.Elf.SrcFn.empty },
.macho => .{ .macho = link.File.MachO.SrcFn.empty },
.c => .{ .c = {} },
.c => .{ .c = link.File.C.FnBlock.empty },
.wasm => .{ .wasm = null },
},
.generation = 0,

File diff suppressed because it is too large Load Diff

View File

@ -130,7 +130,7 @@ pub const File = struct {
elf: Elf.TextBlock,
coff: Coff.TextBlock,
macho: MachO.TextBlock,
c: void,
c: C.DeclBlock,
wasm: void,
};
@ -138,7 +138,7 @@ pub const File = struct {
elf: Elf.SrcFn,
coff: Coff.SrcFn,
macho: MachO.SrcFn,
c: void,
c: C.FnBlock,
wasm: ?Wasm.FnData,
};
@ -291,7 +291,7 @@ pub const File = struct {
.coff => return @fieldParentPtr(Coff, "base", base).updateDecl(module, decl),
.elf => return @fieldParentPtr(Elf, "base", base).updateDecl(module, decl),
.macho => return @fieldParentPtr(MachO, "base", base).updateDecl(module, decl),
.c => {},
.c => return @fieldParentPtr(C, "base", base).updateDecl(module, decl),
.wasm => return @fieldParentPtr(Wasm, "base", base).updateDecl(module, decl),
}
}
@ -301,7 +301,8 @@ pub const File = struct {
.coff => return @fieldParentPtr(Coff, "base", base).updateDeclLineNumber(module, decl),
.elf => return @fieldParentPtr(Elf, "base", base).updateDeclLineNumber(module, decl),
.macho => return @fieldParentPtr(MachO, "base", base).updateDeclLineNumber(module, decl),
.c, .wasm => {},
.c => return @fieldParentPtr(C, "base", base).updateDeclLineNumber(module, decl),
.wasm => {},
}
}
@ -312,7 +313,8 @@ pub const File = struct {
.coff => return @fieldParentPtr(Coff, "base", base).allocateDeclIndexes(decl),
.elf => return @fieldParentPtr(Elf, "base", base).allocateDeclIndexes(decl),
.macho => return @fieldParentPtr(MachO, "base", base).allocateDeclIndexes(decl),
.c, .wasm => {},
.c => return @fieldParentPtr(C, "base", base).allocateDeclIndexes(decl),
.wasm => {},
}
}
@ -407,12 +409,13 @@ pub const File = struct {
}
}
/// Called when a Decl is deleted from the Module.
pub fn freeDecl(base: *File, decl: *Module.Decl) void {
switch (base.tag) {
.coff => @fieldParentPtr(Coff, "base", base).freeDecl(decl),
.elf => @fieldParentPtr(Elf, "base", base).freeDecl(decl),
.macho => @fieldParentPtr(MachO, "base", base).freeDecl(decl),
.c => {},
.c => @fieldParentPtr(C, "base", base).freeDecl(decl),
.wasm => @fieldParentPtr(Wasm, "base", base).freeDecl(decl),
}
}
@ -432,14 +435,14 @@ pub const File = struct {
pub fn updateDeclExports(
base: *File,
module: *Module,
decl: *const Module.Decl,
decl: *Module.Decl,
exports: []const *Module.Export,
) !void {
switch (base.tag) {
.coff => return @fieldParentPtr(Coff, "base", base).updateDeclExports(module, decl, exports),
.elf => return @fieldParentPtr(Elf, "base", base).updateDeclExports(module, decl, exports),
.macho => return @fieldParentPtr(MachO, "base", base).updateDeclExports(module, decl, exports),
.c => return {},
.c => return @fieldParentPtr(C, "base", base).updateDeclExports(module, decl, exports),
.wasm => return @fieldParentPtr(Wasm, "base", base).updateDeclExports(module, decl, exports),
}
}

View File

@ -11,45 +11,28 @@ const trace = @import("../tracy.zig").trace;
const C = @This();
pub const base_tag: link.File.Tag = .c;
pub const Header = struct {
buf: std.ArrayList(u8),
emit_loc: ?Compilation.EmitLoc,
pub fn init(allocator: *Allocator, emit_loc: ?Compilation.EmitLoc) Header {
return .{
.buf = std.ArrayList(u8).init(allocator),
.emit_loc = emit_loc,
};
}
pub fn flush(self: *const Header, writer: anytype) !void {
const tracy = trace(@src());
defer tracy.end();
try writer.writeAll(@embedFile("cbe.h"));
if (self.buf.items.len > 0) {
try writer.print("{s}", .{self.buf.items});
}
}
pub fn deinit(self: *Header) void {
self.buf.deinit();
self.* = undefined;
}
};
pub const zig_h = @embedFile("C/zig.h");
base: link.File,
path: []const u8,
/// Per-declaration data. For functions this is the body, and
/// the forward declaration is stored in the FnBlock.
pub const DeclBlock = struct {
code: std.ArrayListUnmanaged(u8),
// These are only valid during a flush()!
header: Header,
constants: std.ArrayList(u8),
main: std.ArrayList(u8),
called: std.StringHashMap(void),
pub const empty: DeclBlock = .{
.code = .{},
};
};
error_msg: *Compilation.ErrorMsg = undefined,
/// Per-function data.
pub const FnBlock = struct {
fwd_decl: std.ArrayListUnmanaged(u8),
pub const empty: FnBlock = .{
.fwd_decl = .{},
};
};
pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Options) !*C {
assert(options.object_format == .c);
@ -57,6 +40,14 @@ pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Optio
if (options.use_llvm) return error.LLVMHasNoCBackend;
if (options.use_lld) return error.LLDHasNoCBackend;
const file = try options.emit.?.directory.handle.createFile(sub_path, .{
.truncate = true,
.mode = link.determineMode(options),
});
errdefer file.close();
try file.writeAll(zig_h);
var c_file = try allocator.create(C);
errdefer allocator.destroy(c_file);
@ -64,25 +55,75 @@ pub fn openPath(allocator: *Allocator, sub_path: []const u8, options: link.Optio
.base = .{
.tag = .c,
.options = options,
.file = null,
.file = file,
.allocator = allocator,
},
.main = undefined,
.header = undefined,
.constants = undefined,
.called = undefined,
.path = sub_path,
};
return c_file;
}
pub fn fail(self: *C, src: usize, comptime format: []const u8, args: anytype) error{ AnalysisFail, OutOfMemory } {
self.error_msg = try Compilation.ErrorMsg.create(self.base.allocator, src, format, args);
return error.AnalysisFail;
pub fn deinit(self: *C) void {
const module = self.base.options.module orelse return;
for (module.decl_table.items()) |entry| {
self.freeDecl(entry.value);
}
}
pub fn deinit(self: *C) void {}
pub fn allocateDeclIndexes(self: *C, decl: *Module.Decl) !void {}
pub fn freeDecl(self: *C, decl: *Module.Decl) void {
decl.link.c.code.deinit(self.base.allocator);
decl.fn_link.c.fwd_decl.deinit(self.base.allocator);
}
pub fn updateDecl(self: *C, module: *Module, decl: *Module.Decl) !void {
const tracy = trace(@src());
defer tracy.end();
const fwd_decl = &decl.fn_link.c.fwd_decl;
const code = &decl.link.c.code;
fwd_decl.shrinkRetainingCapacity(0);
code.shrinkRetainingCapacity(0);
var object: codegen.Object = .{
.dg = .{
.module = module,
.error_msg = null,
.decl = decl,
.fwd_decl = fwd_decl.toManaged(module.gpa),
},
.gpa = module.gpa,
.code = code.toManaged(module.gpa),
.value_map = codegen.CValueMap.init(module.gpa),
};
defer object.value_map.deinit();
defer object.code.deinit();
defer object.dg.fwd_decl.deinit();
codegen.genDecl(&object) catch |err| switch (err) {
error.AnalysisFail => {},
else => |e| return e,
};
// The code may populate this error without returning error.AnalysisFail.
if (object.dg.error_msg) |msg| {
try module.failed_decls.put(module.gpa, decl, msg);
return;
}
fwd_decl.* = object.dg.fwd_decl.moveToUnmanaged();
code.* = object.code.moveToUnmanaged();
// Free excess allocated memory for this Decl.
fwd_decl.shrink(module.gpa, fwd_decl.items.len);
code.shrink(module.gpa, code.items.len);
}
pub fn updateDeclLineNumber(self: *C, module: *Module, decl: *Module.Decl) !void {
// The C backend does not have the ability to fix line numbers without re-generating
// the entire Decl.
return self.updateDecl(module, decl);
}
pub fn flush(self: *C, comp: *Compilation) !void {
return self.flushModule(comp);
@ -92,41 +133,45 @@ pub fn flushModule(self: *C, comp: *Compilation) !void {
const tracy = trace(@src());
defer tracy.end();
self.main = std.ArrayList(u8).init(self.base.allocator);
self.header = Header.init(self.base.allocator, null);
self.constants = std.ArrayList(u8).init(self.base.allocator);
self.called = std.StringHashMap(void).init(self.base.allocator);
defer self.main.deinit();
defer self.header.deinit();
defer self.constants.deinit();
defer self.called.deinit();
const file = self.base.file.?;
const module = self.base.options.module.?;
for (self.base.options.module.?.decl_table.entries.items) |kv| {
codegen.generate(self, module, kv.value) catch |err| {
if (err == error.AnalysisFail) {
try module.failed_decls.put(module.gpa, kv.value, self.error_msg);
}
return err;
};
}
// The header is written upon opening; here we truncate and seek to after the header.
// TODO: use writev
try file.seekTo(zig_h.len);
try file.setEndPos(zig_h.len);
const file = try self.base.options.emit.?.directory.handle.createFile(self.path, .{ .truncate = true, .read = true, .mode = link.determineMode(self.base.options) });
defer file.close();
var buffered_writer = std.io.bufferedWriter(file.writer());
const writer = buffered_writer.writer();
const writer = file.writer();
try self.header.flush(writer);
if (self.header.buf.items.len > 0) {
try writer.writeByte('\n');
}
if (self.constants.items.len > 0) {
try writer.print("{s}\n", .{self.constants.items});
}
if (self.main.items.len > 1) {
const last_two = self.main.items[self.main.items.len - 2 ..];
if (std.mem.eql(u8, last_two, "\n\n")) {
self.main.items.len -= 1;
const module = self.base.options.module orelse return error.LinkingWithoutZigSourceUnimplemented;
// Forward decls and non-functions first.
// TODO: use writev
for (module.decl_table.items()) |kv| {
const decl = kv.value;
const decl_tv = decl.typed_value.most_recent.typed_value;
if (decl_tv.val.castTag(.function)) |_| {
try writer.writeAll(decl.fn_link.c.fwd_decl.items);
} else {
try writer.writeAll(decl.link.c.code.items);
}
}
try writer.writeAll(self.main.items);
// Now the function bodies.
for (module.decl_table.items()) |kv| {
const decl = kv.value;
const decl_tv = decl.typed_value.most_recent.typed_value;
if (decl_tv.val.castTag(.function)) |_| {
try writer.writeAll(decl.link.c.code.items);
}
}
try buffered_writer.flush();
}
pub fn updateDeclExports(
self: *C,
module: *Module,
decl: *Module.Decl,
exports: []const *Module.Export,
) !void {}

View File

@ -42,3 +42,4 @@
#define int128_t __int128
#define uint128_t unsigned __int128
#include <string.h>

View File

@ -13,7 +13,7 @@ const glibc_multi_install_dir: ?[]const u8 = build_options.glibc_multi_install_d
const ThreadPool = @import("ThreadPool.zig");
const CrossTarget = std.zig.CrossTarget;
const c_header = @embedFile("link/cbe.h");
const zig_h = link.File.C.zig_h;
test "self-hosted" {
var ctx = TestContext.init();
@ -324,11 +324,11 @@ pub const TestContext = struct {
}
pub fn c(ctx: *TestContext, name: []const u8, target: CrossTarget, src: [:0]const u8, comptime out: [:0]const u8) void {
ctx.addC(name, target, .Zig).addCompareObjectFile(src, c_header ++ out);
ctx.addC(name, target, .Zig).addCompareObjectFile(src, zig_h ++ out);
}
pub fn h(ctx: *TestContext, name: []const u8, target: CrossTarget, src: [:0]const u8, comptime out: [:0]const u8) void {
ctx.addC(name, target, .Zig).addHeader(src, c_header ++ out);
ctx.addC(name, target, .Zig).addHeader(src, zig_h ++ out);
}
pub fn addCompareOutput(
@ -700,11 +700,12 @@ pub const TestContext = struct {
},
}
}
if (comp.bin_file.cast(link.File.C)) |c_file| {
std.debug.print("Generated C: \n===============\n{s}\n\n===========\n\n", .{
c_file.main.items,
});
}
// TODO print generated C code
//if (comp.bin_file.cast(link.File.C)) |c_file| {
// std.debug.print("Generated C: \n===============\n{s}\n\n===========\n\n", .{
// c_file.main.items,
// });
//}
std.debug.print("Test failed.\n", .{});
std.process.exit(1);
}

View File

@ -22,8 +22,6 @@ pub fn addCases(ctx: *TestContext) !void {
, "hello world!" ++ std.cstr.line_sep);
// Now change the message only
// TODO fix C backend not supporting updates
// https://github.com/ziglang/zig/issues/7589
case.addCompareOutput(
\\extern fn puts(s: [*:0]const u8) c_int;
\\export fn main() c_int {