dart-lang / dart-lang/language

Expressions that are "as constant as possible"

Open
#4,084 20 comments 28 reactions 0 assignees View on GitHub
enhanced-const feature
Dominant language
TeX
Stars
2.9k
Forks
239
Avg merge
2d 18h
Merged PRs (30d)
14

Description

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:

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

int v = 1;

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

// Now we need to change `[]` to `[v]`, where `v` isn't constant.
A(
const A(),
1,
'Hello',
A([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 []` is the same object as another occurrence of the syntax `const []`, whereas `[]` and `[]` 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:

```ebnf
::= 'const?'
::= ('const' | )
::= ? ? '{' ? '}'
::= ? ? '[' ? ']'
::= ?
::= ?

::= ... // Existing cases unchanged
'(' ')'
```

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:

```dart
void main() {
// Assume that we have this expression somewhere.
const? A(
A(),
1,
'Hello',
A([], 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([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 []`.*
- 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(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:

```ebnf
::=
? 'factory'
::=
? 'factory' '='
::=
?
```

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:

```ebnf
::=
?

::=
? 'required'?
('=' )?
```

### 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.

Contributor guide

Open the contributing guide

Research direction

Start by reading the proposal's grammar changes and the sections defining constant-capable expressions, constructors, and parameters. The work is done only when the language design and its semantic and syntax implications have an agreed specification; this issue does not name implementation files or tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.