spirv-dis: Add --nested-indent and --reorder-blocks (#5671)

With --nested-indent, the SPIR-V blocks are nested according to the
structured control flow.  Each OpLabel is nested that much with the
contents of the block nested a little more.  The blocks are separated by
a blank line for better visualization.

With --reorder-blocks, the SPIR-V blocks are reordered according to the
structured control flow.  This is particularly useful with
--nested-indent.

Note that with --nested-indent, the disassembly does not exactly show
the binary as-is, and the instructions may be reordered.
This commit is contained in:
Shahbaz Youssefi 2024-06-17 09:54:18 -04:00 committed by GitHub
parent bc28ac7c19
commit 7564e142d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 2401 additions and 291 deletions

View File

@ -382,6 +382,11 @@ typedef enum spv_binary_to_text_options_t {
SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES = SPV_BIT(6),
// Add some comments to the generated assembly
SPV_BINARY_TO_TEXT_OPTION_COMMENT = SPV_BIT(7),
// Use nested indentation for more readable SPIR-V
SPV_BINARY_TO_TEXT_OPTION_NESTED_INDENT = SPV_BIT(8),
// Reorder blocks to match the structured control flow of SPIR-V to increase
// readability.
SPV_BINARY_TO_TEXT_OPTION_REORDER_BLOCKS = SPV_BIT(9),
SPV_FORCE_32_BIT_ENUM(spv_binary_to_text_options_t)
} spv_binary_to_text_options_t;

View File

@ -24,6 +24,8 @@
#include <cstring>
#include <iomanip>
#include <memory>
#include <set>
#include <stack>
#include <unordered_map>
#include <utility>
@ -43,6 +45,70 @@
namespace spvtools {
namespace {
// Indices to ControlFlowGraph's list of blocks from one block to its successors
struct BlockSuccessors {
// Merge block in OpLoopMerge and OpSelectionMerge
uint32_t merge_block_id = 0;
// The continue block in OpLoopMerge
uint32_t continue_block_id = 0;
// The true and false blocks in OpBranchConditional
uint32_t true_block_id = 0;
uint32_t false_block_id = 0;
// The body block of a loop, as specified by OpBranch after a merge
// instruction
uint32_t body_block_id = 0;
// The same-nesting-level block that follows this one, indicated by an
// OpBranch with no merge instruction.
uint32_t next_block_id = 0;
// The cases (including default) of an OpSwitch
std::vector<uint32_t> case_block_ids;
};
class ParsedInstruction {
public:
ParsedInstruction(const spv_parsed_instruction_t* instruction) {
// Make a copy of the parsed instruction, including stable memory for its
// operands.
instruction_ = *instruction;
operands_ =
std::make_unique<spv_parsed_operand_t[]>(instruction->num_operands);
memcpy(operands_.get(), instruction->operands,
instruction->num_operands * sizeof(*instruction->operands));
instruction_.operands = operands_.get();
}
const spv_parsed_instruction_t* get() const { return &instruction_; }
private:
spv_parsed_instruction_t instruction_;
std::unique_ptr<spv_parsed_operand_t[]> operands_;
};
// One block in the CFG
struct SingleBlock {
// The byte offset in the SPIR-V where the block starts. Used for printing in
// a comment.
size_t byte_offset;
// Block instructions
std::vector<ParsedInstruction> instructions;
// Successors of this block
BlockSuccessors successors;
// The nesting level for this block.
uint32_t nest_level = 0;
bool nest_level_assigned = false;
// Whether the block was reachable
bool reachable = false;
};
// CFG for one function
struct ControlFlowGraph {
std::vector<SingleBlock> blocks;
};
// A Disassembler instance converts a SPIR-V binary to its assembly
// representation.
class Disassembler {
@ -50,6 +116,10 @@ class Disassembler {
Disassembler(const AssemblyGrammar& grammar, uint32_t options,
NameMapper name_mapper)
: print_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_PRINT, options)),
nested_indent_(
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_NESTED_INDENT, options)),
reorder_blocks_(
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_REORDER_BLOCKS, options)),
text_(),
out_(print_ ? out_stream() : out_stream(text_)),
instruction_disassembler_(grammar, out_.get(), options, name_mapper),
@ -70,7 +140,13 @@ class Disassembler {
spv_result_t SaveTextResult(spv_text* text_result) const;
private:
void EmitCFG();
const bool print_; // Should we also print to the standard output stream?
const bool nested_indent_; // Should the blocks be indented according to the
// control flow structure?
const bool
reorder_blocks_; // Should the blocks be reordered for readability?
spv_endianness_t endian_; // The detected endianness of the binary.
std::stringstream text_; // Captures the text, if not printing.
out_stream out_; // The Output stream. Either to text_ or standard output.
@ -80,6 +156,9 @@ class Disassembler {
bool inserted_decoration_space_ = false;
bool inserted_debug_space_ = false;
bool inserted_type_space_ = false;
// The CFG for the current function
ControlFlowGraph current_function_cfg_;
};
spv_result_t Disassembler::HandleHeader(spv_endianness_t endian,
@ -106,13 +185,336 @@ spv_result_t Disassembler::HandleInstruction(
inserted_debug_space_,
inserted_type_space_);
instruction_disassembler_.EmitInstruction(inst, byte_offset_);
// When nesting needs to be calculated or when the blocks are reordered, we
// have to have the full picture of the CFG first. Defer processing of the
// instructions until the entire function is visited. This is not done
// without those options (even if simpler) to improve debuggability; for
// example to be able to see whatever is parsed so far even if there is a
// parse error.
if (nested_indent_ || reorder_blocks_) {
switch (static_cast<spv::Op>(inst.opcode)) {
case spv::Op::OpLabel: {
// Add a new block to the CFG
SingleBlock new_block;
new_block.byte_offset = byte_offset_;
new_block.instructions.emplace_back(&inst);
current_function_cfg_.blocks.push_back(std::move(new_block));
break;
}
case spv::Op::OpFunctionEnd:
// Process the CFG and output the instructions
EmitCFG();
// Output OpFunctionEnd itself too
[[fallthrough]];
default:
if (!current_function_cfg_.blocks.empty()) {
// If in a function, stash the instruction for later.
current_function_cfg_.blocks.back().instructions.emplace_back(&inst);
} else {
// Otherwise emit the instruction right away.
instruction_disassembler_.EmitInstruction(inst, byte_offset_);
}
break;
}
} else {
instruction_disassembler_.EmitInstruction(inst, byte_offset_);
}
byte_offset_ += inst.num_words * sizeof(uint32_t);
return SPV_SUCCESS;
}
// Helper to get the operand of an instruction as an id.
uint32_t GetOperand(const spv_parsed_instruction_t* instruction,
uint32_t operand) {
return instruction->words[instruction->operands[operand].offset];
}
std::unordered_map<uint32_t, uint32_t> BuildControlFlowGraph(
ControlFlowGraph& cfg) {
std::unordered_map<uint32_t, uint32_t> id_to_index;
for (size_t index = 0; index < cfg.blocks.size(); ++index) {
SingleBlock& block = cfg.blocks[index];
// For future use, build the ID->index map
assert(static_cast<spv::Op>(block.instructions[0].get()->opcode) ==
spv::Op::OpLabel);
const uint32_t id = block.instructions[0].get()->result_id;
id_to_index[id] = static_cast<uint32_t>(index);
// Look for a merge instruction first. The function of OpBranch depends on
// that.
if (block.instructions.size() >= 3) {
const spv_parsed_instruction_t* maybe_merge =
block.instructions[block.instructions.size() - 2].get();
switch (static_cast<spv::Op>(maybe_merge->opcode)) {
case spv::Op::OpLoopMerge:
block.successors.merge_block_id = GetOperand(maybe_merge, 0);
block.successors.continue_block_id = GetOperand(maybe_merge, 1);
break;
case spv::Op::OpSelectionMerge:
block.successors.merge_block_id = GetOperand(maybe_merge, 0);
break;
default:
break;
}
}
// Then look at the last instruction; it must be a branch
assert(block.instructions.size() >= 2);
const spv_parsed_instruction_t* branch = block.instructions.back().get();
switch (static_cast<spv::Op>(branch->opcode)) {
case spv::Op::OpBranch:
if (block.successors.merge_block_id != 0) {
block.successors.body_block_id = GetOperand(branch, 0);
} else {
block.successors.next_block_id = GetOperand(branch, 0);
}
break;
case spv::Op::OpBranchConditional:
block.successors.true_block_id = GetOperand(branch, 1);
block.successors.false_block_id = GetOperand(branch, 2);
break;
case spv::Op::OpSwitch:
for (uint32_t case_index = 1; case_index < branch->num_operands;
case_index += 2) {
block.successors.case_block_ids.push_back(
GetOperand(branch, case_index));
}
break;
default:
break;
}
}
return id_to_index;
}
// Helper to deal with nesting and non-existing ids / previously-assigned
// levels. It assigns a given nesting level `level` to the block identified by
// `id` (unless that block already has a nesting level assigned).
void Nest(ControlFlowGraph& cfg,
const std::unordered_map<uint32_t, uint32_t>& id_to_index,
uint32_t id, uint32_t level) {
if (id == 0) {
return;
}
const uint32_t block_index = id_to_index.at(id);
SingleBlock& block = cfg.blocks[block_index];
if (!block.nest_level_assigned) {
block.nest_level = level;
block.nest_level_assigned = true;
}
}
// For a given block, assign nesting level to its successors.
void NestSuccessors(ControlFlowGraph& cfg, const SingleBlock& block,
const std::unordered_map<uint32_t, uint32_t>& id_to_index) {
assert(block.nest_level_assigned);
// Nest loops as such:
//
// %loop = OpLabel
// OpLoopMerge %merge %cont ...
// OpBranch %body
// %body = OpLabel
// Op...
// %cont = OpLabel
// Op...
// %merge = OpLabel
// Op...
//
// Nest conditional branches as such:
//
// %header = OpLabel
// OpSelectionMerge %merge ...
// OpBranchConditional ... %true %false
// %true = OpLabel
// Op...
// %false = OpLabel
// Op...
// %merge = OpLabel
// Op...
//
// Nest switch/case as such:
//
// %header = OpLabel
// OpSelectionMerge %merge ...
// OpSwitch ... %default ... %case0 ... %case1 ...
// %default = OpLabel
// Op...
// %case0 = OpLabel
// Op...
// %case1 = OpLabel
// Op...
// ...
// %merge = OpLabel
// Op...
//
// The following can be observed:
//
// - In all cases, the merge block has the same nesting as this block
// - The continue block of loops is nested 1 level deeper
// - The body/branches/cases are nested 2 levels deeper
//
// Back branches to the header block, branches to the merge block, etc
// are correctly handled by processing the header block first (that is
// _this_ block, already processed), then following the above rules
// (in the same order) for any block that is not already processed.
Nest(cfg, id_to_index, block.successors.merge_block_id, block.nest_level);
Nest(cfg, id_to_index, block.successors.continue_block_id,
block.nest_level + 1);
Nest(cfg, id_to_index, block.successors.true_block_id, block.nest_level + 2);
Nest(cfg, id_to_index, block.successors.false_block_id, block.nest_level + 2);
Nest(cfg, id_to_index, block.successors.body_block_id, block.nest_level + 2);
Nest(cfg, id_to_index, block.successors.next_block_id, block.nest_level);
for (uint32_t case_block_id : block.successors.case_block_ids) {
Nest(cfg, id_to_index, case_block_id, block.nest_level + 2);
}
}
struct StackEntry {
// The index of the block (in ControlFlowGraph::blocks) to process.
uint32_t block_index;
// Whether this is the pre or post visit of the block. Because a post-visit
// traversal is needed, the same block is pushed back on the stack on
// pre-visit so it can be visited again on post-visit.
bool post_visit = false;
};
// Helper to deal with DFS traversal and non-existing ids
void VisitSuccesor(std::stack<StackEntry>* dfs_stack,
const std::unordered_map<uint32_t, uint32_t>& id_to_index,
uint32_t id) {
if (id != 0) {
dfs_stack->push({id_to_index.at(id), false});
}
}
// Given the control flow graph, calculates and returns the reverse post-order
// ordering of the blocks. The blocks are then disassembled in that order for
// readability.
std::vector<uint32_t> OrderBlocks(
ControlFlowGraph& cfg,
const std::unordered_map<uint32_t, uint32_t>& id_to_index) {
std::vector<uint32_t> post_order;
// Nest level of a function's first block is 0.
cfg.blocks[0].nest_level = 0;
cfg.blocks[0].nest_level_assigned = true;
// Stack of block indices as they are visited.
std::stack<StackEntry> dfs_stack;
dfs_stack.push({0, false});
std::set<uint32_t> visited;
while (!dfs_stack.empty()) {
const uint32_t block_index = dfs_stack.top().block_index;
const bool post_visit = dfs_stack.top().post_visit;
dfs_stack.pop();
// If this is the second time the block is visited, that's the post-order
// visit.
if (post_visit) {
post_order.push_back(block_index);
continue;
}
// If already visited, another path got to it first (like a case
// fallthrough), avoid reprocessing it.
if (visited.count(block_index) > 0) {
continue;
}
visited.insert(block_index);
// Push it back in the stack for post-order visit
dfs_stack.push({block_index, true});
SingleBlock& block = cfg.blocks[block_index];
// Assign nest levels of successors right away. The successors are either
// nested under this block, or are back or forward edges to blocks outside
// this nesting level (no farther than the merge block), whose nesting
// levels are already assigned before this block is visited.
NestSuccessors(cfg, block, id_to_index);
block.reachable = true;
// The post-order visit yields the order in which the blocks are naturally
// ordered _backwards_. So blocks to be ordered last should be visited
// first. In other words, they should be pushed to the DFS stack last.
VisitSuccesor(&dfs_stack, id_to_index, block.successors.true_block_id);
VisitSuccesor(&dfs_stack, id_to_index, block.successors.false_block_id);
VisitSuccesor(&dfs_stack, id_to_index, block.successors.body_block_id);
VisitSuccesor(&dfs_stack, id_to_index, block.successors.next_block_id);
for (uint32_t case_block_id : block.successors.case_block_ids) {
VisitSuccesor(&dfs_stack, id_to_index, case_block_id);
}
VisitSuccesor(&dfs_stack, id_to_index, block.successors.continue_block_id);
VisitSuccesor(&dfs_stack, id_to_index, block.successors.merge_block_id);
}
std::vector<uint32_t> order(post_order.rbegin(), post_order.rend());
// Finally, dump all unreachable blocks at the end
for (size_t index = 0; index < cfg.blocks.size(); ++index) {
SingleBlock& block = cfg.blocks[index];
if (!block.reachable) {
order.push_back(static_cast<uint32_t>(index));
block.nest_level = 0;
block.nest_level_assigned = true;
}
}
return order;
}
void Disassembler::EmitCFG() {
// Build the CFG edges. At the same time, build an ID->block index map to
// simplify building the CFG edges.
const std::unordered_map<uint32_t, uint32_t> id_to_index =
BuildControlFlowGraph(current_function_cfg_);
// Walk the CFG in reverse post-order to find the best ordering of blocks for
// presentation
std::vector<uint32_t> block_order =
OrderBlocks(current_function_cfg_, id_to_index);
assert(block_order.size() == current_function_cfg_.blocks.size());
// Walk the CFG either in block order or input order based on whether the
// reorder_blocks_ option is given.
for (uint32_t index = 0; index < current_function_cfg_.blocks.size();
++index) {
const uint32_t block_index = reorder_blocks_ ? block_order[index] : index;
const SingleBlock& block = current_function_cfg_.blocks[block_index];
// Emit instructions for this block
size_t byte_offset = block.byte_offset;
assert(block.nest_level_assigned);
for (const ParsedInstruction& inst : block.instructions) {
instruction_disassembler_.EmitInstructionInBlock(*inst.get(), byte_offset,
block.nest_level);
byte_offset += inst.get()->num_words * sizeof(uint32_t);
}
}
current_function_cfg_.blocks.clear();
}
spv_result_t Disassembler::SaveTextResult(spv_text* text_result) const {
if (!print_) {
size_t length = text_.str().size();
@ -214,6 +616,8 @@ uint32_t GetLineLengthWithoutColor(const std::string line) {
}
constexpr int kStandardIndent = 15;
constexpr int kBlockNestIndent = 2;
constexpr int kBlockBodyIndentOffset = 2;
constexpr uint32_t kCommentColumn = 50;
} // namespace
@ -229,6 +633,8 @@ InstructionDisassembler::InstructionDisassembler(const AssemblyGrammar& grammar,
indent_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_INDENT, options)
? kStandardIndent
: 0),
nested_indent_(
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_NESTED_INDENT, options)),
comment_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_COMMENT, options)),
show_byte_offset_(
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_SHOW_BYTE_OFFSET, options)),
@ -265,12 +671,29 @@ void InstructionDisassembler::EmitHeaderSchema(uint32_t schema) {
void InstructionDisassembler::EmitInstruction(
const spv_parsed_instruction_t& inst, size_t inst_byte_offset) {
EmitInstructionImpl(inst, inst_byte_offset, 0, false);
}
void InstructionDisassembler::EmitInstructionInBlock(
const spv_parsed_instruction_t& inst, size_t inst_byte_offset,
uint32_t block_indent) {
EmitInstructionImpl(inst, inst_byte_offset, block_indent, true);
}
void InstructionDisassembler::EmitInstructionImpl(
const spv_parsed_instruction_t& inst, size_t inst_byte_offset,
uint32_t block_indent, bool is_in_block) {
auto opcode = static_cast<spv::Op>(inst.opcode);
// To better align the comments (if any), write the instruction to a line
// first so its length can be readily available.
std::ostringstream line;
if (nested_indent_ && opcode == spv::Op::OpLabel) {
// Separate the blocks by an empty line to make them easier to separate
stream_ << std::endl;
}
if (inst.result_id) {
SetBlue();
const std::string id_name = name_mapper_(inst.result_id);
@ -283,6 +706,17 @@ void InstructionDisassembler::EmitInstruction(
line << std::string(indent_, ' ');
}
if (nested_indent_ && is_in_block) {
// Output OpLabel at the specified nest level, and instructions inside
// blocks nested a little more.
uint32_t indent = block_indent;
bool body_indent = opcode != spv::Op::OpLabel;
line << std::string(
indent * kBlockNestIndent + (body_indent ? kBlockBodyIndentOffset : 0),
' ');
}
line << "Op" << spvOpcodeString(opcode);
for (uint16_t i = 0; i < inst.num_operands; i++) {
@ -386,6 +820,11 @@ void InstructionDisassembler::EmitSectionComment(
auto opcode = static_cast<spv::Op>(inst.opcode);
if (comment_ && opcode == spv::Op::OpFunction) {
stream_ << std::endl;
if (nested_indent_) {
// Double the empty lines between Function sections since nested_indent_
// also separates blocks by a blank.
stream_ << std::endl;
}
stream_ << std::string(indent_, ' ');
stream_ << "; Function " << name_mapper_(inst.result_id) << std::endl;
}

View File

@ -58,6 +58,11 @@ class InstructionDisassembler {
// Emits the assembly text for the given instruction.
void EmitInstruction(const spv_parsed_instruction_t& inst,
size_t inst_byte_offset);
// Same as EmitInstruction, but only for block instructions (including
// OpLabel) and useful for nested indentation. If nested indentation is not
// desired, EmitInstruction can still be used for block instructions.
void EmitInstructionInBlock(const spv_parsed_instruction_t& inst,
size_t inst_byte_offset, uint32_t block_indent);
// Emits a comment between different sections of the module.
void EmitSectionComment(const spv_parsed_instruction_t& inst,
@ -82,6 +87,10 @@ class InstructionDisassembler {
void SetRed(std::ostream& stream) const;
void SetGreen(std::ostream& stream) const;
void EmitInstructionImpl(const spv_parsed_instruction_t& inst,
size_t inst_byte_offset, uint32_t block_indent,
bool is_in_block);
// Emits an operand for the given instruction, where the instruction
// is at offset words from the start of the binary.
void EmitOperand(std::ostream& stream, const spv_parsed_instruction_t& inst,
@ -97,10 +106,11 @@ class InstructionDisassembler {
const spvtools::AssemblyGrammar& grammar_;
std::ostream& stream_;
const bool print_; // Should we also print to the standard output stream?
const bool color_; // Should we print in colour?
const int indent_; // How much to indent. 0 means don't indent
const int comment_; // Should we comment the source
const bool print_; // Should we also print to the standard output stream?
const bool color_; // Should we print in colour?
const int indent_; // How much to indent. 0 means don't indent
const bool nested_indent_; // Whether indentation should indicate nesting
const int comment_; // Should we comment the source
const bool show_byte_offset_; // Should we print byte offset, in hex?
spvtools::NameMapper name_mapper_;

View File

@ -24,7 +24,7 @@
namespace spvtools {
// Builds an Module returns the owning IRContext from the given SPIR-V
// Builds a Module and returns the owning IRContext from the given SPIR-V
// |binary|. |size| specifies number of words in |binary|. The |binary| will be
// decoded according to the given target |env|. Returns nullptr if errors occur
// and sends the errors to |consumer|. When |extra_line_tracking| is true,
@ -41,7 +41,7 @@ std::unique_ptr<opt::IRContext> BuildModule(spv_target_env env,
const uint32_t* binary,
size_t size);
// Builds an Module and returns the owning IRContext from the given
// Builds a Module and returns the owning IRContext from the given
// SPIR-V assembly |text|. The |text| will be encoded according to the given
// target |env|. Returns nullptr if errors occur and sends the errors to
// |consumer|.

View File

@ -33,6 +33,7 @@ TEST_P(RoundTripLiteralsTest, Sample) {
for (bool endian_swap : kSwapEndians) {
EXPECT_THAT(
EncodeAndDecodeSuccessfully(GetParam(), SPV_BINARY_TO_TEXT_OPTION_NONE,
SPV_TEXT_TO_BINARY_OPTION_NONE,
SPV_ENV_UNIVERSAL_1_0, endian_swap),
Eq(GetParam()));
}
@ -68,6 +69,7 @@ TEST_P(RoundTripSpecialCaseLiteralsTest, Sample) {
for (bool endian_swap : kSwapEndians) {
EXPECT_THAT(EncodeAndDecodeSuccessfully(std::get<0>(GetParam()),
SPV_BINARY_TO_TEXT_OPTION_NONE,
SPV_TEXT_TO_BINARY_OPTION_NONE,
SPV_ENV_UNIVERSAL_1_0, endian_swap),
Eq(std::get<1>(GetParam())));
}

File diff suppressed because it is too large Load Diff

View File

@ -41,8 +41,7 @@ TEST_F(NonSemanticRoundTripTest, NonSemanticInsts) {
%8 = OpExtInstImport "NonSemantic.Testing.AnotherUnknownExtInstSet"
%9 = OpExtInst %4 %8 613874321 %7 %5 %6
)";
std::string disassembly = EncodeAndDecodeSuccessfully(
spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_ENV_UNIVERSAL_1_0);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}

View File

@ -111,13 +111,15 @@ class TextToBinaryTestBase : public T {
std::string EncodeAndDecodeSuccessfully(
const std::string& txt,
uint32_t disassemble_options = SPV_BINARY_TO_TEXT_OPTION_NONE,
uint32_t assemble_options = SPV_TEXT_TO_BINARY_OPTION_NONE,
spv_target_env env = SPV_ENV_UNIVERSAL_1_0, bool flip_words = false) {
DestroyBinary();
DestroyDiagnostic();
ScopedContext context(env);
disassemble_options |= SPV_BINARY_TO_TEXT_OPTION_NO_HEADER;
spv_result_t error = spvTextToBinary(context.context, txt.c_str(),
txt.size(), &binary, &diagnostic);
spv_result_t error =
spvTextToBinaryWithOptions(context.context, txt.c_str(), txt.size(),
assemble_options, &binary, &diagnostic);
if (error) {
spvDiagnosticPrint(diagnostic);
spvDiagnosticDestroy(diagnostic);

View File

@ -55,10 +55,10 @@ TEST_P(OpDecorateSimpleTest, AnySimpleDecoration) {
{1, uint32_t(std::get<1>(GetParam()).value())},
std::get<1>(GetParam()).operands())));
// Also check disassembly.
EXPECT_THAT(
EncodeAndDecodeSuccessfully(input.str(), SPV_BINARY_TO_TEXT_OPTION_NONE,
std::get<0>(GetParam())),
Eq(input.str()));
EXPECT_THAT(EncodeAndDecodeSuccessfully(
input.str(), SPV_BINARY_TO_TEXT_OPTION_NONE,
SPV_TEXT_TO_BINARY_OPTION_NONE, std::get<0>(GetParam())),
Eq(input.str()));
}
// Like above, but parameters to the decoration are IDs.
@ -78,10 +78,10 @@ TEST_P(OpDecorateSimpleIdTest, AnySimpleDecoration) {
{1, uint32_t(std::get<1>(GetParam()).value())},
std::get<1>(GetParam()).operands())));
// Also check disassembly.
EXPECT_THAT(
EncodeAndDecodeSuccessfully(input.str(), SPV_BINARY_TO_TEXT_OPTION_NONE,
std::get<0>(GetParam())),
Eq(input.str()));
EXPECT_THAT(EncodeAndDecodeSuccessfully(
input.str(), SPV_BINARY_TO_TEXT_OPTION_NONE,
SPV_TEXT_TO_BINARY_OPTION_NONE, std::get<0>(GetParam())),
Eq(input.str()));
}
#define CASE(NAME) spv::Decoration::NAME, #NAME
@ -460,10 +460,10 @@ TEST_P(OpMemberDecorateSimpleTest, AnySimpleDecoration) {
{1, 42, uint32_t(std::get<1>(GetParam()).value())},
std::get<1>(GetParam()).operands())));
// Also check disassembly.
EXPECT_THAT(
EncodeAndDecodeSuccessfully(input.str(), SPV_BINARY_TO_TEXT_OPTION_NONE,
std::get<0>(GetParam())),
Eq(input.str()));
EXPECT_THAT(EncodeAndDecodeSuccessfully(
input.str(), SPV_BINARY_TO_TEXT_OPTION_NONE,
SPV_TEXT_TO_BINARY_OPTION_NONE, std::get<0>(GetParam())),
Eq(input.str()));
}
#define CASE(NAME) spv::Decoration::NAME, #NAME

View File

@ -35,7 +35,8 @@ using CompositeRoundTripTest = RoundTripTest;
TEST_F(CompositeRoundTripTest, Good) {
std::string spirv = "%2 = OpCopyLogical %1 %3\n";
std::string disassembly = EncodeAndDecodeSuccessfully(
spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_ENV_UNIVERSAL_1_4);
spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_TEXT_TO_BINARY_OPTION_NONE,
SPV_ENV_UNIVERSAL_1_4);
EXPECT_THAT(disassembly, Eq(spirv));
}

View File

@ -130,9 +130,10 @@ TEST_P(ExtensionRoundTripTest, Samples) {
EXPECT_THAT(CompiledInstructions(ac.input, env), Eq(ac.expected));
// Check round trip through the disassembler.
EXPECT_THAT(EncodeAndDecodeSuccessfully(ac.input,
SPV_BINARY_TO_TEXT_OPTION_NONE, env),
Eq(ac.input))
EXPECT_THAT(
EncodeAndDecodeSuccessfully(ac.input, SPV_BINARY_TO_TEXT_OPTION_NONE,
SPV_TEXT_TO_BINARY_OPTION_NONE, env),
Eq(ac.input))
<< "target env: " << spvTargetEnvDescription(env) << "\n";
}

View File

@ -107,7 +107,8 @@ TEST_F(MemoryRoundTripTest, OpPtrEqualGood) {
EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_4),
Eq(MakeInstruction(spv::Op::OpPtrEqual, {1, 2, 3, 4})));
std::string disassembly = EncodeAndDecodeSuccessfully(
spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_ENV_UNIVERSAL_1_4);
spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_TEXT_TO_BINARY_OPTION_NONE,
SPV_ENV_UNIVERSAL_1_4);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -124,7 +125,8 @@ TEST_F(MemoryRoundTripTest, OpPtrNotEqualGood) {
EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_4),
Eq(MakeInstruction(spv::Op::OpPtrNotEqual, {1, 2, 3, 4})));
std::string disassembly = EncodeAndDecodeSuccessfully(
spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_ENV_UNIVERSAL_1_4);
spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_TEXT_TO_BINARY_OPTION_NONE,
SPV_ENV_UNIVERSAL_1_4);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -141,7 +143,8 @@ TEST_F(MemoryRoundTripTest, OpPtrDiffGood) {
EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_4),
Eq(MakeInstruction(spv::Op::OpPtrDiff, {1, 2, 3, 4})));
std::string disassembly = EncodeAndDecodeSuccessfully(
spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_ENV_UNIVERSAL_1_4);
spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_TEXT_TO_BINARY_OPTION_NONE,
SPV_ENV_UNIVERSAL_1_4);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -151,7 +154,8 @@ TEST_F(MemoryRoundTripTest, OpPtrDiffV13Good) {
// write tests.
std::string spirv = "%2 = OpPtrDiff %1 %3 %4\n";
std::string disassembly = EncodeAndDecodeSuccessfully(
spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_ENV_UNIVERSAL_1_4);
spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_TEXT_TO_BINARY_OPTION_NONE,
SPV_ENV_UNIVERSAL_1_4);
}
// OpCopyMemory
@ -160,8 +164,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryNoMemAccessGood) {
std::string spirv = "OpCopyMemory %1 %2\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -182,8 +185,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessNoneGood) {
std::string spirv = "OpCopyMemory %1 %2 None\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2, 0})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -191,8 +193,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessVolatileGood) {
std::string spirv = "OpCopyMemory %1 %2 Volatile\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2, 1})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -200,8 +201,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessAligned8Good) {
std::string spirv = "OpCopyMemory %1 %2 Aligned 8\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2, 2, 8})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -209,8 +209,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessNontemporalGood) {
std::string spirv = "OpCopyMemory %1 %2 Nontemporal\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2, 4})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -218,8 +217,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessAvGood) {
std::string spirv = "OpCopyMemory %1 %2 MakePointerAvailable %3\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2, 8, 3})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -227,8 +225,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessVisGood) {
std::string spirv = "OpCopyMemory %1 %2 MakePointerVisible %3\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2, 16, 3})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -236,8 +233,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessNonPrivateGood) {
std::string spirv = "OpCopyMemory %1 %2 NonPrivatePointer\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2, 32})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -248,8 +244,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessMixedGood) {
"MakePointerVisible|NonPrivatePointer 16 %3 %4\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2, 63, 16, 3, 4})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -258,8 +253,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryTwoAccessV13Good) {
// Note: This will assemble but should not validate for SPIR-V 1.3
EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_3),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2, 1, 1})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -267,8 +261,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryTwoAccessV14Good) {
std::string spirv = "OpCopyMemory %1 %2 Volatile Volatile\n";
EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_4),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2, 1, 1})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -280,8 +273,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemoryTwoAccessMixedV14Good) {
EXPECT_THAT(
CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemory, {1, 2, 21, 3, 42, 16, 4})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -291,8 +283,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedNoMemAccessGood) {
std::string spirv = "OpCopyMemorySized %1 %2 %3\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized, {1, 2, 3})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -313,8 +304,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessNoneGood) {
std::string spirv = "OpCopyMemorySized %1 %2 %3 None\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized, {1, 2, 3, 0})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -322,8 +312,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessVolatileGood) {
std::string spirv = "OpCopyMemorySized %1 %2 %3 Volatile\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized, {1, 2, 3, 1})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -331,8 +320,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessAligned8Good) {
std::string spirv = "OpCopyMemorySized %1 %2 %3 Aligned 8\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized, {1, 2, 3, 2, 8})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -340,8 +328,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessNontemporalGood) {
std::string spirv = "OpCopyMemorySized %1 %2 %3 Nontemporal\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized, {1, 2, 3, 4})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -349,8 +336,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessAvGood) {
std::string spirv = "OpCopyMemorySized %1 %2 %3 MakePointerAvailable %4\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized, {1, 2, 3, 8, 4})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -359,8 +345,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessVisGood) {
EXPECT_THAT(
CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized, {1, 2, 3, 16, 4})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -368,8 +353,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessNonPrivateGood) {
std::string spirv = "OpCopyMemorySized %1 %2 %3 NonPrivatePointer\n";
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized, {1, 2, 3, 32})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -381,8 +365,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessMixedGood) {
EXPECT_THAT(
CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized, {1, 2, 3, 63, 16, 4, 5})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -391,8 +374,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedTwoAccessV13Good) {
// Note: This will assemble but should not validate for SPIR-V 1.3
EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_3),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized, {1, 2, 3, 1, 1})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -400,8 +382,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedTwoAccessV14Good) {
std::string spirv = "OpCopyMemorySized %1 %2 %3 Volatile Volatile\n";
EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_4),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized, {1, 2, 3, 1, 1})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}
@ -413,8 +394,7 @@ TEST_F(MemoryRoundTripTest, OpCopyMemorySizedTwoAccessMixedV14Good) {
EXPECT_THAT(CompiledInstructions(spirv),
Eq(MakeInstruction(spv::Op::OpCopyMemorySized,
{1, 2, 3, 21, 4, 42, 16, 5})));
std::string disassembly =
EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
std::string disassembly = EncodeAndDecodeSuccessfully(spirv);
EXPECT_THAT(disassembly, Eq(spirv));
}

