QmlCompiler: Implement resetting of properties

We piggy-back on the mechanism used to handle shadowable properties and
pass the value as QVariant. QVariant can hold undefined and the lookup
functions know how to handle it.

Pick-to: 6.5 6.2
Fixes: QTBUG-120512
Change-Id: I9bca4940256c82bdcf5540b956600eb420be363e
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
(cherry picked from commit da6680cb2e)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
(cherry picked from commit 104c90e883)
This commit is contained in:
Ulf Hermann 2024-01-19 13:26:32 +01:00
parent 787e794af8
commit 49a62c4102
8 changed files with 168 additions and 14 deletions

View File

@ -178,6 +178,14 @@ public:
return result;
}
QQmlJSRegisterContent castTo(const QQmlJSScope::ConstPtr &newContainedType) const
{
// This is not a conversion but a run time cast. It may result in null or undefined.
QQmlJSRegisterContent result = *this;
result.m_content = newContainedType;
return result;
}
private:
enum ContentKind { Type, Property, Enum, Method, ImportNamespace, Conversion };

View File

@ -43,6 +43,9 @@ void QQmlJSShadowCheck::run(
m_error = error;
m_state = initialState(function);
decode(m_function->code.constData(), static_cast<uint>(m_function->code.size()));
for (const auto &store : m_resettableStores)
checkResettable(store.accumulatorIn, store.instructionOffset);
}
void QQmlJSShadowCheck::generate_LoadProperty(int nameIndex)
@ -70,15 +73,37 @@ void QQmlJSShadowCheck::generate_GetLookup(int index)
}
}
void QQmlJSShadowCheck::handleStore(int base, const QString &memberName)
{
const int instructionOffset = currentInstructionOffset();
const QQmlJSRegisterContent &readAccumulator
= (*m_annotations)[instructionOffset].readRegisters[Accumulator].content;
// If the accumulator is already read as var, we don't have to do anything.
if (m_typeResolver->registerContains(readAccumulator, m_typeResolver->varType()))
return;
const auto baseType = m_state.registers[base].content;
if (checkShadowing(baseType, memberName, base) == Shadowable)
return;
// If the property isn't shadowable but resettable, we have to turn the read register into
// var if the accumulator can hold undefined. This has to be done in a second pass
// because the accumulator may still turn into var due to its own shadowing.
const QQmlJSRegisterContent member = m_typeResolver->memberType(baseType, memberName);
if (member.isProperty() && !member.property().reset().isEmpty())
m_resettableStores.append({m_state.accumulatorIn(), instructionOffset});
}
void QQmlJSShadowCheck::generate_StoreProperty(int nameIndex, int base)
{
checkShadowing(
m_state.registers[base].content, m_jsUnitGenerator->stringForIndex(nameIndex), base);
handleStore(base, m_jsUnitGenerator->stringForIndex(nameIndex));
}
void QQmlJSShadowCheck::generate_SetLookup(int index, int base)
{
checkShadowing(m_state.registers[base].content, m_jsUnitGenerator->lookupName(index), base);
handleStore(base, m_jsUnitGenerator->lookupName(index));
}
void QQmlJSShadowCheck::generate_CallProperty(int nameIndex, int base, int argc, int argv)
@ -107,11 +132,11 @@ void QQmlJSShadowCheck::endInstruction(QV4::Moth::Instr::Type)
{
}
void QQmlJSShadowCheck::checkShadowing(
QQmlJSShadowCheck::Shadowability QQmlJSShadowCheck::checkShadowing(
const QQmlJSRegisterContent &baseType, const QString &memberName, int baseRegister)
{
if (baseType.storedType()->accessSemantics() != QQmlJSScope::AccessSemantics::Reference)
return;
return NotShadowable;
switch (baseType.variant()) {
case QQmlJSRegisterContent::ExtensionObjectProperty:
@ -128,14 +153,14 @@ void QQmlJSShadowCheck::checkShadowing(
// those are not shadowable.
if (!member.isValid()) {
Q_ASSERT(m_typeResolver->isPrefix(memberName));
return;
return NotShadowable;
}
if (member.isProperty()) {
if (member.property().isFinal())
return; // final properties can't be shadowed
return NotShadowable; // final properties can't be shadowed
} else if (!member.isMethod()) {
return; // Only properties and methods can be shadowed
return NotShadowable; // Only properties and methods can be shadowed
}
m_logger->log(
@ -160,15 +185,33 @@ void QQmlJSShadowCheck::checkShadowing(
if (it.key() != baseRegister)
it->second.content = m_typeResolver->convert(it->second.content, varContent);
}
return;
return Shadowable;
}
default:
// In particular ObjectById is fine as that cannot change into something else
// Singleton should also be fine, unless the factory function creates an object
// with different property types than the declared class.
return;
return NotShadowable;
}
}
void QQmlJSShadowCheck::checkResettable(
const QQmlJSRegisterContent &accumulatorIn, int instructionOffset)
{
const QQmlJSScope::ConstPtr varType = m_typeResolver->varType();
// The stored type is not necessarily updated by the shadow check, but it
// will be in the basic blocks pass. For the purpose of adjusting newly
// shadowable types we can ignore it. We only want to know if any of the
// contents can hold undefined.
if (!m_typeResolver->canHoldUndefined(accumulatorIn.storedIn(varType)))
return;
const QQmlJSRegisterContent varContent = m_typeResolver->globalType(varType);
QQmlJSRegisterContent &readAccumulator
= (*m_annotations)[instructionOffset].readRegisters[Accumulator].content;
readAccumulator = m_typeResolver->convert(readAccumulator, varContent);
}
QT_END_NAMESPACE

View File

@ -32,6 +32,13 @@ public:
QQmlJS::DiagnosticMessage *error);
private:
struct ResettableStore {
QQmlJSRegisterContent accumulatorIn;
int instructionOffset = -1;
};
void handleStore(int base, const QString &memberName);
void generate_LoadProperty(int nameIndex) override;
void generate_GetLookup(int index) override;
void generate_StoreProperty(int nameIndex, int base) override;
@ -42,9 +49,13 @@ private:
QV4::Moth::ByteCodeHandler::Verdict startInstruction(QV4::Moth::Instr::Type) override;
void endInstruction(QV4::Moth::Instr::Type) override;
void checkShadowing(
enum Shadowability { NotShadowable, Shadowable };
Shadowability checkShadowing(
const QQmlJSRegisterContent &baseType, const QString &propertyName, int baseRegister);
void checkResettable(const QQmlJSRegisterContent &accumulatorIn, int instructionOffset);
QList<ResettableStore> m_resettableStores;
InstructionAnnotations *m_annotations = nullptr;
State m_state;
};

View File

@ -972,9 +972,22 @@ void QQmlJSTypePropagator::generate_StoreProperty(int nameIndex, int base)
getCurrentBindingSourceLocation()));
}
m_state.setHasSideEffects(true);
addReadAccumulator(property);
// If the property is resettable we must not coerce the input to the property type
// as that might eliminate an undefined value. For example, undefined -> string
// becomes "undefined".
// Therefore we explicitly require the value to be given as QVariant. This triggers
// the QVariant fallback path that also used for shadowable properties. QVariant can
// hold undefined and the lookup functions will handle that appropriately.
const QQmlJSScope::ConstPtr varType = m_typeResolver->varType();
const QQmlJSRegisterContent readType
= (property.property().reset().isEmpty()
|| !m_typeResolver->canHoldUndefined(m_state.accumulatorIn()))
? property
: property.storedIn(varType).castTo(varType);
addReadAccumulator(readType);
addReadRegister(base, callBase);
m_state.setHasSideEffects(true);
}
void QQmlJSTypePropagator::generate_SetLookup(int index, int base)

