diff options
author | Ulf Hermann <ulf.hermann@qt.io> | 2023-02-16 10:54:34 +0100 |
---|---|---|
committer | Ulf Hermann <ulf.hermann@qt.io> | 2023-03-03 12:02:00 +0100 |
commit | 05f56d7c78754855c643470ad4e8dfd35c96f927 (patch) | |
tree | fe23c5afec83efd75f06046bc0fdb7b7da465999 | |
parent | e625e39845ccecda871659a8ff39ac081f4aee82 (diff) |
QML: Allow as-casting to value types
If the "Addressable" option to ValueTypeBehavior is set, you can use the
"as" operator to cast a previously unknown type into either undefined
or the given type. We can use this in qmlcachegen to generate efficient
code for further operations on the same type.
In the generated C++ it in fact only works for GetLookup because:
a, We generally don't do SetLookup on value types, yet.
b, We generally don't call methods on value types, yet.
c, We cannot store a union of undefined and a sequence type, yet.
However, getting properties of value types is the most important
application of the new casts so this is well worth it.
As a side effect we can also look up things in potentially undefined
results of other operations now. For example list lookups.
Task-number: QTBUG-94807
Change-Id: Ifdf34f1f3f67b7a0a8953b9ed0e947b74638a28c
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
-rw-r--r-- | src/qml/doc/src/qmllanguageref/documents/definetypes.qdoc | 40 | ||||
-rw-r--r-- | src/qml/jsruntime/qv4qmlcontext.cpp | 2 | ||||
-rw-r--r-- | src/qml/jsruntime/qv4runtime.cpp | 36 | ||||
-rw-r--r-- | src/qml/qml/qqmltypewrapper.cpp | 37 | ||||
-rw-r--r-- | src/qml/qml/qqmlvaluetypewrapper_p.h | 1 | ||||
-rw-r--r-- | src/qmlcompiler/qqmljscodegenerator.cpp | 167 | ||||
-rw-r--r-- | src/qmlcompiler/qqmljscodegenerator_p.h | 11 | ||||
-rw-r--r-- | src/qmlcompiler/qqmljstypepropagator.cpp | 21 | ||||
-rw-r--r-- | src/qmlcompiler/qqmljstyperesolver.cpp | 78 | ||||
-rw-r--r-- | src/qmlcompiler/qqmljstyperesolver_p.h | 6 | ||||
-rw-r--r-- | tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt | 2 | ||||
-rw-r--r-- | tests/auto/qml/qmlcppcodegen/data/math.qml | 1 | ||||
-rw-r--r-- | tests/auto/qml/qmlcppcodegen/data/objectLookupOnListElement.qml | 34 | ||||
-rw-r--r-- | tests/auto/qml/qmlcppcodegen/data/valueTypeCast.qml | 9 | ||||
-rw-r--r-- | tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp | 45 | ||||
-rw-r--r-- | tests/auto/qml/qqmllanguage/data/asValueType.qml | 10 | ||||
-rw-r--r-- | tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp | 26 |
17 files changed, 463 insertions, 63 deletions
diff --git a/src/qml/doc/src/qmllanguageref/documents/definetypes.qdoc b/src/qml/doc/src/qmllanguageref/documents/definetypes.qdoc index dde54e2af6..a1bd1452d0 100644 --- a/src/qml/doc/src/qmllanguageref/documents/definetypes.qdoc +++ b/src/qml/doc/src/qmllanguageref/documents/definetypes.qdoc @@ -384,4 +384,44 @@ that use value types or sequences when generating C++ code. Those functions are then left to be interpreted or JIT-compiled with the default behavior of the interpreter and JIT. +In addition, you can specify \c{Addressable} to refer to value types in all +places. This allows a JavaScript value to be explicitly coerced to a specific, +named, value type. This is done using the \c as operator, like you would do +with object types. Furthermore, you can also check for value types using the +\c instanceof operator: + +\qml +pragma ValueTypeBehavior: Addressable +import QtQml + +QtObject { + property var a + property real b: (a as rect).x + property bool c: a instanceof rect + + property var rect // inaccessible. "rect" is a type name. +} +\endqml + +If the type does not match, casting returns \c undefined. \c instanceof +only checks for inheritance, not for all possible type coercions. So, for +example, a \l{QRect} is not a \c rect value type since \c rect is \l{QRectF} +in C++, and therefore not related by inheritance. With \c as you can cast +to any type compatible via coercion. + +Since \c rect in the above example is now a type name, it will shadow any +properties called \c{rect}. + +Explicitly casting to the desired type helps tooling. It can allow the +\l{Qt Quick Compiler} generate efficient code where it otherwise would not be +able to. You can use \l{qmllint} to find such occurrences. + +There is also a \c{Inaddressable} value you can use to explicitly specify the +default behavior. The pragma values can be combined as comma-separated list: + +\qml +pragma ValueTypeBehavior: Copy, Addressable +\endqml + +\sa {Type annotations and assertions} */ diff --git a/src/qml/jsruntime/qv4qmlcontext.cpp b/src/qml/jsruntime/qv4qmlcontext.cpp index 1cabf7c6c2..e585adfa5b 100644 --- a/src/qml/jsruntime/qv4qmlcontext.cpp +++ b/src/qml/jsruntime/qv4qmlcontext.cpp @@ -196,7 +196,7 @@ ReturnedValue QQmlContextWrapper::getPropertyAndBase(const QQmlContextWrapper *r return result->asReturnedValue(); } - if (context->imports() && name->startsWithUpper()) { + if (context->imports() && (name->startsWithUpper() || context->valueTypesAreAddressable())) { // Search for attached properties, enums and imported scripts QQmlTypeNameCache::Result r = context->imports()->query<QQmlImport::AllowRecursion>(name); diff --git a/src/qml/jsruntime/qv4runtime.cpp b/src/qml/jsruntime/qv4runtime.cpp index aa4d5c875a..c7ee35b810 100644 --- a/src/qml/jsruntime/qv4runtime.cpp +++ b/src/qml/jsruntime/qv4runtime.cpp @@ -329,7 +329,7 @@ ReturnedValue Runtime::DeleteName::call(ExecutionEngine *engine, Function *funct } } -QV4::ReturnedValue Runtime::Instanceof::call(ExecutionEngine *engine, const Value &lval, const Value &rval) +static QV4::ReturnedValue doInstanceof(ExecutionEngine *engine, const Value &lval, const Value &rval) { // 11.8.6, 5: rval must be an Object const Object *rhs = rval.as<Object>(); @@ -345,26 +345,48 @@ QV4::ReturnedValue Runtime::Instanceof::call(ExecutionEngine *engine, const Valu Scope scope(engine); ScopedValue hasInstance(scope, rhs->get(engine->symbol_hasInstance())); if (hasInstance->isUndefined()) - return rhs->instanceOf(lval); + return Encode(rhs->instanceOf(lval)); + FunctionObject *fHasInstance = hasInstance->as<FunctionObject>(); if (!fHasInstance) return engine->throwTypeError(); - ScopedValue result(scope, fHasInstance->call(&rval, &lval, 1)); + return Encode(fHasInstance->call(&rval, &lval, 1)); +} + +QV4::ReturnedValue Runtime::Instanceof::call(ExecutionEngine *engine, const Value &lval, const Value &rval) +{ + Scope scope(engine); + ScopedValue result(scope, doInstanceof(engine, lval, rval)); return scope.hasException() ? Encode::undefined() : Encode(result->toBoolean()); } QV4::ReturnedValue Runtime::As::call(ExecutionEngine *engine, const Value &lval, const Value &rval) { Scope scope(engine); - ScopedValue result(scope, Runtime::Instanceof::call(engine, lval, rval)); + ScopedValue result(scope, doInstanceof(engine, lval, rval)); - if (scope.hasException()) + if (scope.hasException()) { + // "foo instanceof valueType" must not throw an exception. + // So this can only be an object type. engine->catchException(); - else if (result->toBoolean()) + return Encode::null(); + } + + if (result->toBoolean()) return lval.asReturnedValue(); + else if (result->isBoolean()) + return Encode::null(); - return Encode::null(); + // Try to convert the value type + if (Scoped<QQmlTypeWrapper> typeWrapper(scope, rval); typeWrapper) { + const QMetaType metaType = typeWrapper->d()->type().typeId(); + const QVariant result = engine->toVariant(lval, metaType); + if (result.metaType() == metaType) + return engine->metaTypeToJS(metaType, result.constData()); + } + + return Encode::undefined(); } QV4::ReturnedValue Runtime::In::call(ExecutionEngine *engine, const Value &left, const Value &right) diff --git a/src/qml/qml/qqmltypewrapper.cpp b/src/qml/qml/qqmltypewrapper.cpp index 5a3127104c..5c0806e57a 100644 --- a/src/qml/qml/qqmltypewrapper.cpp +++ b/src/qml/qml/qqmltypewrapper.cpp @@ -357,19 +357,11 @@ bool QQmlTypeWrapper::virtualIsEqualTo(Managed *a, Managed *b) return false; } -ReturnedValue QQmlTypeWrapper::virtualInstanceOf(const Object *typeObject, const Value &var) +static ReturnedValue instanceOfQObject(const QV4::QQmlTypeWrapper *typeWrapper, const QObjectWrapper *objectWrapper) { - Q_ASSERT(typeObject->as<QV4::QQmlTypeWrapper>()); - const QV4::QQmlTypeWrapper *typeWrapper = static_cast<const QV4::QQmlTypeWrapper *>(typeObject); - - // can only compare a QObject* against a QML type - const QObjectWrapper *wrapper = var.as<QObjectWrapper>(); - if (!wrapper) - return QV4::Encode(false); - - QV4::ExecutionEngine *engine = typeObject->internalClass()->engine; + QV4::ExecutionEngine *engine = typeWrapper->internalClass()->engine; // in case the wrapper outlived the QObject* - const QObject *wrapperObject = wrapper->object(); + const QObject *wrapperObject = objectWrapper->object(); if (!wrapperObject) return engine->throwTypeError(); @@ -399,6 +391,29 @@ ReturnedValue QQmlTypeWrapper::virtualInstanceOf(const Object *typeObject, const return QV4::Encode(QQmlMetaObject::canConvert(theirType, myQmlType)); } +ReturnedValue QQmlTypeWrapper::virtualInstanceOf(const Object *typeObject, const Value &var) +{ + Q_ASSERT(typeObject->as<QV4::QQmlTypeWrapper>()); + const QV4::QQmlTypeWrapper *typeWrapper = static_cast<const QV4::QQmlTypeWrapper *>(typeObject); + + if (const QObjectWrapper *objectWrapper = var.as<QObjectWrapper>()) + return instanceOfQObject(typeWrapper, objectWrapper); + + if (const QMetaObject *valueTypeMetaObject + = QQmlMetaType::metaObjectForValueType(typeWrapper->d()->type())) { + if (const QQmlValueTypeWrapper *valueWrapper = var.as<QQmlValueTypeWrapper>()) { + return QV4::Encode(QQmlMetaObject::canConvert(valueWrapper->metaObject(), + valueTypeMetaObject)); + } + + // We want "foo as valuetype" to return undefined if it doesn't match. + return Encode::undefined(); + } + + // If the target type is an object type we want null. + return Encode(false); +} + ReturnedValue QQmlTypeWrapper::virtualResolveLookupGetter(const Object *object, ExecutionEngine *engine, Lookup *lookup) { // Keep this code in sync with ::virtualGet diff --git a/src/qml/qml/qqmlvaluetypewrapper_p.h b/src/qml/qml/qqmlvaluetypewrapper_p.h index 0c4cbb7dd3..7a1fa02279 100644 --- a/src/qml/qml/qqmlvaluetypewrapper_p.h +++ b/src/qml/qml/qqmlvaluetypewrapper_p.h @@ -113,6 +113,7 @@ public: QMetaType type() const; bool write(QObject *target, int propertyIndex) const; bool readReferenceValue() const { return d()->readReference(); } + const QMetaObject *metaObject() const { return d()->metaObject(); } QQmlPropertyData dataForPropertyKey(PropertyKey id) const; diff --git a/src/qmlcompiler/qqmljscodegenerator.cpp b/src/qmlcompiler/qqmljscodegenerator.cpp index 41c174d0c8..a4f15054ec 100644 --- a/src/qmlcompiler/qqmljscodegenerator.cpp +++ b/src/qmlcompiler/qqmljscodegenerator.cpp @@ -87,6 +87,13 @@ QString QQmlJSCodeGenerator::metaObject(const QQmlJSScope::ConstPtr &objectType) return QString(); } +QString QQmlJSCodeGenerator::metaType(const QQmlJSScope::ConstPtr &type) +{ + return m_typeResolver->equals(m_typeResolver->genericType(type), type) + ? metaTypeFromType(type) + : metaTypeFromName(type); +} + QQmlJSAotFunction QQmlJSCodeGenerator::run( const Function *function, const InstructionAnnotations *annotations, QQmlJS::DiagnosticMessage *error) @@ -948,6 +955,59 @@ void QQmlJSCodeGenerator::rejectIfNonQObjectOut(const QString &error) } } +bool QQmlJSCodeGenerator::generateContentPointerCheck( + const QQmlJSScope::ConstPtr &required, const QQmlJSRegisterContent &actual, + const QString &variable, const QString &errorMessage) +{ + const QQmlJSScope::ConstPtr scope = required; + const QQmlJSScope::ConstPtr input = m_typeResolver->containedType(actual); + if (QQmlJSUtils::searchBaseAndExtensionTypes(input, + [&](const QQmlJSScope::ConstPtr &base) { + return m_typeResolver->equals(base, scope); + })) { + return false; + } + + if (!m_typeResolver->canHold(input, scope)) { + reject(u"lookup of members of %1 in %2"_s.arg( + scope->internalName(), input->internalName())); + } + if (!m_typeResolver->equals(actual.storedType(), + m_typeResolver->varType())) { + reject(u"retrieving metatype from %1"_s.arg(actual.descriptiveName())); + } + + // Since we have verified the type in qqmljstypepropagator.cpp we now know + // that we can only have either undefined or the actual type here. Therefore, + // it's enough to check the QVariant for isValid(). + + m_body += u"if (!"_s + variable + u".isValid()) {\n "_s; + generateSetInstructionPointer(); + m_body += u" aotContext->engine->throwError(QJSValue::TypeError, "_s; + m_body += u"QLatin1String(\"%1\"));\n"_s.arg(errorMessage); + m_body += u" return "_s + errorReturnValue() + u";\n"_s; + m_body += u"}\n"_s; + return true; +} + +QString QQmlJSCodeGenerator::resolveValueTypeContentPointer( + const QQmlJSScope::ConstPtr &required, const QQmlJSRegisterContent &actual, + const QString &variable, const QString &errorMessage) +{ + if (generateContentPointerCheck(required, actual, variable, errorMessage)) + return variable + u".data()"_s; + return contentPointer(actual, variable); +} + +QString QQmlJSCodeGenerator::resolveQObjectPointer( + const QQmlJSScope::ConstPtr &required, const QQmlJSRegisterContent &actual, + const QString &variable, const QString &errorMessage) +{ + if (generateContentPointerCheck(required, actual, variable, errorMessage)) + return u"*static_cast<QObject *const *>("_s + variable + u".constData())"_s; + return variable; +} + void QQmlJSCodeGenerator::generate_GetLookup(int index) { INJECT_TRACE_INFO(generate_GetLookup); @@ -981,8 +1041,8 @@ void QQmlJSCodeGenerator::generate_GetLookup(int index) ? QString::number(m_state.accumulatorIn().importNamespace()) : u"QQmlPrivate::AOTCompiledContext::InvalidStringId"_s; const auto accumulatorIn = m_state.accumulatorIn(); - const bool isReferenceType = (accumulatorIn.storedType()->accessSemantics() - == QQmlJSScope::AccessSemantics::Reference); + const QQmlJSScope::ConstPtr scope = m_state.accumulatorOut().scopeType(); + const bool isReferenceType = scope->isReferenceType(); switch (m_state.accumulatorOut().variant()) { case QQmlJSRegisterContent::ObjectAttached: { @@ -1016,12 +1076,18 @@ void QQmlJSCodeGenerator::generate_GetLookup(int index) Q_ASSERT(m_state.accumulatorOut().isProperty()); - if (isReferenceType) { + if (m_typeResolver->registerIsStoredIn(accumulatorIn, m_typeResolver->jsValueType())) { + reject(u"lookup in QJSValue"_s); + } else if (isReferenceType) { + const QString inputPointer = resolveQObjectPointer( + scope, accumulatorIn, m_state.accumulatorVariableIn, + u"Cannot read property '%1' of undefined"_s.arg( + m_jsUnitGenerator->lookupName(index))); const QString lookup = u"aotContext->getObjectLookup("_s + indexString - + u", "_s + m_state.accumulatorVariableIn + u", "_s + + u", "_s + inputPointer + u", "_s + contentPointer(m_state.accumulatorOut(), m_state.accumulatorVariableOut) + u')'; const QString initialization = u"aotContext->initGetObjectLookup("_s - + indexString + u", "_s + m_state.accumulatorVariableIn + + indexString + u", "_s + inputPointer + u", "_s + contentType(m_state.accumulatorOut(), m_state.accumulatorVariableOut) + u')'; const QString preparation = getLookupPreparation( @@ -1052,18 +1118,20 @@ void QQmlJSCodeGenerator::generate_GetLookup(int index) m_state.accumulatorOut(), m_state.accumulatorVariableIn + u".length()"_s) + u";\n"_s; - } else if (m_typeResolver->registerIsStoredIn(accumulatorIn, m_typeResolver->jsValueType())) { - reject(u"lookup in QJSValue"_s); } else if (m_typeResolver->canUseValueTypes()) { + + const QString inputContentPointer = resolveValueTypeContentPointer( + scope, accumulatorIn, m_state.accumulatorVariableIn, + u"Cannot read property '%1' of undefined"_s.arg( + m_jsUnitGenerator->lookupName(index))); + const QString lookup = u"aotContext->getValueLookup("_s + indexString - + u", "_s + contentPointer(m_state.accumulatorIn(), - m_state.accumulatorVariableIn) - + u", "_s + contentPointer(m_state.accumulatorOut(), - m_state.accumulatorVariableOut) + + u", "_s + inputContentPointer + + u", "_s + contentPointer(m_state.accumulatorOut(), m_state.accumulatorVariableOut) + u')'; const QString initialization = u"aotContext->initGetValueLookup("_s + indexString + u", "_s - + metaObject(m_state.accumulatorOut().scopeType()) + u", "_s + + metaObject(scope) + u", "_s + contentType(m_state.accumulatorOut(), m_state.accumulatorVariableOut) + u')'; const QString preparation = getLookupPreparation( m_state.accumulatorOut(), m_state.accumulatorVariableOut, index); @@ -1146,12 +1214,16 @@ void QQmlJSCodeGenerator::generate_SetLookup(int index, int baseReg) argType = variableInType; } - switch (callBase.storedType()->accessSemantics()) { + switch (property.scopeType()->accessSemantics()) { case QQmlJSScope::AccessSemantics::Reference: { + const QString basePointer = resolveQObjectPointer( + property.scopeType(), registerType(baseReg), object, + u"TypeError: Value is undefined and could not be converted to an object"_s); + const QString lookup = u"aotContext->setObjectLookup("_s + indexString - + u", "_s + object + u", "_s + variableIn + u')'; + + u", "_s + basePointer + u", "_s + variableIn + u')'; const QString initialization = u"aotContext->initSetObjectLookup("_s - + indexString + u", "_s + object + u", "_s + argType + u')'; + + indexString + u", "_s + basePointer + u", "_s + argType + u')'; generateLookup(lookup, initialization, preparation); break; } @@ -1189,12 +1261,16 @@ void QQmlJSCodeGenerator::generate_SetLookup(int index, int baseReg) const QQmlJSRegisterContent property = specific.storedIn( m_typeResolver->genericType(specific.storedType())); + const QString baseContentPointer = resolveValueTypeContentPointer( + property.scopeType(), registerType(baseReg), object, + u"TypeError: Value is undefined and could not be converted to an object"_s); + const QString lookup = u"aotContext->setValueLookup("_s + indexString - + u", "_s + contentPointer(registerType(baseReg), object) + + u", "_s + baseContentPointer + u", "_s + variableIn + u')'; const QString initialization = u"aotContext->initSetValueLookup("_s + indexString + u", "_s + metaObject(property.scopeType()) - + u", "_s + contentType(registerType(baseReg), object) + u')'; + + u", "_s + argType + u')'; generateLookup(lookup, initialization, preparation); if (m_typeResolver->canUseValueTypes()) @@ -2338,22 +2414,51 @@ void QQmlJSCodeGenerator::generate_As(int lhs) INJECT_TRACE_INFO(generate_As); const QString input = registerVariable(lhs); - const QQmlJSScope::ConstPtr contained - = m_typeResolver->containedType(m_state.readRegister(lhs)); + const QQmlJSRegisterContent inputContent = m_state.readRegister(lhs); + const QQmlJSScope::ConstPtr contained = m_typeResolver->containedType(inputContent); + const QQmlJSRegisterContent outputContent = m_state.accumulatorOut(); - m_body += m_state.accumulatorVariableOut + u" = "_s; - if (m_typeResolver->equals( - m_state.accumulatorIn().storedType(), m_typeResolver->metaObjectType()) - && contained->isComposite()) { - m_body += conversion( - m_typeResolver->genericType(contained), m_state.accumulatorOut().storedType(), - m_state.accumulatorVariableIn + u"->cast("_s + input + u')'); - } else { - m_body += conversion( - m_typeResolver->genericType(contained), m_state.accumulatorOut().storedType(), - u'(' + metaObject(contained) + u")->cast("_s + input + u')'); + if (contained->isReferenceType()) { + m_body += m_state.accumulatorVariableOut + u" = "_s; + if (m_typeResolver->equals( + m_state.accumulatorIn().storedType(), m_typeResolver->metaObjectType()) + && contained->isComposite()) { + m_body += conversion( + m_typeResolver->genericType(contained), outputContent.storedType(), + m_state.accumulatorVariableIn + u"->cast("_s + input + u')'); + } else { + m_body += conversion( + m_typeResolver->genericType(contained), outputContent.storedType(), + u'(' + metaObject(contained) + u")->cast("_s + input + u')'); + } + m_body += u";\n"_s; + return; + } else if (m_typeResolver->equals(inputContent.storedType(), m_typeResolver->varType())) { + // If the original output is a conversion, we're supposed to check for the contained + // type and if it doesn't match, set the result to undefined. + const auto originalContent = m_typeResolver->original(outputContent); + if (originalContent.isConversion()) { + const auto origins = originalContent.conversionOrigins(); + Q_ASSERT(origins.size() == 2); + + const auto target = m_typeResolver->equals(origins[0], m_typeResolver->voidType()) + ? origins[1] + : origins[0]; + + Q_ASSERT(!m_typeResolver->equals(target, m_typeResolver->voidType())); + + m_body += m_state.accumulatorVariableOut + u" = "_s; + m_body += input + u".metaType() == "_s + metaType(target) + + u" ? " + conversion(inputContent, outputContent, input) + + u" : " + conversion(m_typeResolver->globalType(m_typeResolver->voidType()), + outputContent, QString()); + m_body += u";\n"_s; + return; + } } - m_body += u";\n"_s; + + reject(u"unsupported type assertion"_s); + } void QQmlJSCodeGenerator::generate_UNot() diff --git a/src/qmlcompiler/qqmljscodegenerator_p.h b/src/qmlcompiler/qqmljscodegenerator_p.h index 4a7e1dc1dc..debda73ada 100644 --- a/src/qmlcompiler/qqmljscodegenerator_p.h +++ b/src/qmlcompiler/qqmljscodegenerator_p.h @@ -63,6 +63,7 @@ protected: }; virtual QString metaObject(const QQmlJSScope::ConstPtr &objectType); + virtual QString metaType(const QQmlJSScope::ConstPtr &type); void generate_Ret() override; void generate_Debug() override; @@ -288,6 +289,16 @@ private: return m_typeResolver->jsGlobalObject()->property(u"console"_s).type(); } + QString resolveValueTypeContentPointer( + const QQmlJSScope::ConstPtr &required, const QQmlJSRegisterContent &actual, + const QString &variable, const QString &errorMessage); + QString resolveQObjectPointer( + const QQmlJSScope::ConstPtr &required, const QQmlJSRegisterContent &actual, + const QString &variable, const QString &errorMessage); + bool generateContentPointerCheck( + const QQmlJSScope::ConstPtr &required, const QQmlJSRegisterContent &actual, + const QString &variable, const QString &errorMessage); + // map from instruction offset to sequential label number QHash<int, QString> m_labels; diff --git a/src/qmlcompiler/qqmljstypepropagator.cpp b/src/qmlcompiler/qqmljstypepropagator.cpp index 83241dbd90..be6fb24cb7 100644 --- a/src/qmlcompiler/qqmljstypepropagator.cpp +++ b/src/qmlcompiler/qqmljstypepropagator.cpp @@ -1968,16 +1968,25 @@ void QQmlJSTypePropagator::generate_As(int lhs) break; } - addReadRegister(lhs, m_typeResolver->globalType(contained)); + QQmlJSRegisterContent output; - if (m_typeResolver->containedType(input)->accessSemantics() - != QQmlJSScope::AccessSemantics::Reference - || contained->accessSemantics() != QQmlJSScope::AccessSemantics::Reference) { + if (contained->accessSemantics() == QQmlJSScope::AccessSemantics::Reference) { + // A referece type cast can result in either the type or null. + // Reference tpyes can hold null. We don't need to special case that. + output = m_typeResolver->globalType(contained); + } else if (!m_typeResolver->canAddressValueTypes()) { setError(u"invalid cast from %1 to %2. You can only cast object types."_s - .arg(input.descriptiveName(), m_state.accumulatorIn().descriptiveName())); + .arg(input.descriptiveName(), m_state.accumulatorIn().descriptiveName())); + return; } else { - setAccumulator(m_typeResolver->globalType(contained)); + // A value type cast can result in either the type or undefined. + output = m_typeResolver->merge( + m_typeResolver->globalType(contained), + m_typeResolver->globalType(m_typeResolver->voidType())); } + + addReadRegister(lhs, output); + setAccumulator(output); } void QQmlJSTypePropagator::checkConversion( diff --git a/src/qmlcompiler/qqmljstyperesolver.cpp b/src/qmlcompiler/qqmljstyperesolver.cpp index 278e15bcc1..b868495971 100644 --- a/src/qmlcompiler/qqmljstyperesolver.cpp +++ b/src/qmlcompiler/qqmljstyperesolver.cpp @@ -365,7 +365,7 @@ QQmlJSRegisterContent QQmlJSTypeResolver::transformed( Q_UNREACHABLE_RETURN({}); } -QQmlJSRegisterContent QQmlJSTypeResolver::referenceTypeForName( +QQmlJSRegisterContent QQmlJSTypeResolver::registerContentForName( const QString &name, const QQmlJSScope::ConstPtr &scopeType, bool hasObjectModulePrefix) const { @@ -415,7 +415,11 @@ QQmlJSRegisterContent QQmlJSTypeResolver::referenceTypeForName( QQmlJSRegisterContent::MetaType, type); case QQmlJSScope::AccessSemantics::Sequence: case QQmlJSScope::AccessSemantics::Value: - // This is not actually a type reference. You cannot get the metaobject + if (canAddressValueTypes()) { + return QQmlJSRegisterContent::create(metaObjectType(), metaObjectType(), + QQmlJSRegisterContent::MetaType, type); + } + // Else this is not actually a type reference. You cannot get the metaobject // of a value type in QML and sequences don't even have metaobjects. break; } @@ -642,6 +646,49 @@ QQmlJSScope::ConstPtr QQmlJSTypeResolver::merge(const QQmlJSScope::ConstPtr &a, return varType(); } +bool QQmlJSTypeResolver::canHold( + const QQmlJSScope::ConstPtr &container, const QQmlJSScope::ConstPtr &contained) const +{ + if (equals(container, contained) + || equals(container, m_varType) + || equals(container, m_jsValueType)) { + return true; + } + + if (equals(container, m_jsPrimitiveType)) + return isPrimitive(contained); + + if (equals(container, m_variantListType)) + return contained->accessSemantics() == QQmlJSScope::AccessSemantics::Sequence; + + if (equals(container, m_qObjectListType) || equals(container, m_listPropertyType)) { + if (contained->accessSemantics() != QQmlJSScope::AccessSemantics::Sequence) + return false; + if (QQmlJSScope::ConstPtr value = contained->valueType()) + return value->isReferenceType(); + return false; + } + + if (QQmlJSUtils::searchBaseAndExtensionTypes( + container, [&](const QQmlJSScope::ConstPtr &base) { + return equals(base, contained); + })) { + return true; + } + + if (container->isReferenceType()) { + if (QQmlJSUtils::searchBaseAndExtensionTypes( + contained, [&](const QQmlJSScope::ConstPtr &base) { + return equals(base, container); + })) { + return true; + } + } + + return false; +} + + bool QQmlJSTypeResolver::canHoldUndefined(const QQmlJSRegisterContent &content) const { const auto canBeUndefined = [this](const QQmlJSScope::ConstPtr &type) { @@ -857,7 +904,8 @@ QQmlJSRegisterContent QQmlJSTypeResolver::scopedType(const QQmlJSScope::ConstPtr } } - QQmlJSRegisterContent result = referenceTypeForName(name); + QQmlJSRegisterContent result = registerContentForName(name); + if (result.isValid()) return result; @@ -1165,13 +1213,31 @@ QQmlJSRegisterContent QQmlJSTypeResolver::memberType(const QQmlJSRegisterContent return {}; } - return referenceTypeForName( + return registerContentForName( name, type.scopeType(), type.variant() == QQmlJSRegisterContent::ObjectModulePrefix); } if (type.isConversion()) { - const auto result = memberType(type.conversionResult(), name); - return result.isValid() ? result : memberEnumType(type.scopeType(), name); + if (const auto result = memberType(type.conversionResult(), name); result.isValid()) + return result; + if (const auto result = memberEnumType(type.scopeType(), name); result.isValid()) + return result; + + // If the conversion consists of only undefined and one actual type, + // we can produce the members of that one type. + // If the value is then actually undefined, the result is an exception. + + auto origins = type.conversionOrigins(); + const auto begin = origins.begin(); + const auto end = std::remove_if(begin, origins.end(), + [this](const QQmlJSScope::ConstPtr &origin) { + return equals(origin, m_voidType); + }); + + // If the conversion cannot hold the original type, it loses information. + return (end - begin == 1 && canHold(type.conversionResult(), *begin)) + ? memberType(*begin, name) + : QQmlJSRegisterContent(); } Q_UNREACHABLE_RETURN({}); diff --git a/src/qmlcompiler/qqmljstyperesolver_p.h b/src/qmlcompiler/qqmljstyperesolver_p.h index bcd4276390..1276a6b0fd 100644 --- a/src/qmlcompiler/qqmljstyperesolver_p.h +++ b/src/qmlcompiler/qqmljstyperesolver_p.h @@ -148,6 +148,7 @@ public: const QQmlJSScopesById &objectsById() const { return m_objectsById; } bool canCallJSFunctions() const { return m_objectsById.signaturesAreEnforced(); } bool canUseValueTypes() const { return m_objectsById.valueTypesAreCopied(); } + bool canAddressValueTypes() const { return m_objectsById.valueTypesAreAddressable(); } const QHash<QQmlJS::SourceLocation, QQmlJSMetaSignalHandler> &signalHandlers() const { @@ -165,6 +166,9 @@ public: bool canHoldUndefined(const QQmlJSRegisterContent &content) const; bool isNumeric(const QQmlJSScope::ConstPtr &type) const; + bool canHold(const QQmlJSScope::ConstPtr &container, + const QQmlJSScope::ConstPtr &contained) const; + protected: QQmlJSRegisterContent memberType(const QQmlJSScope::ConstPtr &type, const QString &name) const; @@ -180,7 +184,7 @@ protected: const QQmlJSRegisterContent &origin, QQmlJSScope::ConstPtr (QQmlJSTypeResolver::*op)(const QQmlJSScope::ConstPtr &) const) const; - QQmlJSRegisterContent referenceTypeForName( + QQmlJSRegisterContent registerContentForName( const QString &name, const QQmlJSScope::ConstPtr &scopeType = QQmlJSScope::ConstPtr(), bool hasObjectModuelPrefix = false) const; diff --git a/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt b/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt index fb7968892c..8b9b2e786e 100644 --- a/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt +++ b/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt @@ -155,6 +155,7 @@ set(qml_files nullComparison.qml numbersInJsPrimitive.qml objectInVar.qml + objectLookupOnListElement.qml outOfBounds.qml overriddenMember.qml ownProperty.qml @@ -199,6 +200,7 @@ set(qml_files unusedAttached.qml urlString.qml usingCxxTypesFromFileImports.qml + valueTypeCast.qml valueTypeCopy.qml valueTypeLists.qml valueTypeProperty.qml diff --git a/tests/auto/qml/qmlcppcodegen/data/math.qml b/tests/auto/qml/qmlcppcodegen/data/math.qml index cc6cd3741a..ad6303e682 100644 --- a/tests/auto/qml/qmlcppcodegen/data/math.qml +++ b/tests/auto/qml/qmlcppcodegen/data/math.qml @@ -3,4 +3,5 @@ import QML QtObject { property int a: Math.max(5, 7, 9, -111) property var b: 50 / 22 + property real c: Math.PI * 2 } diff --git a/tests/auto/qml/qmlcppcodegen/data/objectLookupOnListElement.qml b/tests/auto/qml/qmlcppcodegen/data/objectLookupOnListElement.qml new file mode 100644 index 0000000000..4804921b02 --- /dev/null +++ b/tests/auto/qml/qmlcppcodegen/data/objectLookupOnListElement.qml @@ -0,0 +1,34 @@ +pragma Strict +import QtQuick + +Item { + id: stack + + property int current: 0 + + onCurrentChanged: setZOrders() + Component.onCompleted: setZOrders() + + function setZOrders() { + for (var i = 0; i < Math.max(stack.children.length, 3); ++i) { + stack.children[i].z = (i == current ? 1 : 0) + stack.children[i].enabled = (i == current) + } + } + + function zOrders() : list<int> { + return [ + stack.children[0].z, + stack.children[1].z, + stack.children[2].z + ] + } + + function clearChildren() { + children.length = 0; + } + + Item {} + Item {} + Item {} +} diff --git a/tests/auto/qml/qmlcppcodegen/data/valueTypeCast.qml b/tests/auto/qml/qmlcppcodegen/data/valueTypeCast.qml new file mode 100644 index 0000000000..d1c9198cbd --- /dev/null +++ b/tests/auto/qml/qmlcppcodegen/data/valueTypeCast.qml @@ -0,0 +1,9 @@ +pragma Strict +pragma ValueTypeBehavior: Addressable +import QtQml + +QtObject { + property rect r: Qt.rect(10, 20, 3, 4) + property var v: r + property real x: (v as rect).x +} diff --git a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp index f19e33017c..131724c5ec 100644 --- a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp +++ b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp @@ -171,6 +171,7 @@ private slots: void variantMapLookup(); void mathMinMax(); void enumFromBadSingleton(); + void objectLookupOnListElement(); }; void tst_QmlCppCodegen::initTestCase() @@ -533,6 +534,7 @@ void tst_QmlCppCodegen::math() QVERIFY(!object.isNull()); QCOMPARE(object->property("a").toInt(), 9); QCOMPARE(object->property("b").toDouble(), 50.0 / 22.0); + QCOMPARE(object->property("c").toDouble(), std::atan(1.0) * 8.0); } void tst_QmlCppCodegen::unknownParameter() @@ -3222,6 +3224,22 @@ void tst_QmlCppCodegen::valueTypeBehavior() QVERIFY(!o2.isNull()); QVERIFY(qIsNaN(o2->property("e").toDouble())); QCOMPARE(o2->property("f").toDouble(), 5.0); + + const QUrl cast(u"qrc:/qt/qml/TestTypes/valueTypeCast.qml"_s); + QQmlComponent c3(&engine, cast); + QVERIFY2(c3.isReady(), qPrintable(c3.errorString())); + QScopedPointer o3(c3.create()); + QVERIFY(!o3.isNull()); + QCOMPARE(o3->property("x"), 10); + + QTest::ignoreMessage( + QtWarningMsg, + qPrintable(cast.toString() + + u":8: TypeError: Cannot read property 'x' of undefined"_s)); + o3->setProperty("v", QLatin1String("not a rect")); + + // If the binding throws an exception, the value doesn't change. + QCOMPARE(o3->property("x"), 10); } void tst_QmlCppCodegen::invisibleSingleton() @@ -3380,6 +3398,33 @@ void tst_QmlCppCodegen::enumFromBadSingleton() QVERIFY(o->objectName().isEmpty()); } +void tst_QmlCppCodegen::objectLookupOnListElement() +{ + QQmlEngine engine; + + const QUrl url(u"qrc:/qt/qml/TestTypes/objectLookupOnListElement.qml"_s); + QQmlComponent c1(&engine, url); + QVERIFY2(c1.isReady(), qPrintable(c1.errorString())); + + QScopedPointer<QObject> object(c1.create()); + QVERIFY(!object.isNull()); + + QList<int> zOrders; + QMetaObject::invokeMethod(object.data(), "zOrders", Q_RETURN_ARG(QList<int>, zOrders)); + QCOMPARE(zOrders, (QList<int>{1, 0, 0})); + object->setProperty("current", 1); + QMetaObject::invokeMethod(object.data(), "zOrders", Q_RETURN_ARG(QList<int>, zOrders)); + QCOMPARE(zOrders, (QList<int>{0, 1, 0})); + + QMetaObject::invokeMethod(object.data(), "clearChildren"); + QTest::ignoreMessage( + QtWarningMsg, + qPrintable(url.toString() + + u":21: TypeError: Cannot read property 'z' of undefined"_s)); + QMetaObject::invokeMethod(object.data(), "zOrders", Q_RETURN_ARG(QList<int>, zOrders)); + QCOMPARE(zOrders, (QList<int>())); +} + QTEST_MAIN(tst_QmlCppCodegen) #include "tst_qmlcppcodegen.moc" diff --git a/tests/auto/qml/qqmllanguage/data/asValueType.qml b/tests/auto/qml/qqmllanguage/data/asValueType.qml new file mode 100644 index 0000000000..5c5cb44ceb --- /dev/null +++ b/tests/auto/qml/qqmllanguage/data/asValueType.qml @@ -0,0 +1,10 @@ +pragma ValueTypeBehavior: Addressable +import QtQml + +QtObject { + property var a + property rect b: a as rect + property bool c: a instanceof rect + property bool d: ({x: 10, y: 20}) instanceof point + property var e: ({x: 10, y: 20}) as point +} diff --git a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp index 87fe0eb3b8..ce7e0a1b1c 100644 --- a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp +++ b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp @@ -406,6 +406,7 @@ private slots: void objectAndGadgetMethodCallsRejectThisObject(); void objectAndGadgetMethodCallsAcceptThisObject(); + void asValueType(); void longConversion(); @@ -7898,6 +7899,31 @@ void tst_qqmllanguage::longConversion() } } +void tst_qqmllanguage::asValueType() +{ + QQmlEngine engine; + const QUrl url = testFileUrl("asValueType.qml"); + QQmlComponent c(&engine, url); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + + QTest::ignoreMessage(QtWarningMsg, qPrintable(url.toString() + ":6:5: Unable to assign [undefined] to QRectF"_L1)); + QScopedPointer<QObject> o(c.create()); + + QCOMPARE(o->property("a"), QVariant()); + QCOMPARE(o->property("b").value<QRectF>(), QRectF()); + QVERIFY(!o->property("c").toBool()); + + const QRectF rect(1, 2, 3, 4); + o->setProperty("a", QVariant(rect)); + QCOMPARE(o->property("b").value<QRectF>(), rect); + QVERIFY(o->property("c").toBool()); + + QVERIFY(!o->property("d").toBool()); + const QPointF point = o->property("e").value<QPointF>(); + QCOMPARE(point.x(), 10.0); + QCOMPARE(point.y(), 20.0); +} + QTEST_MAIN(tst_qqmllanguage) #include "tst_qqmllanguage.moc" |