blob: b3699d337e406871c4278b431d1d518a87c38925 [file] [log] [blame]
// Copyright (c) 2020, 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 <functional>
#include "platform/assert.h"
#include "vm/class_finalizer.h"
#include "vm/compiler/assembler/disassembler.h"
#include "vm/compiler/backend/flow_graph_compiler.h"
#include "vm/compiler/backend/il_test_helper.h"
#include "vm/flags.h"
#include "vm/lockers.h"
#include "vm/stack_frame.h"
#include "vm/symbols.h"
#include "vm/type_testing_stubs.h"
#include "vm/unit_test.h"
#include "vm/zone_text_buffer.h"
#if !defined(TARGET_ARCH_IA32)
namespace dart {
DECLARE_FLAG(int, max_subtype_cache_entries);
// Note that flags that this affects may only mutable in some modes, e.g.,
// tracing type checks can only be done in DEBUG mode.
DEFINE_FLAG(bool,
trace_type_testing_stub_tests,
false,
"Trace type testing stub tests");
DEFINE_FLAG(bool,
print_type_testing_stub_test_headers,
true,
"Print headers for executed type testing stub tests");
class TraceStubInvocationScope : public ValueObject {
public:
TraceStubInvocationScope()
: old_trace_type_checks_(FLAG_trace_type_checks),
old_disassemble_stubs_(FLAG_disassemble_stubs) {
if (FLAG_trace_type_testing_stub_tests) {
#if defined(DEBUG)
FLAG_trace_type_checks = true;
#endif
#if defined(FORCE_INCLUDE_DISASSEMBLER) || !defined(PRODUCT)
FLAG_disassemble_stubs = true;
#endif
}
}
~TraceStubInvocationScope() {
if (FLAG_trace_type_testing_stub_tests) {
#if defined(DEBUG)
FLAG_trace_type_checks = old_trace_type_checks_;
#endif
#if defined(FORCE_INCLUDE_DISASSEMBLER) || !defined(PRODUCT)
FLAG_disassemble_stubs = old_disassemble_stubs_;
#endif
}
}
private:
const bool old_trace_type_checks_;
const bool old_disassemble_stubs_;
};
#define __ assembler->
static void GenerateInvokeTTSStub(compiler::Assembler* assembler) {
auto calculate_breadcrumb = [](const Register& reg) {
return 0x10 + 2 * (static_cast<intptr_t>(reg));
};
__ EnterDartFrame(0);
for (intptr_t i = 0; i < kNumberOfCpuRegisters; ++i) {
if (((1 << i) & kDartAvailableCpuRegs) == 0) continue;
if (((1 << i) & TypeTestABI::kAbiRegisters) != 0) continue;
if (((1 << i) & TTSInternalRegs::kInternalRegisters) != 0) continue;
const Register reg = static_cast<Register>(i);
__ LoadImmediate(reg, calculate_breadcrumb(reg));
}
// Load the arguments into the right TTS calling convention registers.
const intptr_t instance_offset =
(kCallerSpSlotFromFp + 3) * compiler::target::kWordSize;
const intptr_t inst_type_args_offset =
(kCallerSpSlotFromFp + 2) * compiler::target::kWordSize;
const intptr_t fun_type_args_offset =
(kCallerSpSlotFromFp + 1) * compiler::target::kWordSize;
const intptr_t dst_type_offset =
(kCallerSpSlotFromFp + 0) * compiler::target::kWordSize;
__ LoadMemoryValue(TypeTestABI::kInstanceReg, FPREG, instance_offset);
__ LoadMemoryValue(TypeTestABI::kInstantiatorTypeArgumentsReg, FPREG,
inst_type_args_offset);
__ LoadMemoryValue(TypeTestABI::kFunctionTypeArgumentsReg, FPREG,
fun_type_args_offset);
__ LoadMemoryValue(TypeTestABI::kDstTypeReg, FPREG, dst_type_offset);
const intptr_t subtype_test_cache_index = __ object_pool_builder().AddObject(
Object::null_object(), compiler::ObjectPoolBuilderEntry::kPatchable);
const intptr_t dst_name_index = __ object_pool_builder().AddObject(
Symbols::OptimizedOut(), compiler::ObjectPoolBuilderEntry::kPatchable);
ASSERT_EQUAL(subtype_test_cache_index + 1, dst_name_index);
ASSERT(__ constant_pool_allowed());
FlowGraphCompiler::GenerateIndirectTTSCall(
assembler, TypeTestABI::kDstTypeReg, subtype_test_cache_index);
// We have the guarantee that TTS preserves all input registers, if the TTS
// handles the type test successfully.
//
// Let the test know which TTS abi registers were not preserved.
ASSERT(((1 << static_cast<intptr_t>(TypeTestABI::kInstanceReg)) &
TypeTestABI::kPreservedAbiRegisters) != 0);
// First we check the instance register, freeing it up in case there are no
// other safe registers to use since we need two registers: one to accumulate
// the register mask, another to load the array address when saving the mask.
__ LoadFromOffset(TypeTestABI::kScratchReg, FPREG, instance_offset);
compiler::Label instance_matches, done_with_instance;
__ CompareRegisters(TypeTestABI::kScratchReg, TypeTestABI::kInstanceReg);
__ BranchIf(EQUAL, &instance_matches, compiler::Assembler::kNearJump);
__ LoadImmediate(TypeTestABI::kScratchReg,
1 << static_cast<intptr_t>(TypeTestABI::kInstanceReg));
__ Jump(&done_with_instance, compiler::Assembler::kNearJump);
__ Bind(&instance_matches);
__ LoadImmediate(TypeTestABI::kScratchReg, 0);
__ Bind(&done_with_instance);
for (intptr_t i = 0; i < kNumberOfCpuRegisters; ++i) {
if (((1 << i) & TypeTestABI::kPreservedAbiRegisters) == 0) continue;
const Register reg = static_cast<Register>(i);
compiler::Label done;
switch (reg) {
case TypeTestABI::kInstanceReg:
// Skip the already handled instance register.
continue;
case TypeTestABI::kDstTypeReg:
__ LoadFromOffset(TypeTestABI::kInstanceReg, FPREG, dst_type_offset);
break;
case TypeTestABI::kFunctionTypeArgumentsReg:
__ LoadFromOffset(TypeTestABI::kInstanceReg, FPREG,
fun_type_args_offset);
break;
case TypeTestABI::kInstantiatorTypeArgumentsReg:
__ LoadFromOffset(TypeTestABI::kInstanceReg, FPREG,
inst_type_args_offset);
break;
default:
FATAL("Unexpected register %s", RegisterNames::RegisterName(reg));
break;
}
__ CompareRegisters(reg, TypeTestABI::kInstanceReg);
__ BranchIf(EQUAL, &done, compiler::Assembler::kNearJump);
__ AddImmediate(TypeTestABI::kScratchReg, 1 << i);
__ Bind(&done);
}
__ SmiTag(TypeTestABI::kScratchReg);
__ LoadFromOffset(TypeTestABI::kInstanceReg, FPREG,
(kCallerSpSlotFromFp + 5) * compiler::target::kWordSize);
__ StoreFieldToOffset(TypeTestABI::kScratchReg, TypeTestABI::kInstanceReg,
compiler::target::Array::element_offset(0));
// Let the test know which non-TTS abi registers were not preserved.
__ LoadImmediate(TypeTestABI::kScratchReg, 0);
for (intptr_t i = 0; i < kNumberOfCpuRegisters; ++i) {
if (((1 << i) & kDartAvailableCpuRegs) == 0) continue;
if (((1 << i) & TypeTestABI::kAbiRegisters) != 0) continue;
const Register reg = static_cast<Register>(i);
compiler::Label done;
__ CompareImmediate(reg, calculate_breadcrumb(reg));
__ BranchIf(EQUAL, &done, compiler::Assembler::kNearJump);
__ AddImmediate(TypeTestABI::kScratchReg, 1 << i);
__ Bind(&done);
}
__ SmiTag(TypeTestABI::kScratchReg);
__ LoadFromOffset(TypeTestABI::kInstanceReg, FPREG,
(kCallerSpSlotFromFp + 4) * compiler::target::kWordSize);
__ StoreFieldToOffset(TypeTestABI::kScratchReg, TypeTestABI::kInstanceReg,
compiler::target::Array::element_offset(0));
// Set the return from the stub to be null.
__ LoadObject(CallingConventions::kReturnReg, Object::null_object());
__ LeaveDartFrame();
__ Ret();
}
#undef __
static void FinalizeAndCanonicalize(AbstractType* type) {
*type = ClassFinalizer::FinalizeType(*type);
ASSERT(type->IsCanonical());
}
static void CanonicalizeTAV(TypeArguments* tav) {
*tav = tav->Canonicalize(Thread::Current());
}
#if defined(TESTING)
// Defined before DRT_TypeCheck in runtime_entry.cc.
extern bool TESTING_runtime_entered_on_TTS_invocation;
extern bool TESTING_found_hash_STC_entry;
#endif
enum TTSTestResult {
// The TTS invocation should enter the runtime and trigger a TypeError.
kFail,
// The TTS invocation should return a result from the TTS stub without
// entering the runtime and without an associated STC entry.
kTTS,
// The TTS invocation should return a result from checking the STC without
// entering the runtime. The STC prior to invocation should be non-null and
// include a matching entry for the test case.
kExistingSTCEntry,
// The TTS invocation should enter the runtime and add a new entry to the
// STC, creating a new STC if necessary.
kNewSTCEntry,
// The TTS invocation should enter the runtime and return a successful check
// but without adding an entry to the STC. This should only happen when the
// STC has hit the limit described by FLAG_max_subtype_cache_entries prior
// to the invocation.
kRuntimeCheck,
// The TTS invocation should enter the runtime and the TTS is specialized
// after this test case. This test case should not create a new STC or modify
// an existing STC.
//
// Used only within TTSTestState::InvokeLazilySpecializedStub. This is
// needed because there is a single special case for updating the STC that
// happens only when specializing the lazy specialization stub, and so we
// need to distinguish specialization and respecialization.
kSpecialize,
// The TTS invocation should enter the runtime and the TTS is respecialized
// after this test case. This test case should not create a new STC or modify
// an existing STC.
//
// Used only when calling TTSTestState::InvokeExistingStub.
kRespecialize,
// Used for static assert below only.
kNumTestResults,
};
static const char* kTestResultStrings[] = {
"fails in runtime",
"passes in TTS",
"passes in STC stub",
"passes in runtime, adding new STC entry",
"passes in runtime, no changes to max size STC",
"passes in runtime, initial TTS stub specialization",
"passes in runtime, TTS stub respecialized",
};
// Just to make sure the above are kept in sync.
static_assert(sizeof(kTestResultStrings) >=
kNumTestResults * sizeof(*kTestResultStrings),
"kTestResultStrings has too few entries");
static_assert(sizeof(kTestResultStrings) <=
kNumTestResults * sizeof(*kTestResultStrings),
"kTestResultStrings has extra entries");
struct TTSTestCase {
const Object& instance;
const TypeArguments& instantiator_tav;
const TypeArguments& function_tav;
const TTSTestResult expected_result;
TTSTestCase(const Object& obj,
const TypeArguments& i_tav,
const TypeArguments& f_tav,
const TTSTestResult result = kTTS)
: instance(obj),
instantiator_tav(i_tav),
function_tav(f_tav),
expected_result(result) {}
bool ShouldEnterRuntime() const {
switch (expected_result) {
case kTTS:
case kExistingSTCEntry:
return false;
case kFail:
case kNewSTCEntry:
case kRuntimeCheck:
case kSpecialize:
case kRespecialize:
return true;
default:
UNREACHABLE();
}
}
bool HasSameSTCEntry(const TTSTestCase& other) const {
if (instantiator_tav.ptr() != other.instantiator_tav.ptr()) {
return false;
}
if (function_tav.ptr() != other.function_tav.ptr()) {
return false;
}
if (instance.IsClosure() && other.instance.IsClosure()) {
const auto& closure = Closure::Cast(instance);
const auto& other_closure = Closure::Cast(other.instance);
const auto& sig = FunctionType::Handle(
Function::Handle(closure.function()).signature());
const auto& other_sig = FunctionType::Handle(
Function::Handle(other_closure.function()).signature());
return sig.ptr() == other_sig.ptr() &&
closure.instantiator_type_arguments() ==
other_closure.instantiator_type_arguments() &&
closure.function_type_arguments() ==
other_closure.function_type_arguments() &&
closure.delayed_type_arguments() ==
other_closure.delayed_type_arguments();
}
const intptr_t cid = instance.GetClassId();
const intptr_t other_cid = other.instance.GetClassId();
if (cid != other_cid) {
return false;
}
const auto& cls = Class::Handle(instance.clazz());
if (cls.NumTypeArguments() == 0) {
return true;
}
return Instance::Cast(instance).GetTypeArguments() ==
Instance::Cast(other.instance).GetTypeArguments();
}
bool HasSTCEntry(const SubtypeTestCache& cache,
const AbstractType& dst_type,
Bool* out_result = nullptr,
intptr_t* out_index = nullptr) const {
if (cache.IsNull()) return false;
if (instance.IsClosure()) {
const auto& closure = Closure::Cast(instance);
const auto& sig = FunctionType::Handle(
Function::Handle(closure.function()).signature());
const auto& closure_instantiator_type_arguments =
TypeArguments::Handle(closure.instantiator_type_arguments());
const auto& closure_function_type_arguments =
TypeArguments::Handle(closure.function_type_arguments());
const auto& closure_delayed_type_arguments =
TypeArguments::Handle(closure.delayed_type_arguments());
return cache.HasCheck(
sig, dst_type, closure_instantiator_type_arguments, instantiator_tav,
function_tav, closure_function_type_arguments,
closure_delayed_type_arguments, out_index, out_result);
}
const auto& id_smi = Smi::Handle(Smi::New(instance.GetClassId()));
const auto& cls = Class::Handle(instance.clazz());
auto& instance_type_arguments = TypeArguments::Handle();
if (cls.NumTypeArguments() > 0) {
instance_type_arguments = Instance::Cast(instance).GetTypeArguments();
}
return cache.HasCheck(id_smi, dst_type, instance_type_arguments,
instantiator_tav, function_tav,
Object::null_type_arguments(),
Object::null_type_arguments(), out_index, out_result);
}
private:
DISALLOW_ALLOCATION();
};
// Takes an existing test case and creates a failing test case from it.
static TTSTestCase Failure(const TTSTestCase& original) {
return TTSTestCase(original.instance, original.instantiator_tav,
original.function_tav, kFail);
}
// Takes an existing test case and creates a passing test case that finds an
// existing STC entry without entering the runtime.
static TTSTestCase STCCheck(const TTSTestCase& original) {
return TTSTestCase(original.instance, original.instantiator_tav,
original.function_tav, kExistingSTCEntry);
}
// Takes an existing test case and creates a passing test case that adds a
// new STC entry.
static TTSTestCase FalseNegative(const TTSTestCase& original) {
return TTSTestCase(original.instance, original.instantiator_tav,
original.function_tav, kNewSTCEntry);
}
// Takes an existing test case and creates a test case that should go to the
// runtime and pass but not modify the STC, as the STC has grown too large.
static TTSTestCase RuntimeCheck(const TTSTestCase& original) {
return TTSTestCase(original.instance, original.instantiator_tav,
original.function_tav, kRuntimeCheck);
}
// Takes an existing test case and creates a passing test case that should go t
// the runtime and (re)specialize the STC.
static TTSTestCase Respecialization(const TTSTestCase& original) {
return TTSTestCase(original.instance, original.instantiator_tav,
original.function_tav, kRespecialize);
}
class TTSTestState : public ValueObject {
public:
TTSTestState(Thread* thread, const AbstractType& type)
: thread_(thread),
type_(AbstractType::Handle(zone(), type.ptr())),
modified_abi_regs_box_(Array::Handle(zone(), Array::New(1))),
modified_rest_regs_box_(Array::Handle(zone(), Array::New(1))),
tts_invoker_(
Code::Handle(zone(), CreateInvocationStub(thread_, zone()))),
pool_(ObjectPool::Handle(zone(), tts_invoker_.object_pool())),
arguments_descriptor_(
Array::Handle(ArgumentsDescriptor::NewBoxed(0, 6))),
previous_tts_stub_(Code::Handle(zone())),
previous_stc_(SubtypeTestCache::Handle(zone())),
last_arguments_(Array::Handle(zone())),
last_tested_type_(AbstractType::Handle(zone())),
new_tts_stub_(Code::Handle(zone())),
last_stc_(SubtypeTestCache::Handle(zone())),
last_result_(Object::Handle(zone())) {
if (FLAG_print_type_testing_stub_test_headers) {
THR_Print("Creating test state for type %s\n", type.ToCString());
}
}
Zone* zone() const { return thread_->zone(); }
const SubtypeTestCache& last_stc() const { return last_stc_; }
// For cases where the STC may have been reset/removed, like reloading.
const SubtypeTestCachePtr current_stc() const {
return SubtypeTestCache::RawCast(pool_.ObjectAt(kSubtypeTestCacheIndex));
}
AbstractTypePtr TypeToTest(const TTSTestCase& test_case) const {
if (type_.IsTypeParameter()) {
return TypeParameter::Cast(type_).GetFromTypeArguments(
test_case.instantiator_tav, test_case.function_tav);
}
return type_.ptr();
}
void ClearCache() {
pool_.SetObjectAt(kSubtypeTestCacheIndex, Object::null_object());
}
void InvokeEagerlySpecializedStub(const TTSTestCase& test_case) {
// This shouldn't be used except within InvokeLazilySpecializedStub.
EXPECT_NE(kSpecialize, test_case.expected_result);
// A new stub is being installed, so we won't respecialize.
EXPECT_NE(kRespecialize, test_case.expected_result);
last_tested_type_ = TypeToTest(test_case);
{
// To make sure we output the disassembled stub if desired.
TraceStubInvocationScope scope;
previous_tts_stub_ = TypeTestingStubGenerator::SpecializeStubFor(
thread_, last_tested_type_);
}
last_tested_type_.SetTypeTestingStub(previous_tts_stub_);
PrintInvocationHeader("eagerly specialized", test_case);
InvokeStubHelper(test_case);
// Treat it as a failure if the stub respecializes, since we're attempting
// to simulate AOT mode.
EXPECT(previous_tts_stub_.ptr() == new_tts_stub_.ptr());
}
void InvokeLazilySpecializedStub(const TTSTestCase& test_case,
bool should_specialize = true) {
// This is governed by the should_specialize parameter.
EXPECT_NE(kSpecialize, test_case.expected_result);
// A new stub is being installed, so we won't respecialize.
EXPECT_NE(kRespecialize, test_case.expected_result);
last_tested_type_ = TypeToTest(test_case);
const auto& specializing_stub =
Code::Handle(zone(), TypeTestingStubGenerator::DefaultCodeForType(
last_tested_type_, /*lazy_specialize=*/true));
last_tested_type_.SetTypeTestingStub(specializing_stub);
const TTSTestCase initial_case{
test_case.instance, test_case.instantiator_tav, test_case.function_tav,
should_specialize && test_case.expected_result != kFail
? kSpecialize
: test_case.expected_result};
PrintInvocationHeader("lazy specialized", initial_case);
InvokeStubHelper(initial_case);
if (initial_case.expected_result == kSpecialize) {
EXPECT(previous_tts_stub_.ptr() != new_tts_stub_.ptr());
} else {
EXPECT(previous_tts_stub_.ptr() == new_tts_stub_.ptr());
}
}
void InvokeExistingStub(const TTSTestCase& test_case) {
// This shouldn't be used except within InvokeLazilySpecializedStub.
EXPECT_NE(kSpecialize, test_case.expected_result);
last_tested_type_ = TypeToTest(test_case);
PrintInvocationHeader("existing", test_case);
InvokeStubHelper(test_case);
// Only respecialization should result in a new stub.
EXPECT_EQ(test_case.expected_result == kRespecialize,
previous_tts_stub_.ptr() != new_tts_stub_.ptr());
}
private:
static constexpr intptr_t kSubtypeTestCacheIndex = 0;
SmiPtr modified_abi_regs() const {
if (modified_abi_regs_box_.At(0)->IsHeapObject()) return Smi::null();
return Smi::RawCast(modified_abi_regs_box_.At(0));
}
SmiPtr modified_rest_regs() const {
if (modified_rest_regs_box_.At(0)->IsHeapObject()) return Smi::null();
return Smi::RawCast(modified_rest_regs_box_.At(0));
}
void PrintInvocationHeader(const char* stub_type,
const TTSTestCase& test_case) {
if (!FLAG_print_type_testing_stub_test_headers) return;
LogBlock lb;
const auto& tts = Code::Handle(zone(), last_tested_type_.type_test_stub());
auto* const stub_name = StubCode::NameOfStub(tts.EntryPoint());
THR_Print("Testing %s %s stub for type %s\n",
stub_name == nullptr ? "optimized" : stub_name, stub_type,
last_tested_type_.ToCString());
if (last_tested_type_.ptr() != type_.ptr()) {
THR_Print(" Original type: %s\n", type_.ToCString());
}
THR_Print(" Instance: %s\n", test_case.instance.ToCString());
THR_Print(" Instantiator TAV: %s\n",
test_case.instantiator_tav.ToCString());
THR_Print(" Function TAV: %s\n", test_case.function_tav.ToCString());
THR_Print(" Expected result: %s\n",
kTestResultStrings[test_case.expected_result]);
}
static CodePtr CreateInvocationStub(Thread* thread, Zone* zone) {
const auto& klass = Class::Handle(
zone, thread->isolate_group()->class_table()->At(kInstanceCid));
const auto& symbol = String::Handle(
zone, Symbols::New(thread, OS::SCreate(zone, "TTSTest")));
const auto& signature = FunctionType::Handle(zone, FunctionType::New());
const auto& function = Function::Handle(
zone, Function::New(
signature, symbol, UntaggedFunction::kRegularFunction, false,
false, false, false, false, klass, TokenPosition::kNoSource));
TraceStubInvocationScope scope;
compiler::ObjectPoolBuilder pool_builder;
SafepointWriteRwLocker ml(thread, thread->isolate_group()->program_lock());
compiler::Assembler assembler(&pool_builder);
GenerateInvokeTTSStub(&assembler);
const Code& invoke_tts = Code::Handle(Code::FinalizeCodeAndNotify(
"InvokeTTS", nullptr, &assembler, Code::PoolAttachment::kNotAttachPool,
/*optimized=*/false));
const auto& pool =
ObjectPool::Handle(zone, ObjectPool::NewFromBuilder(pool_builder));
invoke_tts.set_object_pool(pool.ptr());
invoke_tts.set_owner(function);
invoke_tts.set_exception_handlers(
ExceptionHandlers::Handle(zone, ExceptionHandlers::New(0)));
EXPECT_EQ(2, pool.Length());
if (FLAG_support_disassembler && FLAG_disassemble_stubs) {
Disassembler::DisassembleStub(symbol.ToCString(), invoke_tts);
}
return invoke_tts.ptr();
}
void InvokeStubHelper(const TTSTestCase& test_case) {
ASSERT(test_case.instantiator_tav.IsNull() ||
test_case.instantiator_tav.IsCanonical());
ASSERT(test_case.function_tav.IsNull() ||
test_case.function_tav.IsCanonical());
modified_abi_regs_box_.SetAt(0, Object::null_object());
modified_rest_regs_box_.SetAt(0, Object::null_object());
last_arguments_ = Array::New(6);
last_arguments_.SetAt(0, modified_abi_regs_box_);
last_arguments_.SetAt(1, modified_rest_regs_box_);
last_arguments_.SetAt(2, test_case.instance);
last_arguments_.SetAt(3, test_case.instantiator_tav);
last_arguments_.SetAt(4, test_case.function_tav);
last_arguments_.SetAt(5, type_);
previous_tts_stub_ = last_tested_type_.type_test_stub();
previous_stc_ = current_stc();
{
SafepointMutexLocker ml(
thread_->isolate_group()->subtype_test_cache_mutex());
previous_stc_ = previous_stc_.Copy(thread_);
}
#if defined(TESTING)
// Clear the runtime entered flag prior to invocation.
TESTING_runtime_entered_on_TTS_invocation = false;
#endif
{
TraceStubInvocationScope scope;
last_result_ = DartEntry::InvokeCode(tts_invoker_, arguments_descriptor_,
last_arguments_, thread_);
}
#if defined(TESTING)
EXPECT_EQ(test_case.ShouldEnterRuntime(),
TESTING_runtime_entered_on_TTS_invocation);
#endif
new_tts_stub_ = last_tested_type_.type_test_stub();
last_stc_ = current_stc();
if (test_case.expected_result == kFail) {
EXPECT(!last_result_.IsNull());
EXPECT(last_result_.IsError());
EXPECT(last_result_.IsUnhandledException());
if (last_result_.IsUnhandledException()) {
const auto& error = Instance::Handle(
UnhandledException::Cast(last_result_).exception());
EXPECT(strstr(error.ToCString(), "_TypeError"));
}
} else {
EXPECT(last_result_.IsNull());
if (!last_result_.IsNull()) {
EXPECT(last_result_.IsError());
EXPECT(last_result_.IsUnhandledException());
if (last_result_.IsUnhandledException()) {
const auto& exception = UnhandledException::Cast(last_result_);
FAIL("%s", exception.ToErrorCString());
}
} else {
EXPECT(new_tts_stub_.ptr() != StubCode::LazySpecializeTypeTest().ptr());
ReportModifiedRegisters(modified_abi_regs());
// If we shouldn't go to the runtime, report any unexpected changes in
// non-ABI registers.
if (!test_case.ShouldEnterRuntime()) {
ReportModifiedRegisters(modified_rest_regs());
}
}
}
ReportUnexpectedSTCChanges(test_case);
}
static void ReportModifiedRegisters(SmiPtr encoded_reg_mask) {
if (encoded_reg_mask == Smi::null()) {
FAIL("No modified register information");
return;
}
const intptr_t reg_mask = Smi::Value(encoded_reg_mask);
for (intptr_t i = 0; i < kNumberOfCpuRegisters; i++) {
if (((1 << i) & reg_mask) != 0) {
const Register reg = static_cast<Register>(i);
FAIL("%s was modified", RegisterNames::RegisterName(reg));
}
}
}
void ReportMissingOrChangedEntries(const SubtypeTestCache& old_cache,
const SubtypeTestCache& new_cache) {
auto& cid_or_sig = Object::Handle(zone());
auto& type = AbstractType::Handle(zone());
auto& instance_type_args = TypeArguments::Handle(zone());
auto& instantiator_type_args = TypeArguments::Handle(zone());
auto& function_type_args = TypeArguments::Handle(zone());
auto& instance_parent_type_args = TypeArguments::Handle(zone());
auto& instance_delayed_type_args = TypeArguments::Handle(zone());
auto& old_result = Bool::Handle(zone());
auto& new_result = Bool::Handle(zone());
SafepointMutexLocker ml(
thread_->isolate_group()->subtype_test_cache_mutex());
intptr_t i = 0;
while (old_cache.GetNextCheck(&i, &cid_or_sig, &type, &instance_type_args,
&instantiator_type_args, &function_type_args,
&instance_parent_type_args,
&instance_delayed_type_args, &old_result)) {
if (!new_cache.HasCheck(
cid_or_sig, type, instance_type_args, instantiator_type_args,
function_type_args, instance_parent_type_args,
instance_delayed_type_args, /*index=*/nullptr, &new_result)) {
FAIL("New STC is missing check in old STC");
}
if (old_result.value() != new_result.value()) {
FAIL("New STC has different result from old STC");
}
}
}
// Returns whether the given test case is expected to have updated the STC.
// Should only be called after TTS invocation, as it uses last_tested_type_.
bool ShouldUpdateCache(const TTSTestCase& test_case) const {
if (test_case.expected_result == kSpecialize) {
// Check to see if we got a default stub from specializing. Should match
// the definition of would_update_cache_if_not_lazy in DRT_TypeCheck.
const bool would_update_cache_if_not_lazy =
!test_case.instance.IsNull() &&
(last_tested_type_.type_test_stub() ==
StubCode::DefaultNullableTypeTest().ptr() ||
last_tested_type_.type_test_stub() ==
StubCode::DefaultTypeTest().ptr());
return would_update_cache_if_not_lazy && previous_stc_.IsNull();
} else {
return test_case.expected_result == kNewSTCEntry;
}
}
void ReportUnexpectedSTCChanges(const TTSTestCase& test_case) {
const bool hit_check_cap =
!previous_stc_.IsNull() &&
previous_stc_.NumberOfChecks() >= FLAG_max_subtype_cache_entries;
EXPECT(test_case.expected_result != kRuntimeCheck || hit_check_cap);
const bool should_update_cache = ShouldUpdateCache(test_case);
if (should_update_cache) {
// We should not have already had an entry.
EXPECT(!test_case.HasSTCEntry(previous_stc_, type_));
// We should have changed the STC to include the new entry.
EXPECT(!last_stc_.IsNull());
if (!last_stc_.IsNull()) {
EXPECT(previous_stc_.IsNull() ||
previous_stc_.cache() != last_stc_.cache());
// We only should have added one check.
EXPECT_EQ(
previous_stc_.IsNull() ? 1 : previous_stc_.NumberOfChecks() + 1,
last_stc_.NumberOfChecks());
if (!previous_stc_.IsNull()) {
// Make sure all the checks in the previous STC are still there.
ReportMissingOrChangedEntries(previous_stc_, last_stc_);
}
}
} else {
// Whatever STC existed before, if any, should be unchanged.
if (previous_stc_.IsNull()) {
EXPECT(last_stc_.IsNull());
} else {
EXPECT(!last_stc_.IsNull());
const auto& previous_array =
Array::Handle(zone(), previous_stc_.cache());
const auto& last_array = Array::Handle(zone(), last_stc_.cache());
EXPECT(last_array.Equals(previous_array));
}
}
const bool has_stc_entry = test_case.HasSTCEntry(last_stc_, type_);
// We also expect an entry if we expect an STC hit in the STC stub or in
// the runtime.
const bool expects_stc_entry =
should_update_cache || test_case.expected_result == kExistingSTCEntry;
if ((!expects_stc_entry && has_stc_entry) ||
(expects_stc_entry && !has_stc_entry)) {
ZoneTextBuffer buffer(zone());
buffer.Printf("%s STC entry for:\n instance:%s\n destination type: %s",
expects_stc_entry ? "Expected" : "Did not expect",
test_case.instance.ToCString(), type_.ToCString());
if (last_tested_type_.ptr() != type_.ptr()) {
buffer.Printf("\n tested type: %s", last_tested_type_.ToCString());
}
buffer.AddString("\ngot:");
if (last_stc_.IsNull()) {
buffer.AddString(" null");
} else {
buffer.AddString("\n");
SafepointMutexLocker ml(
thread_->isolate_group()->subtype_test_cache_mutex());
last_stc_.WriteToBuffer(zone(), &buffer, " ");
}
THR_Print("%s\n", buffer.buffer());
FAIL("unexpected STC modification");
}
}
Thread* const thread_;
const AbstractType& type_;
const Array& modified_abi_regs_box_;
const Array& modified_rest_regs_box_;
const Code& tts_invoker_;
const ObjectPool& pool_;
const Array& arguments_descriptor_;
Code& previous_tts_stub_;
SubtypeTestCache& previous_stc_;
Array& last_arguments_;
AbstractType& last_tested_type_;
Code& new_tts_stub_;
SubtypeTestCache& last_stc_;
Object& last_result_;
};
// Tests three situations in turn with the test case and with an
// appropriate null object test:
// 1) Install the lazy specialization stub for JIT and test.
// 2) Test again without installing a stub, so using the stub resulting from 1.
// 3) Install an eagerly specialized stub, similar to AOT mode but keeping any
// STC created by the earlier steps, and test.
static void RunTTSTest(const AbstractType& dst_type,
const TTSTestCase& test_case,
bool should_specialize = true) {
// We're testing null here, there's no need to call this with null.
EXPECT(!test_case.instance.IsNull());
// should_specialize is what governs whether specialization is expected here.
EXPECT_NE(kSpecialize, test_case.expected_result);
EXPECT_NE(kRespecialize, test_case.expected_result);
// We're creating a new STC so it _can't_ use an existing entry.
EXPECT_NE(kExistingSTCEntry, test_case.expected_result);
bool null_should_fail = !Instance::NullIsAssignableTo(
dst_type, test_case.instantiator_tav, test_case.function_tav);
const TTSTestCase null_test{Instance::Handle(), test_case.instantiator_tav,
test_case.function_tav,
null_should_fail ? kFail : kTTS};
// First test the lazy case.
{
TTSTestState state(Thread::Current(), dst_type);
// We only get specialization when testing null if the type being tested
// isn't nullable and it is either a Type or a RecordType.
const auto& type_to_test =
AbstractType::Handle(state.TypeToTest(test_case));
const bool null_should_specialize =
null_should_fail &&
(type_to_test.IsType() || type_to_test.IsRecordType());
// First check the null case. This should _never_ create an STC.
state.InvokeLazilySpecializedStub(null_test, null_should_specialize);
state.InvokeExistingStub(null_test);
EXPECT(state.last_stc().IsNull());
// Now run the actual test case, starting with a fresh stub.
state.InvokeLazilySpecializedStub(test_case, should_specialize);
if (test_case.expected_result == kNewSTCEntry) {
if (state.last_stc().IsNull()) {
// The stub specialized to a non-default stub, so we need to run the
// test again to see it added to the cache.
state.InvokeExistingStub(test_case);
} else {
// The stub doesn't specialize or it specialized to a default stub,
// in which case the entry did get added to the STC already.
}
state.InvokeExistingStub(STCCheck(test_case));
} else {
state.InvokeExistingStub(test_case);
}
}
// Now test the eager case with a fresh stub/null STC.
{
TTSTestState state(Thread::Current(), dst_type);
// First check the null case. This should _never_ create an STC.
state.InvokeEagerlySpecializedStub(null_test);
state.InvokeExistingStub(null_test);
EXPECT(state.last_stc().IsNull());
state.InvokeEagerlySpecializedStub(test_case);
if (test_case.expected_result == kNewSTCEntry) {
// When eagerly specializing, the first invocation always adds to the STC.
state.InvokeExistingStub(STCCheck(test_case));
} else {
// Other cases should be idempotent.
state.InvokeExistingStub(test_case);
}
}
}
const char* kSubtypeRangeCheckScript =
R"(
class I<T, U> {}
class I2 {}
class Base<T> {}
class A extends Base<int> {}
class A1 extends A implements I2 {}
class A2<T> extends A implements I<int, T> {}
class B extends Base<String> {}
class B1 extends B implements I2 {}
class B2<T> extends B implements I<T, String> {}
genericFun<A, B>() {}
createI() => I<int, String>();
createI2() => I2();
createBaseInt() => Base<int>();
createBaseNull() => Base<Null>();
createBaseNever() => Base<Never>();
createA() => A();
createA1() => A1();
createA2() => A2<int>();
createB() => B();
createB1() => B1();
createB2() => B2<int>();
createBaseIStringDouble() => Base<I<String, double>>();
createBaseA2Int() => Base<A2<int>>();
createBaseA2A1() => Base<A2<A1>>();
createBaseB2Int() => Base<B2<int>>();
)";
ISOLATE_UNIT_TEST_CASE(TTS_SubtypeRangeCheck) {
const auto& root_library =
Library::Handle(LoadTestScript(kSubtypeRangeCheckScript));
const auto& class_a = Class::Handle(GetClass(root_library, "A"));
const auto& class_base = Class::Handle(GetClass(root_library, "Base"));
const auto& class_i = Class::Handle(GetClass(root_library, "I"));
const auto& class_i2 = Class::Handle(GetClass(root_library, "I2"));
const auto& obj_i = Object::Handle(Invoke(root_library, "createI"));
const auto& obj_i2 = Object::Handle(Invoke(root_library, "createI2"));
const auto& obj_base_int =
Object::Handle(Invoke(root_library, "createBaseInt"));
const auto& obj_base_null =
Object::Handle(Invoke(root_library, "createBaseNull"));
const auto& obj_base_never =
Object::Handle(Invoke(root_library, "createBaseNever"));
const auto& obj_a = Object::Handle(Invoke(root_library, "createA"));
const auto& obj_a1 = Object::Handle(Invoke(root_library, "createA1"));
const auto& obj_a2 = Object::Handle(Invoke(root_library, "createA2"));
const auto& obj_b = Object::Handle(Invoke(root_library, "createB"));
const auto& obj_b1 = Object::Handle(Invoke(root_library, "createB1"));
const auto& obj_b2 = Object::Handle(Invoke(root_library, "createB2"));
const auto& type_dynamic = Type::Handle(Type::DynamicType());
auto& type_object = Type::Handle(Type::ObjectType());
type_object = type_object.ToNullability(Nullability::kNullable, Heap::kNew);
const auto& tav_null = TypeArguments::Handle(TypeArguments::null());
auto& tav_object = TypeArguments::Handle(TypeArguments::New(1));
tav_object.SetTypeAt(0, type_object);
CanonicalizeTAV(&tav_object);
auto& tav_object_dynamic = TypeArguments::Handle(TypeArguments::New(2));
tav_object_dynamic.SetTypeAt(0, type_object);
tav_object_dynamic.SetTypeAt(1, type_dynamic);
CanonicalizeTAV(&tav_object_dynamic);
auto& tav_dynamic_t = TypeArguments::Handle(TypeArguments::New(2));
tav_dynamic_t.SetTypeAt(0, type_dynamic);
tav_dynamic_t.SetTypeAt(
1, TypeParameter::Handle(GetClassTypeParameter(class_base, 0)));
CanonicalizeTAV(&tav_dynamic_t);
// We will generate specialized TTS for instantiated interface types
// where there are no type arguments or the type arguments are top
// types.
//
// obj as A // Subclass ranges
// obj as Base<Object?> // Subclass ranges with top-type tav
// obj as I2 // Subtype ranges
// obj as I<Object?, dynamic> // Subtype ranges with top-type tav
//
// <...> as A
const auto& type_a = AbstractType::Handle(class_a.RareType());
RunTTSTest(type_a, Failure({obj_i, tav_null, tav_null}));
RunTTSTest(type_a, Failure({obj_i2, tav_null, tav_null}));
RunTTSTest(type_a, Failure({obj_base_int, tav_null, tav_null}));
RunTTSTest(type_a, {obj_a, tav_null, tav_null});
RunTTSTest(type_a, {obj_a1, tav_null, tav_null});
RunTTSTest(type_a, {obj_a2, tav_null, tav_null});
RunTTSTest(type_a, Failure({obj_b, tav_null, tav_null}));
RunTTSTest(type_a, Failure({obj_b1, tav_null, tav_null}));
RunTTSTest(type_a, Failure({obj_b2, tav_null, tav_null}));
// <...> as Base<Object?>
auto& type_base = AbstractType::Handle(Type::New(class_base, tav_object));
FinalizeAndCanonicalize(&type_base);
RunTTSTest(type_base, Failure({obj_i, tav_null, tav_null}));
RunTTSTest(type_base, Failure({obj_i2, tav_null, tav_null}));
RunTTSTest(type_base, {obj_base_int, tav_null, tav_null});
RunTTSTest(type_base, {obj_base_null, tav_null, tav_null});
RunTTSTest(type_base, {obj_a, tav_null, tav_null});
RunTTSTest(type_base, {obj_a1, tav_null, tav_null});
RunTTSTest(type_base, {obj_a2, tav_null, tav_null});
RunTTSTest(type_base, {obj_b, tav_null, tav_null});
RunTTSTest(type_base, {obj_b1, tav_null, tav_null});
RunTTSTest(type_base, {obj_b2, tav_null, tav_null});
// Base<Null|Never> as Base<int?>
// This is a regression test verifying that we don't fall through into
// runtime for Null and Never.
auto& type_nullable_int = Type::Handle(Type::IntType());
type_nullable_int =
type_nullable_int.ToNullability(Nullability::kNullable, Heap::kNew);
auto& tav_nullable_int = TypeArguments::Handle(TypeArguments::New(1));
tav_nullable_int.SetTypeAt(0, type_nullable_int);
CanonicalizeTAV(&tav_nullable_int);
auto& type_base_nullable_int =
AbstractType::Handle(Type::New(class_base, tav_nullable_int));
FinalizeAndCanonicalize(&type_base_nullable_int);
RunTTSTest(type_base_nullable_int, {obj_base_null, tav_null, tav_null});
RunTTSTest(type_base_nullable_int, {obj_base_never, tav_null, tav_null});
// Base<Null|Never> as Base<int>
auto& type_int = Type::Handle(Type::IntType());
type_int = type_int.ToNullability(Nullability::kNonNullable, Heap::kNew);
auto& tav_int = TypeArguments::Handle(TypeArguments::New(1));
tav_int.SetTypeAt(0, type_int);
CanonicalizeTAV(&tav_int);
auto& type_base_int = Type::Handle(Type::New(class_base, tav_int));
type_base_int =
type_base_int.ToNullability(Nullability::kNonNullable, Heap::kNew);
FinalizeAndCanonicalize(&type_base_int);
RunTTSTest(type_base_int, Failure({obj_base_null, tav_null, tav_null}));
RunTTSTest(type_base_int, {obj_base_never, tav_null, tav_null});
// <...> as I2
const auto& type_i2 = AbstractType::Handle(class_i2.RareType());
RunTTSTest(type_i2, Failure({obj_i, tav_null, tav_null}));
RunTTSTest(type_i2, {obj_i2, tav_null, tav_null});
RunTTSTest(type_i2, Failure({obj_base_int, tav_null, tav_null}));
RunTTSTest(type_i2, Failure({obj_a, tav_null, tav_null}));
RunTTSTest(type_i2, {obj_a1, tav_null, tav_null});
RunTTSTest(type_i2, Failure({obj_a2, tav_null, tav_null}));
RunTTSTest(type_i2, Failure({obj_b, tav_null, tav_null}));
RunTTSTest(type_i2, {obj_b1, tav_null, tav_null});
RunTTSTest(type_i2, Failure({obj_b2, tav_null, tav_null}));
// <...> as I<Object, dynamic>
auto& type_i_object_dynamic =
AbstractType::Handle(Type::New(class_i, tav_object_dynamic));
FinalizeAndCanonicalize(&type_i_object_dynamic);
RunTTSTest(type_i_object_dynamic, {obj_i, tav_null, tav_null});
RunTTSTest(type_i_object_dynamic, Failure({obj_i2, tav_null, tav_null}));
RunTTSTest(type_i_object_dynamic,
Failure({obj_base_int, tav_null, tav_null}));
RunTTSTest(type_i_object_dynamic, Failure({obj_a, tav_null, tav_null}));
RunTTSTest(type_i_object_dynamic, Failure({obj_a1, tav_null, tav_null}));
RunTTSTest(type_i_object_dynamic, {obj_a2, tav_null, tav_null});
RunTTSTest(type_i_object_dynamic, Failure({obj_b, tav_null, tav_null}));
RunTTSTest(type_i_object_dynamic, Failure({obj_b1, tav_null, tav_null}));
RunTTSTest(type_i_object_dynamic, {obj_b2, tav_null, tav_null});
// We do generate TTSes for uninstantiated types when we need to use
// subtype range checks for the class of the interface type, but the TTS
// may be partial (returns a false negative in some cases that means going
// to the STC/runtime).
//
// obj as I<dynamic, T>
//
auto& type_dynamic_t =
AbstractType::Handle(Type::New(class_i, tav_dynamic_t));
FinalizeAndCanonicalize(&type_dynamic_t);
RunTTSTest(type_dynamic_t, {obj_i, tav_object, tav_null});
RunTTSTest(type_dynamic_t, Failure({obj_i2, tav_object, tav_null}));
RunTTSTest(type_dynamic_t, Failure({obj_base_int, tav_object, tav_null}));
RunTTSTest(type_dynamic_t, Failure({obj_a, tav_object, tav_null}));
RunTTSTest(type_dynamic_t, Failure({obj_a1, tav_object, tav_null}));
RunTTSTest(type_dynamic_t, {obj_a2, tav_object, tav_null});
RunTTSTest(type_dynamic_t, Failure({obj_b, tav_object, tav_null}));
RunTTSTest(type_dynamic_t, Failure({obj_b1, tav_object, tav_null}));
RunTTSTest(type_dynamic_t, FalseNegative({obj_b2, tav_object, tav_null}));
// obj as Object (with null safety)
auto isolate_group = IsolateGroup::Current();
auto& type_non_nullable_object =
Type::Handle(isolate_group->object_store()->non_nullable_object_type());
RunTTSTest(type_non_nullable_object, {obj_a, tav_null, tav_null});
}
ISOLATE_UNIT_TEST_CASE(TTS_GenericSubtypeRangeCheck) {
const auto& root_library =
Library::Handle(LoadTestScript(kSubtypeRangeCheckScript));
const auto& class_a1 = Class::Handle(GetClass(root_library, "A1"));
const auto& class_a2 = Class::Handle(GetClass(root_library, "A2"));
const auto& class_base = Class::Handle(GetClass(root_library, "Base"));
const auto& class_i = Class::Handle(GetClass(root_library, "I"));
const auto& fun_generic =
Function::Handle(GetFunction(root_library, "genericFun"));
const auto& obj_i = Object::Handle(Invoke(root_library, "createI"));
const auto& obj_i2 = Object::Handle(Invoke(root_library, "createI2"));
const auto& obj_base_int =
Object::Handle(Invoke(root_library, "createBaseInt"));
const auto& obj_a = Object::Handle(Invoke(root_library, "createA"));
const auto& obj_a1 = Object::Handle(Invoke(root_library, "createA1"));
const auto& obj_a2 = Object::Handle(Invoke(root_library, "createA2"));
const auto& obj_b = Object::Handle(Invoke(root_library, "createB"));
const auto& obj_b1 = Object::Handle(Invoke(root_library, "createB1"));
const auto& obj_b2 = Object::Handle(Invoke(root_library, "createB2"));
const auto& obj_basea2int =
Object::Handle(Invoke(root_library, "createBaseA2Int"));
const auto& obj_basea2a1 =
Object::Handle(Invoke(root_library, "createBaseA2A1"));
const auto& obj_baseb2int =
Object::Handle(Invoke(root_library, "createBaseB2Int"));
const auto& obj_baseistringdouble =
Object::Handle(Invoke(root_library, "createBaseIStringDouble"));
const auto& type_dynamic = Type::Handle(Type::DynamicType());
auto& type_int = Type::Handle(Type::IntType());
auto& type_string = Type::Handle(Type::StringType());
auto& type_object = Type::Handle(Type::ObjectType());
type_object = type_object.ToNullability(Nullability::kNullable, Heap::kNew);
auto& type_a1 = Type::Handle(class_a1.DeclarationType());
FinalizeAndCanonicalize(&type_a1);
const auto& tav_null = TypeArguments::Handle(TypeArguments::null());
auto& tav_object_dynamic = TypeArguments::Handle(TypeArguments::New(2));
tav_object_dynamic.SetTypeAt(0, type_object);
tav_object_dynamic.SetTypeAt(1, type_dynamic);
CanonicalizeTAV(&tav_object_dynamic);
auto& tav_dynamic_int = TypeArguments::Handle(TypeArguments::New(2));
tav_dynamic_int.SetTypeAt(0, type_dynamic);
tav_dynamic_int.SetTypeAt(1, type_int);
CanonicalizeTAV(&tav_dynamic_int);
auto& tav_dynamic_string = TypeArguments::Handle(TypeArguments::New(2));
tav_dynamic_string.SetTypeAt(0, type_dynamic);
tav_dynamic_string.SetTypeAt(1, type_string);
CanonicalizeTAV(&tav_dynamic_string);
auto& tav_int = TypeArguments::Handle(TypeArguments::New(1));
tav_int.SetTypeAt(0, type_int);
CanonicalizeTAV(&tav_int);
auto& type_i_object_dynamic =
AbstractType::Handle(Type::New(class_i, tav_object_dynamic));
FinalizeAndCanonicalize(&type_i_object_dynamic);
const auto& tav_iod = TypeArguments::Handle(TypeArguments::New(1));
tav_iod.SetTypeAt(0, type_i_object_dynamic);
// We will generate specialized TTS for instantiated interface types
// where there are no type arguments or the type arguments are top
// types.
//
// obj as Base<I<Object, dynamic>> // Subclass ranges for Base, subtype
// // ranges tav arguments.
// obj as Base<T> // Subclass ranges for Base, type
// // equality for instantiator type arg T
// obj as Base<B> // Subclass ranges for Base, type
// // equality for function type arg B.
//
// <...> as Base<I<Object, dynamic>>
auto& type_base_i_object_dynamic =
AbstractType::Handle(Type::New(class_base, tav_iod));
FinalizeAndCanonicalize(&type_base_i_object_dynamic);
RunTTSTest(type_base_i_object_dynamic, {obj_baseb2int, tav_null, tav_null});
RunTTSTest(type_base_i_object_dynamic,
{obj_baseistringdouble, tav_null, tav_null});
RunTTSTest(type_base_i_object_dynamic, Failure({obj_a, tav_null, tav_null}));
RunTTSTest(type_base_i_object_dynamic, Failure({obj_a1, tav_null, tav_null}));
RunTTSTest(type_base_i_object_dynamic, Failure({obj_a2, tav_null, tav_null}));
RunTTSTest(type_base_i_object_dynamic, Failure({obj_b, tav_null, tav_null}));
RunTTSTest(type_base_i_object_dynamic, Failure({obj_b1, tav_null, tav_null}));
RunTTSTest(type_base_i_object_dynamic, Failure({obj_b2, tav_null, tav_null}));
// <...> as Base<T> with T instantiantiator type parameter (T == int)
const auto& tav_baset = TypeArguments::Handle(TypeArguments::New(1));
tav_baset.SetTypeAt(
0, TypeParameter::Handle(GetClassTypeParameter(class_base, 0)));
auto& type_base_t = AbstractType::Handle(Type::New(class_base, tav_baset));
FinalizeAndCanonicalize(&type_base_t);
RunTTSTest(type_base_t, {obj_base_int, tav_int, tav_null});
RunTTSTest(type_base_t, Failure({obj_baseistringdouble, tav_int, tav_null}));
// <...> as Base<B> with B function type parameter
const auto& tav_baseb = TypeArguments::Handle(TypeArguments::New(1));
tav_baseb.SetTypeAt(
0, TypeParameter::Handle(GetFunctionTypeParameter(fun_generic, 1)));
auto& type_base_b = AbstractType::Handle(Type::New(class_base, tav_baseb));
FinalizeAndCanonicalize(&type_base_b);
// With B == int
RunTTSTest(type_base_b, {obj_base_int, tav_null, tav_dynamic_int});
RunTTSTest(type_base_b,
Failure({obj_baseistringdouble, tav_null, tav_dynamic_int}));
// With B == dynamic (null vector)
RunTTSTest(type_base_b, {obj_base_int, tav_null, tav_null});
RunTTSTest(type_base_b, Failure({obj_i2, tav_null, tav_null}));
// We generate TTS for implemented classes and uninstantiated types, but
// any class that implements the type class but does not match in both
// instance TAV offset and type argument indices is guaranteed to be a
// false negative.
//
// obj as I<dynamic, String> // I is generic & implemented.
// obj as Base<A2<T>> // A2<T> is not instantiated.
// obj as Base<A2<A1>> // A2<A1> is not a rare type.
//
// <...> as I<dynamic, String>
RELEASE_ASSERT(class_i.is_implemented());
auto& type_i_dynamic_string =
Type::Handle(Type::New(class_i, tav_dynamic_string));
type_i_dynamic_string = type_i_dynamic_string.ToNullability(
Nullability::kNonNullable, Heap::kNew);
FinalizeAndCanonicalize(&type_i_dynamic_string);
RunTTSTest(type_i_dynamic_string, {obj_i, tav_null, tav_null});
RunTTSTest(type_i_dynamic_string,
Failure({obj_base_int, tav_null, tav_null}));
// <...> as Base<A2<T>>
const auto& tav_t = TypeArguments::Handle(TypeArguments::New(1));
tav_t.SetTypeAt(0,
TypeParameter::Handle(GetClassTypeParameter(class_base, 0)));
auto& type_a2_t = Type::Handle(Type::New(class_a2, tav_t));
FinalizeAndCanonicalize(&type_a2_t);
const auto& tav_a2_t = TypeArguments::Handle(TypeArguments::New(1));
tav_a2_t.SetTypeAt(0, type_a2_t);
auto& type_base_a2_t = Type::Handle(Type::New(class_base, tav_a2_t));
type_base_a2_t =
type_base_a2_t.ToNullability(Nullability::kNonNullable, Heap::kNew);
FinalizeAndCanonicalize(&type_base_a2_t);
RunTTSTest(type_base_a2_t,
FalseNegative({obj_basea2int, tav_null, tav_null}));
RunTTSTest(type_base_a2_t, Failure({obj_base_int, tav_null, tav_null}));
// <...> as Base<A2<A1>>
const auto& tav_a1 = TypeArguments::Handle(TypeArguments::New(1));
tav_a1.SetTypeAt(0, type_a1);
auto& type_a2_a1 = Type::Handle(Type::New(class_a2, tav_a1));
FinalizeAndCanonicalize(&type_a2_a1);
const auto& tav_a2_a1 = TypeArguments::Handle(TypeArguments::New(1));
tav_a2_a1.SetTypeAt(0, type_a2_a1);
auto& type_base_a2_a1 = Type::Handle(Type::New(class_base, tav_a2_a1));
type_base_a2_a1 =
type_base_a2_a1.ToNullability(Nullability::kNonNullable, Heap::kNew);
FinalizeAndCanonicalize(&type_base_a2_a1);
RunTTSTest(type_base_a2_a1,
FalseNegative({obj_basea2a1, tav_null, tav_null}));
RunTTSTest(type_base_a2_a1, Failure({obj_basea2int, tav_null, tav_null}));
}
const char* kRecordSubtypeRangeCheckScript =
R"(
class A {}
class B extends A {}
class C implements A {}
class D<T> {}
getType<T>() => T;
getRecordType1() => getType<(int, A)>();
getRecordType2() => getType<(A, int, String)>();
getRecordType3() => getType<(int, D)>();
createObj1() => (1, B());
createObj2() => (1, 'bye');
createObj3() => (1, foo: B());
createObj4() => (1, B(), 2);
createObj5() => (C(), 2, 'hi');
createObj6() => (D(), 2, 'hi');
createObj7() => (3, D<int>());
createObj8() => (D<int>(), 3);
)";
ISOLATE_UNIT_TEST_CASE(TTS_RecordSubtypeRangeCheck) {
const auto& root_library =
Library::Handle(LoadTestScript(kRecordSubtypeRangeCheckScript));
const auto& type1 = AbstractType::Cast(
Object::Handle(Invoke(root_library, "getRecordType1")));
const auto& type2 = AbstractType::Cast(
Object::Handle(Invoke(root_library, "getRecordType2")));
const auto& type3 = AbstractType::Cast(
Object::Handle(Invoke(root_library, "getRecordType3")));
const auto& obj1 = Object::Handle(Invoke(root_library, "createObj1"));
const auto& obj2 = Object::Handle(Invoke(root_library, "createObj2"));
const auto& obj3 = Object::Handle(Invoke(root_library, "createObj3"));
const auto& obj4 = Object::Handle(Invoke(root_library, "createObj4"));
const auto& obj5 = Object::Handle(Invoke(root_library, "createObj5"));
const auto& obj6 = Object::Handle(Invoke(root_library, "createObj6"));
const auto& obj7 = Object::Handle(Invoke(root_library, "createObj7"));
const auto& obj8 = Object::Handle(Invoke(root_library, "createObj8"));
const auto& tav_null = TypeArguments::Handle(TypeArguments::null());
// (1, B()) as (int, A)
// (1, 'bye') as (int, A)
// (1, foo: B()) as (int, A)
// (1, B(), 2) as (int, A)
RunTTSTest(type1, {obj1, tav_null, tav_null});
RunTTSTest(type1, Failure({obj2, tav_null, tav_null}));
RunTTSTest(type1, Failure({obj3, tav_null, tav_null}));
RunTTSTest(type1, Failure({obj4, tav_null, tav_null}));
// (C(), 2, 'hi') as (A, int, String)
// (D(), 2, 'hi') as (A, int, String)
RunTTSTest(type2, {obj5, tav_null, tav_null});
RunTTSTest(type2, Failure({obj6, tav_null, tav_null}));
// (3, D<int>()) as (int, D)
// (D<int>(), 3) as (int, D)
RunTTSTest(type3, {obj7, tav_null, tav_null});
RunTTSTest(type3, Failure({obj8, tav_null, tav_null}));
}
ISOLATE_UNIT_TEST_CASE(TTS_Generic_Implements_Instantiated_Interface) {
const char* kScript =
R"(
abstract class I<T> {}
class B<R> implements I<String> {}
createBInt() => B<int>();
)";
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& class_i = Class::Handle(GetClass(root_library, "I"));
const auto& obj_b_int = Object::Handle(Invoke(root_library, "createBInt"));
const auto& tav_null = Object::null_type_arguments();
auto& tav_string = TypeArguments::Handle(TypeArguments::New(1));
tav_string.SetTypeAt(0, Type::Handle(Type::StringType()));
CanonicalizeTAV(&tav_string);
auto& type_i_string = Type::Handle(Type::New(class_i, tav_string));
FinalizeAndCanonicalize(&type_i_string);
const auto& type_i_t = Type::Handle(class_i.DeclarationType());
RunTTSTest(type_i_string, {obj_b_int, tav_null, tav_null});
// Optimized TTSees don't currently handle the case where the implemented
// type is known, but the type being checked requires instantiation at
// runtime.
RunTTSTest(type_i_t, FalseNegative({obj_b_int, tav_string, tav_null}));
}
ISOLATE_UNIT_TEST_CASE(TTS_Future) {
const char* kScript =
R"(
import "dart:async";
Future<int> createFutureInt() async => 3;
Future<int Function()> createFutureFunction() async => () => 3;
Future<int Function()?> createFutureNullableFunction() async =>
(() => 3) as int Function()?;
)";
SetupCoreLibrariesForUnitTest();
const auto& class_future =
Class::Handle(IsolateGroup::Current()->object_store()->future_class());
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& class_closure =
Class::Handle(IsolateGroup::Current()->object_store()->closure_class());
const auto& obj_futureint =
Object::Handle(Invoke(root_library, "createFutureInt"));
const auto& obj_futurefunction =
Object::Handle(Invoke(root_library, "createFutureFunction"));
const auto& obj_futurenullablefunction =
Object::Handle(Invoke(root_library, "createFutureNullableFunction"));
const auto& tav_null = Object::null_type_arguments();
const auto& type_object = Type::Handle(
IsolateGroup::Current()->object_store()->non_nullable_object_type());
const auto& type_nullable_object = Type::Handle(
IsolateGroup::Current()->object_store()->nullable_object_type());
const auto& type_int = Type::Handle(
IsolateGroup::Current()->object_store()->non_nullable_int_type());
auto& type_string = Type::Handle(Type::StringType());
type_string =
type_string.ToNullability(Nullability::kNonNullable, Heap::kNew);
FinalizeAndCanonicalize(&type_string);
auto& type_num = Type::Handle(Type::Number());
type_num = type_num.ToNullability(Nullability::kNonNullable, Heap::kNew);
FinalizeAndCanonicalize(&type_num);
auto& tav_dynamic = TypeArguments::Handle(TypeArguments::New(1));
tav_dynamic.SetTypeAt(0, Object::dynamic_type());
CanonicalizeTAV(&tav_dynamic);
auto& tav_object = TypeArguments::Handle(TypeArguments::New(1));
tav_object.SetTypeAt(0, type_object);
CanonicalizeTAV(&tav_object);
auto& tav_nullable_object = TypeArguments::Handle(TypeArguments::New(1));
tav_nullable_object.SetTypeAt(0, type_nullable_object);
CanonicalizeTAV(&tav_nullable_object);
auto& tav_int = TypeArguments::Handle(TypeArguments::New(1));
tav_int.SetTypeAt(0, type_int);
CanonicalizeTAV(&tav_int);
auto& tav_num = TypeArguments::Handle(TypeArguments::New(1));
tav_num.SetTypeAt(0, type_num);
CanonicalizeTAV(&tav_num);
auto& tav_string = TypeArguments::Handle(TypeArguments::New(1));
tav_string.SetTypeAt(0, type_string);
CanonicalizeTAV(&tav_string);
auto& type_future = Type::Handle(
Type::New(class_future, tav_null, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_future);
auto& type_future_dynamic = Type::Handle(
Type::New(class_future, tav_dynamic, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_future_dynamic);
auto& type_future_object = Type::Handle(
Type::New(class_future, tav_object, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_future_object);
auto& type_future_nullable_object = Type::Handle(
Type::New(class_future, tav_nullable_object, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_future_nullable_object);
auto& type_future_int =
Type::Handle(Type::New(class_future, tav_int, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_future_int);
auto& type_future_string = Type::Handle(
Type::New(class_future, tav_string, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_future_string);
auto& type_future_num =
Type::Handle(Type::New(class_future, tav_num, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_future_num);
const auto& type_future_t = Type::Handle(class_future.DeclarationType());
THR_Print("********************************************************\n");
THR_Print(" Testing Future<int>\n");
THR_Print("********************************************************\n\n");
// Some more tests of generic implemented classes, using Future. Here,
// obj is an object of type Future<int>.
//
// True positives from TTS:
// obj as Future : Null type args
// obj as Future<dynamic> : Canonicalized to same as previous case.
// obj as Future<Object?> : Type arg is top type
// obj as Future<Object*> : Type arg is top type
// obj as Future<Object> : Type arg is certain supertype
// obj as Future<int> : Type arg is the same type
// obj as Future<num> : Type arg is a supertype that can be matched
// with cid range
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = int : ... the same type
//
RunTTSTest(type_future, {obj_futureint, tav_null, tav_null});
RunTTSTest(type_future_dynamic, {obj_futureint, tav_null, tav_null});
RunTTSTest(type_future_object, {obj_futureint, tav_null, tav_null});
RunTTSTest(type_future_nullable_object, {obj_futureint, tav_null, tav_null});
RunTTSTest(type_future_int, {obj_futureint, tav_null, tav_null});
RunTTSTest(type_future_num, {obj_futureint, tav_null, tav_null});
RunTTSTest(type_future_t, {obj_futureint, tav_int, tav_null});
// False negatives from TTS (caught by STC/runtime):
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = num : ... a supertype
RunTTSTest(type_future_t, FalseNegative({obj_futureint, tav_num, tav_null}));
// Errors:
// obj as Future<String> : Type arg is not a supertype
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = String : ... an unrelated type
//
RunTTSTest(type_future_string, Failure({obj_futureint, tav_null, tav_null}));
RunTTSTest(type_future_t, Failure({obj_futureint, tav_string, tav_null}));
auto& type_function = Type::Handle(Type::DartFunctionType());
type_function =
type_function.ToNullability(Nullability::kNonNullable, Heap::kNew);
FinalizeAndCanonicalize(&type_function);
auto& type_nullable_function = Type::Handle(
type_function.ToNullability(Nullability::kNullable, Heap::kOld));
FinalizeAndCanonicalize(&type_nullable_function);
auto& type_closure = Type::Handle(
Type::New(class_closure, tav_null, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_closure);
auto& type_nullable_closure = Type::Handle(
type_closure.ToNullability(Nullability::kNullable, Heap::kOld));
FinalizeAndCanonicalize(&type_nullable_closure);
auto& type_function_int_nullary =
FunctionType::Handle(FunctionType::New(0, Nullability::kNonNullable));
// Testing with a closure, so it has an implicit parameter, and we want a
// type that is canonically equal to the type of the closure.
type_function_int_nullary.set_num_implicit_parameters(1);
type_function_int_nullary.set_num_fixed_parameters(1);
type_function_int_nullary.set_parameter_types(Array::Handle(Array::New(1)));
type_function_int_nullary.SetParameterTypeAt(0, Type::dynamic_type());
type_function_int_nullary.set_result_type(type_int);
FinalizeAndCanonicalize(&type_function_int_nullary);
auto& type_nullable_function_int_nullary =
FunctionType::Handle(type_function_int_nullary.ToNullability(
Nullability::kNullable, Heap::kOld));
FinalizeAndCanonicalize(&type_nullable_function_int_nullary);
auto& tav_function = TypeArguments::Handle(TypeArguments::New(1));
tav_function.SetTypeAt(0, type_function);
CanonicalizeTAV(&tav_function);
auto& tav_nullable_function = TypeArguments::Handle(TypeArguments::New(1));
tav_nullable_function.SetTypeAt(0, type_nullable_function);
CanonicalizeTAV(&tav_nullable_function);
auto& tav_closure = TypeArguments::Handle(TypeArguments::New(1));
tav_closure.SetTypeAt(0, type_closure);
CanonicalizeTAV(&tav_closure);
auto& tav_nullable_closure = TypeArguments::Handle(TypeArguments::New(1));
tav_nullable_closure.SetTypeAt(0, type_nullable_closure);
CanonicalizeTAV(&tav_nullable_closure);
auto& tav_function_int_nullary = TypeArguments::Handle(TypeArguments::New(1));
tav_function_int_nullary.SetTypeAt(0, type_function_int_nullary);
CanonicalizeTAV(&tav_function_int_nullary);
auto& tav_nullable_function_int_nullary =
TypeArguments::Handle(TypeArguments::New(1));
tav_nullable_function_int_nullary.SetTypeAt(
0, type_nullable_function_int_nullary);
CanonicalizeTAV(&tav_nullable_function_int_nullary);
auto& type_future_function = Type::Handle(
Type::New(class_future, tav_function, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_future_function);
auto& type_future_nullable_function = Type::Handle(Type::New(
class_future, tav_nullable_function, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_future_nullable_function);
auto& type_future_closure = Type::Handle(
Type::New(class_future, tav_closure, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_future_closure);
auto& type_future_nullable_closure = Type::Handle(
Type::New(class_future, tav_nullable_closure, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_future_nullable_closure);
auto& type_future_function_int_nullary =
Type::Handle(Type::New(class_future, tav_function_int_nullary));
FinalizeAndCanonicalize(&type_future_function_int_nullary);
auto& type_future_nullable_function_int_nullary =
Type::Handle(Type::New(class_future, tav_nullable_function_int_nullary));
FinalizeAndCanonicalize(&type_future_nullable_function_int_nullary);
THR_Print("\n********************************************************\n");
THR_Print(" Testing Future<int Function()>\n");
THR_Print("********************************************************\n\n");
// And here, obj is an object of type Future<int Function()>. Note that
// int Function() <: Function, but int Function() </: _Closure. That is,
// _Closure is a separate subtype of Function from FunctionTypes.
//
// True positive from TTS:
// obj as Future : Null type args
// obj as Future<dynamic> : Canonicalized to same as previous case.
// obj as Future<Object?> : Type arg is top type
// obj as Future<Object*> : Type arg is top typ
// obj as Future<Object> : Type arg is certain supertype
// obj as Future<Function?> : Type arg is certain supertype
// obj as Future<Function*> : Type arg is certain supertype
// obj as Future<Function> : Type arg is certain supertype
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = dynamic : ... a top type
// X = Object? : ... a top type
// X = Object* : ... a top type
// X = Object : ... a certain supertype
// X = int Function() : ... the same type.
//
RunTTSTest(type_future, {obj_futurefunction, tav_null, tav_null});
RunTTSTest(type_future_dynamic, {obj_futurefunction, tav_null, tav_null});
RunTTSTest(type_future_nullable_object,
{obj_futurefunction, tav_null, tav_null});
RunTTSTest(type_future_object, {obj_futurefunction, tav_null, tav_null});
RunTTSTest(type_future_nullable_object,
{obj_futurefunction, tav_null, tav_null});
RunTTSTest(type_future_object, {obj_futurefunction, tav_null, tav_null});
RunTTSTest(type_future_nullable_function,
{obj_futurefunction, tav_null, tav_null});
RunTTSTest(type_future_function, {obj_futurefunction, tav_null, tav_null});
RunTTSTest(type_future_t, {obj_futurefunction, tav_null, tav_null});
RunTTSTest(type_future_t,
{obj_futurefunction, tav_nullable_object, tav_null});
RunTTSTest(type_future_t, {obj_futurefunction, tav_object, tav_null});
RunTTSTest(type_future_t,
{obj_futurefunction, tav_function_int_nullary, tav_null});
// False negative from TTS (caught by runtime or STC):
// obj as Future<int Function()?> : No specialization.
// obj as Future<int Function()*> : No specialization.
// obj as Future<int Function()> : No specialization.
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = Function? : ... a certain supertype (but not checked)
// X = Function* : ... a certain supertype (but not checked)
// X = Function : ... a certain supertype (but not checked)
// X = int Function()? : ... a canonically different type.
// X = int Function()* : ... a canonically different type.
//
RunTTSTest(type_future_nullable_function_int_nullary,
FalseNegative({obj_futurefunction, tav_null, tav_null}));
RunTTSTest(type_future_function_int_nullary,
FalseNegative({obj_futurefunction, tav_null, tav_null}));
RunTTSTest(type_future_t, FalseNegative({obj_futurefunction,
tav_nullable_function, tav_null}));
RunTTSTest(type_future_t,
FalseNegative({obj_futurefunction, tav_function, tav_null}));
RunTTSTest(type_future_t,
FalseNegative({obj_futurefunction,
tav_nullable_function_int_nullary, tav_null}));
// Errors:
// obj as Future<_Closure?> : Type arg is not a supertype
// obj as Future<_Closure*> : Type arg is not a supertype
// obj as Future<_Closure> : Type arg is not a supertype
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = _Closure? : ... an unrelated type.
// X = _Closure* : ... an unrelated type.
// X = _Closure : ... an unrelated type.
//
RunTTSTest(type_future_nullable_closure,
Failure({obj_futurefunction, tav_null, tav_null}));
RunTTSTest(type_future_closure,
Failure({obj_futurefunction, tav_null, tav_null}));
RunTTSTest(type_future_t,
Failure({obj_futurefunction, tav_nullable_closure, tav_null}));
RunTTSTest(type_future_t,
Failure({obj_futurefunction, tav_closure, tav_null}));
THR_Print("\n********************************************************\n");
THR_Print(" Testing Future<int Function()?>\n");
THR_Print("********************************************************\n\n");
// And here, obj is an object of type Future<int Function()?>.
//
// True positive from TTS:
// obj as Future : Null type args
// obj as Future<dynamic> : Canonicalized to same as previous case.
// obj as Future<Object?> : Type arg is top type
// obj as Future<Object*> : Type arg is top typ
// obj as Future<Function?> : Type arg is certain supertype
// obj as Future<Function*> : Type arg is certain supertype
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = dynamic : ... a top type
// X = Object? : ... a top type
// X = Object* : ... a top type
// X = int Function()? : ... the same type.
//
// If not null safe:
// obj as Future<Object> : Type arg is certain supertype
// obj as Future<Function> : Type arg is certain supertype
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = Object : ... a certain supertype
RunTTSTest(type_future, {obj_futurenullablefunction, tav_null, tav_null});
RunTTSTest(type_future_dynamic,
{obj_futurenullablefunction, tav_null, tav_null});
RunTTSTest(type_future_nullable_object,
{obj_futurenullablefunction, tav_null, tav_null});
RunTTSTest(type_future_nullable_object,
{obj_futurefunction, tav_null, tav_null});
RunTTSTest(type_future_nullable_function,
{obj_futurenullablefunction, tav_null, tav_null});
RunTTSTest(type_future_t, {obj_futurenullablefunction, tav_null, tav_null});
RunTTSTest(type_future_t,
{obj_futurenullablefunction, tav_nullable_object, tav_null});
RunTTSTest(type_future_t, {obj_futurenullablefunction,
tav_nullable_function_int_nullary, tav_null});
// False negative from TTS (caught by runtime or STC):
// obj as Future<int Function()?> : No specialization.
// obj as Future<int Function()*> : No specialization.
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = Function? : ... a certain supertype (but not checked)
// X = Function* : ... a certain supertype (but not checked)
// X = int Function()* : ... a canonically different type.
//
// If not null safe:
// obj as Future<int Function()> : No specialization.
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = Function : ... a certain supertype (but not checked)
// X = int Function() : ... a canonically different type.
RunTTSTest(type_future_nullable_function_int_nullary,
FalseNegative({obj_futurenullablefunction, tav_null, tav_null}));
RunTTSTest(type_future_t, FalseNegative({obj_futurenullablefunction,
tav_nullable_function, tav_null}));
// Errors:
// obj as Future<_Closure?> : Type arg is not a supertype
// obj as Future<_Closure*> : Type arg is not a supertype
// obj as Future<_Closure> : Type arg is not a supertype
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = _Closure? : ... an unrelated type.
// X = _Closure* : ... an unrelated type.
// X = _Closure : ... an unrelated type.
//
// If null safe:
// obj as Future<int Function()> : Nullable type cannot be subtype of a
// non-nullable type.
// obj as Future<Object> : Nullable type cannot be subtype of a
// non-nullable type.
// obj as Future<Function> : Nullable type cannot be subtype of a
// non-nullable type.
// obj as Future<X>, : Type arg is a type parameter instantiated with
// X = Object : ... a non-nullable type.
// X = Function : ... a non-nullable type.
// X = int Function() : ... a non-nullable type.
RunTTSTest(type_future_nullable_closure,
Failure({obj_futurenullablefunction, tav_null, tav_null}));
RunTTSTest(type_future_closure,
Failure({obj_futurenullablefunction, tav_null, tav_null}));
RunTTSTest(type_future_t, Failure({obj_futurenullablefunction,
tav_nullable_closure, tav_null}));
RunTTSTest(type_future_t,
Failure({obj_futurenullablefunction, tav_closure, tav_null}));
RunTTSTest(type_future_function_int_nullary,
Failure({obj_futurenullablefunction, tav_null, tav_null}));
RunTTSTest(type_future_object,
Failure({obj_futurenullablefunction, tav_null, tav_null}));
RunTTSTest(type_future_function,
Failure({obj_futurenullablefunction, tav_null, tav_null}));
RunTTSTest(type_future_t,
Failure({obj_futurenullablefunction, tav_object, tav_null}));
RunTTSTest(type_future_t,
Failure({obj_futurenullablefunction, tav_function, tav_null}));
RunTTSTest(type_future_t, Failure({obj_futurenullablefunction,
tav_function_int_nullary, tav_null}));
}
ISOLATE_UNIT_TEST_CASE(TTS_Regress40964) {
const char* kScript =
R"(
class A<T> {
test(x) => x as B<T>;
}
class B<T> {}
class C<T> {}
createACint() => A<C<int>>();
createBCint() => B<C<int>>();
createBCnum() => B<C<num>>();
)";
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& class_b = Class::Handle(GetClass(root_library, "B"));
const auto& acint = Object::Handle(Invoke(root_library, "createACint"));
const auto& bcint = Object::Handle(Invoke(root_library, "createBCint"));
const auto& bcnum = Object::Handle(Invoke(root_library, "createBCnum"));
// dst_type = B<T>
const auto& dst_tav = TypeArguments::Handle(TypeArguments::New(1));
dst_tav.SetTypeAt(0,
TypeParameter::Handle(GetClassTypeParameter(class_b, 0)));
auto& dst_type = Type::Handle(Type::New(class_b, dst_tav));
FinalizeAndCanonicalize(&dst_type);
const auto& cint_tav =
TypeArguments::Handle(Instance::Cast(acint).GetTypeArguments());
const auto& function_tav = TypeArguments::Handle();
// a as B<T> -- a==B<C<int>, T==<C<int>>
RunTTSTest(dst_type, {bcint, cint_tav, function_tav});
// a as B<T> -- a==B<C<num>, T==<C<int>>
RunTTSTest(dst_type, Failure({bcnum, cint_tav, function_tav}));
}
ISOLATE_UNIT_TEST_CASE(TTS_TypeParameter) {
const char* kScript =
R"(
class A<T> {
T test(dynamic x) => x as T;
}
H genericFun<H>(dynamic x) => x as H;
createAInt() => A<int>();
createAString() => A<String>();
)";
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& class_a = Class::Handle(GetClass(root_library, "A"));
ClassFinalizer::FinalizeTypesInClass(class_a);
const auto& fun_generic =
Function::Handle(GetFunction(root_library, "genericFun"));
const auto& dst_type_t =
TypeParameter::Handle(GetClassTypeParameter(class_a, 0));
const auto& dst_type_h =
TypeParameter::Handle(GetFunctionTypeParameter(fun_generic, 0));
const auto& aint = Object::Handle(Invoke(root_library, "createAInt"));
const auto& astring = Object::Handle(Invoke(root_library, "createAString"));
const auto& int_tav =
TypeArguments::Handle(Instance::Cast(aint).GetTypeArguments());
const auto& string_tav =
TypeArguments::Handle(Instance::Cast(astring).GetTypeArguments());
const auto& int_instance = Integer::Handle(Integer::New(1));
const auto& string_instance = String::Handle(String::New("foo"));
THR_Print("Testing int instance, class parameter instantiated to int\n");
RunTTSTest(dst_type_t, {int_instance, int_tav, string_tav});
THR_Print("\nTesting string instance, class parameter instantiated to int\n");
RunTTSTest(dst_type_t, Failure({string_instance, int_tav, string_tav}));
THR_Print(
"\nTesting string instance, function parameter instantiated to string\n");
RunTTSTest(dst_type_h, {string_instance, int_tav, string_tav});
RunTTSTest(dst_type_h, Failure({int_instance, int_tav, string_tav}));
}
// Check that we generate correct TTS for _Smi type.
ISOLATE_UNIT_TEST_CASE(TTS_Smi) {
const auto& type_smi = Type::Handle(Type::SmiType());
const auto& tav_null = Object::null_type_arguments();
// Test on some easy-to-make instances.
RunTTSTest(type_smi, {Smi::Handle(Smi::New(0)), tav_null, tav_null});
RunTTSTest(type_smi, Failure({Integer::Handle(Integer::New(kMaxInt64)),
tav_null, tav_null}));
RunTTSTest(type_smi,
Failure({Double::Handle(Double::New(1.0)), tav_null, tav_null}));
RunTTSTest(type_smi, Failure({Symbols::Empty(), tav_null, tav_null}));
RunTTSTest(type_smi,
Failure({Array::Handle(Array::New(1)), tav_null, tav_null}));
}
// Check that we generate correct TTS for int type.
ISOLATE_UNIT_TEST_CASE(TTS_Int) {
const auto& type_int = Type::Handle(Type::IntType());
const auto& tav_null = Object::null_type_arguments();
// Test on some easy-to-make instances.
RunTTSTest(type_int, {Smi::Handle(Smi::New(0)), tav_null, tav_null});
RunTTSTest(type_int,
{Integer::Handle(Integer::New(kMaxInt64)), tav_null, tav_null});
RunTTSTest(type_int,
Failure({Double::Handle(Double::New(1.0)), tav_null, tav_null}));
RunTTSTest(type_int, Failure({Symbols::Empty(), tav_null, tav_null}));
RunTTSTest(type_int,
Failure({Array::Handle(Array::New(1)), tav_null, tav_null}));
}
// Check that we generate correct TTS for num type.
ISOLATE_UNIT_TEST_CASE(TTS_Num) {
const auto& type_num = Type::Handle(Type::Number());
const auto& tav_null = Object::null_type_arguments();
// Test on some easy-to-make instances.
RunTTSTest(type_num, {Smi::Handle(Smi::New(0)), tav_null, tav_null});
RunTTSTest(type_num,
{Integer::Handle(Integer::New(kMaxInt64)), tav_null, tav_null});
RunTTSTest(type_num, {Double::Handle(Double::New(1.0)), tav_null, tav_null});
RunTTSTest(type_num, Failure({Symbols::Empty(), tav_null, tav_null}));
RunTTSTest(type_num,
Failure({Array::Handle(Array::New(1)), tav_null, tav_null}));
}
// Check that we generate correct TTS for Double type.
ISOLATE_UNIT_TEST_CASE(TTS_Double) {
const auto& type_num = Type::Handle(Type::Double());
const auto& tav_null = Object::null_type_arguments();
// Test on some easy-to-make instances.
RunTTSTest(type_num, Failure({Smi::Handle(Smi::New(0)), tav_null, tav_null}));
RunTTSTest(type_num, Failure({Integer::Handle(Integer::New(kMaxInt64)),
tav_null, tav_null}));
RunTTSTest(type_num, {Double::Handle(Double::New(1.0)), tav_null, tav_null});
RunTTSTest(type_num, Failure({Symbols::Empty(), tav_null, tav_null}));
RunTTSTest(type_num,
Failure({Array::Handle(Array::New(1)), tav_null, tav_null}));
}
// Check that we generate correct TTS for Object type.
ISOLATE_UNIT_TEST_CASE(TTS_Object) {
const auto& type_obj =
Type::Handle(IsolateGroup::Current()->object_store()->object_type());
const auto& tav_null = Object::null_type_arguments();
// Non-nullable Object is not a top type,
// so its TTS specializes the first time it is invoked.
const bool should_specialize = true;
auto make_test_case = [&](const Instance& instance) -> TTSTestCase {
return {instance, tav_null, tav_null};
};
// Test on some easy-to-make instances.
RunTTSTest(type_obj, make_test_case(Smi::Handle(Smi::New(0))),
should_specialize);
RunTTSTest(type_obj, make_test_case(Integer::Handle(Integer::New(kMaxInt64))),
should_specialize);
RunTTSTest(type_obj, make_test_case(Double::Handle(Double::New(1.0))),
should_specialize);
RunTTSTest(type_obj, make_test_case(Symbols::Empty()), should_specialize);
RunTTSTest(type_obj, make_test_case(Array::Handle(Array::New(1))),
should_specialize);
}
// Check that we generate correct TTS for type Function (the non-FunctionType
// version).
ISOLATE_UNIT_TEST_CASE(TTS_Function) {
const char* kScript =
R"(
class A<T> {}
createF() => (){};
createG() => () => 3;
createH() => (int x, String y, {int z = 0}) => x + z;
createAInt() => A<int>();
createAFunction() => A<Function>();
)";
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& obj_f = Object::Handle(Invoke(root_library, "createF"));
const auto& obj_g = Object::Handle(Invoke(root_library, "createG"));
const auto& obj_h = Object::Handle(Invoke(root_library, "createH"));
const auto& tav_null = TypeArguments::Handle(TypeArguments::null());
const auto& type_function = Type::Handle(Type::DartFunctionType());
RunTTSTest(type_function, {obj_f, tav_null, tav_null});
RunTTSTest(type_function, {obj_g, tav_null, tav_null});
RunTTSTest(type_function, {obj_h, tav_null, tav_null});
const auto& class_a = Class::Handle(GetClass(root_library, "A"));
const auto& obj_a_int = Object::Handle(Invoke(root_library, "createAInt"));
const auto& obj_a_function =
Object::Handle(Invoke(root_library, "createAFunction"));
auto& tav_function = TypeArguments::Handle(TypeArguments::New(1));
tav_function.SetTypeAt(0, type_function);
CanonicalizeTAV(&tav_function);
auto& type_a_function = Type::Handle(Type::New(class_a, tav_function));
FinalizeAndCanonicalize(&type_a_function);
RunTTSTest(type_a_function, {obj_a_function, tav_null, tav_null});
RunTTSTest(type_a_function, Failure({obj_a_int, tav_null, tav_null}));
}
ISOLATE_UNIT_TEST_CASE(TTS_Partial) {
const char* kScript =
R"(
class B<T> {}
class C {}
class D extends C {}
class E extends D {}
F<A>() {}
createBE() => B<E>();
createBENullable() => B<E?>();
createBNull() => B<Null>();
createBNever() => B<Never>();
)";
const auto& root_library = Library::Handle(LoadTestScript(kScript));
const auto& class_b = Class::Handle(GetClass(root_library, "B"));
const auto& class_c = Class::Handle(GetClass(root_library, "C"));
const auto& class_d = Class::Handle(GetClass(root_library, "D"));
const auto& class_e = Class::Handle(GetClass(root_library, "E"));
const auto& fun_f = Function::Handle(GetFunction(root_library, "F"));
const auto& obj_b_e = Object::Handle(Invoke(root_library, "createBE"));
const auto& obj_b_e_nullable =
Object::Handle(Invoke(root_library, "createBENullable"));
const auto& obj_b_null = Object::Handle(Invoke(root_library, "createBNull"));
const auto& obj_b_never =
Object::Handle(Invoke(root_library, "createBNever"));
const auto& tav_null = Object::null_type_arguments();
auto& tav_nullable_object = TypeArguments::Handle(TypeArguments::New(1));
tav_nullable_object.SetTypeAt(
0, Type::Handle(
IsolateGroup::Current()->object_store()->nullable_object_type()));
CanonicalizeTAV(&tav_nullable_object);
auto& tav_object = TypeArguments::Handle(TypeArguments::New(1));
tav_object.SetTypeAt(
0, Type::Handle(IsolateGroup::Current()->object_store()->object_type()));
CanonicalizeTAV(&tav_object);
auto& type_e =
Type::Handle(Type::New(class_e, tav_null, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_e);
auto& type_d =
Type::Handle(Type::New(class_d, tav_null, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_d);
auto& type_c =
Type::Handle(Type::New(class_c, tav_null, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_c);
auto& type_c_nullable =
Type::Handle(Type::New(class_c, tav_null, Nullability::kNullable));
FinalizeAndCanonicalize(&type_c_nullable);
auto& tav_e = TypeArguments::Handle(TypeArguments::New(1));
tav_e.SetTypeAt(0, type_e);
CanonicalizeTAV(&tav_e);
auto& tav_d = TypeArguments::Handle(TypeArguments::New(1));
tav_d.SetTypeAt(0, type_d);
CanonicalizeTAV(&tav_d);
auto& tav_c = TypeArguments::Handle(TypeArguments::New(1));
tav_c.SetTypeAt(0, type_c);
CanonicalizeTAV(&tav_c);
auto& tav_nullable_c = TypeArguments::Handle(TypeArguments::New(1));
tav_nullable_c.SetTypeAt(0, type_c_nullable);
CanonicalizeTAV(&tav_nullable_c);
// One case where optimized TTSes can be partial is if the type is
// uninstantiated with a type parameter at the same position as one of the
// class's type parameters. The type parameter in the type is instantiated at
// runtime and compared with the corresponding instance type argument using
// pointer equality, which misses the case where the instantiated type
// parameter in the type is a supertype of the instance type argument.
const auto& type_a =
TypeParameter::Handle(GetFunctionTypeParameter(fun_f, 0));
auto& tav_a = TypeArguments::Handle(TypeArguments::New(1));
tav_a.SetTypeAt(0, type_a);
CanonicalizeTAV(&tav_a);
auto& type_b_a = AbstractType::Handle(
Type::New(class_b, tav_a, Nullability::kNonNullable));
FinalizeAndCanonicalize(&type_b_a);
TTSTestState state(thread, type_b_a);
// Should be checked by the TTS.
TTSTestCase b_e_testcase{obj_b_e, tav_null, tav_e};
// Needs to be cached in the STC and checked there.
TTSTestCase b_d_testcase{obj_b_e, tav_null, tav_d};
TTSTestCase b_c_testcase{obj_b_e, tav_null, tav_c};
// First, test that the positive test case is handled by the TTS.
state.InvokeLazilySpecializedStub(b_e_testcase);
state.InvokeExistingStub(b_e_testcase);
// Now restart, using the false negative test cases.
state.ClearCache();
// This specializes the stub, so no STC update.
state.InvokeLazilySpecializedStub(b_d_testcase);
state.InvokeExistingStub(FalseNegative(b_d_testcase));
// Replacing the stub with an eager version without clearing the STC means
// it's still in the STC to be checked.
state.InvokeEagerlySpecializedStub(STCCheck(b_d_testcase));
state.InvokeExistingStub(b_e_testcase);
state.InvokeExistingStub(FalseNegative(b_c_testcase));
state.InvokeExistingStub(STCCheck(b_d_testcase));
state.InvokeExistingStub(b_e_testcase);
state.InvokeExistingStub({obj_b_never, tav_null, tav_d});
state.InvokeExistingStub({obj_b_null, tav_null, tav_nullable_c});
state.InvokeExistingStub(Failure({obj_b_null, tav_null, tav_c}));
state.InvokeExistingStub({obj_b_e, tav_null, tav_nullable_object});
state.InvokeExistingStub({obj_b_e_nullable, tav_null, tav_nullable_object});
state.InvokeExistingStub({obj_b_e, tav_null, tav_object});
state.InvokeExistingStub(Failure({obj_b_e_nullable, tav_null, tav_object}));
}
ISOLATE_UNIT_TEST_CASE(TTS_Partial_Incremental) {
#define FILE_RESOLVE_URI(Uri) "file:///" Uri
#define FIRST_PARTIAL_LIBRARY_NAME "test-lib"
#define SECOND_PARTIAL_LIBRARY_NAME "test-lib-2"
#define THIRD_PARTIAL_LIBRARY_NAME "test-lib-3"
// Same test script as TTS_Partial.
const char* kFirstScript =
R"(
class B<T> {}
createB() => B<int>();
)";
// A test script which imports the B class and extend it, to test
// respecialization when the hierarchy changes without reloading.
const char* kSecondScript =
R"(
import ")" FIRST_PARTIAL_LIBRARY_NAME R"(";
class B2<T> extends B<T> {}
createB2() => B2<int>();
)";
// Another one to test respecialization a second time.
const char* kThirdScript =
R"(
import ")" FIRST_PARTIAL_LIBRARY_NAME R"(";
class B3<T> extends B<T> {}
createB3() => B3<int>();
)";
const char* kFirstUri = FILE_RESOLVE_URI(FIRST_PARTIAL_LIBRARY_NAME);
const char* kSecondUri = FILE_RESOLVE_URI(SECOND_PARTIAL_LIBRARY_NAME);
const char* kThirdUri = FILE_RESOLVE_URI(THIRD_PARTIAL_LIBRARY_NAME);
#undef THIRD_PARTIAL_LIBRARY_URI
#undef SECOND_PARTIAL_LIBRARY_URI
#undef FIRST_PARTIAL_LIBRARY_URI
#undef FILE_RESOLVE_URI
THR_Print("------------------------------------------------------\n");
THR_Print(" Loading %s\n", kFirstUri);
THR_Print("------------------------------------------------------\n");
const auto& first_library = Library::Handle(
LoadTestScript(kFirstScript, /*resolver=*/nullptr, kFirstUri));
const auto& class_b = Class::Handle(GetClass(first_library, "B"));
const auto& obj_b = Object::Handle(Invoke(first_library, "createB"));
const auto& tav_null = Object::null_type_arguments();
auto& tav_int = TypeArguments::Handle(TypeArguments::New(1));
tav_int.SetTypeAt(0, Type::Handle(Type::IntType()));
CanonicalizeTAV(&tav_int);
auto& tav_num = TypeArguments::Handle(TypeArguments::New(1));
tav_num.SetTypeAt(0, Type::Handle(Type::Number()));
CanonicalizeTAV(&tav_num);
auto& type_b2_t = AbstractType::Handle(class_b.DeclarationType());
FinalizeAndCanonicalize(&type_b2_t);
TTSTestState state(thread, type_b2_t);
TTSTestCase first_positive{obj_b, tav_int, tav_null};
TTSTestCase first_false_negative = {obj_b, tav_num, tav_null};
// No test case should possibly hit the same STC entry as another.
ASSERT(!first_false_negative.HasSameSTCEntry(first_positive));
// The type with the tested stub must be the same in all test cases.
ASSERT(state.TypeToTest(first_positive) ==
state.TypeToTest(first_false_negative));
state.InvokeLazilySpecializedStub(first_false_negative);
state.InvokeExistingStub(FalseNegative(first_false_negative));
state.InvokeEagerlySpecializedStub(STCCheck(first_false_negative));
state.InvokeExistingStub(first_positive);
state.InvokeExistingStub(STCCheck(first_false_negative));
Array& stc_cache = Array::Handle(
state.last_stc().IsNull() ? Array::null() : state.last_stc().cache());
THR_Print("------------------------------------------------------\n");
THR_Print(" Loading %s\n", kSecondUri);
THR_Print("------------------------------------------------------\n");
const auto& second_library = Library::Handle(
LoadTestScript(kSecondScript, /*resolver=*/nullptr, kSecondUri));
// Loading the new library shouldn't invalidate the old STC.
EXPECT(state.last_stc().ptr() == state.current_stc());
// Loading the new library should not reset the STCs, as no respecialization
// should happen yet.
EXPECT((state.last_stc().IsNull() && stc_cache.IsNull()) ||
stc_cache.ptr() == state.last_stc().cache());
const auto& obj_b2 = Object::Handle(Invoke(second_library, "createB2"));
TTSTestCase second_positive{obj_b2, tav_int, tav_null};
TTSTestCase second_false_negative = {obj_b2, tav_num, tav_null};
// No test case should possibly hit the same STC entry as another.
ASSERT(!second_positive.HasSameSTCEntry(second_false_negative));
ASSERT(!second_positive.HasSameSTCEntry(first_positive));
ASSERT(!second_positive.HasSameSTCEntry(first_false_negative));
ASSERT(!second_false_negative.HasSameSTCEntry(first_positive));
ASSERT(!second_false_negative.HasSameSTCEntry(first_false_negative));
// The type with the tested stub must be the same in all test cases.
ASSERT(state.TypeToTest(second_positive) ==
state.TypeToTest(second_false_negative));
ASSERT(state.TypeToTest(first_positive) == state.TypeToTest(second_positive));
// Old positive should still be caught by TTS.
state.InvokeExistingStub(first_positive);
// Same false negative should still be caught by STC and not cause
// respecialization.
state.InvokeExistingStub(STCCheck(first_false_negative));
// The new positive should be a false negative at the TTS level that causes
// respecialization, as the class hierarchy has changed.
state.InvokeExistingStub(Respecialization(second_positive));
// The first false positive is still in the cache.
state.InvokeExistingStub(STCCheck(first_false_negative));
// This false negative is not yet in the cache.
state.InvokeExistingStub(FalseNegative(second_false_negative));
state.InvokeExistingStub(first_positive);
state.InvokeExistingStub(second_positive);
// Now the second false negative is in the cache.
state.InvokeExistingStub(STCCheck(second_false_negative));
stc_cache =
state.last_stc().IsNull() ? Array::null() : state.last_stc().cache();
THR_Print("------------------------------------------------------\n");
THR_Print(" Loading %s\n", kThirdUri);
THR_Print("------------------------------------------------------\n");
const auto& third_library = Library::Handle(
LoadTestScript(kThirdScript, /*resolver=*/nullptr, kThirdUri));
// Loading the new library shouldn't invalidate the old STC.
EXPECT(state.last_stc().ptr() == state.current_stc());
// Loading the new library should not reset the STCs, as no respecialization
// should happen yet.
EXPECT((state.last_stc().IsNull() && stc_cache.IsNull()) ||
stc_cache.ptr() == state.last_stc().cache());
const auto& obj_b3 = Object::Handle(Invoke(third_library, "createB3"));
TTSTestCase third_positive{obj_b3, tav_int, tav_null};
TTSTestCase third_false_negative = {obj_b3, tav_num, tav_null};
// No test case should possibly hit the same STC entry as another.
ASSERT(!third_positive.HasSameSTCEntry(third_false_negative));
ASSERT(!third_positive.HasSameSTCEntry(first_positive));
ASSERT(!third_positive.HasSameSTCEntry(first_false_negative));
ASSERT(!third_positive.HasSameSTCEntry(second_positive));
ASSERT(!third_positive.HasSameSTCEntry(second_false_negative));
ASSERT(!third_false_negative.HasSameSTCEntry(first_positive));
ASSERT(!third_false_negative.HasSameSTCEntry(first_false_negative));
ASSERT(!third_false_negative.HasSameSTCEntry(second_positive));
ASSERT(!third_false_negative.HasSameSTCEntry(second_false_negative));
// The type with the tested stub must be the same in all test cases.
ASSERT(state.TypeToTest(third_positive) ==
state.TypeToTest(third_false_negative));
ASSERT(state.TypeToTest(first_positive) == state.TypeToTest(third_positive));
// Again, cases that have run before should still pass as before without STC
// changes/respecialization.
state.InvokeExistingStub(first_positive);
state.InvokeExistingStub(second_positive);
state.InvokeExistingStub(STCCheck(first_false_negative));
state.InvokeExistingStub(STCCheck(second_false_negative));
// Now we lead with the new false negative, to make sure it also triggers
// respecialization but doesn't get immediately added to the STC.
state.InvokeExistingStub(Respecialization(third_false_negative));
// True positives still work as before.
state.InvokeExistingStub(third_positive);
state.InvokeExistingStub(second_positive);
state.InvokeExistingStub(first_positive);
// No additional checks added by rerunning the previous false negatives.
state.InvokeExistingStub(STCCheck(first_false_negative));
state.InvokeExistingStub(STCCheck(second_false_negative));
// Now a check is recorded when rerunning the third false negative.
state.InvokeExistingStub(FalseNegative(third_false_negative));
state.InvokeExistingStub(STCCheck(third_false_negative));
}
// TTS deoptimization on reload only happens in non-product mode currently.
#if !defined(PRODUCT)
static const char* kLoadedScript =
R"(
class A<T> {}
createAInt() => A<int>();
createAString() => A<String>();
(int, int) createRecordIntInt() => (1, 2);
(String, int) createRecordStringInt() => ("foo", 2);
(int, String) createRecordIntString() => (1, "bar");
)";
static const char* kReloadedScript =
R"(
class A<T> {}
class A2<T> extends A<T> {}
createAInt() => A<int>();
createAString() => A<String>();
createA2Int() => A2<int>();
createA2String() => A2<String>();
(int, int) createRecordIntInt() => (1, 2);
(String, int) createRecordStringInt() => ("foo", 2);
(int, String) createRecordIntString() => (1, "bar");
)";
ISOLATE_UNIT_TEST_CASE(TTS_Reload) {
auto* const zone = thread->zone();
auto& root_library = Library::Handle(LoadTestScript(kLoadedScript));
const auto& class_a = Class::Handle(GetClass(root_library, "A"));
ClassFinalizer::FinalizeTypesInClass(class_a);
const auto& aint = Object::Handle(Invoke(root_library, "createAInt"));
const auto& astring = Object::Handle(Invoke(root_library, "createAString"));
const auto& record_int_int =
Instance::CheckedHandle(zone, Invoke(root_library, "createRecordIntInt"));
const auto& record_int_string = Instance::CheckedHandle(
zone, Invoke(root_library, "createRecordIntString"));
const auto& record_string_int = Instance::CheckedHandle(
zone, Invoke(root_library, "createRecordStringInt"));
const auto& tav_null = Object::null_type_arguments();
const auto& tav_int =
TypeArguments::Handle(Instance::Cast(aint).GetTypeArguments());
auto& tav_num = TypeArguments::Handle(TypeArguments::New(1));
tav_num.SetTypeAt(0, Type::Handle(Type::Number()));
CanonicalizeTAV(&tav_num);
auto& type_a_int = Type::Handle(Type::New(class_a, tav_int));
FinalizeAndCanonicalize(&type_a_int);
auto& type_record_int_int =
AbstractType::Handle(record_int_int.GetType(Heap::kNew));
FinalizeAndCanonicalize(&type_record_int_int);
TTSTestState state(thread, type_a_int);
state.InvokeLazilySpecializedStub({aint, tav_null, tav_null});
state.InvokeExistingStub(Failure({astring, tav_null, tav_null}));
TTSTestState record_state(thread, type_record_int_int);
record_state.InvokeLazilySpecializedStub(
{record_int_int, tav_null, tav_null});
record_state.InvokeExistingStub(
Failure({record_string_int, tav_null, tav_null}));
record_state.InvokeExistingStub(
Failure({record_int_string, tav_null, tav_null}));
// Make sure the stubs are specialized prior to reload.
EXPECT(type_a_int.type_test_stub() !=
TypeTestingStubGenerator::DefaultCodeForType(type_a_int));
EXPECT(type_record_int_int.type_test_stub() !=
TypeTestingStubGenerator::DefaultCodeForType(type_record_int_int));
root_library = ReloadTestScript(kReloadedScript);
const auto& a2int = Object::Handle(Invoke(root_library, "createA2Int"));
const auto& a2string = Object::Handle(Invoke(root_library, "createA2String"));
// Reloading resets all type testing stubs to the (possibly lazy specializing)
// default stub for that type.
EXPECT(type_a_int.type_test_stub() ==
TypeTestingStubGenerator::DefaultCodeForType(type_a_int));
EXPECT(type_record_int_int.type_test_stub() ==
TypeTestingStubGenerator::DefaultCodeForType(type_record_int_int));
// Reloading either removes or resets the type testing cache.
EXPECT(state.current_stc() == SubtypeTestCache::null() ||
(state.current_stc() == state.last_stc().ptr() &&
state.last_stc().NumberOfChecks() == 0));
EXPECT(record_state.current_stc() == SubtypeTestCache::null() ||
(record_state.current_stc() == record_state.last_stc().ptr() &&
record_state.last_stc().NumberOfChecks() == 0));
state.InvokeExistingStub(Respecialization({aint, tav_null, tav_null}));
state.InvokeExistingStub(Failure({astring, tav_null, tav_null}));
state.InvokeExistingStub({a2int, tav_null, tav_null});
state.InvokeExistingStub(Failure({a2string, tav_null, tav_null}));
record_state.InvokeExistingStub(
Respecialization({record_int_int, tav_null, tav_null}));
record_state.InvokeExistingStub(
Failure({record_string_int, tav_null, tav_null}));
record_state.InvokeExistingStub(
Failure({record_int_string, tav_null, tav_null}));
}
ISOLATE_UNIT_TEST_CASE(TTS_Partial_Reload) {
auto& root_library = Library::Handle(LoadTestScript(kLoadedScript));
const auto& class_a = Class::Handle(GetClass(root_library, "A"));
ClassFinalizer::FinalizeTypesInClass(class_a);
const auto& aint = Object::Handle(Invoke(root_library, "createAInt"));
const auto& astring = Object::Handle(Invoke(root_library, "createAString"));
const auto& tav_null = Object::null_type_arguments();
const auto& tav_int =
TypeArguments::Handle(Instance::Cast(aint).GetTypeArguments());
const auto& tav_string =
TypeArguments::Handle(Instance::Cast(astring).GetTypeArguments());
auto& tav_num = TypeArguments::Handle(TypeArguments::New(1));
tav_num.SetTypeAt(0, Type::Handle(Type::Number()));
CanonicalizeTAV(&tav_num);
// Create a partial TTS to test resets of STCs with false negatives.
const auto& type_a_t = Type::Handle(class_a.DeclarationType());
TTSTestCase positive1{aint, tav_int, tav_null};
TTSTestCase positive2{astring, tav_string, tav_null};
TTSTestCase negative1 = Failure({astring, tav_int, tav_null});
TTSTestCase negative2 = Failure({aint, tav_string, tav_null});
TTSTestCase false_negative = FalseNegative({aint, tav_num, tav_null});
TTSTestState state(thread, type_a_t);
state.InvokeLazilySpecializedStub(positive1);
state.InvokeExistingStub(positive2);
state.InvokeExistingStub(negative1);
state.InvokeExistingStub(negative2);
state.InvokeExistingStub(false_negative);
root_library = ReloadTestScript(kReloadedScript);
const auto& a2int = Object::Handle(Invoke(root_library, "createA2Int"));
const auto& a2string = Object::Handle(Invoke(root_library, "createA2String"));
// Reloading resets all type testing stubs to the (possibly lazy specializing)
// default stub for that type.
EXPECT(type_a_t.type_test_stub() ==
TypeTestingStubGenerator::DefaultCodeForType(type_a_t));
// Reloading either removes or resets the type testing cache.
EXPECT(state.current_stc() == SubtypeTestCache::null() ||
(state.current_stc() == state.last_stc().ptr() &&
state.last_stc().NumberOfChecks() == 0));
state.InvokeExistingStub(Respecialization(positive1));
state.InvokeExistingStub(positive2);
state.InvokeExistingStub(negative1);
state.InvokeExistingStub(negative2);
state.InvokeExistingStub(false_negative);
state.InvokeExistingStub({a2int, tav_int, tav_null});
state.InvokeExistingStub({a2string, tav_string, tav_null});
state.InvokeExistingStub(Failure({a2string, tav_int, tav_null}));
state.InvokeExistingStub(Failure({a2int, tav_string, tav_null}));
state.InvokeExistingStub(FalseNegative({a2int, tav_num, tav_null}));
}
#endif // !defined(PRODUCT)
// This test checks for a failure due to not reloading the class id between
// different uses of GenerateCidRangeChecks when loading the instance type
// arguments vector in a TTS for an implemented class. GenerateCidRangeChecks
// might clobber the register that holds the class ID to check, hence the need
// to reload.
//
// To ensure that the register is clobbered on all architectures, we set things
// up by generating the following classes:
// * B<X>, a generic abstract class which is implemented by the others.
// * I, implements B<String>, has a single int field x, and is
// used to create the checked instance.
// * G<Y>, which implements B<Y> and has no fields (so its TAV field
// offset will correspond to that of the offset of x in I).
// * C and D, consecutively defined non-generic classes which both implement
// B<int>.
// * U0 - UN, unrelated concrete classes as needed for cid alignment.
//
// We'll carefully set things up so that the following equation between their
// class ids holds:
//
// G = I - C.
//
// Thus, when we create a TTS for B<int> and check it against an instance V
// of I. The cid for I will be loaded into a register R, and then two
// check blocks will be generated:
//
// * A check for the cid range [C-D], which has the side effect of
// subtracting the cid of C from the contents of R (here, the cid of I).
//
// * A check that R contains the cid for G.
//
// Thus, if the cid of I is not reloaded into R before the second check, and
// the equation earlier holds, we'll get a false positive that V is an instance
// of G, so the code will then try to load the instance type arguments from V
// as if it was an instance of G. This means the contents of x will be loaded
// and attempted to be used as a TypeArgumentsPtr, which will cause a crash
// during the checks that the instantiation of Y is int.
ISOLATE_UNIT_TEST_CASE(TTS_Regress_CidRangeChecks) {
// We create the classes in this order: B, G, C, D, U..., I. We need
// G = I - C => G + C = I
// => G + C = D + N + 1 (where N is the number of U classes)
// => (B + 1) + C = (C + 1) + N + 1
// => B - 1 = N.
// The cid for B will be the next allocated cid, which is the number of
// non-top-level cids in the current class table.
ClassTable* const class_table = IsolateGroup::Current()->class_table();
const intptr_t kNumUnrelated = class_table->NumCids() - 1;
TextBuffer buffer(1024);
buffer.AddString(R"(
abstract class B<X> {}
class G<Y> implements B<Y> {}
class C implements B<int> {}
class D implements B<int> {}
)");
for (intptr_t i = 0; i < kNumUnrelated; i++) {
buffer.Printf(R"(
class U%)" Pd R"( {}
)",
i);
}
buffer.AddString(R"(
class I implements B<String> {
final x = 1;
}
createI() => I();
)");
const auto& root_library = Library::Handle(LoadTestScript(buffer.buffer()));
const auto& class_b = Class::Handle(GetClass(root_library, "B"));
const auto& class_g = Class::Handle(GetClass(root_library, "G"));
const auto& class_c = Class::Handle(GetClass(root_library, "C"));
const auto& class_d = Class::Handle(GetClass(root_library, "D"));
const auto& class_u0 = Class::Handle(GetClass(root_library, "U0"));
const auto& class_i = Class::Handle(GetClass(root_library, "I"));
const auto& obj_i = Object::Handle(Invoke(root_library, "createI"));
{
SafepointWriteRwLocker ml(thread, thread->isolate_group()->program_lock());
ClassFinalizer::FinalizeClass(class_g);
}
// Double-check assumptions from calculating kNumUnrelated.
EXPECT_EQ(kNumUnrelated, class_b.id() - 1);
EXPECT_EQ(class_b.id() + 1, class_g.id());
EXPECT_EQ(class_c.id() + 1, class_d.id());
EXPECT_EQ(class_d.id() + 1, class_u0.id());
EXPECT_EQ(class_u0.id() + kNumUnrelated, class_i.id());
EXPECT_EQ(class_g.id(), class_i.id() - class_c.id());
const auto& tav_null = Object::null_type_arguments();
auto& tav_int = TypeArguments::Handle(TypeArguments::New(1));
tav_int.SetTypeAt(0, Type::Handle(Type::IntType()));
CanonicalizeTAV(&tav_int);
auto& type_b_int = Type::Handle(Type::New(class_b, tav_int));
FinalizeAndCanonicalize(&type_b_int);
TTSTestState state(thread, type_b_int);
state.InvokeEagerlySpecializedStub(Failure({obj_i, tav_null, tav_null}));
}
struct STCTestResults {
bool became_hash_cache = false;
bool cache_capped = false;
};
static STCTestResults SubtypeTestCacheTest(Thread* thread,
intptr_t num_classes) {
TextBuffer buffer(MB);
buffer.AddString("class D<S> {}\n");
buffer.AddString("D<int> Function() createClosureD() => () => D<int>();\n");
for (intptr_t i = 0; i < num_classes; i++) {
buffer.Printf(R"(class C%)" Pd R"(<S> extends D<S> {}
C%)" Pd R"(<int> Function() createClosureC%)" Pd R"(() => () => C%)" Pd
R"(<int>();
)",
i, i, i, i);
}
Dart_Handle api_lib = TestCase::LoadTestScript(buffer.buffer(), nullptr);
EXPECT_VALID(api_lib);
// D + C0...CN, where N = kNumClasses - 1
EXPECT(IsolateGroup::Current()->class_table()->NumCids() > num_classes);
TransitionNativeToVM transition(thread);
Zone* const zone = thread->zone();
const auto& root_lib =
Library::CheckedHandle(zone, Api::UnwrapHandle(api_lib));
EXPECT(!root_lib.IsNull());
const auto& class_d = Class::Handle(zone, GetClass(root_lib, "D"));
ASSERT(!class_d.IsNull());
{
SafepointWriteRwLocker ml(thread, thread->isolate_group()->program_lock());
ClassFinalizer::FinalizeClass(class_d);
}
const auto& object_d =
Instance::CheckedHandle(zone, Invoke(root_lib, "createClosureD"));
ASSERT(!object_d.IsNull());
auto& type_closure_d_int =
AbstractType::Handle(zone, object_d.GetType(Heap::kNew));
const bool can_be_null = Instance::NullIsAssignableTo(type_closure_d_int);
const auto& tav_null = Object::null_type_arguments();
TTSTestState state(thread, type_closure_d_int);
// Prime the stub before the loop with the null object.
state.InvokeEagerlySpecializedStub(
{Object::null_object(), tav_null, tav_null, can_be_null ? kTTS : kFail});
auto& class_c = Class::Handle(zone);
auto& object_c = Object::Handle(zone);
STCTestResults results;
for (intptr_t i = 0; i < num_classes; ++i) {
auto const class_name = OS::SCreate(zone, "C%" Pd "", i);
class_c = GetClass(root_lib, class_name);
ASSERT(!class_c.IsNull());
{
SafepointWriteRwLocker ml(thread,
thread->isolate_group()->program_lock());
ClassFinalizer::FinalizeClass(class_c);
}
auto const function_name = OS::SCreate(zone, "createClosureC%" Pd "", i);
object_c = Invoke(root_lib, function_name);
TTSTestCase base_case = {object_c, tav_null, tav_null};
if (i >= FLAG_max_subtype_cache_entries) {
ASSERT(state.last_stc().NumberOfChecks() >=
FLAG_max_subtype_cache_entries);
state.InvokeExistingStub(RuntimeCheck(base_case));
// Rerunning the test doesn't change the fact we can't add to the STC.
state.InvokeExistingStub(RuntimeCheck(base_case));
results.cache_capped = true;
} else {
const bool was_hash = state.last_stc().IsHash();
// All the rest of the tests should create or modify an STC.
state.InvokeExistingStub(FalseNegative(base_case));
if (i == 0) {
// We should get a linear cache the first time.
EXPECT(!state.last_stc().IsHash());
} else if (was_hash) {
// We should never change from hash back to linear.
EXPECT(state.last_stc().IsHash());
} else if (state.last_stc().IsHash()) {
results.became_hash_cache = true;
}
state.InvokeExistingStub(STCCheck(base_case));
}
}
return results;
}
// The smallest test that just checks linear caches.
TEST_CASE(TTS_STC_LinearOnly) {
const intptr_t num_classes =
Utils::Minimum(static_cast<intptr_t>(FLAG_max_subtype_cache_entries),
SubtypeTestCache::kMaxLinearCacheEntries);
EXPECT(num_classes > 0);
const auto& results = SubtypeTestCacheTest(thread, num_classes);
EXPECT(!results.became_hash_cache);
EXPECT(!results.cache_capped);
}
// A larger test that ensures we convert to a hash table at some point.
TEST_CASE(TTS_STC_Hash) {