1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const assert = std.debug.assert;
4
5const CodeGen = @import("CodeGen.zig");
6const Decl = @import("CodeGen.zig").Decl;
7
8const spec = @import("spec.zig");
9const Opcode = spec.Opcode;
10const Word = spec.Word;
11const Id = spec.Id;
12const StorageClass = spec.StorageClass;
13
14const Assembler = @This();
15
16cg: *CodeGen,
17errors: std.ArrayList(ErrorMsg) = .empty,
18src: []const u8 = undefined,
19/// `ass.src` tokenized.
20tokens: std.ArrayList(Token) = .empty,
21current_token: u32 = 0,
22/// The instruction that is currently being parsed or has just been parsed.
23inst: struct {
24 opcode: Opcode = undefined,
25 operands: std.ArrayList(Operand) = .empty,
26 string_bytes: std.ArrayList(u8) = .empty,
27 inst_offset: u32 = 0,
28
29 fn result(ass: *const @This()) ?AsmValue.Ref {
30 for (ass.operands.items[0..@min(ass.operands.items.len, 2)]) |op| {
31 switch (op) {
32 .result_id => |index| return index,
33 else => {},
34 }
35 }
36 return null;
37 }
38} = .{},
39value_map: std.array_hash_map.String(AsmValue) = .empty,
40inst_map: std.array_hash_map.String(void) = .empty,
41
42const Operand = union(enum) {
43 /// Any 'simple' 32-bit value. This could be a mask or
44 /// enumerant, etc, depending on the operands.
45 value: u32,
46 /// An int- or float literal encoded as 1 word.
47 literal32: u32,
48 /// An int- or float literal encoded as 2 words.
49 literal64: u64,
50 /// A result-id which is assigned to in this instruction.
51 /// If present, this is the first operand of the instruction.
52 result_id: AsmValue.Ref,
53 /// A result-id which referred to (not assigned to) in this instruction.
54 ref_id: AsmValue.Ref,
55 /// Offset into `inst.string_bytes`. The string ends at the next zero-terminator.
56 string: u32,
57};
58
59pub fn deinit(ass: *Assembler) void {
60 const gpa = ass.cg.gpa;
61 for (ass.errors.items) |err| gpa.free(err.msg);
62 for (ass.value_map.values()) |v| switch (v) {
63 .constant_composite => |cc| gpa.free(cc.values),
64 else => {},
65 };
66 ass.tokens.deinit(gpa);
67 ass.errors.deinit(gpa);
68 ass.inst.operands.deinit(gpa);
69 ass.inst.string_bytes.deinit(gpa);
70 ass.value_map.deinit(gpa);
71 ass.inst_map.deinit(gpa);
72}
73
74const Error = error{ AssembleFail, OutOfMemory };
75
76pub fn assemble(ass: *Assembler, src: []const u8) Error!void {
77 const gpa = ass.cg.gpa;
78
79 ass.src = src;
80 ass.errors.clearRetainingCapacity();
81
82 // Populate the opcode map if it isn't already
83 if (ass.inst_map.count() == 0) {
84 const instructions = spec.InstructionSet.core.instructions();
85 try ass.inst_map.ensureUnusedCapacity(gpa, @intCast(instructions.len));
86 for (instructions, 0..) |inst, i| {
87 const entry = try ass.inst_map.getOrPut(gpa, inst.name);
88 assert(entry.index == i);
89 }
90 }
91
92 try ass.tokenize();
93 while (!ass.testToken(.eof)) {
94 try ass.parseInstruction();
95 try ass.processInstruction();
96 }
97
98 if (ass.errors.items.len > 0) return error.AssembleFail;
99}
100
101const ErrorMsg = struct {
102 /// The offset in bytes from the start of `src` that this error occured.
103 byte_offset: u32,
104 msg: []const u8,
105};
106
107fn addError(ass: *Assembler, offset: u32, comptime fmt: []const u8, args: anytype) !void {
108 const gpa = ass.cg.gpa;
109 const msg = try std.fmt.allocPrint(gpa, fmt, args);
110 errdefer gpa.free(msg);
111 try ass.errors.append(gpa, .{
112 .byte_offset = offset,
113 .msg = msg,
114 });
115}
116
117fn fail(ass: *Assembler, offset: u32, comptime fmt: []const u8, args: anytype) Error {
118 @branchHint(.cold);
119 try ass.addError(offset, fmt, args);
120 return error.AssembleFail;
121}
122
123fn todo(ass: *Assembler, comptime fmt: []const u8, args: anytype) Error {
124 return ass.fail(ass.inst.inst_offset, "todo: " ++ fmt, args);
125}
126
127const AsmValue = union(enum) {
128 /// The results are stored in an array hash map, and can be referred
129 /// to either by name (without the %), or by values of this index type.
130 pub const Ref = u32;
131
132 /// The RHS of the current instruction.
133 just_declared,
134 /// A placeholder for ref-ids of which the result-id is not yet known.
135 /// It will be further resolved at a later stage to a more concrete forward reference.
136 unresolved_forward_reference,
137 /// A normal result produced by a different instruction.
138 value: Id,
139 /// A type registered into the module's type system.
140 ty: Id,
141 /// A pre-supplied constant value, holding the raw bit pattern of the input.
142 /// For integers the value is sign-extended (for signed) or zero-extended
143 /// (for unsigned) to 64 bits. For floats, the value is the bit pattern
144 /// zero-extended from the float's width to 64 bits.
145 constant: u64,
146 /// A vector "c" input expanded by `processSpecConstVector`.
147 constant_composite: struct {
148 child: Id,
149 child_kind: std.lang.TypeId,
150 child_bit_width: u16,
151 values: []u64,
152 },
153 string: []const u8,
154
155 /// Retrieve the result-id of this AsmValue. Asserts that this AsmValue
156 /// is of a variant that allows the result to be obtained (not an unresolved
157 /// forward declaration, not in the process of being declared, etc).
158 pub fn resultId(value: AsmValue) Id {
159 return switch (value) {
160 .just_declared,
161 .unresolved_forward_reference,
162 // TODO: Lower this value as constant?
163 .constant,
164 .constant_composite,
165 .string,
166 => unreachable,
167 .value => |result| result,
168 .ty => |result| result,
169 };
170 }
171};
172
173/// Attempt to process the instruction currently in `ass.inst`.
174/// This for example emits the instruction in the module or function, or
175/// records type definitions.
176/// If this function returns `error.AssembleFail`, an explanatory
177/// error message has already been emitted into `ass.errors`.
178fn processInstruction(ass: *Assembler) !void {
179 const cg = ass.cg;
180 const result: AsmValue = switch (ass.inst.opcode) {
181 .OpEntryPoint => {
182 return ass.fail(ass.inst.inst_offset, "cannot export entry points in assembly", .{});
183 },
184 .OpExecutionMode, .OpExecutionModeId => {
185 return ass.fail(ass.inst.inst_offset, "cannot set execution mode in assembly", .{});
186 },
187 .OpCapability, .OpExtension => {
188 return ass.fail(ass.inst.inst_offset, "cannot declare capabilities or extensions in assembly; use -mcpu instead", .{});
189 },
190 .OpExtInstImport => blk: {
191 const set_name_offset = ass.inst.operands.items[1].string;
192 const set_name = std.mem.sliceTo(ass.inst.string_bytes.items[set_name_offset..], 0);
193 const set_tag = std.meta.stringToEnum(spec.InstructionSet, set_name) orelse {
194 return ass.fail(ass.inst.inst_offset, "unknown instruction set: {s}", .{set_name});
195 };
196 break :blk .{ .value = try cg.importInstructionSet(set_tag) };
197 },
198 .OpSpecConstantComposite => blk: {
199 if (try ass.processSpecConstVector()) |result| {
200 break :blk result;
201 }
202 break :blk (try ass.processGenericInstruction()) orelse return;
203 },
204 else => switch (ass.inst.opcode.class()) {
205 .type_declaration => try ass.processTypeInstruction(),
206 else => (try ass.processGenericInstruction()) orelse return,
207 },
208 };
209
210 const result_ref = ass.inst.result().?;
211 switch (ass.value_map.values()[result_ref]) {
212 .just_declared => ass.value_map.values()[result_ref] = result,
213 else => {
214 const name = ass.value_map.keys()[result_ref];
215 return ass.fail(ass.inst.inst_offset, "duplicate definition of %{s}", .{name});
216 },
217 }
218}
219
220fn processTypeInstruction(ass: *Assembler) !AsmValue {
221 const cg = ass.cg;
222 const gpa = cg.gpa;
223 const operands = ass.inst.operands.items;
224 const section = &cg.sections.globals;
225 const id = switch (ass.inst.opcode) {
226 .OpTypeVoid => try cg.voidType(),
227 .OpTypeBool => try cg.boolType(),
228 .OpTypeInt => blk: {
229 const signedness: std.lang.Signedness = switch (operands[2].literal32) {
230 0 => .unsigned,
231 1 => .signed,
232 else => {
233 return ass.fail(ass.inst.inst_offset, "{} is not a valid signedness (expected 0 or 1)", .{operands[2].literal32});
234 },
235 };
236 const width = std.math.cast(u16, operands[1].literal32) orelse {
237 return ass.fail(ass.inst.inst_offset, "int type of {} bits is too large", .{operands[1].literal32});
238 };
239 break :blk try cg.intType(signedness, width);
240 },
241 .OpTypeFloat => blk: {
242 const bits = operands[1].literal32;
243 switch (bits) {
244 16, 32, 64 => {},
245 else => {
246 return ass.fail(ass.inst.inst_offset, "{} is not a valid bit count for floats (expected 16, 32 or 64)", .{bits});
247 },
248 }
249 break :blk try cg.floatType(@intCast(bits));
250 },
251 .OpTypeVector => blk: {
252 const child_type = try ass.resolveRefId(operands[1].ref_id);
253 break :blk try cg.vectorType(operands[2].literal32, child_type);
254 },
255 .OpTypeArray => {
256 // TODO: The length of an OpTypeArray is determined by a constant (which may be a spec constant),
257 // and so some consideration must be taken when entering this in the type system.
258 return ass.todo("process OpTypeArray", .{});
259 },
260 .OpTypeRuntimeArray => blk: {
261 const element_type = try ass.resolveRefId(operands[1].ref_id);
262 const result_id = cg.allocId();
263 try section.emit(cg.gpa, .OpTypeRuntimeArray, .{
264 .id_result = result_id,
265 .element_type = element_type,
266 });
267 break :blk result_id;
268 },
269 .OpTypePointer => blk: {
270 const storage_class: StorageClass = @fromBackingInt(@intCast(operands[1].value));
271 const child_type = try ass.resolveRefId(operands[2].ref_id);
272 const result_id = cg.allocId();
273 try section.emit(cg.gpa, .OpTypePointer, .{
274 .id_result = result_id,
275 .storage_class = storage_class,
276 .type = child_type,
277 });
278 break :blk result_id;
279 },
280 .OpTypeStruct => blk: {
281 const scratch_top = cg.id_scratch.items.len;
282 defer cg.id_scratch.shrinkRetainingCapacity(scratch_top);
283 const ids = try cg.id_scratch.addManyAsSlice(gpa, operands[1..].len);
284 for (operands[1..], ids) |op, *id| id.* = try ass.resolveRefId(op.ref_id);
285 break :blk try cg.structType(ids, null, .none);
286 },
287 .OpTypeImage => blk: {
288 const sampled_type = try ass.resolveRefId(operands[1].ref_id);
289 const result_id = cg.allocId();
290 try section.emit(gpa, .OpTypeImage, .{
291 .id_result = result_id,
292 .sampled_type = sampled_type,
293 .dim = @fromBackingInt(@intCast(operands[2].value)),
294 .depth = operands[3].literal32,
295 .arrayed = operands[4].literal32,
296 .ms = operands[5].literal32,
297 .sampled = operands[6].literal32,
298 .image_format = @fromBackingInt(@intCast(operands[7].value)),
299 });
300 break :blk result_id;
301 },
302 .OpTypeSampler => blk: {
303 const result_id = cg.allocId();
304 try section.emit(gpa, .OpTypeSampler, .{ .id_result = result_id });
305 break :blk result_id;
306 },
307 .OpTypeSampledImage => blk: {
308 const image_type = try ass.resolveRefId(operands[1].ref_id);
309 const result_id = cg.allocId();
310 try section.emit(gpa, .OpTypeSampledImage, .{ .id_result = result_id, .image_type = image_type });
311 break :blk result_id;
312 },
313 .OpTypeFunction => blk: {
314 const param_operands = operands[2..];
315 const return_type = try ass.resolveRefId(operands[1].ref_id);
316
317 const scratch_top = cg.id_scratch.items.len;
318 defer cg.id_scratch.shrinkRetainingCapacity(scratch_top);
319 const param_types = try cg.id_scratch.addManyAsSlice(gpa, param_operands.len);
320
321 for (param_types, param_operands) |*param, operand| {
322 param.* = try ass.resolveRefId(operand.ref_id);
323 }
324 const result_id = cg.allocId();
325 try section.emit(cg.gpa, .OpTypeFunction, .{
326 .id_result = result_id,
327 .return_type = return_type,
328 .id_ref_2 = param_types,
329 });
330 break :blk result_id;
331 },
332 else => return ass.todo("process type instruction {s}", .{@tagName(ass.inst.opcode)}),
333 };
334
335 return .{ .ty = id };
336}
337
338/// - No forward references are allowed in operands.
339/// - Target section is determined from instruction type.
340fn processGenericInstruction(ass: *Assembler) !?AsmValue {
341 const cg = ass.cg;
342 const target = cg.zcu.getTarget();
343 const operands = ass.inst.operands.items;
344 var maybe_spv_decl_index: ?Decl.Index = null;
345 const section = switch (ass.inst.opcode.class()) {
346 .constant_creation => &cg.sections.globals,
347 .annotation => &cg.sections.annotations,
348 .type_declaration => unreachable, // Handled elsewhere.
349 else => switch (ass.inst.opcode) {
350 .OpEntryPoint => unreachable,
351 .OpExecutionMode, .OpExecutionModeId => &cg.sections.execution_modes,
352 .OpVariable => section: {
353 const storage_class: spec.StorageClass = @fromBackingInt(@intCast(operands[2].value));
354 if (storage_class == .function) break :section &ass.cg.prologue;
355 maybe_spv_decl_index = try cg.allocDecl(.global);
356 if (!target.cpu.has(.spirv, .v1_4) and storage_class != .input and storage_class != .output) {
357 // Before version 1.4, the interface’s storage classes are limited to the Input and Output
358 break :section &cg.sections.globals;
359 }
360 try ass.cg.decl_deps.append(cg.gpa, maybe_spv_decl_index.?);
361 break :section &cg.sections.globals;
362 },
363 else => &ass.cg.body,
364 },
365 };
366
367 var maybe_result_id: ?Id = null;
368 const first_word = section.instructions.items.len;
369
370 // Pre-calculate exact instruction size to avoid per-operand capacity checks.
371 var total_words: usize = 1; // 1 word for the opcode itself
372 for (operands) |operand| {
373 total_words += switch (operand) {
374 .value, .literal32, .result_id, .ref_id => 1,
375 .literal64 => 2,
376 .string => |offset| blk: {
377 const text = std.mem.sliceTo(ass.inst.string_bytes.items[offset..], 0);
378 break :blk @divCeil(text.len + 1, @sizeOf(Word));
379 },
380 };
381 }
382
383 try section.ensureUnusedCapacity(cg.gpa, total_words);
384 section.writeWord(0);
385
386 for (operands) |operand| {
387 switch (operand) {
388 .value, .literal32 => |word| section.writeWord(word),
389 .literal64 => |dword| section.writeDoubleWord(dword),
390 .result_id => {
391 maybe_result_id = if (maybe_spv_decl_index) |spv_decl_index|
392 cg.declPtr(spv_decl_index).result_id
393 else
394 cg.allocId();
395 section.writeOperand(Id, maybe_result_id.?);
396 },
397 .ref_id => |index| {
398 const result = try ass.resolveRef(index);
399 section.writeOperand(spec.Id, result.resultId());
400 },
401 .string => |offset| {
402 const text = std.mem.sliceTo(ass.inst.string_bytes.items[offset..], 0);
403 section.writeOperand(spec.LiteralString, text);
404 },
405 }
406 }
407
408 const actual_word_count = section.instructions.items.len - first_word;
409 section.instructions.items[first_word] |= @as(u32, @as(u16, @intCast(actual_word_count))) << 16 | @backingInt(ass.inst.opcode);
410
411 switch (ass.inst.opcode) {
412 .OpKill,
413 .OpReturn,
414 .OpReturnValue,
415 .OpUnreachable,
416 => ass.cg.block_terminated = true,
417 else => {},
418 }
419
420 if (maybe_result_id) |result| return .{ .value = result };
421 return null;
422}
423
424/// Handles `%ret = OpSpecConstantComposite %ty %vec %spec_id` where `%vec` is a
425/// vector `"c"` input and `%spec_id` is a base SpecId `"c"` input.
426/// returns null to fall back to normal processing.
427fn processSpecConstVector(ass: *Assembler) !?AsmValue {
428 if (ass.inst.operands.items.len != 4) return null;
429 const vec_ref = switch (ass.inst.operands.items[2]) {
430 .ref_id => |i| i,
431 else => return null,
432 };
433 const sid_ref = switch (ass.inst.operands.items[3]) {
434 .ref_id => |i| i,
435 else => return null,
436 };
437 const cc = switch (try ass.resolveRef(vec_ref)) {
438 .constant_composite => |cc| cc,
439 else => return null,
440 };
441 const spec_id_base = switch (try ass.resolveRef(sid_ref)) {
442 .constant => |v| v,
443 else => return null,
444 };
445
446 const cg = ass.cg;
447 const gpa = cg.gpa;
448 const ty_ref = switch (ass.inst.operands.items[0]) {
449 .ref_id => |i| i,
450 else => return ass.fail(ass.inst.inst_offset, "missing result type", .{}),
451 };
452 const composite_ty_id = switch (try ass.resolveRef(ty_ref)) {
453 .ty => |id| id,
454 else => return ass.fail(ass.inst.inst_offset, "%ty must be a type", .{}),
455 };
456
457 const globals = &cg.sections.globals;
458 const annotations = &cg.sections.annotations;
459 const literal_words: usize = if (cc.child_bit_width <= @bitSizeOf(Word)) 1 else 2;
460
461 const elem_ids = try gpa.alloc(Id, cc.values.len);
462 defer gpa.free(elem_ids);
463 for (cc.values, elem_ids, 0..) |value, *elem_id_out, i| {
464 const elem_id = cg.allocId();
465 elem_id_out.* = elem_id;
466
467 switch (cc.child_kind) {
468 .bool => {
469 const opcode: Opcode = if (value & 1 != 0) .OpSpecConstantTrue else .OpSpecConstantFalse;
470 try globals.emitRaw(gpa, opcode, 2);
471 globals.writeOperand(Id, cc.child);
472 globals.writeOperand(Id, elem_id);
473 },
474 .int, .float => {
475 try globals.emitRaw(gpa, .OpSpecConstant, 2 + literal_words);
476 globals.writeOperand(Id, cc.child);
477 globals.writeOperand(Id, elem_id);
478 if (literal_words == 1) {
479 globals.writeWord(@truncate(value));
480 } else {
481 globals.writeDoubleWord(value);
482 }
483 },
484 else => unreachable,
485 }
486
487 const spec_id_word = std.math.cast(u32, spec_id_base + i) orelse {
488 return ass.fail(ass.inst.inst_offset, "SpecId {} does not fit in 32 bits", .{spec_id_base + i});
489 };
490 try annotations.emitRaw(gpa, .OpDecorate, 3);
491 annotations.writeOperand(Id, elem_id);
492 annotations.writeWord(@backingInt(spec.Decoration.spec_id));
493 annotations.writeWord(spec_id_word);
494 }
495
496 const result_id = cg.allocId();
497 try globals.emitRaw(gpa, .OpSpecConstantComposite, 2 + cc.values.len);
498 globals.writeOperand(Id, composite_ty_id);
499 globals.writeOperand(Id, result_id);
500 for (elem_ids) |id| globals.writeOperand(Id, id);
501
502 return .{ .value = result_id };
503}
504
505fn resolveMaybeForwardRef(ass: *Assembler, ref: AsmValue.Ref) !AsmValue {
506 const value = ass.value_map.values()[ref];
507 switch (value) {
508 .just_declared => {
509 const name = ass.value_map.keys()[ref];
510 return ass.fail(ass.inst.inst_offset, "self-referential parameter %{s}", .{name});
511 },
512 else => return value,
513 }
514}
515
516fn resolveRef(ass: *Assembler, ref: AsmValue.Ref) !AsmValue {
517 const value = try ass.resolveMaybeForwardRef(ref);
518 switch (value) {
519 .just_declared => unreachable,
520 .unresolved_forward_reference => {
521 const name = ass.value_map.keys()[ref];
522 return ass.fail(ass.inst.inst_offset, "reference to undeclared result-id %{s}", .{name});
523 },
524 else => return value,
525 }
526}
527
528fn resolveRefId(ass: *Assembler, ref: AsmValue.Ref) !Id {
529 const value = try ass.resolveRef(ref);
530 return value.resultId();
531}
532
533fn parseInstruction(ass: *Assembler) !void {
534 const gpa = ass.cg.gpa;
535
536 ass.inst.opcode = undefined;
537 ass.inst.operands.clearRetainingCapacity();
538 ass.inst.string_bytes.clearRetainingCapacity();
539 ass.inst.inst_offset = ass.currentToken().start;
540
541 const lhs_result_tok = ass.currentToken();
542 const maybe_lhs_result: ?AsmValue.Ref = if (ass.eatToken(.result_id_assign)) blk: {
543 const name = ass.tokenText(lhs_result_tok)[1..];
544 const entry = try ass.value_map.getOrPut(gpa, name);
545 try ass.expectToken(.equals);
546 if (!entry.found_existing) {
547 entry.value_ptr.* = .just_declared;
548 }
549 break :blk @intCast(entry.index);
550 } else null;
551
552 const opcode_tok = ass.currentToken();
553 if (maybe_lhs_result != null) {
554 try ass.expectToken(.opcode);
555 } else if (!ass.eatToken(.opcode)) {
556 return ass.fail(opcode_tok.start, "expected start of instruction, found {s}", .{opcode_tok.tag.name()});
557 }
558
559 const opcode_text = ass.tokenText(opcode_tok);
560 const index = ass.inst_map.getIndex(opcode_text) orelse {
561 return ass.fail(opcode_tok.start, "invalid opcode '{s}'", .{opcode_text});
562 };
563
564 const inst = spec.InstructionSet.core.instructions()[index];
565 ass.inst.opcode = @fromBackingInt(@intCast(inst.opcode));
566
567 const expected_operands = inst.operands;
568 // This is a loop because the result-id is not always the first operand.
569 const requires_lhs_result = for (expected_operands) |op| {
570 if (op.kind == .id_result) break true;
571 } else false;
572
573 if (requires_lhs_result and maybe_lhs_result == null) {
574 return ass.fail(opcode_tok.start, "opcode '{s}' expects result on left-hand side", .{@tagName(ass.inst.opcode)});
575 } else if (!requires_lhs_result and maybe_lhs_result != null) {
576 return ass.fail(
577 lhs_result_tok.start,
578 "opcode '{s}' does not expect a result-id on the left-hand side",
579 .{@tagName(ass.inst.opcode)},
580 );
581 }
582
583 for (expected_operands) |operand| {
584 if (operand.kind == .id_result) {
585 try ass.inst.operands.append(gpa, .{ .result_id = maybe_lhs_result.? });
586 continue;
587 }
588
589 switch (operand.quantifier) {
590 .required => if (ass.isAtInstructionBoundary()) {
591 return ass.fail(
592 ass.currentToken().start,
593 "missing required operand '{s}'",
594 .{@tagName(operand.kind)},
595 );
596 } else {
597 try ass.parseOperand(operand.kind);
598 },
599 .optional => if (!ass.isAtInstructionBoundary()) {
600 try ass.parseOperand(operand.kind);
601 },
602 .variadic => while (!ass.isAtInstructionBoundary()) {
603 try ass.parseOperand(operand.kind);
604 },
605 }
606 }
607}
608
609fn parseOperand(ass: *Assembler, kind: spec.OperandKind) Error!void {
610 switch (kind.category()) {
611 .bit_enum => try ass.parseBitEnum(kind),
612 .value_enum => try ass.parseValueEnum(kind),
613 .id => try ass.parseRefId(),
614 else => switch (kind) {
615 .literal_integer => try ass.parseLiteralInteger(),
616 .literal_string => try ass.parseString(),
617 .literal_context_dependent_number => try ass.parseContextDependentNumber(),
618 .literal_ext_inst_integer => try ass.parseLiteralExtInstInteger(),
619 .pair_id_ref_id_ref => try ass.parsePhiSource(),
620 else => return ass.todo("parse operand of type {s}", .{@tagName(kind)}),
621 },
622 }
623}
624
625/// Also handles parsing any required extra operands.
626fn parseBitEnum(ass: *Assembler, kind: spec.OperandKind) !void {
627 const gpa = ass.cg.gpa;
628
629 var tok = ass.currentToken();
630 try ass.expectToken(.value);
631
632 var text = ass.tokenText(tok);
633 if (std.mem.eql(u8, text, "None")) {
634 try ass.inst.operands.append(gpa, .{ .value = 0 });
635 return;
636 }
637
638 const enumerants = kind.enumerants();
639 var mask: u32 = 0;
640 while (true) {
641 const enumerant = for (enumerants) |enumerant| {
642 if (std.mem.eql(u8, enumerant.name, text))
643 break enumerant;
644 } else {
645 return ass.fail(tok.start, "'{s}' is not a valid flag for bitmask {s}", .{ text, @tagName(kind) });
646 };
647 mask |= enumerant.value;
648 if (!ass.eatToken(.pipe))
649 break;
650
651 tok = ass.currentToken();
652 try ass.expectToken(.value);
653 text = ass.tokenText(tok);
654 }
655
656 try ass.inst.operands.append(gpa, .{ .value = mask });
657
658 // Assume values are sorted.
659 // TODO: ensure in generator.
660 for (enumerants) |enumerant| {
661 if ((mask & enumerant.value) == 0)
662 continue;
663
664 for (enumerant.parameters) |param_kind| {
665 if (ass.isAtInstructionBoundary()) {
666 return ass.fail(ass.currentToken().start, "missing required parameter for bit flag '{s}'", .{enumerant.name});
667 }
668
669 try ass.parseOperand(param_kind);
670 }
671 }
672}
673
674/// Also handles parsing any required extra operands.
675fn parseValueEnum(ass: *Assembler, kind: spec.OperandKind) !void {
676 const gpa = ass.cg.gpa;
677
678 const tok = ass.currentToken();
679 if (ass.eatToken(.placeholder)) {
680 const name = ass.tokenText(tok)[1..];
681 const value = ass.value_map.get(name) orelse {
682 return ass.fail(tok.start, "invalid placeholder '${s}'", .{name});
683 };
684 switch (value) {
685 .constant => |literal| {
686 const literal32 = std.math.cast(u32, literal) orelse {
687 return ass.fail(
688 tok.start,
689 "placeholder value {} does not fit in 32 bits",
690 .{literal},
691 );
692 };
693 try ass.inst.operands.append(gpa, .{ .value = literal32 });
694 },
695 .string => |str| {
696 const enumerant = for (kind.enumerants()) |enumerant| {
697 if (std.mem.eql(u8, enumerant.name, str)) break enumerant;
698 } else {
699 return ass.fail(tok.start, "'{s}' is not a valid value for enumeration {s}", .{ str, @tagName(kind) });
700 };
701 try ass.inst.operands.append(gpa, .{ .value = enumerant.value });
702 },
703 else => return ass.fail(tok.start, "value '{s}' cannot be used as placeholder", .{name}),
704 }
705 return;
706 }
707
708 try ass.expectToken(.value);
709
710 const text = ass.tokenText(tok);
711 const int_value = std.fmt.parseInt(u32, text, 0) catch null;
712 const enumerant = for (kind.enumerants()) |enumerant| {
713 if (int_value) |v| {
714 if (v == enumerant.value) break enumerant;
715 } else {
716 if (std.mem.eql(u8, enumerant.name, text)) break enumerant;
717 }
718 } else {
719 return ass.fail(tok.start, "'{s}' is not a valid value for enumeration {s}", .{ text, @tagName(kind) });
720 };
721
722 try ass.inst.operands.append(gpa, .{ .value = enumerant.value });
723
724 for (enumerant.parameters) |param_kind| {
725 if (ass.isAtInstructionBoundary()) {
726 return ass.fail(ass.currentToken().start, "missing required parameter for enum variant '{s}'", .{enumerant.name});
727 }
728
729 try ass.parseOperand(param_kind);
730 }
731}
732
733fn parseRefId(ass: *Assembler) !void {
734 const gpa = ass.cg.gpa;
735
736 const tok = ass.currentToken();
737 try ass.expectToken(.result_id);
738
739 const name = ass.tokenText(tok)[1..];
740 const entry = try ass.value_map.getOrPut(gpa, name);
741 if (!entry.found_existing) {
742 entry.value_ptr.* = .unresolved_forward_reference;
743 }
744
745 const index: AsmValue.Ref = @intCast(entry.index);
746 try ass.inst.operands.append(gpa, .{ .ref_id = index });
747}
748
749fn parseLiteralInteger(ass: *Assembler) !void {
750 const gpa = ass.cg.gpa;
751
752 const tok = ass.currentToken();
753 if (ass.eatToken(.placeholder)) {
754 const name = ass.tokenText(tok)[1..];
755 const value = ass.value_map.get(name) orelse {
756 return ass.fail(tok.start, "invalid placeholder '${s}'", .{name});
757 };
758 switch (value) {
759 .constant => |literal| {
760 const literal32 = std.math.cast(u32, literal) orelse {
761 return ass.fail(
762 tok.start,
763 "placeholder value {} does not fit in 32 bits",
764 .{literal},
765 );
766 };
767 try ass.inst.operands.append(gpa, .{ .literal32 = literal32 });
768 },
769 else => {
770 return ass.fail(tok.start, "value '{s}' cannot be used as placeholder", .{name});
771 },
772 }
773 return;
774 }
775
776 try ass.expectToken(.value);
777 // According to the SPIR-V machine readable grammar, a LiteralInteger
778 // may consist of one or more words. From the SPIR-V docs it seems like there
779 // only one instruction where multiple words are allowed, the literals that make up the
780 // switch cases of OpSwitch. This case is handled separately, and so we just assume
781 // everything is a 32-bit integer in this function.
782 const text = ass.tokenText(tok);
783 const value = std.fmt.parseInt(u32, text, 0) catch {
784 return ass.fail(tok.start, "'{s}' is not a valid 32-bit integer literal", .{text});
785 };
786 try ass.inst.operands.append(gpa, .{ .literal32 = value });
787}
788
789fn parseLiteralExtInstInteger(ass: *Assembler) !void {
790 const gpa = ass.cg.gpa;
791
792 const tok = ass.currentToken();
793 if (ass.eatToken(.placeholder)) {
794 const name = ass.tokenText(tok)[1..];
795 const value = ass.value_map.get(name) orelse {
796 return ass.fail(tok.start, "invalid placeholder '${s}'", .{name});
797 };
798 switch (value) {
799 .constant => |literal| {
800 const literal32 = std.math.cast(u32, literal) orelse {
801 return ass.fail(
802 tok.start,
803 "placeholder value {} does not fit in 32 bits",
804 .{literal},
805 );
806 };
807 try ass.inst.operands.append(gpa, .{ .literal32 = literal32 });
808 },
809 else => {
810 return ass.fail(tok.start, "value '{s}' cannot be used as placeholder", .{name});
811 },
812 }
813 return;
814 }
815
816 try ass.expectToken(.value);
817 const text = ass.tokenText(tok);
818 const value = std.fmt.parseInt(u32, text, 0) catch {
819 return ass.fail(tok.start, "'{s}' is not a valid 32-bit integer literal", .{text});
820 };
821 try ass.inst.operands.append(gpa, .{ .literal32 = value });
822}
823
824fn parseString(ass: *Assembler) !void {
825 const gpa = ass.cg.gpa;
826
827 const tok = ass.currentToken();
828 try ass.expectToken(.string);
829 // Note, the string might not have a closing quote. In this case,
830 // an error is already emitted but we are trying to continue processing
831 // anyway, so in this function we have to deal with that situation.
832 const text = ass.tokenText(tok);
833 assert(text.len > 0 and text[0] == '"');
834 const literal = if (text.len != 1 and text[text.len - 1] == '"')
835 text[1 .. text.len - 1]
836 else
837 text[1..];
838
839 const string_offset: u32 = @intCast(ass.inst.string_bytes.items.len);
840 try ass.inst.string_bytes.ensureUnusedCapacity(gpa, literal.len + 1);
841 ass.inst.string_bytes.appendSliceAssumeCapacity(literal);
842 ass.inst.string_bytes.appendAssumeCapacity(0);
843
844 try ass.inst.operands.append(gpa, .{ .string = string_offset });
845}
846
847fn parseContextDependentNumber(ass: *Assembler) !void {
848 const cg = ass.cg;
849 assert(ass.inst.opcode == .OpConstant or ass.inst.opcode == .OpSpecConstant);
850
851 const tok = ass.currentToken();
852 const result = try ass.resolveRef(ass.inst.operands.items[0].ref_id);
853 const result_id = result.resultId();
854
855 const words = cg.sections.globals.instructions.items;
856 var offset: usize = 0;
857 while (offset < words.len) {
858 const word_count = words[offset] >> 16;
859 const opcode: Opcode = @fromBackingInt(@intCast(words[offset] & 0xFFFF));
860 defer offset += word_count;
861 if (word_count == 0) break;
862 switch (opcode) {
863 .OpTypeInt => if (word_count >= 4 and @as(Id, @fromBackingInt(@intCast(words[offset + 1]))) == result_id) {
864 const width: u16 = @intCast(words[offset + 2]);
865 const signedness: std.lang.Signedness = if (words[offset + 3] == 0) .unsigned else .signed;
866 return ass.parseContextDependentInt(signedness, width);
867 },
868 .OpTypeFloat => if (word_count >= 3 and @as(Id, @fromBackingInt(@intCast(words[offset + 1]))) == result_id) {
869 const bits = words[offset + 2];
870 return switch (bits) {
871 16 => ass.parseContextDependentFloat(16),
872 32 => ass.parseContextDependentFloat(32),
873 64 => ass.parseContextDependentFloat(64),
874 else => ass.fail(tok.start, "cannot parse {}-bit info literal", .{bits}),
875 };
876 },
877 else => {},
878 }
879 }
880
881 return ass.fail(tok.start, "cannot parse literal constant", .{});
882}
883
884fn parseContextDependentInt(ass: *Assembler, signedness: std.lang.Signedness, width: u32) !void {
885 const gpa = ass.cg.gpa;
886
887 const tok = ass.currentToken();
888 if (ass.eatToken(.placeholder)) {
889 const name = ass.tokenText(tok)[1..];
890 const value = ass.value_map.get(name) orelse {
891 return ass.fail(tok.start, "invalid placeholder '${s}'", .{name});
892 };
893 switch (value) {
894 .constant => |literal| {
895 if (width <= @bitSizeOf(spec.Word)) {
896 try ass.inst.operands.append(gpa, .{ .literal32 = @truncate(literal) });
897 } else {
898 try ass.inst.operands.append(gpa, .{ .literal64 = literal });
899 }
900 },
901 else => {
902 return ass.fail(tok.start, "value '{s}' cannot be used as placeholder", .{name});
903 },
904 }
905 return;
906 }
907
908 try ass.expectToken(.value);
909
910 if (width == 0 or width > 2 * @bitSizeOf(spec.Word)) {
911 return ass.fail(tok.start, "cannot parse {}-bit integer literal", .{width});
912 }
913
914 const text = ass.tokenText(tok);
915 invalid: {
916 // Just parse the integer as the next larger integer type, and check if it overflows afterwards.
917 const int = std.fmt.parseInt(i128, text, 0) catch break :invalid;
918 const min = switch (signedness) {
919 .unsigned => 0,
920 .signed => -(@as(i128, 1) << (@as(u7, @intCast(width)) - 1)),
921 };
922 const max = (@as(i128, 1) << (@as(u7, @intCast(width)) - @intFromBool(signedness == .signed))) - 1;
923 if (int < min or int > max) {
924 break :invalid;
925 }
926
927 // Note, we store the sign-extended version here.
928 if (width <= @bitSizeOf(spec.Word)) {
929 try ass.inst.operands.append(gpa, .{ .literal32 = @truncate(@as(u128, @bitCast(int))) });
930 } else {
931 try ass.inst.operands.append(gpa, .{ .literal64 = @truncate(@as(u128, @bitCast(int))) });
932 }
933 return;
934 }
935
936 return ass.fail(tok.start, "'{s}' is not a valid {s} {}-bit int literal", .{ text, @tagName(signedness), width });
937}
938
939fn parseContextDependentFloat(ass: *Assembler, comptime width: u16) !void {
940 const gpa = ass.cg.gpa;
941
942 const Float = std.meta.Float(width);
943 const Int = @Int(.unsigned, width);
944
945 const tok = ass.currentToken();
946 if (ass.eatToken(.placeholder)) {
947 const name = ass.tokenText(tok)[1..];
948 const value = ass.value_map.get(name) orelse {
949 return ass.fail(tok.start, "invalid placeholder '${s}'", .{name});
950 };
951 switch (value) {
952 .constant => |literal| {
953 if (width <= @bitSizeOf(spec.Word)) {
954 try ass.inst.operands.append(gpa, .{ .literal32 = @truncate(literal) });
955 } else {
956 try ass.inst.operands.append(gpa, .{ .literal64 = literal });
957 }
958 },
959 else => {
960 return ass.fail(tok.start, "value '{s}' cannot be used as placeholder", .{name});
961 },
962 }
963 return;
964 }
965 try ass.expectToken(.value);
966
967 const text = ass.tokenText(tok);
968
969 const value = std.fmt.parseFloat(Float, text) catch {
970 return ass.fail(tok.start, "'{s}' is not a valid {}-bit float literal", .{ text, width });
971 };
972
973 const float_bits: Int = @bitCast(value);
974 if (width <= @bitSizeOf(spec.Word)) {
975 try ass.inst.operands.append(gpa, .{ .literal32 = float_bits });
976 } else {
977 assert(width <= 2 * @bitSizeOf(spec.Word));
978 try ass.inst.operands.append(gpa, .{ .literal64 = float_bits });
979 }
980}
981
982fn parsePhiSource(ass: *Assembler) !void {
983 try ass.parseRefId();
984 if (ass.isAtInstructionBoundary()) {
985 return ass.fail(ass.currentToken().start, "missing phi block parent", .{});
986 }
987 try ass.parseRefId();
988}
989
990/// Returns whether the `current_token` cursor
991/// is currently pointing at the start of a new instruction.
992fn isAtInstructionBoundary(ass: Assembler) bool {
993 return switch (ass.currentToken().tag) {
994 .opcode, .result_id_assign, .eof => true,
995 else => false,
996 };
997}
998
999fn expectToken(ass: *Assembler, tag: Token.Tag) !void {
1000 if (ass.eatToken(tag))
1001 return;
1002
1003 return ass.fail(ass.currentToken().start, "unexpected {s}, expected {s}", .{
1004 ass.currentToken().tag.name(),
1005 tag.name(),
1006 });
1007}
1008
1009fn eatToken(ass: *Assembler, tag: Token.Tag) bool {
1010 if (ass.testToken(tag)) {
1011 ass.current_token += 1;
1012 return true;
1013 }
1014 return false;
1015}
1016
1017fn testToken(ass: Assembler, tag: Token.Tag) bool {
1018 return ass.currentToken().tag == tag;
1019}
1020
1021fn currentToken(ass: Assembler) Token {
1022 return ass.tokens.items[ass.current_token];
1023}
1024
1025fn tokenText(ass: Assembler, tok: Token) []const u8 {
1026 return ass.src[tok.start..tok.end];
1027}
1028
1029/// Tokenize `ass.src` and put the tokens in `ass.tokens`.
1030/// Any errors encountered are appended to `ass.errors`.
1031fn tokenize(ass: *Assembler) !void {
1032 const gpa = ass.cg.gpa;
1033
1034 ass.tokens.clearRetainingCapacity();
1035
1036 var offset: u32 = 0;
1037 while (true) {
1038 const tok = try ass.nextToken(offset);
1039 // Resolve result-id assignment now.
1040 // NOTE: If the previous token wasn't a result-id, just ignore it,
1041 // we will catch it while parsing.
1042 if (tok.tag == .equals and ass.tokens.items[ass.tokens.items.len - 1].tag == .result_id) {
1043 ass.tokens.items[ass.tokens.items.len - 1].tag = .result_id_assign;
1044 }
1045 try ass.tokens.append(gpa, tok);
1046 if (tok.tag == .eof)
1047 break;
1048 offset = tok.end;
1049 }
1050}
1051
1052const Token = struct {
1053 tag: Tag,
1054 start: u32,
1055 end: u32,
1056
1057 const Tag = enum {
1058 /// Returned when there was no more input to match.
1059 eof,
1060 /// %identifier
1061 result_id,
1062 /// %identifier when appearing on the LHS of an equals sign.
1063 /// While not technically a token, its relatively easy to resolve
1064 /// this during lexical analysis and relieves a bunch of headaches
1065 /// during parsing.
1066 result_id_assign,
1067 /// Mask, int, or float. These are grouped together as some
1068 /// SPIR-V enumerants look a bit like integers as well (for example
1069 /// "3D"), and so it is easier to just interpret them as the expected
1070 /// type when resolving an instruction's operands.
1071 value,
1072 /// An enumerant that looks like an opcode, that is, OpXxxx.
1073 /// Not necessarily a *valid* opcode.
1074 opcode,
1075 /// String literals.
1076 /// Note, this token is also returned for unterminated
1077 /// strings. In this case the closing " is not present.
1078 string,
1079 /// |.
1080 pipe,
1081 /// =.
1082 equals,
1083 /// $identifier. This is used (for now) for constant values, like integers.
1084 /// These can be used in place of a normal `value`.
1085 placeholder,
1086
1087 fn name(tag: Tag) []const u8 {
1088 return switch (tag) {
1089 .eof => "<end of input>",
1090 .result_id => "<result-id>",
1091 .result_id_assign => "<assigned result-id>",
1092 .value => "<value>",
1093 .opcode => "<opcode>",
1094 .string => "<string literal>",
1095 .pipe => "'|'",
1096 .equals => "'='",
1097 .placeholder => "<placeholder>",
1098 };
1099 }
1100 };
1101};
1102
1103/// Retrieve the next token from the input. This function will assert
1104/// that the token is surrounded by whitespace if required, but will not
1105/// interpret the token yet.
1106/// NOTE: This function doesn't handle .result_id_assign - this is handled in tokenize().
1107fn nextToken(ass: *Assembler, start_offset: u32) !Token {
1108 // We generally separate the input into the following types:
1109 // - Whitespace. Generally ignored, but also used as delimiter for some
1110 // tokens.
1111 // - Values. This entails integers, floats, enums - anything that
1112 // consists of alphanumeric characters, delimited by whitespace.
1113 // - Result-IDs. This entails anything that consists of alphanumeric characters and _, and
1114 // starts with a %. In contrast to values, this entity can be checked for complete correctness
1115 // relatively easily here.
1116 // - Strings. This entails quote-delimited text such as "abc".
1117 // SPIR-V strings have only two escapes, \" and \\.
1118 // - Sigils, = and |. In this assembler, these are not required to have whitespace
1119 // around them (they act as delimiters) as they do in SPIRV-Tools.
1120
1121 var state: enum {
1122 start,
1123 value,
1124 result_id,
1125 string,
1126 string_end,
1127 escape,
1128 placeholder,
1129 } = .start;
1130 var token_start = start_offset;
1131 var offset = start_offset;
1132 var tag = Token.Tag.eof;
1133 while (offset < ass.src.len) : (offset += 1) {
1134 const c = ass.src[offset];
1135 switch (state) {
1136 .start => switch (c) {
1137 ' ', '\t', '\r', '\n' => token_start = offset + 1,
1138 '"' => {
1139 state = .string;
1140 tag = .string;
1141 },
1142 '%' => {
1143 state = .result_id;
1144 tag = .result_id;
1145 },
1146 '|' => {
1147 tag = .pipe;
1148 offset += 1;
1149 break;
1150 },
1151 '=' => {
1152 tag = .equals;
1153 offset += 1;
1154 break;
1155 },
1156 '$' => {
1157 state = .placeholder;
1158 tag = .placeholder;
1159 },
1160 else => {
1161 state = .value;
1162 tag = .value;
1163 },
1164 },
1165 .value => switch (c) {
1166 '"' => {
1167 try ass.addError(offset, "unexpected string literal", .{});
1168 // The user most likely just forgot a delimiter here - keep
1169 // the tag as value.
1170 break;
1171 },
1172 ' ', '\t', '\r', '\n', '=', '|' => break,
1173 else => {},
1174 },
1175 .result_id, .placeholder => switch (c) {
1176 '_', 'a'...'z', 'A'...'Z', '0'...'9' => {},
1177 ' ', '\t', '\r', '\n', '=', '|' => break,
1178 else => {
1179 try ass.addError(offset, "illegal character in result-id or placeholder", .{});
1180 // Again, probably a forgotten delimiter here.
1181 break;
1182 },
1183 },
1184 .string => switch (c) {
1185 '\\' => state = .escape,
1186 '"' => state = .string_end,
1187 else => {}, // Note, strings may include newlines
1188 },
1189 .string_end => switch (c) {
1190 ' ', '\t', '\r', '\n', '=', '|' => break,
1191 else => {
1192 try ass.addError(offset, "unexpected character after string literal", .{});
1193 // The token is still unmistakibly a string.
1194 break;
1195 },
1196 },
1197 // Escapes simply skip the next char.
1198 .escape => state = .string,
1199 }
1200 }
1201
1202 var tok: Token = .{
1203 .tag = tag,
1204 .start = token_start,
1205 .end = offset,
1206 };
1207
1208 switch (state) {
1209 .string, .escape => {
1210 try ass.addError(token_start, "unterminated string", .{});
1211 },
1212 .result_id => if (offset - token_start == 1) {
1213 try ass.addError(token_start, "result-id must have at least one name character", .{});
1214 },
1215 .value => {
1216 const text = ass.tokenText(tok);
1217 const prefix = "Op";
1218 const looks_like_opcode = text.len > prefix.len and
1219 std.mem.startsWith(u8, text, prefix) and
1220 std.ascii.isUpper(text[prefix.len]);
1221 if (looks_like_opcode)
1222 tok.tag = .opcode;
1223 },
1224 else => {},
1225 }
1226
1227 return tok;
1228}