From 824bec596f522769bdee75c4d8b9dea785b685b5 Mon Sep 17 00:00:00 2001 From: Alexander Markov Date: Fri, 13 Aug 2021 18:20:51 +0000 Subject: [PATCH] [vm] Hide internal implementation List types and expose them as List When taking a type of an instance with x.runtimeType we can map internal classes _List, _ImmutableList and _GrowableList to a user-visible List class. This is similar to what we do for implementation classes of int, String and Type. After that, result of x.runtimeType for built-in lists would be compatible with List type literals. Also, both intrinsic and native implementations of _haveSameRuntimeType are updated to agree with new semantic of runtimeType. TEST=co19/LanguageFeatures/Constructor-tear-offs/type_literal_A01_t01 TEST=runtime/tests/vm/dart/have_same_runtime_type_test Fixes https://github.com/dart-lang/sdk/issues/46893 Issue https://github.com/dart-lang/sdk/issues/46231 Change-Id: Ie24a9f527f66a06118427b7a09e49c03dff93d8e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/210066 Commit-Queue: Alexander Markov Reviewed-by: Tess Strickland --- runtime/lib/object.cc | 14 ++++--- .../vm/dart/have_same_runtime_type_test.dart | 32 ++++++++++++++++ .../dart_2/have_same_runtime_type_test.dart | 34 +++++++++++++++++ runtime/vm/class_id.h | 18 ++++++--- runtime/vm/compiler/asm_intrinsifier_arm.cc | 26 +++++++++++-- runtime/vm/compiler/asm_intrinsifier_arm64.cc | 26 +++++++++++-- runtime/vm/compiler/asm_intrinsifier_ia32.cc | 37 +++++++++++++++---- runtime/vm/compiler/asm_intrinsifier_x64.cc | 37 +++++++++++++++---- .../compiler/backend/flow_graph_compiler.cc | 12 +++--- runtime/vm/object.cc | 8 +++- runtime/vm/object_graph_copy.cc | 1 + runtime/vm/service.cc | 1 - 12 files changed, 206 insertions(+), 40 deletions(-) create mode 100644 runtime/tests/vm/dart/have_same_runtime_type_test.dart create mode 100644 runtime/tests/vm/dart_2/have_same_runtime_type_test.dart diff --git a/runtime/lib/object.cc b/runtime/lib/object.cc index fb33534f55c3..a5911b8d5bd2 100644 --- a/runtime/lib/object.cc +++ b/runtime/lib/object.cc @@ -101,14 +101,18 @@ static bool HaveSameRuntimeTypeHelper(Zone* zone, if (left_cid != right_cid) { if (IsIntegerClassId(left_cid)) { return IsIntegerClassId(right_cid); - } - if (IsStringClassId(left_cid)) { + } else if (IsStringClassId(left_cid)) { return IsStringClassId(right_cid); - } - if (IsTypeClassId(left_cid)) { + } else if (IsTypeClassId(left_cid)) { return IsTypeClassId(right_cid); + } else if (IsArrayClassId(left_cid)) { + if (!IsArrayClassId(right_cid)) { + return false; + } + // Still need to check type arguments. + } else { + return false; } - return false; } if (left_cid == kClosureCid) { diff --git a/runtime/tests/vm/dart/have_same_runtime_type_test.dart b/runtime/tests/vm/dart/have_same_runtime_type_test.dart new file mode 100644 index 000000000000..b7ade2aba34b --- /dev/null +++ b/runtime/tests/vm/dart/have_same_runtime_type_test.dart @@ -0,0 +1,32 @@ +// Copyright (c) 2021, 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. + +// Test for corner cases of 'a.runtimeType == b.runtimeType' pattern +// which is recognized and optimized in AOT mode. + +import "package:expect/expect.dart"; + +@pragma('vm:never-inline') +Object getType(Object obj) => obj.runtimeType; + +@pragma('vm:never-inline') +void test(bool expected, Object a, Object b) { + bool result1 = getType(a) == getType(b); + bool result2 = a.runtimeType == b.runtimeType; + Expect.equals(expected, result1); + Expect.equals(expected, result2); +} + +typedef Func = void Function(); + +void main() { + test(true, 0x7fffffffffffffff, int.parse('42')); + test(true, 'hi', String.fromCharCode(1114111)); + test(false, 'hi', 1); + test(true, List, Func); + test(true, [1], const [2]); + test(true, const [], List.filled(1, '')); + test(true, []..add('hi'), List.filled(2, '')); + test(false, [], []); +} diff --git a/runtime/tests/vm/dart_2/have_same_runtime_type_test.dart b/runtime/tests/vm/dart_2/have_same_runtime_type_test.dart new file mode 100644 index 000000000000..119e40ff4bec --- /dev/null +++ b/runtime/tests/vm/dart_2/have_same_runtime_type_test.dart @@ -0,0 +1,34 @@ +// Copyright (c) 2021, 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. + +// Test for corner cases of 'a.runtimeType == b.runtimeType' pattern +// which is recognized and optimized in AOT mode. + +// @dart = 2.9 + +import "package:expect/expect.dart"; + +@pragma('vm:never-inline') +Object getType(Object obj) => obj.runtimeType; + +@pragma('vm:never-inline') +void test(bool expected, Object a, Object b) { + bool result1 = getType(a) == getType(b); + bool result2 = a.runtimeType == b.runtimeType; + Expect.equals(expected, result1); + Expect.equals(expected, result2); +} + +typedef Func = void Function(); + +void main() { + test(true, 0x7fffffffffffffff, int.parse('42')); + test(true, 'hi', String.fromCharCode(1114111)); + test(false, 'hi', 1); + test(true, List, Func); + test(true, [1], const [2]); + test(true, const [], List.filled(1, '')); + test(true, []..add('hi'), List.filled(2, '')); + test(false, [], []); +} diff --git a/runtime/vm/class_id.h b/runtime/vm/class_id.h index eeb6d4110d81..9f3caf9a5ac3 100644 --- a/runtime/vm/class_id.h +++ b/runtime/vm/class_id.h @@ -78,7 +78,6 @@ typedef uint16_t ClassIdTagType; V(Mint) \ V(Double) \ V(Bool) \ - V(GrowableObjectArray) \ V(Float32x4) \ V(Int32x4) \ V(Float64x2) \ @@ -108,10 +107,14 @@ typedef uint16_t ClassIdTagType; // TODO(http://dartbug.com/45908): Add ImmutableLinkedHashSet. #define CLASS_LIST_SETS(V) V(LinkedHashSet) -#define CLASS_LIST_ARRAYS(V) \ +#define CLASS_LIST_FIXED_LENGTH_ARRAYS(V) \ V(Array) \ V(ImmutableArray) +#define CLASS_LIST_ARRAYS(V) \ + CLASS_LIST_FIXED_LENGTH_ARRAYS(V) \ + V(GrowableObjectArray) + #define CLASS_LIST_STRINGS(V) \ V(String) \ V(OneByteString) \ @@ -182,6 +185,7 @@ typedef uint16_t ClassIdTagType; V(LinkedHashMap) \ V(LinkedHashSet) \ V(Array) \ + V(GrowableObjectArray) \ V(String) #define CLASS_LIST_NO_OBJECT(V) \ @@ -329,11 +333,15 @@ inline bool IsExternalStringClassId(intptr_t index) { index == kExternalTwoByteStringCid); } +inline bool IsArrayClassId(intptr_t index) { + COMPILE_ASSERT(kImmutableArrayCid == kArrayCid + 1); + COMPILE_ASSERT(kGrowableObjectArrayCid == kArrayCid + 2); + return (index >= kArrayCid && index <= kGrowableObjectArrayCid); +} + inline bool IsBuiltinListClassId(intptr_t index) { // Make sure this function is updated when new builtin List types are added. - COMPILE_ASSERT(kImmutableArrayCid == kArrayCid + 1); - return ((index >= kArrayCid && index <= kImmutableArrayCid) || - (index == kGrowableObjectArrayCid) || IsTypedDataBaseClassId(index) || + return (IsArrayClassId(index) || IsTypedDataBaseClassId(index) || (index == kByteBufferCid)); } diff --git a/runtime/vm/compiler/asm_intrinsifier_arm.cc b/runtime/vm/compiler/asm_intrinsifier_arm.cc index 375c454710fd..e7f2ee15cf62 100644 --- a/runtime/vm/compiler/asm_intrinsifier_arm.cc +++ b/runtime/vm/compiler/asm_intrinsifier_arm.cc @@ -1198,6 +1198,14 @@ static void JumpIfNotString(Assembler* assembler, kIfNotInRange, target); } +static void JumpIfNotList(Assembler* assembler, + Register cid, + Register tmp, + Label* target) { + RangeCheck(assembler, cid, tmp, kArrayCid, kGrowableObjectArrayCid, + kIfNotInRange, target); +} + static void JumpIfType(Assembler* assembler, Register cid, Register tmp, @@ -1284,7 +1292,7 @@ static void EquivalentClassIds(Assembler* assembler, Register scratch, bool testing_instance_cids) { Label different_cids, equal_cids_but_generic, not_integer, - not_integer_or_string; + not_integer_or_string, not_integer_or_string_or_list; // Check if left hand side is a closure. Closures are handled in the runtime. __ CompareImmediate(cid1, kClosureCid); @@ -1310,7 +1318,8 @@ static void EquivalentClassIds(Assembler* assembler, __ b(equal); // Class ids are different. Check if we are comparing two string types (with - // different representations) or two integer types or two type types. + // different representations), two integer types, two list types or two type + // types. __ Bind(&different_cids); __ CompareImmediate(cid1, kNumPredefinedCids); __ b(not_equal, HI); @@ -1335,9 +1344,20 @@ static void EquivalentClassIds(Assembler* assembler, if (testing_instance_cids) { __ Bind(¬_integer_or_string); + // Check if both are List types. + JumpIfNotList(assembler, cid1, scratch, ¬_integer_or_string_or_list); + + // First type is a List. Check if the second is a List too. + JumpIfNotList(assembler, cid2, scratch, not_equal); + ASSERT(compiler::target::Array::type_arguments_offset() == + compiler::target::GrowableObjectArray::type_arguments_offset()); + __ LoadImmediate(scratch, compiler::target::Array::type_arguments_offset()); + __ b(&equal_cids_but_generic); + + __ Bind(¬_integer_or_string_or_list); // Check if the first type is a Type. If it is not then types are not // equivalent because they have different class ids and they are not String - // or integer or Type. + // or integer or List or Type. JumpIfNotType(assembler, cid1, scratch, not_equal); // First type is a Type. Check if the second is a Type too. diff --git a/runtime/vm/compiler/asm_intrinsifier_arm64.cc b/runtime/vm/compiler/asm_intrinsifier_arm64.cc index 36cb205d1ffc..1e0134aeb189 100644 --- a/runtime/vm/compiler/asm_intrinsifier_arm64.cc +++ b/runtime/vm/compiler/asm_intrinsifier_arm64.cc @@ -1342,6 +1342,14 @@ static void JumpIfNotString(Assembler* assembler, kIfNotInRange, target); } +static void JumpIfNotList(Assembler* assembler, + Register cid, + Register tmp, + Label* target) { + RangeCheck(assembler, cid, tmp, kArrayCid, kGrowableObjectArrayCid, + kIfNotInRange, target); +} + static void JumpIfType(Assembler* assembler, Register cid, Register tmp, @@ -1432,7 +1440,7 @@ static void EquivalentClassIds(Assembler* assembler, Register scratch, bool testing_instance_cids) { Label different_cids, equal_cids_but_generic, not_integer, - not_integer_or_string; + not_integer_or_string, not_integer_or_string_or_list; // Check if left hand side is a closure. Closures are handled in the runtime. __ CompareImmediate(cid1, kClosureCid); @@ -1458,7 +1466,8 @@ static void EquivalentClassIds(Assembler* assembler, __ b(equal); // Class ids are different. Check if we are comparing two string types (with - // different representations) or two integer types or two type types. + // different representations), two integer types, two list types or two type + // types. __ Bind(&different_cids); __ CompareImmediate(cid1, kNumPredefinedCids); __ b(not_equal, HI); @@ -1483,9 +1492,20 @@ static void EquivalentClassIds(Assembler* assembler, if (testing_instance_cids) { __ Bind(¬_integer_or_string); + // Check if both are List types. + JumpIfNotList(assembler, cid1, scratch, ¬_integer_or_string_or_list); + + // First type is a List. Check if the second is a List too. + JumpIfNotList(assembler, cid2, scratch, not_equal); + ASSERT(compiler::target::Array::type_arguments_offset() == + compiler::target::GrowableObjectArray::type_arguments_offset()); + __ LoadImmediate(scratch, compiler::target::Array::type_arguments_offset()); + __ b(&equal_cids_but_generic); + + __ Bind(¬_integer_or_string_or_list); // Check if the first type is a Type. If it is not then types are not // equivalent because they have different class ids and they are not String - // or integer or Type. + // or integer or List or Type. JumpIfNotType(assembler, cid1, scratch, not_equal); // First type is a Type. Check if the second is a Type too. diff --git a/runtime/vm/compiler/asm_intrinsifier_ia32.cc b/runtime/vm/compiler/asm_intrinsifier_ia32.cc index a17f7a834825..a6f1c9daae0c 100644 --- a/runtime/vm/compiler/asm_intrinsifier_ia32.cc +++ b/runtime/vm/compiler/asm_intrinsifier_ia32.cc @@ -1281,6 +1281,11 @@ static void JumpIfNotString(Assembler* assembler, Register cid, Label* target) { kIfNotInRange, target); } +static void JumpIfNotList(Assembler* assembler, Register cid, Label* target) { + RangeCheck(assembler, cid, kArrayCid, kGrowableObjectArrayCid, kIfNotInRange, + target); +} + static void JumpIfType(Assembler* assembler, Register cid, Label* target) { RangeCheck(assembler, cid, kTypeCid, kFunctionTypeCid, kIfInRange, target); } @@ -1370,7 +1375,7 @@ static void EquivalentClassIds(Assembler* assembler, Register scratch, bool testing_instance_cids) { Label different_cids, equal_cids_but_generic, not_integer, - not_integer_or_string; + not_integer_or_string, not_integer_or_string_or_list; // Check if left hand side is a closure. Closures are handled in the runtime. __ cmpl(cid1, Immediate(kClosureCid)); @@ -1392,11 +1397,12 @@ static void EquivalentClassIds(Assembler* assembler, scratch, target::Class::host_type_arguments_field_offset_in_words_offset())); __ cmpl(scratch, Immediate(target::Class::kNoTypeArguments)); - __ j(NOT_EQUAL, &equal_cids_but_generic, Assembler::kNearJump); + __ j(NOT_EQUAL, &equal_cids_but_generic); __ jmp(equal); // Class ids are different. Check if we are comparing two string types (with - // different representations) or two integer types or two type types. + // different representations), two integer types, two list types or two type + // types. __ Bind(&different_cids); __ cmpl(cid1, Immediate(kNumPredefinedCids)); __ j(ABOVE_EQUAL, not_equal); @@ -1406,25 +1412,42 @@ static void EquivalentClassIds(Assembler* assembler, JumpIfNotInteger(assembler, scratch, ¬_integer); // First type is an integer. Check if the second is an integer too. - JumpIfInteger(assembler, cid2, equal); + __ movl(scratch, cid2); + JumpIfInteger(assembler, scratch, equal); // Integer types are only equivalent to other integer types. __ jmp(not_equal); __ Bind(¬_integer); // Check if both are String types. - JumpIfNotString(assembler, cid1, + __ movl(scratch, cid1); + JumpIfNotString(assembler, scratch, testing_instance_cids ? ¬_integer_or_string : not_equal); // First type is a String. Check if the second is a String too. - JumpIfString(assembler, cid2, equal); + __ movl(scratch, cid2); + JumpIfString(assembler, scratch, equal); // String types are only equivalent to other String types. __ jmp(not_equal); if (testing_instance_cids) { __ Bind(¬_integer_or_string); + // Check if both are List types. + __ movl(scratch, cid1); + JumpIfNotList(assembler, scratch, ¬_integer_or_string_or_list); + + // First type is a List. Check if the second is a List too. + __ movl(scratch, cid2); + JumpIfNotList(assembler, scratch, not_equal); + ASSERT(compiler::target::Array::type_arguments_offset() == + compiler::target::GrowableObjectArray::type_arguments_offset()); + __ movl(scratch, + Immediate(compiler::target::Array::type_arguments_offset())); + __ jmp(&equal_cids_but_generic, Assembler::kNearJump); + + __ Bind(¬_integer_or_string_or_list); // Check if the first type is a Type. If it is not then types are not // equivalent because they have different class ids and they are not String - // or integer or Type. + // or integer or List or Type. JumpIfNotType(assembler, cid1, not_equal); // First type is a Type. Check if the second is a Type too. diff --git a/runtime/vm/compiler/asm_intrinsifier_x64.cc b/runtime/vm/compiler/asm_intrinsifier_x64.cc index b7109e88fa6c..3196c49d1c42 100644 --- a/runtime/vm/compiler/asm_intrinsifier_x64.cc +++ b/runtime/vm/compiler/asm_intrinsifier_x64.cc @@ -1184,6 +1184,11 @@ static void JumpIfNotString(Assembler* assembler, Register cid, Label* target) { kIfNotInRange, target); } +static void JumpIfNotList(Assembler* assembler, Register cid, Label* target) { + RangeCheck(assembler, cid, kArrayCid, kGrowableObjectArrayCid, kIfNotInRange, + target); +} + static void JumpIfType(Assembler* assembler, Register cid, Label* target) { RangeCheck(assembler, cid, kTypeCid, kFunctionTypeCid, kIfInRange, target); } @@ -1275,7 +1280,7 @@ static void EquivalentClassIds(Assembler* assembler, Register scratch, bool testing_instance_cids) { Label different_cids, equal_cids_but_generic, not_integer, - not_integer_or_string; + not_integer_or_string, not_integer_or_string_or_list; // Check if left hand side is a closure. Closures are handled in the runtime. __ cmpq(cid1, Immediate(kClosureCid)); @@ -1297,11 +1302,12 @@ static void EquivalentClassIds(Assembler* assembler, scratch, target::Class::host_type_arguments_field_offset_in_words_offset())); __ cmpl(scratch, Immediate(target::Class::kNoTypeArguments)); - __ j(NOT_EQUAL, &equal_cids_but_generic, Assembler::kNearJump); + __ j(NOT_EQUAL, &equal_cids_but_generic); __ jmp(equal); // Class ids are different. Check if we are comparing two string types (with - // different representations) or two integer types or two type types. + // different representations), two integer types, two list types or two type + // types. __ Bind(&different_cids); __ cmpq(cid1, Immediate(kNumPredefinedCids)); __ j(ABOVE_EQUAL, not_equal); @@ -1311,25 +1317,42 @@ static void EquivalentClassIds(Assembler* assembler, JumpIfNotInteger(assembler, scratch, ¬_integer); // First type is an integer. Check if the second is an integer too. - JumpIfInteger(assembler, cid2, equal); + __ movq(scratch, cid2); + JumpIfInteger(assembler, scratch, equal); // Integer types are only equivalent to other integer types. __ jmp(not_equal); __ Bind(¬_integer); // Check if both are String types. - JumpIfNotString(assembler, cid1, + __ movq(scratch, cid1); + JumpIfNotString(assembler, scratch, testing_instance_cids ? ¬_integer_or_string : not_equal); // First type is a String. Check if the second is a String too. - JumpIfString(assembler, cid2, equal); + __ movq(scratch, cid2); + JumpIfString(assembler, scratch, equal); // String types are only equivalent to other String types. __ jmp(not_equal); if (testing_instance_cids) { __ Bind(¬_integer_or_string); + // Check if both are List types. + __ movq(scratch, cid1); + JumpIfNotList(assembler, scratch, ¬_integer_or_string_or_list); + + // First type is a List. Check if the second is a List too. + __ movq(scratch, cid2); + JumpIfNotList(assembler, scratch, not_equal); + ASSERT(compiler::target::Array::type_arguments_offset() == + compiler::target::GrowableObjectArray::type_arguments_offset()); + __ movq(scratch, + Immediate(compiler::target::Array::type_arguments_offset())); + __ jmp(&equal_cids_but_generic, Assembler::kNearJump); + + __ Bind(¬_integer_or_string_or_list); // Check if the first type is a Type. If it is not then types are not // equivalent because they have different class ids and they are not String - // or integer or Type. + // or integer or List or Type. JumpIfNotType(assembler, cid1, not_equal); // First type is a Type. Check if the second is a Type too. diff --git a/runtime/vm/compiler/backend/flow_graph_compiler.cc b/runtime/vm/compiler/backend/flow_graph_compiler.cc index 5ecdd9ed25c0..1382c165a5b2 100644 --- a/runtime/vm/compiler/backend/flow_graph_compiler.cc +++ b/runtime/vm/compiler/backend/flow_graph_compiler.cc @@ -1531,13 +1531,11 @@ void FlowGraphCompiler::GenerateListTypeCheck( Register class_id_reg, compiler::Label* is_instance_lbl) { assembler()->Comment("ListTypeCheck"); - compiler::Label unknown; - GrowableArray args; - args.Add(kArrayCid); - args.Add(kGrowableObjectArrayCid); - args.Add(kImmutableArrayCid); - CheckClassIds(class_id_reg, args, is_instance_lbl, &unknown); - assembler()->Bind(&unknown); + COMPILE_ASSERT((kImmutableArrayCid == kArrayCid + 1) && + (kGrowableObjectArrayCid == kArrayCid + 2)); + CidRangeVector ranges; + ranges.Add({kArrayCid, kGrowableObjectArrayCid}); + GenerateCidRangesCheck(assembler(), class_id_reg, ranges, is_instance_lbl); } void FlowGraphCompiler::EmitComment(Instruction* instr) { diff --git a/runtime/vm/object.cc b/runtime/vm/object.cc index 50cd1465fba8..2364b3a5e79a 100644 --- a/runtime/vm/object.cc +++ b/runtime/vm/object.cc @@ -607,6 +607,7 @@ void Object::InitVtables() { builtin_vtables_[k##clazz##Cid] = fake_handle.vtable(); \ } CLASS_LIST_NO_OBJECT_NOR_STRING_NOR_ARRAY_NOR_MAP(INIT_VTABLE) + INIT_VTABLE(GrowableObjectArray) #undef INIT_VTABLE #define INIT_VTABLE(clazz) \ @@ -630,7 +631,7 @@ void Object::InitVtables() { Array fake_handle; \ builtin_vtables_[k##clazz##Cid] = fake_handle.vtable(); \ } - CLASS_LIST_ARRAYS(INIT_VTABLE) + CLASS_LIST_FIXED_LENGTH_ARRAYS(INIT_VTABLE) #undef INIT_VTABLE #define INIT_VTABLE(clazz) \ @@ -19265,7 +19266,7 @@ AbstractTypePtr Instance::GetType(Heap::Space space) const { } Thread* thread = Thread::Current(); Zone* zone = thread->zone(); - const Class& cls = Class::Handle(zone, clazz()); + Class& cls = Class::Handle(zone, clazz()); if (!cls.is_finalized()) { // Various predefined classes can be instantiated by the VM or // Dart_NewString/Integer/TypedData/... before the class is finalized. @@ -19281,6 +19282,9 @@ AbstractTypePtr Instance::GetType(Heap::Space space) const { signature ^= signature.Canonicalize(thread, nullptr); return signature.ptr(); } + if (IsArrayClassId(cls.id())) { + cls = thread->isolate_group()->object_store()->list_class(); + } Type& type = Type::Handle(zone); if (!cls.IsGeneric()) { type = cls.DeclarationType(); diff --git a/runtime/vm/object_graph_copy.cc b/runtime/vm/object_graph_copy.cc index 76d65b65572b..1a54883aeced 100644 --- a/runtime/vm/object_graph_copy.cc +++ b/runtime/vm/object_graph_copy.cc @@ -1009,6 +1009,7 @@ class ObjectCopy : public Base { CLASS_LIST_NO_OBJECT_NOR_STRING_NOR_ARRAY_NOR_MAP(COPY_TO) COPY_TO(Array) + COPY_TO(GrowableObjectArray) COPY_TO(LinkedHashMap) COPY_TO(LinkedHashSet) #undef COPY_TO diff --git a/runtime/vm/service.cc b/runtime/vm/service.cc index 71e0b4899cf3..0efa24b5c4fb 100644 --- a/runtime/vm/service.cc +++ b/runtime/vm/service.cc @@ -5208,7 +5208,6 @@ static void GetDefaultClassesAliases(Thread* thread, JSONStream* js) { { JSONArray internals(&map, "List"); CLASS_LIST_ARRAYS(DEFINE_ADD_VALUE_F_CID) - DEFINE_ADD_VALUE_F_CID(GrowableObjectArray) DEFINE_ADD_VALUE_F_CID(ByteBuffer) } {