1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const Path = std.Build.Cache.Path;
4const assert = std.debug.assert;
5const Zcu = @import("../Zcu.zig");
6const InternPool = @import("../InternPool.zig");
7const Compilation = @import("../Compilation.zig");
8const link = @import("../link.zig");
9const codegen = @import("../codegen.zig");
10const CodeGen = @import("../codegen/spirv/CodeGen.zig");
11const Mir = @import("../codegen/spirv/Mir.zig");
12const spec = @import("../codegen/spirv/spec.zig");
13const Section = @import("../codegen/spirv/Section.zig");
14const Flush = @import("Spirv/Flush.zig");
15const Word = spec.Word;
16const Linker = @This();
17
18base: link.File,
19mirs: std.array_hash_map.Auto(InternPool.Nav.Index, Mir),
20objects: std.ArrayList(Object),
21exports: std.array_hash_map.Auto(InternPool.Nav.Index, InternPool.NullTerminatedString),
22
23pub const Object = struct { path: Path, mir: Mir };
24
25pub fn createEmpty(
26 arena: Allocator,
27 comp: *Compilation,
28 emit: Path,
29 options: link.File.OpenOptions,
30) !*Linker {
31 const file = try emit.root_dir.handle.createFile(comp.io, emit.sub_path, .{});
32 errdefer file.close(comp.io);
33 const linker = try arena.create(Linker);
34 linker.* = .{
35 .base = .{
36 .tag = .spirv,
37 .comp = comp,
38 .emit = emit,
39 .gc_sections = options.gc_sections orelse false,
40 .print_gc_sections = options.print_gc_sections,
41 .stack_size = options.stack_size orelse 0,
42 .allow_shlib_undefined = options.allow_shlib_undefined orelse false,
43 .file = file,
44 .build_id = options.build_id,
45 },
46 .mirs = .empty,
47 .objects = .empty,
48 .exports = .empty,
49 };
50 return linker;
51}
52
53pub fn open(
54 arena: Allocator,
55 comp: *Compilation,
56 emit: Path,
57 options: link.File.OpenOptions,
58) !*Linker {
59 return createEmpty(arena, comp, emit, options);
60}
61
62pub fn deinit(linker: *Linker) void {
63 const gpa = linker.base.comp.gpa;
64 for (linker.mirs.values()) |*mir| mir.deinit(gpa);
65 linker.mirs.deinit(gpa);
66 for (linker.objects.items) |*object| object.mir.deinit(gpa);
67 linker.objects.deinit(gpa);
68 linker.exports.deinit(gpa);
69}
70
71pub fn loadInput(linker: *Linker, input: link.Input) !void {
72 const diags = &linker.base.comp.link_diags;
73 switch (input) {
74 .object => |obj| try linker.loadObject(obj),
75 else => return diags.fail("unsupported link input for SPIR-V target", .{}),
76 }
77}
78
79fn loadObject(linker: *Linker, obj: link.Input.Object) !void {
80 const comp = linker.base.comp;
81 const gpa = comp.gpa;
82 const io = comp.io;
83 const diags = &comp.link_diags;
84
85 const file_size = obj.file.length(io) catch |err| {
86 return diags.failParse(obj.path, "failed to get file size: {t}", .{err});
87 };
88 if (file_size % @sizeOf(Word) != 0) {
89 return diags.failParse(obj.path, "file size is not a multiple of {d}", .{
90 @sizeOf(Word),
91 });
92 }
93
94 const words = try gpa.alloc(Word, @intCast(file_size / @sizeOf(Word)));
95 defer gpa.free(words);
96 const bytes = std.mem.sliceAsBytes(words);
97 const n_read = obj.file.readPositionalAll(io, bytes, 0) catch |err| {
98 return diags.failParse(obj.path, "failed to read: {t}", .{err});
99 };
100
101 if (n_read != bytes.len) {
102 return diags.failParse(obj.path, "incomplete read", .{});
103 }
104 const header_len = @sizeOf(spec.Header) / @sizeOf(Word);
105 if (words.len < header_len) {
106 return diags.failParse(obj.path, "header too small", .{});
107 }
108 const header = std.mem.bytesToValue(spec.Header, std.mem.sliceAsBytes(words[0..header_len]));
109 if (header.magic != spec.magic_number) {
110 return diags.failParse(obj.path, "invalid magic number", .{});
111 }
112 if (header.id_bound == 0) {
113 return diags.failParse(obj.path, "invalid id bound", .{});
114 }
115
116 const instructions = words[header_len..];
117 var module = Section.fromWords(gpa, instructions, header.id_bound) catch |err| switch (err) {
118 error.OutOfMemory => |e| return e,
119 else => return diags.failParse(obj.path, "failed to parse instructions: {t}", .{err}),
120 };
121 errdefer module.deinit(gpa);
122 try linker.objects.append(gpa, .{ .path = obj.path, .mir = .{
123 .id_bound = header.id_bound,
124 .module = module,
125 .nav_refs = &.{},
126 .externs = &.{},
127 .entry_points = &.{},
128 } });
129}
130
131pub fn updateFunc(
132 linker: *Linker,
133 pt: Zcu.PerThread,
134 func_index: InternPool.Index,
135 mir: *codegen.AnyMir,
136) !void {
137 const gpa = linker.base.comp.gpa;
138 const nav_index = pt.zcu.funcInfo(func_index).owner_nav;
139 const gop = try linker.mirs.getOrPut(gpa, nav_index);
140 if (gop.found_existing) gop.value_ptr.deinit(gpa);
141 gop.value_ptr.* = mir.spirv;
142 mir.spirv = .{
143 .id_bound = 1,
144 .module = .empty,
145 .nav_refs = &.{},
146 .externs = &.{},
147 .entry_points = &.{},
148 };
149}
150
151pub fn updateNav(
152 linker: *Linker,
153 pt: Zcu.PerThread,
154 nav_index: InternPool.Nav.Index,
155) link.Error!void {
156 const gpa = linker.base.comp.gpa;
157 const ip = &pt.zcu.intern_pool;
158 const resolved = ip.getNav(nav_index).resolved.?;
159 switch (ip.indexToKey(resolved.value)) {
160 .func => return,
161 .@"extern" => |@"extern"| if (ip.isFunctionType(@"extern".ty)) return,
162 else => if (!resolved.@"const") assert(resolved.@"threadlocal") else return,
163 }
164 var mir = CodeGen.generateNav(pt, nav_index) catch |err| switch (err) {
165 error.AlreadyReported => return,
166 else => |e| return e,
167 };
168 errdefer mir.deinit(gpa);
169 const gop = try linker.mirs.getOrPut(gpa, nav_index);
170 if (gop.found_existing) gop.value_ptr.deinit(gpa);
171 gop.value_ptr.* = mir;
172}
173
174pub fn updateExports(
175 linker: *Linker,
176 pt: Zcu.PerThread,
177 export_indices: []const Zcu.Export.Index,
178) link.Error!void {
179 const zcu = pt.zcu;
180 const ip = &zcu.intern_pool;
181 const gpa = linker.base.comp.gpa;
182 const diags = &linker.base.comp.link_diags;
183
184 linker.exports.clearRetainingCapacity();
185 try linker.exports.ensureUnusedCapacity(gpa, export_indices.len);
186 for (export_indices) |exp_index| {
187 const exp = exp_index.ptr(zcu);
188 const nav_index = switch (exp.exported) {
189 .nav => |nav| nav,
190 .uav => return diags.fail("cannot export a uav constant for SPIR-V target", .{}),
191 };
192 const nav = ip.getNav(nav_index);
193 const resolved = nav.resolved.?;
194 if (!ip.isFunctionType(resolved.type) and !resolved.@"threadlocal") {
195 return diags.fail("cannot export constant '{f}' for SPIR-V target", .{
196 nav.fqn.fmt(ip),
197 });
198 }
199 linker.exports.putAssumeCapacity(nav_index, exp.opts.name);
200 }
201}
202
203pub fn flush(
204 linker: *Linker,
205 arena: Allocator,
206 tid: Zcu.PerThread.Id,
207 prog_node: std.Progress.Node,
208) link.Error!void {
209 _ = tid;
210 const comp = linker.base.comp;
211 const gpa = comp.gpa;
212 const diags = &comp.link_diags;
213
214 const sub_node = prog_node.start("Flush Module", 1);
215 defer sub_node.end();
216
217 var f: Flush = .{
218 .linker = linker,
219 .arena = arena,
220 .id_bound = 1,
221 .nonsemantic_info = false,
222 .navs = .empty,
223 .symbols = .empty,
224 .interned = .empty,
225 .variables = .empty,
226 .fn_refs = .empty,
227 .entry_points = .empty,
228 .deps = .empty,
229 .scratch = .empty,
230 .extended_instruction_sets = .empty,
231 .object_entry_points = .empty,
232 .object_execution_modes = .empty,
233 .debug_strings = .empty,
234 .names = .empty,
235 .decorations = .empty,
236 .globals = .empty,
237 .declarations = .empty,
238 .functions = .empty,
239 };
240 var out: Section = .empty;
241 defer out.deinit(gpa);
242
243 try f.emitModule(&out);
244 sub_node.completeOne();
245
246 var writer = linker.base.file.?.writer(comp.io, &.{});
247 writer.interface.writeSliceEndian(Word, out.instructions.items, .little) catch {
248 return diags.fail("failed to write {f}: {t}", .{ linker.base.emit, writer.err.? });
249 };
250 writer.end() catch |err| {
251 return diags.fail("failed to write {f}: {t}", .{ linker.base.emit, err });
252 };
253}