View File

@ -15,6 +15,7 @@ set(cpp_sources
multiforeign.h
objectwithmethod.h
person.cpp person.h
resettable.h
sequenceToIterable.h
sequencetypeexample.cpp sequencetypeexample.h
state.h
@ -199,6 +200,7 @@ set(qml_files
registerPropagation.qml
registerelimination.qml
renameAdjust.qml
resettable.qml
revisions.qml
scopeIdLookup.qml
scopeVsObject.qml

View File

@ -0,0 +1,39 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#ifndef RESETTABLE_H
#define RESETTABLE_H
#include <QtCore/qobject.h>
#include <QtQml/qqml.h>
class ResettableProperty : public QObject
{
Q_OBJECT
QML_NAMED_ELEMENT(Resettable)
Q_PROPERTY(qreal value READ value WRITE setValue RESET resetValue NOTIFY valueChanged FINAL)
Q_PROPERTY(qreal shadowable READ shadowable CONSTANT)
public:
explicit ResettableProperty(QObject *parent = nullptr) : QObject(parent) {}
qreal value() const { return m_value; }
qreal shadowable() const { return 25; }
public slots:
void resetValue() { setValue(0); }
void setValue(qreal value)
{
if (m_value == value)
return;
m_value = value;
emit valueChanged();
}
signals:
void valueChanged();
private:
qreal m_value = 0;
};
#endif // RESETTABLE_H

View File

@ -0,0 +1,15 @@
pragma Strict
import QtQml
import TestTypes
Resettable {
id: self
value: 999
property Resettable shadowing: Resettable {
property var shadowable: undefined
}
function doReset() { self.value = undefined }
function doReset2() { self.value = shadowing.shadowable }
}

View File

@ -7,6 +7,7 @@
#include <data/cppbaseclass.h>
#include <data/enumproblems.h>
#include <data/objectwithmethod.h>
#include <data/resettable.h>
#include <QtQml/private/qqmlengine_p.h>
#include <QtQml/private/qqmlpropertycachecreator_p.h>
@ -164,6 +165,7 @@ private slots:
void registerElimination();
void registerPropagation();
void renameAdjust();
void resettableProperty();
void revisions();
void scopeIdLookup();
void scopeObjectDestruction();
@ -3462,6 +3464,27 @@ void tst_QmlCppCodegen::renameAdjust()
QVERIFY(o);
}
void tst_QmlCppCodegen::resettableProperty()
{
QQmlEngine engine;
QQmlComponent c(&engine, QUrl(u"qrc:/qt/qml/TestTypes/resettable.qml"_s));
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QScopedPointer<QObject> o(c.create());
QVERIFY(o);
ResettableProperty *resettable = qobject_cast<ResettableProperty *>(o.data());
QVERIFY(resettable);
QCOMPARE(resettable->value(), 999);
QMetaObject::invokeMethod(resettable, "doReset");
QCOMPARE(resettable->value(), 0);
resettable->setValue(82);
QCOMPARE(resettable->value(), 82);
QMetaObject::invokeMethod(resettable, "doReset2");
QCOMPARE(resettable->value(), 0);
}
void tst_QmlCppCodegen::revisions()
{
QQmlEngine engine;