1// Copyright (c) 2015-2020 The Khronos Group Inc.
2// Modifications Copyright (C) 2020 Advanced Micro Devices, Inc. All rights
3// reserved.
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17// This file contains a disassembler: It converts a SPIR-V binary
18// to text.
19
20#include "source/disassemble.h"
21
22#include <algorithm>
23#include <cassert>
24#include <cstring>
25#include <iomanip>
26#include <memory>
27#include <unordered_map>
28#include <utility>
29
30#include "source/assembly_grammar.h"
31#include "source/binary.h"
32#include "source/diagnostic.h"
33#include "source/ext_inst.h"
34#include "source/opcode.h"
35#include "source/parsed_operand.h"
36#include "source/print.h"
37#include "source/spirv_constant.h"
38#include "source/spirv_endian.h"
39#include "source/util/hex_float.h"
40#include "source/util/make_unique.h"
41#include "spirv-tools/libspirv.h"
42
43namespace spvtools {
44namespace {
45
46// A Disassembler instance converts a SPIR-V binary to its assembly
47// representation.
48class Disassembler {
49 public:
50 Disassembler(const AssemblyGrammar& grammar, uint32_t options,
51 NameMapper name_mapper)
52 : print_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_PRINT, options)),
53 text_(),
54 out_(print_ ? out_stream() : out_stream(text_)),
55 instruction_disassembler_(grammar, out_.get(), options, name_mapper),
56 header_(!spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_NO_HEADER, options)),
57 byte_offset_(0) {}
58
59 // Emits the assembly header for the module, and sets up internal state
60 // so subsequent callbacks can handle the cases where the entire module
61 // is either big-endian or little-endian.
62 spv_result_t HandleHeader(spv_endianness_t endian, uint32_t version,
63 uint32_t generator, uint32_t id_bound,
64 uint32_t schema);
65 // Emits the assembly text for the given instruction.
66 spv_result_t HandleInstruction(const spv_parsed_instruction_t& inst);
67
68 // If not printing, populates text_result with the accumulated text.
69 // Returns SPV_SUCCESS on success.
70 spv_result_t SaveTextResult(spv_text* text_result) const;
71
72 private:
73 const bool print_; // Should we also print to the standard output stream?
74 spv_endianness_t endian_; // The detected endianness of the binary.
75 std::stringstream text_; // Captures the text, if not printing.
76 out_stream out_; // The Output stream. Either to text_ or standard output.
77 disassemble::InstructionDisassembler instruction_disassembler_;
78 const bool header_; // Should we output header as the leading comment?
79 size_t byte_offset_; // The number of bytes processed so far.
80 bool inserted_decoration_space_ = false;
81 bool inserted_debug_space_ = false;
82 bool inserted_type_space_ = false;
83};
84
85spv_result_t Disassembler::HandleHeader(spv_endianness_t endian,
86 uint32_t version, uint32_t generator,
87 uint32_t id_bound, uint32_t schema) {
88 endian_ = endian;
89
90 if (header_) {
91 instruction_disassembler_.EmitHeaderSpirv();
92 instruction_disassembler_.EmitHeaderVersion(version);
93 instruction_disassembler_.EmitHeaderGenerator(generator);
94 instruction_disassembler_.EmitHeaderIdBound(id_bound);
95 instruction_disassembler_.EmitHeaderSchema(schema);
96 }
97
98 byte_offset_ = SPV_INDEX_INSTRUCTION * sizeof(uint32_t);
99
100 return SPV_SUCCESS;
101}
102
103spv_result_t Disassembler::HandleInstruction(
104 const spv_parsed_instruction_t& inst) {
105 instruction_disassembler_.EmitSectionComment(inst, inserted_decoration_space_,
106 inserted_debug_space_,
107 inserted_type_space_);
108
109 instruction_disassembler_.EmitInstruction(inst, byte_offset_);
110
111 byte_offset_ += inst.num_words * sizeof(uint32_t);
112
113 return SPV_SUCCESS;
114}
115
116spv_result_t Disassembler::SaveTextResult(spv_text* text_result) const {
117 if (!print_) {
118 size_t length = text_.str().size();
119 char* str = new char[length + 1];
120 if (!str) return SPV_ERROR_OUT_OF_MEMORY;
121 strncpy(str, text_.str().c_str(), length + 1);
122 spv_text text = new spv_text_t();
123 if (!text) {
124 delete[] str;
125 return SPV_ERROR_OUT_OF_MEMORY;
126 }
127 text->str = str;
128 text->length = length;
129 *text_result = text;
130 }
131 return SPV_SUCCESS;
132}
133
134spv_result_t DisassembleHeader(void* user_data, spv_endianness_t endian,
135 uint32_t /* magic */, uint32_t version,
136 uint32_t generator, uint32_t id_bound,
137 uint32_t schema) {
138 assert(user_data);
139 auto disassembler = static_cast<Disassembler*>(user_data);
140 return disassembler->HandleHeader(endian, version, generator, id_bound,
141 schema);
142}
143
144spv_result_t DisassembleInstruction(
145 void* user_data, const spv_parsed_instruction_t* parsed_instruction) {
146 assert(user_data);
147 auto disassembler = static_cast<Disassembler*>(user_data);
148 return disassembler->HandleInstruction(*parsed_instruction);
149}
150
151// Simple wrapper class to provide extra data necessary for targeted
152// instruction disassembly.
153class WrappedDisassembler {
154 public:
155 WrappedDisassembler(Disassembler* dis, const uint32_t* binary, size_t wc)
156 : disassembler_(dis), inst_binary_(binary), word_count_(wc) {}
157
158 Disassembler* disassembler() { return disassembler_; }
159 const uint32_t* inst_binary() const { return inst_binary_; }
160 size_t word_count() const { return word_count_; }
161
162 private:
163 Disassembler* disassembler_;
164 const uint32_t* inst_binary_;
165 const size_t word_count_;
166};
167
168spv_result_t DisassembleTargetHeader(void* user_data, spv_endianness_t endian,
169 uint32_t /* magic */, uint32_t version,
170 uint32_t generator, uint32_t id_bound,
171 uint32_t schema) {
172 assert(user_data);
173 auto wrapped = static_cast<WrappedDisassembler*>(user_data);
174 return wrapped->disassembler()->HandleHeader(endian, version, generator,
175 id_bound, schema);
176}
177
178spv_result_t DisassembleTargetInstruction(
179 void* user_data, const spv_parsed_instruction_t* parsed_instruction) {
180 assert(user_data);
181 auto wrapped = static_cast<WrappedDisassembler*>(user_data);
182 // Check if this is the instruction we want to disassemble.
183 if (wrapped->word_count() == parsed_instruction->num_words &&
184 std::equal(wrapped->inst_binary(),
185 wrapped->inst_binary() + wrapped->word_count(),
186 parsed_instruction->words)) {
187 // Found the target instruction. Disassemble it and signal that we should
188 // stop searching so we don't output the same instruction again.
189 if (auto error =
190 wrapped->disassembler()->HandleInstruction(*parsed_instruction))
191 return error;
192 return SPV_REQUESTED_TERMINATION;
193 }
194 return SPV_SUCCESS;
195}
196
197constexpr int kStandardIndent = 15;
198} // namespace
199
200namespace disassemble {
201InstructionDisassembler::InstructionDisassembler(const AssemblyGrammar& grammar,
202 std::ostream& stream,
203 uint32_t options,
204 NameMapper name_mapper)
205 : grammar_(grammar),
206 stream_(stream),
207 print_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_PRINT, options)),
208 color_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_COLOR, options)),
209 indent_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_INDENT, options)
210 ? kStandardIndent
211 : 0),
212 comment_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_COMMENT, options)),
213 show_byte_offset_(
214 spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_SHOW_BYTE_OFFSET, options)),
215 name_mapper_(std::move(name_mapper)) {}
216
217void InstructionDisassembler::EmitHeaderSpirv() { stream_ << "; SPIR-V\n"; }
218
219void InstructionDisassembler::EmitHeaderVersion(uint32_t version) {
220 stream_ << "; Version: " << SPV_SPIRV_VERSION_MAJOR_PART(version) << "."
221 << SPV_SPIRV_VERSION_MINOR_PART(version) << "\n";
222}
223
224void InstructionDisassembler::EmitHeaderGenerator(uint32_t generator) {
225 const char* generator_tool =
226 spvGeneratorStr(SPV_GENERATOR_TOOL_PART(generator));
227 stream_ << "; Generator: " << generator_tool;
228 // For unknown tools, print the numeric tool value.
229 if (0 == strcmp("Unknown", generator_tool)) {
230 stream_ << "(" << SPV_GENERATOR_TOOL_PART(generator) << ")";
231 }
232 // Print the miscellaneous part of the generator word on the same
233 // line as the tool name.
234 stream_ << "; " << SPV_GENERATOR_MISC_PART(generator) << "\n";
235}
236
237void InstructionDisassembler::EmitHeaderIdBound(uint32_t id_bound) {
238 stream_ << "; Bound: " << id_bound << "\n";
239}
240
241void InstructionDisassembler::EmitHeaderSchema(uint32_t schema) {
242 stream_ << "; Schema: " << schema << "\n";
243}
244
245void InstructionDisassembler::EmitInstruction(
246 const spv_parsed_instruction_t& inst, size_t inst_byte_offset) {
247 auto opcode = static_cast<SpvOp>(inst.opcode);
248
249 if (inst.result_id) {
250 SetBlue();
251 const std::string id_name = name_mapper_(inst.result_id);
252 if (indent_)
253 stream_ << std::setw(std::max(0, indent_ - 3 - int(id_name.size())));
254 stream_ << "%" << id_name;
255 ResetColor();
256 stream_ << " = ";
257 } else {
258 stream_ << std::string(indent_, ' ');
259 }
260
261 stream_ << "Op" << spvOpcodeString(opcode);
262
263 for (uint16_t i = 0; i < inst.num_operands; i++) {
264 const spv_operand_type_t type = inst.operands[i].type;
265 assert(type != SPV_OPERAND_TYPE_NONE);
266 if (type == SPV_OPERAND_TYPE_RESULT_ID) continue;
267 stream_ << " ";
268 EmitOperand(inst, i);
269 }
270
271 if (comment_ && opcode == SpvOpName) {
272 const spv_parsed_operand_t& operand = inst.operands[0];
273 const uint32_t word = inst.words[operand.offset];
274 stream_ << " ; id %" << word;
275 }
276
277 if (show_byte_offset_) {
278 SetGrey();
279 auto saved_flags = stream_.flags();
280 auto saved_fill = stream_.fill();
281 stream_ << " ; 0x" << std::setw(8) << std::hex << std::setfill('0')
282 << inst_byte_offset;
283 stream_.flags(saved_flags);
284 stream_.fill(saved_fill);
285 ResetColor();
286 }
287 stream_ << "\n";
288}
289
290void InstructionDisassembler::EmitSectionComment(
291 const spv_parsed_instruction_t& inst, bool& inserted_decoration_space,
292 bool& inserted_debug_space, bool& inserted_type_space) {
293 auto opcode = static_cast<SpvOp>(inst.opcode);
294 if (comment_ && opcode == SpvOpFunction) {
295 stream_ << std::endl;
296 stream_ << std::string(indent_, ' ');
297 stream_ << "; Function " << name_mapper_(inst.result_id) << std::endl;
298 }
299 if (comment_ && !inserted_decoration_space && spvOpcodeIsDecoration(opcode)) {
300 inserted_decoration_space = true;
301 stream_ << std::endl;
302 stream_ << std::string(indent_, ' ');
303 stream_ << "; Annotations" << std::endl;
304 }
305 if (comment_ && !inserted_debug_space && spvOpcodeIsDebug(opcode)) {
306 inserted_debug_space = true;
307 stream_ << std::endl;
308 stream_ << std::string(indent_, ' ');
309 stream_ << "; Debug Information" << std::endl;
310 }
311 if (comment_ && !inserted_type_space && spvOpcodeGeneratesType(opcode)) {
312 inserted_type_space = true;
313 stream_ << std::endl;
314 stream_ << std::string(indent_, ' ');
315 stream_ << "; Types, variables and constants" << std::endl;
316 }
317}
318
319void InstructionDisassembler::EmitOperand(const spv_parsed_instruction_t& inst,
320 const uint16_t operand_index) {
321 assert(operand_index < inst.num_operands);
322 const spv_parsed_operand_t& operand = inst.operands[operand_index];
323 const uint32_t word = inst.words[operand.offset];
324 switch (operand.type) {
325 case SPV_OPERAND_TYPE_RESULT_ID:
326 assert(false && "<result-id> is not supposed to be handled here");
327 SetBlue();
328 stream_ << "%" << name_mapper_(word);
329 break;
330 case SPV_OPERAND_TYPE_ID:
331 case SPV_OPERAND_TYPE_TYPE_ID:
332 case SPV_OPERAND_TYPE_SCOPE_ID:
333 case SPV_OPERAND_TYPE_MEMORY_SEMANTICS_ID:
334 SetYellow();
335 stream_ << "%" << name_mapper_(word);
336 break;
337 case SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER: {
338 spv_ext_inst_desc ext_inst;
339 SetRed();
340 if (grammar_.lookupExtInst(inst.ext_inst_type, word, &ext_inst) ==
341 SPV_SUCCESS) {
342 stream_ << ext_inst->name;
343 } else {
344 if (!spvExtInstIsNonSemantic(inst.ext_inst_type)) {
345 assert(false && "should have caught this earlier");
346 } else {
347 // for non-semantic instruction sets we can just print the number
348 stream_ << word;
349 }
350 }
351 } break;
352 case SPV_OPERAND_TYPE_SPEC_CONSTANT_OP_NUMBER: {
353 spv_opcode_desc opcode_desc;
354 if (grammar_.lookupOpcode(SpvOp(word), &opcode_desc))
355 assert(false && "should have caught this earlier");
356 SetRed();
357 stream_ << opcode_desc->name;
358 } break;
359 case SPV_OPERAND_TYPE_LITERAL_INTEGER:
360 case SPV_OPERAND_TYPE_TYPED_LITERAL_NUMBER: {
361 SetRed();
362 EmitNumericLiteral(&stream_, inst, operand);
363 ResetColor();
364 } break;
365 case SPV_OPERAND_TYPE_LITERAL_STRING: {
366 stream_ << "\"";
367 SetGreen();
368
369 std::string str = spvDecodeLiteralStringOperand(inst, operand_index);
370 for (char const& c : str) {
371 if (c == '"' || c == '\\') stream_ << '\\';
372 stream_ << c;
373 }
374 ResetColor();
375 stream_ << '"';
376 } break;
377 case SPV_OPERAND_TYPE_CAPABILITY:
378 case SPV_OPERAND_TYPE_SOURCE_LANGUAGE:
379 case SPV_OPERAND_TYPE_EXECUTION_MODEL:
380 case SPV_OPERAND_TYPE_ADDRESSING_MODEL:
381 case SPV_OPERAND_TYPE_MEMORY_MODEL:
382 case SPV_OPERAND_TYPE_EXECUTION_MODE:
383 case SPV_OPERAND_TYPE_STORAGE_CLASS:
384 case SPV_OPERAND_TYPE_DIMENSIONALITY:
385 case SPV_OPERAND_TYPE_SAMPLER_ADDRESSING_MODE:
386 case SPV_OPERAND_TYPE_SAMPLER_FILTER_MODE:
387 case SPV_OPERAND_TYPE_SAMPLER_IMAGE_FORMAT:
388 case SPV_OPERAND_TYPE_FP_ROUNDING_MODE:
389 case SPV_OPERAND_TYPE_LINKAGE_TYPE:
390 case SPV_OPERAND_TYPE_ACCESS_QUALIFIER:
391 case SPV_OPERAND_TYPE_FUNCTION_PARAMETER_ATTRIBUTE:
392 case SPV_OPERAND_TYPE_DECORATION:
393 case SPV_OPERAND_TYPE_BUILT_IN:
394 case SPV_OPERAND_TYPE_GROUP_OPERATION:
395 case SPV_OPERAND_TYPE_KERNEL_ENQ_FLAGS:
396 case SPV_OPERAND_TYPE_KERNEL_PROFILING_INFO:
397 case SPV_OPERAND_TYPE_RAY_FLAGS:
398 case SPV_OPERAND_TYPE_RAY_QUERY_INTERSECTION:
399 case SPV_OPERAND_TYPE_RAY_QUERY_COMMITTED_INTERSECTION_TYPE:
400 case SPV_OPERAND_TYPE_RAY_QUERY_CANDIDATE_INTERSECTION_TYPE:
401 case SPV_OPERAND_TYPE_DEBUG_BASE_TYPE_ATTRIBUTE_ENCODING:
402 case SPV_OPERAND_TYPE_DEBUG_COMPOSITE_TYPE:
403 case SPV_OPERAND_TYPE_DEBUG_TYPE_QUALIFIER:
404 case SPV_OPERAND_TYPE_DEBUG_OPERATION:
405 case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_BASE_TYPE_ATTRIBUTE_ENCODING:
406 case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_COMPOSITE_TYPE:
407 case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_TYPE_QUALIFIER:
408 case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_OPERATION:
409 case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_IMPORTED_ENTITY:
410 case SPV_OPERAND_TYPE_FPDENORM_MODE:
411 case SPV_OPERAND_TYPE_FPOPERATION_MODE:
412 case SPV_OPERAND_TYPE_QUANTIZATION_MODES:
413 case SPV_OPERAND_TYPE_OVERFLOW_MODES: {
414 spv_operand_desc entry;
415 if (grammar_.lookupOperand(operand.type, word, &entry))
416 assert(false && "should have caught this earlier");
417 stream_ << entry->name;
418 } break;
419 case SPV_OPERAND_TYPE_FP_FAST_MATH_MODE:
420 case SPV_OPERAND_TYPE_FUNCTION_CONTROL:
421 case SPV_OPERAND_TYPE_LOOP_CONTROL:
422 case SPV_OPERAND_TYPE_IMAGE:
423 case SPV_OPERAND_TYPE_MEMORY_ACCESS:
424 case SPV_OPERAND_TYPE_SELECTION_CONTROL:
425 case SPV_OPERAND_TYPE_DEBUG_INFO_FLAGS:
426 case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_INFO_FLAGS:
427 EmitMaskOperand(operand.type, word);
428 break;
429 default:
430 if (spvOperandIsConcreteMask(operand.type)) {
431 EmitMaskOperand(operand.type, word);
432 } else if (spvOperandIsConcrete(operand.type)) {
433 spv_operand_desc entry;
434 if (grammar_.lookupOperand(operand.type, word, &entry))
435 assert(false && "should have caught this earlier");
436 stream_ << entry->name;
437 } else {
438 assert(false && "unhandled or invalid case");
439 }
440 break;
441 }
442 ResetColor();
443}
444
445void InstructionDisassembler::EmitMaskOperand(const spv_operand_type_t type,
446 const uint32_t word) {
447 // Scan the mask from least significant bit to most significant bit. For each
448 // set bit, emit the name of that bit. Separate multiple names with '|'.
449 uint32_t remaining_word = word;
450 uint32_t mask;
451 int num_emitted = 0;
452 for (mask = 1; remaining_word; mask <<= 1) {
453 if (remaining_word & mask) {
454 remaining_word ^= mask;
455 spv_operand_desc entry;
456 if (grammar_.lookupOperand(type, mask, &entry))
457 assert(false && "should have caught this earlier");
458 if (num_emitted) stream_ << "|";
459 stream_ << entry->name;
460 num_emitted++;
461 }
462 }
463 if (!num_emitted) {
464 // An operand value of 0 was provided, so represent it by the name
465 // of the 0 value. In many cases, that's "None".
466 spv_operand_desc entry;
467 if (SPV_SUCCESS == grammar_.lookupOperand(type, 0, &entry))
468 stream_ << entry->name;
469 }
470}
471
472void InstructionDisassembler::ResetColor() {
473 if (color_) stream_ << spvtools::clr::reset{print_};
474}
475void InstructionDisassembler::SetGrey() {
476 if (color_) stream_ << spvtools::clr::grey{print_};
477}
478void InstructionDisassembler::SetBlue() {
479 if (color_) stream_ << spvtools::clr::blue{print_};
480}
481void InstructionDisassembler::SetYellow() {
482 if (color_) stream_ << spvtools::clr::yellow{print_};
483}
484void InstructionDisassembler::SetRed() {
485 if (color_) stream_ << spvtools::clr::red{print_};
486}
487void InstructionDisassembler::SetGreen() {
488 if (color_) stream_ << spvtools::clr::green{print_};
489}
490} // namespace disassemble
491
492std::string spvInstructionBinaryToText(const spv_target_env env,
493 const uint32_t* instCode,
494 const size_t instWordCount,
495 const uint32_t* code,
496 const size_t wordCount,
497 const uint32_t options) {
498 spv_context context = spvContextCreate(env);
499 const AssemblyGrammar grammar(context);
500 if (!grammar.isValid()) {
501 spvContextDestroy(context);
502 return "";
503 }
504
505 // Generate friendly names for Ids if requested.
506 std::unique_ptr<FriendlyNameMapper> friendly_mapper;
507 NameMapper name_mapper = GetTrivialNameMapper();
508 if (options & SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES) {
509 friendly_mapper = MakeUnique<FriendlyNameMapper>(context, code, wordCount);
510 name_mapper = friendly_mapper->GetNameMapper();
511 }
512
513 // Now disassemble!
514 Disassembler disassembler(grammar, options, name_mapper);
515 WrappedDisassembler wrapped(&disassembler, instCode, instWordCount);
516 spvBinaryParse(context, &wrapped, code, wordCount, DisassembleTargetHeader,
517 DisassembleTargetInstruction, nullptr);
518
519 spv_text text = nullptr;
520 std::string output;
521 if (disassembler.SaveTextResult(&text) == SPV_SUCCESS) {
522 output.assign(text->str, text->str + text->length);
523 // Drop trailing newline characters.
524 while (!output.empty() && output.back() == '\n') output.pop_back();
525 }
526 spvTextDestroy(text);
527 spvContextDestroy(context);
528
529 return output;
530}
531} // namespace spvtools
532
533spv_result_t spvBinaryToText(const spv_const_context context,
534 const uint32_t* code, const size_t wordCount,
535 const uint32_t options, spv_text* pText,
536 spv_diagnostic* pDiagnostic) {
537 spv_context_t hijack_context = *context;
538 if (pDiagnostic) {
539 *pDiagnostic = nullptr;
540 spvtools::UseDiagnosticAsMessageConsumer(&hijack_context, pDiagnostic);
541 }
542
543 const spvtools::AssemblyGrammar grammar(&hijack_context);
544 if (!grammar.isValid()) return SPV_ERROR_INVALID_TABLE;
545
546 // Generate friendly names for Ids if requested.
547 std::unique_ptr<spvtools::FriendlyNameMapper> friendly_mapper;
548 spvtools::NameMapper name_mapper = spvtools::GetTrivialNameMapper();
549 if (options & SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES) {
550 friendly_mapper = spvtools::MakeUnique<spvtools::FriendlyNameMapper>(
551 &hijack_context, code, wordCount);
552 name_mapper = friendly_mapper->GetNameMapper();
553 }
554
555 // Now disassemble!
556 spvtools::Disassembler disassembler(grammar, options, name_mapper);
557 if (auto error =
558 spvBinaryParse(&hijack_context, &disassembler, code, wordCount,
559 spvtools::DisassembleHeader,
560 spvtools::DisassembleInstruction, pDiagnostic)) {
561 return error;
562 }
563
564 return disassembler.SaveTextResult(pText);
565}
566