fc613d85a7
When disassembling functions, respect whether absolute addresses should be used for PC offsets when printing stack map, PC descriptor and exception handler information. Also don't print out results for empty auxiliary information. TEST=ci Change-Id: Ieda990c1eabcce8be968981a81123a78746c6b6b Cq-Include-Trybots: luci.dart.try:vm-aot-linux-release-x64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/336663 Commit-Queue: Tess Strickland <sstrickl@google.com> Reviewed-by: Daco Harkes <dacoharkes@google.com>
542 lines
19 KiB
C++
542 lines
19 KiB
C++
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
#include "vm/compiler/assembler/disassembler.h"
|
|
|
|
#include "platform/text_buffer.h"
|
|
#include "platform/unaligned.h"
|
|
#include "vm/code_comments.h"
|
|
#include "vm/code_patcher.h"
|
|
#include "vm/dart_entry.h"
|
|
#include "vm/deopt_instructions.h"
|
|
#include "vm/globals.h"
|
|
#include "vm/instructions.h"
|
|
#include "vm/json_stream.h"
|
|
#include "vm/log.h"
|
|
#include "vm/os.h"
|
|
|
|
namespace dart {
|
|
|
|
#if !defined(PRODUCT) || defined(FORCE_INCLUDE_DISASSEMBLER)
|
|
|
|
#if !defined(DART_PRECOMPILED_RUNTIME)
|
|
DECLARE_FLAG(bool, trace_inlining_intervals);
|
|
#endif
|
|
|
|
DEFINE_FLAG(bool, trace_source_positions, false, "Source position diagnostics");
|
|
DEFINE_FLAG(bool,
|
|
include_inlining_info_in_disassembly,
|
|
true,
|
|
"Include inlining information when printing disassembly")
|
|
|
|
void DisassembleToStdout::ConsumeInstruction(char* hex_buffer,
|
|
intptr_t hex_size,
|
|
char* human_buffer,
|
|
intptr_t human_size,
|
|
Object* object,
|
|
uword pc) {
|
|
const int kHexColumnWidth = 23;
|
|
#if defined(TARGET_ARCH_IS_32_BIT)
|
|
THR_Print("0x%" Px32 " %s", static_cast<uint32_t>(pc), hex_buffer);
|
|
#else
|
|
THR_Print("0x%" Px64 " %s", static_cast<uint64_t>(pc), hex_buffer);
|
|
#endif
|
|
int hex_length = strlen(hex_buffer);
|
|
if (hex_length < kHexColumnWidth) {
|
|
for (int i = kHexColumnWidth - hex_length; i > 0; i--) {
|
|
THR_Print(" ");
|
|
}
|
|
}
|
|
THR_Print("%s", human_buffer);
|
|
if (object != nullptr) {
|
|
THR_Print(" %s", object->ToCString());
|
|
}
|
|
THR_Print("\n");
|
|
}
|
|
|
|
void DisassembleToStdout::Print(const char* format, ...) {
|
|
va_list args;
|
|
va_start(args, format);
|
|
THR_VPrint(format, args);
|
|
va_end(args);
|
|
}
|
|
|
|
void DisassembleToMemory::ConsumeInstruction(char* hex_buffer,
|
|
intptr_t hex_size,
|
|
char* human_buffer,
|
|
intptr_t human_size,
|
|
Object* object,
|
|
uword pc) {
|
|
if (overflowed_) {
|
|
return;
|
|
}
|
|
intptr_t len;
|
|
|
|
// TODO(compiler): Update assembler tests for other architectures so there is
|
|
// coverage of encodings, not just mnemonics.
|
|
#if defined(TARGET_ARCH_RISCV32) || defined(TARGET_ARCH_RISCV64)
|
|
len = strlen(hex_buffer);
|
|
if (remaining_ < len + 100) {
|
|
*buffer_++ = '.';
|
|
*buffer_++ = '.';
|
|
*buffer_++ = '.';
|
|
*buffer_++ = '\n';
|
|
*buffer_++ = '\0';
|
|
overflowed_ = true;
|
|
return;
|
|
}
|
|
memmove(buffer_, hex_buffer, len);
|
|
buffer_ += len;
|
|
remaining_ -= len;
|
|
*buffer_++ = ' ';
|
|
remaining_--;
|
|
*buffer_ = '\0';
|
|
#endif
|
|
|
|
len = strlen(human_buffer);
|
|
if (remaining_ < len + 100) {
|
|
*buffer_++ = '.';
|
|
*buffer_++ = '.';
|
|
*buffer_++ = '.';
|
|
*buffer_++ = '\n';
|
|
*buffer_++ = '\0';
|
|
overflowed_ = true;
|
|
return;
|
|
}
|
|
memmove(buffer_, human_buffer, len);
|
|
buffer_ += len;
|
|
remaining_ -= len;
|
|
*buffer_++ = '\n';
|
|
remaining_--;
|
|
*buffer_ = '\0';
|
|
}
|
|
|
|
void DisassembleToMemory::Print(const char* format, ...) {
|
|
if (overflowed_) {
|
|
return;
|
|
}
|
|
va_list measure_args;
|
|
va_start(measure_args, format);
|
|
intptr_t len = Utils::VSNPrint(nullptr, 0, format, measure_args);
|
|
va_end(measure_args);
|
|
if (remaining_ < len + 100) {
|
|
*buffer_++ = '.';
|
|
*buffer_++ = '.';
|
|
*buffer_++ = '.';
|
|
*buffer_++ = '\n';
|
|
*buffer_++ = '\0';
|
|
overflowed_ = true;
|
|
return;
|
|
}
|
|
va_list print_args;
|
|
va_start(print_args, format);
|
|
intptr_t len2 = Utils::VSNPrint(buffer_, len, format, print_args);
|
|
va_end(print_args);
|
|
ASSERT(len == len2);
|
|
buffer_ += len;
|
|
remaining_ -= len;
|
|
*buffer_++ = '\n';
|
|
remaining_--;
|
|
*buffer_ = '\0';
|
|
}
|
|
|
|
void Disassembler::Disassemble(uword start,
|
|
uword end,
|
|
DisassemblyFormatter* formatter,
|
|
const Code& code,
|
|
const CodeComments* comments) {
|
|
if (comments == nullptr) {
|
|
comments = code.IsNull() ? &Code::Comments::New(0) : &code.comments();
|
|
}
|
|
ASSERT(formatter != nullptr);
|
|
char hex_buffer[kHexadecimalBufferSize]; // Instruction in hexadecimal form.
|
|
char human_buffer[kUserReadableBufferSize]; // Human-readable instruction.
|
|
uword pc = start;
|
|
intptr_t comment_finger = 0;
|
|
GrowableArray<const Function*> inlined_functions;
|
|
GrowableArray<TokenPosition> token_positions;
|
|
while (pc < end) {
|
|
const intptr_t offset = pc - start;
|
|
const intptr_t old_comment_finger = comment_finger;
|
|
while (comment_finger < comments->Length() &&
|
|
comments->PCOffsetAt(comment_finger) <= offset) {
|
|
formatter->Print(" ;; %s\n", comments->CommentAt(comment_finger));
|
|
comment_finger++;
|
|
}
|
|
if (FLAG_include_inlining_info_in_disassembly &&
|
|
old_comment_finger != comment_finger && !code.IsNull()) {
|
|
char str[4000];
|
|
BufferFormatter f(str, sizeof(str));
|
|
// Comment emitted, emit inlining information.
|
|
code.GetInlinedFunctionsAtInstruction(offset, &inlined_functions,
|
|
&token_positions);
|
|
// Skip top scope function printing (last entry in 'inlined_functions').
|
|
bool first = true;
|
|
for (intptr_t i = 1; i < inlined_functions.length(); i++) {
|
|
const char* name = inlined_functions[i]->ToQualifiedCString();
|
|
if (first) {
|
|
f.Printf(" ;; Inlined [%s", name);
|
|
first = false;
|
|
} else {
|
|
f.Printf(" -> %s", name);
|
|
}
|
|
}
|
|
if (!first) {
|
|
f.AddString("]\n");
|
|
formatter->Print("%s", str);
|
|
}
|
|
}
|
|
int instruction_length;
|
|
Object* object;
|
|
DecodeInstruction(hex_buffer, sizeof(hex_buffer), human_buffer,
|
|
sizeof(human_buffer), &instruction_length, code, &object,
|
|
pc);
|
|
formatter->ConsumeInstruction(hex_buffer, sizeof(hex_buffer), human_buffer,
|
|
sizeof(human_buffer), object,
|
|
FLAG_disassemble_relative ? offset : pc);
|
|
pc += instruction_length;
|
|
}
|
|
}
|
|
|
|
void Disassembler::DisassembleCodeHelper(const char* function_fullname,
|
|
const char* function_info,
|
|
const Code& code,
|
|
bool optimized) {
|
|
Thread* thread = Thread::Current();
|
|
Zone* zone = thread->zone();
|
|
THR_Print("Code for %sfunction '%s' (%s) {\n", optimized ? "optimized " : "",
|
|
function_fullname, function_info);
|
|
code.Disassemble();
|
|
THR_Print("}\n");
|
|
|
|
#if defined(TARGET_ARCH_IA32)
|
|
if (code.pointer_offsets_length() > 0) {
|
|
THR_Print("Pointer offsets for function: {\n");
|
|
// Pointer offsets are stored in descending order.
|
|
Object& obj = Object::Handle(zone);
|
|
for (intptr_t i = code.pointer_offsets_length() - 1; i >= 0; i--) {
|
|
const uword addr = code.GetPointerOffsetAt(i) + code.PayloadStart();
|
|
obj = LoadUnaligned(reinterpret_cast<ObjectPtr*>(addr));
|
|
THR_Print(" %d : %#" Px " '%s'\n", code.GetPointerOffsetAt(i), addr,
|
|
obj.ToCString());
|
|
}
|
|
THR_Print("}\n");
|
|
}
|
|
#else
|
|
ASSERT(code.pointer_offsets_length() == 0);
|
|
#endif
|
|
|
|
if (FLAG_precompiled_mode) {
|
|
// Global object pool emitted after it is finalized instead of per-function.
|
|
} else {
|
|
const ObjectPool& object_pool =
|
|
ObjectPool::Handle(zone, code.GetObjectPool());
|
|
if (!object_pool.IsNull() && object_pool.Length() > 0) {
|
|
object_pool.DebugPrint();
|
|
}
|
|
}
|
|
|
|
code.DumpSourcePositions(/*relative_addresses=*/FLAG_disassemble_relative);
|
|
|
|
const uword start = code.PayloadStart();
|
|
const uword base = FLAG_disassemble_relative ? 0 : start;
|
|
|
|
const PcDescriptors& descriptors =
|
|
PcDescriptors::Handle(zone, code.pc_descriptors());
|
|
if (descriptors.Length() > 0) {
|
|
TextBuffer buffer(100);
|
|
buffer.Printf("PC Descriptors for function '%s' {\n", function_fullname);
|
|
descriptors.WriteToBuffer(&buffer, base);
|
|
buffer.AddString("}\n");
|
|
THR_Print("%s", buffer.buffer());
|
|
}
|
|
|
|
#if !defined(DART_PRECOMPILED_RUNTIME)
|
|
const Array& deopt_table = Array::Handle(zone, code.deopt_info_array());
|
|
if (!deopt_table.IsNull()) {
|
|
intptr_t deopt_table_length = DeoptTable::GetLength(deopt_table);
|
|
if (deopt_table_length > 0) {
|
|
THR_Print("DeoptInfo: {\n");
|
|
Smi& offset = Smi::Handle(zone);
|
|
TypedData& info = TypedData::Handle(zone);
|
|
Smi& reason_and_flags = Smi::Handle(zone);
|
|
for (intptr_t i = 0; i < deopt_table_length; ++i) {
|
|
DeoptTable::GetEntry(deopt_table, i, &offset, &info, &reason_and_flags);
|
|
const intptr_t reason =
|
|
DeoptTable::ReasonField::decode(reason_and_flags.Value());
|
|
ASSERT((0 <= reason) && (reason < ICData::kDeoptNumReasons));
|
|
THR_Print(
|
|
"%4" Pd ": 0x%" Px " %s (%s)\n", i, base + offset.Value(),
|
|
DeoptInfo::ToCString(deopt_table, info),
|
|
DeoptReasonToCString(static_cast<ICData::DeoptReasonId>(reason)));
|
|
}
|
|
THR_Print("}\n");
|
|
}
|
|
}
|
|
#endif // !defined(DART_PRECOMPILED_RUNTIME)
|
|
|
|
const auto& stackmaps =
|
|
CompressedStackMaps::Handle(zone, code.compressed_stackmaps());
|
|
if (!stackmaps.IsNull() && stackmaps.payload_size() > 0) {
|
|
TextBuffer buffer(100);
|
|
buffer.Printf("StackMaps for function '%s' {\n", function_fullname);
|
|
stackmaps.WriteToBuffer(&buffer, base, "\n");
|
|
buffer.AddString("\n}\n");
|
|
THR_Print("%s", buffer.buffer());
|
|
}
|
|
|
|
LocalVarDescriptors& var_descriptors = LocalVarDescriptors::Handle(zone);
|
|
if (FLAG_print_variable_descriptors) {
|
|
var_descriptors = code.GetLocalVarDescriptors();
|
|
}
|
|
const intptr_t var_desc_length =
|
|
var_descriptors.IsNull() ? 0 : var_descriptors.Length();
|
|
if (var_desc_length > 0) {
|
|
THR_Print("Variable Descriptors for function '%s' {\n", function_fullname);
|
|
String& var_name = String::Handle(zone);
|
|
for (intptr_t i = 0; i < var_desc_length; i++) {
|
|
var_name = var_descriptors.GetName(i);
|
|
UntaggedLocalVarDescriptors::VarInfo var_info;
|
|
var_descriptors.GetInfo(i, &var_info);
|
|
const int8_t kind = var_info.kind();
|
|
if (kind == UntaggedLocalVarDescriptors::kSavedCurrentContext) {
|
|
THR_Print(" saved current CTX reg offset %d\n", var_info.index());
|
|
} else {
|
|
if (kind == UntaggedLocalVarDescriptors::kContextLevel) {
|
|
THR_Print(" context level %d scope %d", var_info.index(),
|
|
var_info.scope_id);
|
|
} else if (kind == UntaggedLocalVarDescriptors::kStackVar) {
|
|
THR_Print(" stack var '%s' offset %d", var_name.ToCString(),
|
|
var_info.index());
|
|
} else {
|
|
ASSERT(kind == UntaggedLocalVarDescriptors::kContextVar);
|
|
THR_Print(" context var '%s' level %d offset %d",
|
|
var_name.ToCString(), var_info.scope_id, var_info.index());
|
|
}
|
|
THR_Print(" (valid %s-%s)\n", var_info.begin_pos.ToCString(),
|
|
var_info.end_pos.ToCString());
|
|
}
|
|
}
|
|
THR_Print("}\n");
|
|
}
|
|
|
|
const ExceptionHandlers& handlers =
|
|
ExceptionHandlers::Handle(zone, code.exception_handlers());
|
|
if (handlers.num_entries() > 0 || handlers.has_async_handler()) {
|
|
TextBuffer buffer(100);
|
|
buffer.Printf("Exception Handlers for function '%s' {\n",
|
|
function_fullname);
|
|
handlers.WriteToBuffer(&buffer, base);
|
|
buffer.AddString("}\n");
|
|
THR_Print("%s", buffer.buffer());
|
|
}
|
|
|
|
#if defined(DART_PRECOMPILED_RUNTIME) || defined(DART_PRECOMPILER)
|
|
if (FLAG_precompiled_mode &&
|
|
code.catch_entry_moves_maps() != Object::null()) {
|
|
THR_Print("Catch entry moves for function '%s' {\n", function_fullname);
|
|
CatchEntryMovesMapReader reader(
|
|
TypedData::Handle(code.catch_entry_moves_maps()));
|
|
reader.PrintEntries();
|
|
THR_Print("}\n");
|
|
}
|
|
#endif // defined(DART_PRECOMPILED_RUNTIME) || defined(DART_PRECOMPILER)
|
|
|
|
{
|
|
THR_Print("Entry points for function '%s' {\n", function_fullname);
|
|
THR_Print(" [code+0x%02" Px "] %" Px " kNormal\n",
|
|
Code::entry_point_offset(CodeEntryKind::kNormal) - kHeapObjectTag,
|
|
code.EntryPoint() - start + base);
|
|
THR_Print(
|
|
" [code+0x%02" Px "] %" Px " kMonomorphic\n",
|
|
Code::entry_point_offset(CodeEntryKind::kMonomorphic) - kHeapObjectTag,
|
|
code.MonomorphicEntryPoint() - start + base);
|
|
THR_Print(
|
|
" [code+0x%02" Px "] %" Px " kUnchecked\n",
|
|
Code::entry_point_offset(CodeEntryKind::kUnchecked) - kHeapObjectTag,
|
|
code.UncheckedEntryPoint() - start + base);
|
|
THR_Print(" [code+0x%02" Px "] %" Px " kMonomorphicUnchecked\n",
|
|
Code::entry_point_offset(CodeEntryKind::kMonomorphicUnchecked) -
|
|
kHeapObjectTag,
|
|
code.MonomorphicUncheckedEntryPoint() - start + base);
|
|
THR_Print("}\n");
|
|
}
|
|
|
|
#if defined(DART_PRECOMPILED_RUNTIME)
|
|
THR_Print("(Cannot show static call target functions in AOT runtime.)\n");
|
|
#else
|
|
const auto& table = Array::Handle(zone, code.static_calls_target_table());
|
|
if (!table.IsNull()) {
|
|
StaticCallsTable static_calls(table);
|
|
if (static_calls.Length() > 0) {
|
|
THR_Print("Static call target functions {\n");
|
|
auto& cls = Class::Handle(zone);
|
|
auto& kind_type_and_offset = Smi::Handle(zone);
|
|
auto& function = Function::Handle(zone);
|
|
auto& object = Object::Handle(zone);
|
|
auto& code = Code::Handle(zone);
|
|
auto& dst_type = AbstractType::Handle(zone);
|
|
for (auto& call : static_calls) {
|
|
kind_type_and_offset = call.Get<Code::kSCallTableKindAndOffset>();
|
|
function = call.Get<Code::kSCallTableFunctionTarget>();
|
|
object = call.Get<Code::kSCallTableCodeOrTypeTarget>();
|
|
|
|
dst_type = AbstractType::null();
|
|
if (object.IsAbstractType()) {
|
|
dst_type = AbstractType::Cast(object).ptr();
|
|
} else if (object.IsCode()) {
|
|
code = Code::Cast(object).ptr();
|
|
}
|
|
|
|
auto kind = Code::KindField::decode(kind_type_and_offset.Value());
|
|
auto offset = Code::OffsetField::decode(kind_type_and_offset.Value());
|
|
auto entry_point =
|
|
Code::EntryPointField::decode(kind_type_and_offset.Value());
|
|
|
|
const char* s_entry_point =
|
|
entry_point == Code::kUncheckedEntry ? " <unchecked-entry>" : "";
|
|
const char* skind = nullptr;
|
|
switch (kind) {
|
|
case Code::kPcRelativeCall:
|
|
skind = "pc-relative-call";
|
|
break;
|
|
case Code::kPcRelativeTTSCall:
|
|
skind = "pc-relative-tts-call";
|
|
break;
|
|
case Code::kPcRelativeTailCall:
|
|
skind = "pc-relative-tail-call";
|
|
break;
|
|
case Code::kCallViaCode:
|
|
skind = "call-via-code";
|
|
break;
|
|
default:
|
|
UNREACHABLE();
|
|
}
|
|
if (!dst_type.IsNull()) {
|
|
THR_Print(" 0x%" Px ": type testing stub %s, (%s)%s\n",
|
|
base + offset, dst_type.ToCString(), skind, s_entry_point);
|
|
} else if (function.IsNull()) {
|
|
cls ^= code.owner();
|
|
if (cls.IsNull()) {
|
|
THR_Print(
|
|
" 0x%" Px ": %s, (%s)%s\n", base + offset,
|
|
code.QualifiedName(NameFormattingParams(
|
|
Object::kScrubbedName, Object::NameDisambiguation::kYes)),
|
|
skind, s_entry_point);
|
|
} else {
|
|
THR_Print(" 0x%" Px ": allocation stub for %s, (%s)%s\n",
|
|
base + offset, cls.ToCString(), skind, s_entry_point);
|
|
}
|
|
} else {
|
|
THR_Print(" 0x%" Px ": %s, (%s)%s\n", base + offset,
|
|
function.ToFullyQualifiedCString(), skind, s_entry_point);
|
|
}
|
|
}
|
|
THR_Print("}\n");
|
|
}
|
|
}
|
|
#endif // defined(DART_PRECOMPILED_RUNTIME)
|
|
|
|
#if !defined(DART_PRECOMPILED_RUNTIME)
|
|
if (optimized && FLAG_trace_inlining_intervals) {
|
|
code.DumpInlineIntervals();
|
|
}
|
|
#endif
|
|
|
|
if (FLAG_trace_source_positions) {
|
|
code.DumpSourcePositions();
|
|
}
|
|
}
|
|
|
|
void Disassembler::DisassembleCode(const Function& function,
|
|
const Code& code,
|
|
bool optimized) {
|
|
if (code.IsUnknownDartCode()) {
|
|
return;
|
|
}
|
|
TextBuffer buffer(128);
|
|
const char* function_fullname = function.ToFullyQualifiedCString();
|
|
buffer.Printf("%s", Function::KindToCString(function.kind()));
|
|
if (function.HasSavedArgumentsDescriptor()) {
|
|
const auto& args_desc_array = Array::Handle(function.saved_args_desc());
|
|
const ArgumentsDescriptor args_desc(args_desc_array);
|
|
buffer.AddString(", ");
|
|
args_desc.PrintTo(&buffer);
|
|
}
|
|
LogBlock lb;
|
|
DisassembleCodeHelper(function_fullname, buffer.buffer(), code, optimized);
|
|
}
|
|
|
|
void Disassembler::DisassembleStub(const char* name, const Code& code) {
|
|
LogBlock lb;
|
|
THR_Print("Code for stub '%s': {\n", name);
|
|
DisassembleToStdout formatter;
|
|
code.Disassemble(&formatter);
|
|
THR_Print("}\n");
|
|
const ObjectPool& object_pool = ObjectPool::Handle(code.object_pool());
|
|
if (FLAG_precompiled_mode) {
|
|
THR_Print("(No object pool for bare instructions.)\n");
|
|
} else if (!object_pool.IsNull() && object_pool.Length() > 0) {
|
|
object_pool.DebugPrint();
|
|
}
|
|
}
|
|
|
|
#else // !defined(PRODUCT) || defined(FORCE_INCLUDE_DISASSEMBLER)
|
|
|
|
void Disassembler::DisassembleCode(const Function& function,
|
|
const Code& code,
|
|
bool optimized) {}
|
|
#endif // !defined(PRODUCT) || defined(FORCE_INCLUDE_DISASSEMBLER)
|
|
|
|
#if !defined(PRODUCT)
|
|
void DisassembleToJSONStream::ConsumeInstruction(char* hex_buffer,
|
|
intptr_t hex_size,
|
|
char* human_buffer,
|
|
intptr_t human_size,
|
|
Object* object,
|
|
uword pc) {
|
|
// Instructions are represented as four consecutive values in a JSON array.
|
|
// The first is the address of the instruction, the second is the hex string,
|
|
// of the code, and the third is a human readable string, and the fourth is
|
|
// the object loaded by the instruction.
|
|
jsarr_.AddValueF("%" Pp "", pc);
|
|
jsarr_.AddValue(hex_buffer);
|
|
jsarr_.AddValue(human_buffer);
|
|
|
|
if (object != nullptr) {
|
|
jsarr_.AddValue(*object);
|
|
} else {
|
|
jsarr_.AddValueNull(); // Not a reference to null.
|
|
}
|
|
}
|
|
|
|
void DisassembleToJSONStream::Print(const char* format, ...) {
|
|
va_list measure_args;
|
|
va_start(measure_args, format);
|
|
intptr_t len = Utils::VSNPrint(nullptr, 0, format, measure_args);
|
|
va_end(measure_args);
|
|
|
|
char* p = reinterpret_cast<char*>(malloc(len + 1));
|
|
va_list print_args;
|
|
va_start(print_args, format);
|
|
intptr_t len2 = Utils::VSNPrint(p, len, format, print_args);
|
|
va_end(print_args);
|
|
ASSERT(len == len2);
|
|
for (intptr_t i = 0; i < len; i++) {
|
|
if (p[i] == '\n' || p[i] == '\r') {
|
|
p[i] = ' ';
|
|
}
|
|
}
|
|
// Instructions are represented as four consecutive values in a JSON array.
|
|
// Comments only use the third slot. See above comment for more information.
|
|
jsarr_.AddValueNull();
|
|
jsarr_.AddValueNull();
|
|
jsarr_.AddValue(p);
|
|
jsarr_.AddValueNull();
|
|
free(p);
|
|
}
|
|
#endif // !defined(PRODUCT)
|
|
|
|
} // namespace dart
|