Skip to content

Expressions that are "as constant as possible" #4084

@eernstg

Description

@eernstg

The language team has had discussions about the need to change many parts of an expression in response to a small modification that makes a previously constant subexpression non-constant, or vice versa. For example:

class A {
  const A([_, _, _, _]);
}

int v = 1;

void main() {
  // Assume that we have this expression somewhere.
  const A(
    A(), 
    1, 
    'Hello', 
    A(<int>[], A(), A()),
  );

  // Now we need to change `[]` to `[v]`, where `v` isn't constant.
  A(
    const A(), 
    1, 
    'Hello', 
    A(<int>[v], const A(), const A()),
  );
}

This causes a lot of inconveniences in daily development because it's a non-trivial editing operation to make "as much as possible" constant in a given expression when we can't just say "all of it".

Arguably, the process is even more tricky in the other direction, because we can't see locally whether or not the list is expected to be modified in the future.

A more subtle semantic difference is the behavior of identical, where constant expressions yield canonicalized instances, which means that const <int>[] is the same object as another occurrence of the syntax const <int>[], whereas <int>[] and <int>[] are two distinct objects (assuming that they aren't constant based on a const modifier on some enclosing expression), even in the case where we are considering two distinct evaluations of the same expression in the source code.

Proposal: const? expressions

We introduce the expression modifier const?, which amounts to a request that every subexpression of the expression that carries this modifier is constant, if possible. The grammar changes as follows:

<constQuestion> ::= 'const?'
<constOrQuestion> ::= ('const' | <constQuestion>)
<setOrMapLiteral> ::= <constOrQuestion>? <typeArguments>? '{' <elements>? '}'
<listLiteral> ::= <constOrQuestion>? <typeArguments>? '[' <elements>? ']'
<recordLiteral> ::= <constOrQuestion>? <recordLiteralNoConst>
<constObjectExpression> ::= <constOrQuestion>? <constructorDesignation> <arguments>

<primary> ::= ... // Existing cases unchanged
    <constQuestion> '(' <expression> ')'

The point is that it is much safer for an automatic mechanism to introduce const modifiers on a set of expressions if this operation is justified by an explicit request in the source code. We could say that the developer promises that it won't be a bug to implicitly add const to any of the subexpressions of the expression that has the modifier const?, nor will it be a bug if no such modifier is added.

The example above then becomes simpler:

void main() {
  // Assume that we have this expression somewhere.
  const? A(
    A(), 
    1, 
    'Hello', 
    A(<int>[], A(), A()),
  );

  // Now we need to change `[]` to `[v]`, where `v` isn't constant.
  // Just go ahead and add `v`, no other change is needed.
  const? A(
    A(), 
    1, 
    'Hello', 
    A(<int>[v], A(), A()),
  );
}

This would largely eliminate the churn which is caused by changes to the constness of subexpressions of a bigger expression.

We define the semantics of the const? modifier in terms of constant capable expressions. On the target expression, the mechanism would add const modifiers implicitly, in a bottom-up traversal, such that every expression gets the const modifier if and only if it is constant capable.

  • An expression is constant capable if it has the modifier const. For example, const A() or const <int>[].
  • An identifier is constant capable if and only if it is a constant expression (this is a reference to the rules that we have today). For example, a name that denotes a constant variable or a top-level function.
  • An instance creation expression is constant capable if (1) every actual type argument is a constant type expression, or actual type arguments have been inferred and they do not contain any type variables, (2) every actual argument is constant capable, and (3) it invokes a constant constructor. For example, C<int>(1).
  • A list literal is constant capable if (1) it has an actual type argument which is a constant type expression, or it has no actual type argument and the inferred type argument (fully alias-expanded) does not contain any type variables, and (2) every element is constant capable. Similarly for a set literal and a map literal.
  • An operator expression (like e1 + e2, !e3, e4 ? e5 : e6) is constant capable if the operands are constant capable, and if it yields a constant expression when const is added (where it can be added syntactically) in a bottom-up traversal of the expression, to every subexpression which is constant capable. Similarly for other composite expressions (that is, expressions whose constness depends on the constness of its subexpressions, and possibly other criteria like the actual value or its type).

With this mechanism in place, it would be possible, for example, for a Flutter developer to put a const? at the outermost level of a large expression creating a widget tree. This expression would then be a constant expression "as far as possible". This would amount to the same thing as adding const at the top, or adding const to any number of children, depending on the ability of each subtree of the widget tree to be constant or not. Crucially, there's no need to change anything about constness in the entire expression when some tiny subexpression is changed from constant to non-constant; we just proceed to make that change, and the constness of the entire expression will then implicitly be modified as needed.

Proposal: const? constructors

@abitofevrything introduced the following idea.

A constructor can have the modifier const?. The grammar changes as follows:

<factoryConstructorSignature> ::=
    <constOrQuestion>? 'factory' <constructorName> <formalParameterList>
<redirectingFactoryConstructorSignature> ::=
    <constOrQuestion>? 'factory' <constructorName> <formalParameterList> '=' <constructorDesignation>
<constantConstructorSignature> ::=
    <constOrQuestion>? <constructorName> <formalParameterList>

Exactly the same compile-time errors are raised for such a constructor declaration as would be the case if it had had the modifier const.

A constructor that has the modifier const? is a constant constructor. This implies that it can be invoked with the const modifier, yielding a constant expression, subject to all the usual checks to confirm that said expression is indeed a correct constant expression. Also, const can be omitted when the constructor is invoked in a constant context, just like other constant constructors.

Assume that e is an instance creation expression that invokes a const? constructor k. If e does not start with const or new, and e does not occur in a constant context then e is treated as const? e.

This implies that invocations of this constructor will always be "as constant as possible", except that an e of the form const e1 requires the invocation to be a constant expression, no ifs or buts, and new e1 requires the invocation to be non-constant. Note that new has the same effect as removing const? from the constructor, in the sense that it also cancels the automatic conversion of subexpressions to constant expressions, unless they have their own reason te become constant, e.g., by having their own const modifier, or being invocations of a const? constructor, etc.

Proposal: const? parameters

A formal parameter declaration in a function declaration would be able to have the modifier const?. The effect would be that every actual argument a which is passed to this parameter in a situation where the statically known signature of the function has this modifier on the given parameter will be treated as const? (a). In other words, every actual argument passed to this parameter will be "as constant as possible".

The grammar changes as follows:

<normalFormalParameter> ::=
    <metadata> <constQuestion>? <normalFormalParameterNoMetadata>

<defaultNamedParameter> ::=
    <metadata> <constQuestion>? 'required'? 
    <normalFormalParameterNoMetadata> ('=' <expression>)?

Discussion

A fundamental property of this proposal is that it makes the request for constant expressions more flexible ("just write const? at the outermost level, and the compiler/analyzer will sort it out"), but it preserves the property that it takes a human expression of intent to make an expression constant.

With the first proposal (supporting const? e where e is an expression), the expression of intent is local: It is always possible to see the keyword const? somewhere in the physically enclosing source code.

The second proposal is intended to be an addition to the first proposal, not a replacement. It is more aggressive than the first one, in the sense that it will make some expressions constant without any locally visible indication.

The second proposal relies on the assumption that there will be some classes (or at least some constructors of some classes) whose semantics is such that it is always OK to choose to make the constructor arguments constant as far as possible, because there are no bugs which can be caused by making the choice to turn some of those constructor arguments and/or their subexpressions into constant expressions, and there are no bugs which can be caused by not turning them into constant expressions. This is a heavy burden to lift for the authors and maintainers of that constructor, and the enclosing class, and its collaborators, but it might work quite well in practice. The community will have to develop a culture around how to use this feature well.

Conversely, the notion of a const? constructor will allow code that creates instances of these classes to obtain a maximal amount of const-ness in client code, with no effort (writing or maintaining) from the client developer.

Finally, the third proposal allows us to specify that every actual argument a to a specific formal parameter must be treated as const? (a), which means that we make "constness" the default for those actual arguments.

The second and third proposals are somewhat overlapping: If we choose to make a constructor const? then the constructor must also itself be a constant constructor, and every actual argument passed in an invocation of that constructor is automatically made "as constant as possible". We can achieve very much the same effect by marking every formal parameter of the same constructor as a const? parameter. This means that every subexpression will be "as constant as possible", but the invocation of this constructor itself will not be constant (unless there is some other reason why it is requested to be constant).

const? formal parameters can be parameters of any function, including static and instance methods, local functions, function literals, etc. This means that they are a much broader mechanism than const? constructors.

Finally, note that function types do not support marking a parameter as const?, it is a mechanism which is only available for invocations of function declarations, and it has no effect on invocations of function objects, nor on dynamic invocations.

Versions

  • Sep 25, 2024: Add proposal about const? parameters.
  • Sep 10, 2024: Add proposal about const? constructors. Change the phrase 'evidently constant' to 'constant capable'.
  • Sep 6, 2024: Initial version.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhanced-constRequests or proposals about enhanced constant expressionsfeatureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions