// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 // Qt-Security score:significant #include "qqmljsoptimizations_p.h" #include "qqmljsbasicblocks_p.h" #include "qqmljsutils_p.h" QT_BEGIN_NAMESPACE using namespace Qt::Literals::StringLiterals; QQmlJSCompilePass::BlocksAndAnnotations QQmlJSOptimizations::run(const Function *function) { m_function = function; populateBasicBlocks(); populateReaderLocations(); removeDeadStoresUntilStable(); adjustTypes(); return { std::move(m_basicBlocks), std::move(m_annotations) }; } struct PendingBlock { QQmlJSOptimizations::Conversions conversions; int start = -1; bool registerActive = false; }; template static bool containsAny(const ContainerA &container, const ContainerB &elements) { for (const auto &element : elements) { if (container.contains(element)) return true; } return false; } template, class KeyContainer = QList, class MappedContainer = QList> class NewFlatMap { public: using OriginalFlatMap = QFlatMap; void appendOrdered(const typename OriginalFlatMap::iterator &i) { keys.append(i.key()); values.append(i.value()); } OriginalFlatMap take() { OriginalFlatMap result(Qt::OrderedUniqueRange, std::move(keys), std::move(values)); keys.clear(); values.clear(); return result; } private: typename OriginalFlatMap::key_container_type keys; typename OriginalFlatMap::mapped_container_type values; }; void QQmlJSOptimizations::populateReaderLocations() { for (auto writeIt = m_annotations.begin(), writeEnd = m_annotations.end(); writeIt != writeEnd; ++writeIt) { const int writtenRegister = writeIt->second.changedRegisterIndex; // Instructions that don't write can't be dead stores, no need to populate reader locations if (writtenRegister == InvalidRegister) continue; RegisterAccess &access = m_readerLocations[writeIt.key()]; access.trackedRegister = writtenRegister; if (writeIt->second.changedRegister.isConversion()) { // If it's a conversion, we have to check for all readers of the conversion origins. // This happens at jump targets where different types are merged. A StoreReg or similar // instruction must be optimized out if none of the types it can hold is read anymore. access.trackedTypes.clear(); const auto origins = writeIt->second.changedRegister.conversionOrigins(); for (QQmlJSRegisterContent origin : origins) access.trackedTypes.append(origin); } else { access.trackedTypes.append(writeIt->second.changedRegister); Q_ASSERT(!access.trackedTypes.last().isNull()); } auto blockIt = QQmlJSBasicBlocks::basicBlockForInstruction(m_basicBlocks, writeIt.key()); QList blocks = { { {}, blockIt->first, true } }; QHash processedBlocks; bool isFirstBlock = true; while (!blocks.isEmpty()) { const PendingBlock block = blocks.takeLast(); // We can re-enter the first block from the beginning. // We will then find any reads before the write we're currently examining. if (!isFirstBlock) processedBlocks.insert(block.start, block); auto nextBlock = m_basicBlocks.find(block.start); auto currentBlock = nextBlock++; bool registerActive = block.registerActive; Conversions conversions = block.conversions; const auto blockEnd = (nextBlock == m_basicBlocks.end()) ? m_annotations.end() : m_annotations.find(nextBlock->first); auto blockInstr = isFirstBlock ? (writeIt + 1) : m_annotations.find(currentBlock->first); for (; blockInstr != blockEnd; ++blockInstr) { if (registerActive && blockInstr->second.typeConversions.contains(writtenRegister)) { conversions.insert(blockInstr.key()); } for (auto readIt = blockInstr->second.readRegisters.constBegin(), end = blockInstr->second.readRegisters.constEnd(); readIt != end; ++readIt) { if (blockInstr->second.isRename) { // Nothing to do } else if (readIt->second.content.isConversion()) { const QList conversionOrigins = readIt->second.content.conversionOrigins(); for (QQmlJSRegisterContent origin : conversionOrigins) { if (!access.trackedTypes.contains(origin)) continue; Q_ASSERT(readIt->second.content.conversionResultType()); access.typeReaders[blockInstr.key()] = readIt->second.content; break; } } else if (access.trackedTypes.contains(readIt->second.content)) { // We've used the original content instead of converting it access.typeReaders[blockInstr.key()] = readIt->second.content; } if (registerActive && readIt->first == writtenRegister) access.registerReadersAndConversions[blockInstr.key()] = conversions; } if (blockInstr->second.changedRegisterIndex == writtenRegister) { conversions.clear(); registerActive = false; } } auto scheduleBlock = [&](int blockStart) { // If we find that an already processed block has the register activated by this jump, // we need to re-evaluate it. We also need to propagate any newly found conversions. const auto processed = processedBlocks.find(blockStart); if (processed == processedBlocks.end()) { blocks.append({conversions, blockStart, registerActive}); } else if (registerActive && !processed->registerActive) { blocks.append({conversions, blockStart, registerActive}); } else { Conversions merged = processed->conversions; merged.unite(conversions); if (merged.size() > processed->conversions.size()) blocks.append({std::move(merged), blockStart, registerActive}); } }; if (!currentBlock->second.jumpIsUnconditional && nextBlock != m_basicBlocks.end()) scheduleBlock(nextBlock->first); const int jumpTarget = currentBlock->second.jumpTarget; if (jumpTarget != -1) scheduleBlock(jumpTarget); if (isFirstBlock) isFirstBlock = false; } } } bool QQmlJSOptimizations::eraseDeadStore(const InstructionAnnotations::iterator &it, bool &erasedReaders) { auto reader = m_readerLocations.find(it.key()); if (reader != m_readerLocations.end() && (reader->typeReaders.isEmpty() || reader->registerReadersAndConversions.isEmpty())) { if (it->second.isRename) { // If it's a rename, it doesn't "own" its output type. The type may // still be read elsewhere, even if this register isn't. However, we're // not interested in the variant or any other details of the register. // Therefore just delete it. it->second.changedRegisterIndex = InvalidRegister; it->second.changedRegister = QQmlJSRegisterContent(); } else { // We can't do this with certain QObjects because they still need tracking as // implicitly destructible by the garbage collector. We may be calling a factory // function and then forgetting the object after all. // // However, objects we need to track that way can only be produced through external // side effects (i.e. function calls). const QQmlJSScope::ConstPtr contained = it->second.changedRegister.containedType(); if (!it->second.hasExternalSideEffects || (!contained->isReferenceType() && !m_typeResolver->canHold(contained, m_typeResolver->qObjectType()))) { // void the output, rather than deleting it. We still need its variant. const bool adjusted = m_typeResolver->adjustTrackedType( it->second.changedRegister, m_typeResolver->voidType()); Q_ASSERT(adjusted); // Can always convert to void } } m_readerLocations.erase(reader); // If it's not a label and has no side effects, we can drop the instruction. if (!it->second.hasInternalSideEffects) { if (!it->second.readRegisters.isEmpty()) { it->second.readRegisters.clear(); erasedReaders = true; } if (m_basicBlocks.find(it.key()) == m_basicBlocks.end()) return true; } } return false; } void QQmlJSOptimizations::removeDeadStoresUntilStable() { using NewInstructionAnnotations = NewFlatMap; NewInstructionAnnotations newAnnotations; bool erasedReaders = true; while (erasedReaders) { erasedReaders = false; for (auto it = m_annotations.begin(), end = m_annotations.end(); it != end; ++it) { InstructionAnnotation &instruction = it->second; // Don't touch the function prolog instructions if (instruction.changedRegisterIndex < InvalidRegister) { newAnnotations.appendOrdered(it); continue; } removeReadsFromErasedInstructions(it); if (!eraseDeadStore(it, erasedReaders)) newAnnotations.appendOrdered(it); } m_annotations = newAnnotations.take(); } } void QQmlJSOptimizations::removeReadsFromErasedInstructions( const QFlatMap::const_iterator &it) { auto readers = m_readerLocations.find(it.key()); if (readers == m_readerLocations.end()) return; for (auto typeIt = readers->typeReaders.begin(); typeIt != readers->typeReaders.end();) { if (m_annotations.contains(typeIt.key())) ++typeIt; else typeIt = readers->typeReaders.erase(typeIt); } for (auto registerIt = readers->registerReadersAndConversions.begin(); registerIt != readers->registerReadersAndConversions.end();) { if (m_annotations.contains(registerIt.key())) ++registerIt; else registerIt = readers->registerReadersAndConversions.erase(registerIt); } } bool QQmlJSOptimizations::canMove(int instructionOffset, const QQmlJSOptimizations::RegisterAccess &access) const { if (access.typeReaders.size() != 1) return false; return QQmlJSBasicBlocks::basicBlockForInstruction(m_basicBlocks, instructionOffset) == QQmlJSBasicBlocks::basicBlockForInstruction(m_basicBlocks, access.typeReaders.begin().key()); } QList QQmlJSBasicBlocks::objectAndArrayDefinitions() const { return m_objectAndArrayDefinitions; } static QString adjustErrorMessage( QQmlJSRegisterContent origin, const QQmlJSScope::ConstPtr &conversion) { return QLatin1String("Cannot convert from ") + origin.containedType()->internalName() + QLatin1String(" to ") + conversion->internalName(); } static QString adjustErrorMessage( QQmlJSRegisterContent origin, QQmlJSRegisterContent conversion) { return adjustErrorMessage(origin, conversion.containedType()); } static QString adjustErrorMessage( QQmlJSRegisterContent origin, const QList &conversions) { if (conversions.size() == 1) return adjustErrorMessage(origin, conversions[0]); QString types; for (QQmlJSRegisterContent type : conversions) { if (!types.isEmpty()) types += QLatin1String(", "); types += type.containedType()->internalName(); } return QLatin1String("Cannot convert from ") + origin.containedType()->internalName() + QLatin1String(" to union of ") + types; } void QQmlJSOptimizations::adjustTypes() { using NewVirtualRegisters = NewFlatMap; QHash> liveConversions; QHash> movableReads; const auto handleRegisterReadersAndConversions = [&](QHash::const_iterator it) { for (auto conversions = it->registerReadersAndConversions.constBegin(), end = it->registerReadersAndConversions.constEnd(); conversions != end; ++conversions) { if (conversions->isEmpty() && canMove(it.key(), it.value())) movableReads[conversions.key()].append(it->trackedRegister); for (int conversion : *conversions) liveConversions[conversion].append(it->trackedRegister); } }; // Handle the array definitions first. // Changing the array type changes the expected element types. auto adjustArray = [&](int instructionOffset, int mode) { auto it = m_readerLocations.constFind(instructionOffset); if (it == m_readerLocations.cend()) return; const InstructionAnnotation &annotation = m_annotations[instructionOffset]; if (annotation.readRegisters.isEmpty()) return; Q_ASSERT(it->trackedTypes.size() == 1); Q_ASSERT(it->trackedTypes[0] == annotation.changedRegister); if (it->trackedTypes[0].containedType()->accessSemantics() != QQmlJSScope::AccessSemantics::Sequence) { return; // Constructed something else. } if (!m_typeResolver->adjustTrackedType(it->trackedTypes[0], it->typeReaders.values())) addError(adjustErrorMessage(it->trackedTypes[0], it->typeReaders.values())); // Now we don't adjust the type we store, but rather the type we expect to read. We // can do this because we've tracked the read type when we defined the array in // QQmlJSTypePropagator. if (const QQmlJSScope::ConstPtr elementType = it->trackedTypes[0].containedType()->elementType()) { const QQmlJSRegisterContent content = annotation.readRegisters.begin().value().content; const QQmlJSScope::ConstPtr contained = content.containedType(); // If it's the 1-arg Array ctor, and the argument is a number, that's special. if (mode != ObjectOrArrayDefinition::ArrayConstruct1ArgId || contained != m_typeResolver->realType()) { if (!m_typeResolver->adjustTrackedType(content, elementType)) addError(adjustErrorMessage(content, elementType)); } } handleRegisterReadersAndConversions(it); m_readerLocations.erase(it); }; // Handle the object definitions. // Changing the object type changes the expected property types. const auto adjustObject = [&](const ObjectOrArrayDefinition &object) { auto it = m_readerLocations.find(object.instructionOffset); if (it == m_readerLocations.end()) return; const InstructionAnnotation &annotation = m_annotations[object.instructionOffset]; Q_ASSERT(it->trackedTypes.size() == 1); const QQmlJSRegisterContent resultType = it->trackedTypes[0]; Q_ASSERT(resultType == annotation.changedRegister); Q_ASSERT(!annotation.readRegisters.isEmpty()); if (!m_typeResolver->adjustTrackedType(resultType, it->typeReaders.values())) addError(adjustErrorMessage(resultType, it->typeReaders.values())); m_readerLocations.erase(it); if (resultType.contains(m_typeResolver->varType()) || resultType.contains(m_typeResolver->variantMapType()) || resultType.contains(m_typeResolver->jsValueType())) { // It's all variant anyway return; } const int classSize = m_jsUnitGenerator->jsClassSize(object.internalClassId); Q_ASSERT(object.argc >= classSize); for (int i = 0; i < classSize; ++i) { // Now we don't adjust the type we store, but rather the types we expect to read. We // can do this because we've tracked the read types when we defined the object in // QQmlJSTypePropagator. const QString propName = m_jsUnitGenerator->jsClassMember(object.internalClassId, i); const QQmlJSMetaProperty property = resultType.containedType()->property(propName); if (!property.isValid()) { addError(resultType.containedType()->internalName() + QLatin1String(" has no property called ") + propName); continue; } const QQmlJSScope::ConstPtr propType = property.type(); if (propType.isNull()) { addError(QLatin1String("Cannot resolve type of property ") + propName); continue; } const QQmlJSRegisterContent content = annotation.readRegisters[object.argv + i].content; if (!m_typeResolver->adjustTrackedType(content, propType)) addError(adjustErrorMessage(content, propType)); } // The others cannot be adjusted. We don't know their names, yet. // But we might still be able to use the variants. }; // Iterate in reverse so that we can have nested lists and objects and the types are propagated // from the outer lists/objects to the inner ones. for (auto it = m_objectAndArrayDefinitions.crbegin(), end = m_objectAndArrayDefinitions.crend(); it != end; ++it) { switch (it->internalClassId) { case ObjectOrArrayDefinition::ArrayClassId: case ObjectOrArrayDefinition::ArrayConstruct1ArgId: adjustArray(it->instructionOffset, it->internalClassId); break; default: adjustObject(*it); break; } } for (auto it = m_readerLocations.cbegin(), end = m_readerLocations.cend(); it != end; ++it) { handleRegisterReadersAndConversions(it); // There is always one first occurrence of any tracked type. Conversions don't change // the type. if (it->trackedTypes.size() != 1) continue; // Don't adjust renamed values. We only adjust the originals. const int writeLocation = it.key(); if (writeLocation >= 0 && m_annotations[writeLocation].isRename) continue; if (!m_typeResolver->adjustTrackedType(it->trackedTypes[0], it->typeReaders.values())) addError(adjustErrorMessage(it->trackedTypes[0], it->typeReaders.values())); } NewVirtualRegisters newRegisters; for (auto i = m_annotations.begin(), iEnd = m_annotations.end(); i != iEnd; ++i) { for (auto conversion = i->second.typeConversions.begin(), conversionEnd = i->second.typeConversions.end(); conversion != conversionEnd; ++conversion) { if (!liveConversions[i.key()].contains(conversion.key())) continue; QQmlJSScope::ConstPtr newResult; const auto content = conversion->second.content; if (content.isConversion() && !content.original().isValid()) { const auto conversionOrigins = content.conversionOrigins(); for (const auto &origin : conversionOrigins) newResult = m_typeResolver->merge(newResult, origin.containedType()); if (!m_typeResolver->adjustTrackedType(content, newResult)) addError(adjustErrorMessage(content, newResult)); } newRegisters.appendOrdered(conversion); } i->second.typeConversions = newRegisters.take(); for (int movable : std::as_const(movableReads[i.key()])) i->second.readRegisters[movable].canMove = true; } } void QQmlJSOptimizations::populateBasicBlocks() { for (auto blockNext = m_basicBlocks.begin(), blockEnd = m_basicBlocks.end(); blockNext != blockEnd;) { const auto blockIt = blockNext++; BasicBlock &block = blockIt->second; QList writtenTypes; QList writtenRegisters; const auto instrEnd = (blockNext == blockEnd) ? m_annotations.end() : m_annotations.find(blockNext->first); for (auto instrIt = m_annotations.find(blockIt->first); instrIt != instrEnd; ++instrIt) { const InstructionAnnotation &instruction = instrIt->second; for (auto it = instruction.readRegisters.begin(), end = instruction.readRegisters.end(); it != end; ++it) { if (!writtenRegisters.contains(it->first)) block.readRegisters.append(it->first); } // If it's just a renaming, the type has existed in a different register before. if (instruction.changedRegisterIndex != InvalidRegister) { if (!instruction.isRename) writtenTypes.append(instruction.changedRegister.containedType()); writtenRegisters.append(instruction.changedRegisterIndex); } } QQmlJSUtils::deduplicate(block.readRegisters); } } QT_END_NAMESPACE