View File

@ -35,42 +35,51 @@ or if the filename is "-", then the binary is read from standard input.
Options:
-h, --help Print this help.
--version Display disassembler version information.
-h, --help Print this help.
--version Display disassembler version information.
-o <filename> Set the output filename.
Output goes to standard output if this option is
not specified, or if the filename is "-".
-o <filename> Set the output filename.
Output goes to standard output if this option is
not specified, or if the filename is "-".
--color Force color output. The default when printing to a terminal.
Overrides a previous --no-color option.
--no-color Don't print in color. Overrides a previous --color option.
The default when output goes to something other than a
terminal (e.g. a file, a pipe, or a shell redirection).
--color Force color output. The default when printing to a terminal.
Overrides a previous --no-color option.
--no-color Don't print in color. Overrides a previous --color option.
The default when output goes to something other than a
terminal (e.g. a file, a pipe, or a shell redirection).
--no-indent Don't indent instructions.
--no-indent Don't indent instructions.
--no-header Don't output the header as leading comments.
--no-header Don't output the header as leading comments.
--raw-id Show raw Id values instead of friendly names.
--raw-id Show raw Id values instead of friendly names.
--offsets Show byte offsets for each instruction.
--nested-indent Indentation is adjusted to indicate nesting in structured
control flow.
--comment Add comments to make reading easier
--reorder-blocks Reorder blocks to match the structured control flow of SPIR-V.
With this option, the order of instructions will no longer
match the input binary, but the result will be more readable.
--offsets Show byte offsets for each instruction.
--comment Add comments to make reading easier
)";
// clang-format off
FLAG_SHORT_bool (h, /* default_value= */ false, /* required= */ false);
FLAG_SHORT_string(o, /* default_value= */ "-", /* required= */ false);
FLAG_LONG_bool (help, /* default_value= */ false, /* required= */false);
FLAG_LONG_bool (version, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (color, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (no_color, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (no_indent, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (no_header, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (raw_id, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (offsets, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (comment, /* default_value= */ false, /* required= */ false);
FLAG_SHORT_bool (h, /* default_value= */ false, /* required= */ false);
FLAG_SHORT_string(o, /* default_value= */ "-", /* required= */ false);
FLAG_LONG_bool (help, /* default_value= */ false, /* required= */false);
FLAG_LONG_bool (version, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (color, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (no_color, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (no_indent, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (no_header, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (raw_id, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (nested_indent, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (reorder_blocks, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (offsets, /* default_value= */ false, /* required= */ false);
FLAG_LONG_bool (comment, /* default_value= */ false, /* required= */ false);
// clang-format on
static const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_5;
@ -120,6 +129,12 @@ int main(int, const char** argv) {
if (!flags::raw_id.value())
options |= SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES;
if (flags::nested_indent.value())
options |= SPV_BINARY_TO_TEXT_OPTION_NESTED_INDENT;
if (flags::reorder_blocks.value())
options |= SPV_BINARY_TO_TEXT_OPTION_REORDER_BLOCKS;
if (flags::comment.value()) options |= SPV_BINARY_TO_TEXT_OPTION_COMMENT;
if (flags::o.value() == "-") {