Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-124662

Reconsider addressable value types

    XMLWordPrintable

Details

    • 59c5038af (dev)

    Description

      The problem

      Addressable value types

      Value types are inaddressable by default. This means you cannot obtain type references to them by just stating their name in script code. For example, without special preparation you cannot do var b = font.Bold. You have to use the extra namespace we provide to make the enums of QFont available: var b = Font.Bold. We have introduced a pragma to make value types addressable and with this pragma value types enjoy the same lookup precedence as object types, which is above properties and locals. The problem with this is that there are lots of properties called "font" or "color" etc that are shadowed by enabling the pragma. Therefore, hardly anyone can use this new capability. However, as was discovered in recent discussions, value types are actually not completely inaddressible by default. If you import a module into an import namespace, you can retrieve value type references from the import namespace. Usage of import namespaces generally solves a number of other problems, too. In particular shadowing of types between different modules. It therefore remains debatable whether the step of allowing value types to be addressable without qualification was actually a good idea.

      Precedence hierarchy

      Generally, both scopes and contexts form a hierarchy that for lookups is traversed in lockstep: First the immediate context of the object, then the immediate scope, then the parent context, the parent scope, etc. Types rank above all of this (and the code generated by qmlcachegen actually gets this wrong), and the JavaScript global object ranks at the bottom (and the generated code is also wrong in this). The immediate context of an object is typically internal, so that only IDs end up there, and you cannot poison it with extra context properties. This arrangement is inconvenient for a number of reasons:

      1. Having types on top of everything makes the introduction of new types potentially break everything. We've avoided this problem in the past by mandating that types have upper case names and that everything else have lower case names. Value types, however, have lower case names.
      2. Having the JavaScript global object at the bottom of everything makes it impossible to guarantee that a given name will resolve to a particular JavaScript global. For example, formally we cannot know that "console" is actually the console object or that "qsTr" is the translation method. This is very inconvenient for compiling code ahead of time (and so far we ignore the problem). The qsTr case is even ignored by the object creator. Factually "qsTr" is a super-global when invoked from certain places.

      as-casting to value types

      We've introduced functionality that allows you to give a value type reference as the right hand side of an as-cast:

      {x: 2, y: 5} as point
      

      This constructs a point from the given object. Since you can also implicitly construct value types by passing compatible values to properties and as function arguments, this seemed like a good idea. However, while the implicit conversion for properties and arguments is born out of the necessity to avoid naming value types in script code, the as-cast does name the value type! This mechanism significantly weakens the as-cast as type check, though. A structured value type can be produced from any object this way and there is no way to check if the left hand value was actually of the desired type. Furthermore, a failed as-cast to a value type produces undefined while a failed as-cast to an object type produces null. This is unnecessary since any wrapper type that can hold undefined can also hold null. We could have therefore stuck to always producing null on failed as-casts. The idea behind it was that the "none" value for objects is null while the "none" value for primitives is undefined. However, since value types blur the line between objects and primitives, there is no clear way to tell which one should be produced for which type.

      Luckily, you have to either make value types addressable using the pragma or retrieve a value type from a type namespace to actually use the new functionality. This limits the scope of the problem.

      The solution

      The three problems outlined above seem distinct at first glance, but since addressability of value types is a precondition (mostly) for as-casting to value types, it makes sense to fix them together.

      Allow calling type references as constructors

      By providing a better way to create value types in script code, we can make as-casting to value types redundant. However, this should be done for all types, not only value types. If you have obtained a reference to a type that has a Q_INVOKABLE ctor in whatever way, you should be able to produce an instance of it using "new":

      import QtQuick as QQ
      
      [...]
      
      var a = new QQ.QtObject();            // QObject indeed has a Q_INVOKABLE ctor
      var b = new QQ.rect();                     // Let's make the default ctors of the relevant value types invokable
      var c = new QQ.color("#f3422e"); // QQuickColorValueType has an invokable ctor taking QString
      

      By only allowing this for explictly invokable ctors we implictly disallow things like:

      var d = new QQ.int(); // number or object??
      

      All of this would be independent of whether the value type is declared constructible or not. We might allow constructing structured value types from an object property-by-property using the "new" syntax, though:

      var e = new QQ.point({x : 5, y: 6})
      

      As a nice side effect of this we also expose invokable ctors with multiple arguments this way. A downside is that we violate the common "new-cap" rule for JavaScript that says that constructor names should start with an uppercase letter. Qt Creator spits out an "M307" warning for this. However, I find this rule somewhat moot since you can of course store constructors into any variable you like, including lower case ones.

      Since not everyone will be able to obtain type references to value types we leave the implicit construction of value types as part of assignment to properties and as typed function arguments in place. We aggressively coerce values in those places anyway. Therefore, there is no reason to stop at value types, in particular since those are relatively hard to explicitly create.

      Provide a pragma to re-organize the lookup precedence and expose types below the local scope and context

      The precedence hierarchy would be re-organized as follows:

      1. function locals
      2. the local context
      3. the local scope
      4. types (including value types)
      5. the JavaScript global object
      6. further contexts and scopes, alternating

      This has two important benefits over the traditional precedence hierarchy:

      1. We can make value types addressable without hiding locals
      2. We can determine lower case JavaScript globals with certainty

      Provide an opt-out pragma for as-casting to value types

      We invent a new attribute to the ValueTypeBehavior pragma that says:

      1. as-casting to value types does not construct the value type any more, but only type-checks
      2. a failed as-cast is always null, not undefined

      The pragma for re-organizing the precedence hierarchy might imply this new attribute, but maybe that's too magic. The "Adressable" attribute to ValueTypeBehavior would explicitly state the opposite. Having both would be an error.

      In the unlikely case that someone is as-casting to value types without either attribute, we should warn about it and announce that the behavior will change and they should explicitly opt for one way. Furthermore, we should discourage "Addressable" in the documentation and point to the new pragma instead.

      After a few versions we can then change the default behavior of "as".

      Edge cases

      There are a number of interesting edge cases that need to be decided upon:

      1. Under what circumstances should we default-construct value types with new and an empty arguments lists?
        1. If QML_CONSTRUCTIBLE_VALUE given? Otherwise people will wonder why it doesn't work.
        2. If QML_STRUCTURED_VALUE given? By the above rules you can already default-construct the value type from an empty object then.
        3. If the type has an invokable default ctor? This would follow the rules above, no matter if any of the macros are set
        4. Some combination?
        5. Always?
        6. Never?
      2. Under what circumstances should we default-construct object types with new and an empty arguments list?
        1. If deemed creatable by the QML_* macros? This would follow the rules of general object creation in other places.
        2. If the type has an invokable default ctor? This would follow the rules above, but a, hardly any object type has that and b, this would allow constructing singletons and uncreatables this way.
        3. If both are given?
        4. Always?
        5. Never?
      3. What about creating object types with a parent as argument to the ctor? This is so common that it's almost on the level of default construction.
      4. What about creating object types with an object as argument, like the property-by-property construction of value types.

      Another thing to be considered is that we already have a generic QMetaObject wrapper. That's the thing you get by calling engine->newMetaObject(). This one already acts as constructor by invoking any invokable ctor on a metaobject. It currently can only return a QObject* but that looks not very hard to change. See https://codereview.qt-project.org/c/qt/qtdeclarative/+/160759 for how that came to be.

      Attachments

        For Gerrit Dashboard: QTBUG-124662
        # Subject Branch Project Status CR V

        Activity

          People

            ulherman Ulf Hermann
            ulherman Ulf Hermann
            Votes:
            0 Vote for this issue
            Watchers:
            11 Start watching this issue

            Dates

              Created:
              Updated:

              Gerrit Reviews

                There are 5 open Gerrit changes