You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The “Enhanced Constructors” (#4246) proposal moves initializer lists of non-redirecting generative (NRG) constructors into a constructor body. Since constant NRG constructors cannot currently have a body, and potentially constant expressions are not generalized to statements, it has special rules for what can go into this new body. In the most basic form, it's just the initializer list entries separated by ; instead of ,, which is obviously safe.
I propose to allow more statements even in const constructors. The goal is still to be efficiently computable at compile-time without running any user-written code, without reading any visible mutable state , and such that every const object creation expression evaluates to precisely one value. Potentially constant expressions can create new objects on each evaluation, but only ones not invoking user written constructors: Records and instances of platform types like int, String and Type.
Potentially constant NRG constructor body
A const NRG constructor body must be a block with statements that are all potentially constant statements, followed (optionally) by a single super-constructor invocation. (If no super-constructor invocation is present, it defaults to doing what super(); would do.)
It might be possible to allow code after the super-constructor invocation, as long as that code is still potentially constant statements, but the code won't be able to access this anyway, since this is not a potentially constant expression. Might as well do all the work first. (In fact, the constructor can likely do less after the superconstructor invocation than before, because before it can access instance variables declared by the current class without going through this, and after it can't and it still has no this.)
Potentially constant statements
A potentially constant statement is one of the following:
Assert statements with a potentially constant first operand, and no requirement on the second operand. _(If evaluated as constant and the first operand is false, it’s a compile-time error. The second operand is only evaluated for non-const evaluation.)
Expression statement whose expression is a potentially constant expression. (This proposal adds potentially constant expressions with side effects, like this.x = e and throw e, otherwise a potentially constant expression statement would be useless.)
An empty statement, ;. (Unless we remove this from the language. Which we should. Just use {} for “do nothing”.)
A statement block where each statement is a potentially constant statement.
An if statement where the condition is a potentially constant expression and each included branch is a potentially constant statement. (Can omit an else branch.)
A non-destructuring local variable declaration. Any initializer expression must be a potentially constant expression. (For example var counter = 0;, Object? o;, const cr = 0x0d; or var x = 0, y = 0;.)
A labeled statement label: stmt where stmt is a potentially constant statement.
A break statement, with or without label. Can be used with switch, or just to break out of deeply nested ifs.
Possibly also:
A destructuring local variable declaration or assignment with a potentially constant pattern.
var <pattern> = <expr> (declaration pattern).
<pattern> = <expr> (assignment pattern).
An if statement with a case condition with a potentially constant pattern and potentially constant when clause, if present.
A switch statement with only potentially constant patterns and potentially constant statements in branches. Rather than having to write the same thing using if … else if … else if … else.
There are no loops. Constant evaluation should not diverge, and a constant constructor doing, effectively while (true); should not block the compiler, or have it need to time-out.
That’s also why there is no continue allowing a switch case to go to another switch case. You can implement loops using ”goto”.
It might be possible to have a for/in loop over a constant platform List instance, since it’s known to terminate. I’d suggest starting without it, and see if it becomes a priority. It would be the only way to destructure and inspect a constant list’s elements. A spread like const [1, …list, 3] does not get to inspect the list elements, only include them all, and it only works in constant expressions, not potentially constant.
Potentially constant expressions
The following expression forms are added as potentially constant expressions:
throw <expression> is a potentially constant expression, the <expression> does not have to be. If evaluated as a constant, it immediately causes a compile-time error without trying to evaluate <expression>.
Identifiers denoting local variables or instance variables declared by the current class. (Extended from just variables of the initializer list scope, or constant variables).
Instance variable reads, this.id, for variables declared by the current class. Must not be read before it’s assigned.
Variable assignments, id = expr, including composite assignment, like id += expr or id++, where id is either a local writable variable in scope, or an instance variable of the surrounding class (for initializing).
Instance variable assignment: this.id = expr, this.id += expr, this.id++ for variables declared by the surrounding class.
expr.id where expr evaluates to a record and id is a record field getter for that record.
expr! where expr is potentially constant and it’s a compile-time error if expr is null during constant evaluation. We allow expr as Object, but not expr!. There is no reason for that omission. If we allow p! as a pattern, it should be allowed as an expression as well.
A switch expression whose patterns are potentially constant patterns and branch expressions are potentially constant expressions.
There are still no fancy composite object allocations. List, map and set literals must be constant, which precludes having any references to local variables or fields. Object creation expressions must also be const, invoking a constant constructor with constant arguments, not potentially constant. Function literals are right out, as long as we don’t have “constant function literals”, in which case they wouldn’t be able to reference any non-global state. You can allocate records in potentially constant expressions, but not much else (we ignore int, String, Type, etc. values here, those are “simple” non-composite values).
During evaluation of a constructor, all instance variables declared by the surrounding class are treated like unassigned local variables. It’s a compile-time error to read them unless they’re definitely assigned, it’s a compile-time error to assign to them if they’re final and not definitely unassigned. If the variable is late, that doesn’t affect initialization, which doesn’t go through getters. Even in non-constant constructors, you cannot capture this until the super-constructor has been invoked, so you cannot create a closure which accesses instance variables. The “this variable” is considered unassigned until the superconstructor has been called, and accessing instance variables is special cased to be allowed anyway, with that this.
Potentially constant patterns
Pattern sub-expressions are always constant, so there is no issue with having them inside constant execution. The issues are with whether the pattern matching can be allowed.
A pattern is potentially constant if it’s one of the following.
Catch-all: _, var id, final id, where id includes _.
Type-checks: SomeType id, final SomeType id, SomeType(), p as SomeType, p?, p! where id includes _, the object pattern has no property patterns, p is a potentially constant pattern and SomeType is a constant type. If a type-check is trivially satisfied by the matched value type, then it’s really a catch-all. Makes no difference for whether it’s allowed.
Constant patterns: Literals, const expr, if and only if the value has constant equality.
Relational patterns, == expr, != expr, < expr, …, >= expr. During constant evaluation, it’s a compile-time error if the matched value is not a constant value v where v == expr, …, v >= expr would be a valid constant expression (has constant equality for == and !=, is int or double for <, … >=).
A record pattern, (p1, ..., pn) where p1…pn are potentially constant patterns.
The missing patterns are: List patterns, map patterns and object patterns with field extraction patterns, which would be destructuring a constructed (non-record) object. Record objects are treated specially, as if they’re just bundles of single values, and we allow constructing records as potentially constant expressions, so we can also allow destructuring.
(We can allow String(length: p) since String.length is the one named property that is allowed as a constant. Not sure it’s worth specifying, and allowing String(length: 0) but not String(isEmpty: true) isn’t helping anyone. Maybe if we’d also allow List.length, it might be useful.)
If a pattern has a when clause, that expression must also be a potentially constant expression.
The variables introduced by pattern matching are normal local variables and can be used as such.
Potentially constant types
A type variable in scope is a potentially constant type. A type literal is a constant type if it doesn’t contain a type variable, and a potentially constant type if it does.
Since T or List<T> where T denotes a type variable is allowed as potentially constant expressions, an invocation of a constant constructor can create a new Type object for each const allocation.
Summary
This generalizes constant NRG constructor initializer lists to sequences of statements, but allows local variables and if/switch statements for better control flow.
The goal is still to reach the end of initialization, the call to the super-constructor, with all instance variables definitely assigned, and without ever giving access to any uninitialized state.
You can simulate having local variables today, by chaining redirecting generative constructors to introduce new variables each time a new value is needed. I don’t expect there is anything allowed by the potentially constant statements here, that you can’t do with such a constructor chain if you really want to. It’s just much more convenient to do it in a single constructor body.
We can require that all local variables are declared as final. That just forces the author to write things in static-single-assignment form. Might as well let the compiler do that. With no loops, variable assignment should be easy to track.
This proposal also extends what you can do with potentially constant expressions. It’s mainly for working with the new local variables, but that does fall through to the existing syntax, Foo(int x) : this.v1 = x++, this.v2 = x++, this.v3 = x; becomes valid. That’s probably not an issue.
Allowing switche and if/case with patterns is an improvement over writing the same thing using ?/: chains. Without the ability to destructure anything other than records, the switch/if's will just be about selecting a branch.
Future extensions
If Dart wants to generalize “potentially constant evaluation” to, fx, constant getters and functions which can be invoked during constant evaluation, it makes sense to have a potentially constant sub-language that is not just expressions.
Constant/stable functions
A constant instance getter or method would be one that can be invoked as a constant on a constant receiver, and is guaranteed to return the same value every time with no side-effects if evaluated as a constant.
That getter or method has to be declared as such in the interface (to ensure that it’s not overridden by a non-constant implementation in a subclass). A constant getter can be implemented using a final variable, but to preserve getter/field symmetry, it can also be a getter with a potentially constant function body, either => expr where expr is a potentially constant expression, or {…} which contains nothing but potentially constant statements, with return expr; being added as a potentially constant statement (which is still not allowed in an NRG constructor body). The body can then refer to the class’s type argument and to other constant getters or methods of the this object, or to constant static helper functions.
A constant static/top-level function or getter would only be able to refer to its own parameters (and constant global state).
(It’s tricky if we want to abstract over being a constant evaluation, like List.any would be a constant computation if the argument function is a function with a potentially constant implementation, but not otherwise, so it abstracting over const, which seems about as tricky as abstracting over asynchrony. So probably no higher level const-ness.)
Allowing more constant expressions
We can, safely, allow constant expressions to invoke most methods on int, double and String objects, and even on List/Set/Map objects if we require them to be platform List/Set/Map constant instances. That will immediately give access to the individual elements/entries just from using [] and length on List, but without a way to loop, it will still be “constant time”.
It’s a little inconsistent that we allow String.length, but not String.isEmpty, but a line has to be drawn somewhere, and moving it anywhere other than “as many members of platform types as possible” feels arbitrary. And I’m not sure we’re ready for all the members of platform types being available (even though it’s probably safe as long as there is constant no iteration and no methods with callbacks like fx Iterable.any).
Usage in const non-redirecting factory constructors
Dart currently doesn't allow const non-redirecting factory constructors, which it should.
Having potentially constant statements could allow those constructors' bodies to be blocks, not just => bodies.
The text was updated successfully, but these errors were encountered:
lrhn
added
the
feature
Proposed language feature that solves one or more problems
label
Feb 11, 2025
The “Enhanced Constructors” (#4246) proposal moves initializer lists of non-redirecting generative (NRG) constructors into a constructor body. Since constant NRG constructors cannot currently have a body, and potentially constant expressions are not generalized to statements, it has special rules for what can go into this new body. In the most basic form, it's just the initializer list entries separated by
;
instead of,
, which is obviously safe.I propose to allow more statements even in
const
constructors. The goal is still to be efficiently computable at compile-time without running any user-written code, without reading any visible mutable state , and such that everyconst
object creation expression evaluates to precisely one value. Potentially constant expressions can create new objects on each evaluation, but only ones not invoking user written constructors: Records and instances of platform types likeint
, String andType
.Potentially constant NRG constructor body
A
const
NRG constructor body must be a block with statements that are all potentially constant statements, followed (optionally) by a single super-constructor invocation. (If no super-constructor invocation is present, it defaults to doing whatsuper();
would do.)It might be possible to allow code after the super-constructor invocation, as long as that code is still potentially constant statements, but the code won't be able to access
this
anyway, sincethis
is not a potentially constant expression. Might as well do all the work first. (In fact, the constructor can likely do less after the superconstructor invocation than before, because before it can access instance variables declared by the current class without going throughthis
, and after it can't and it still has nothis
.)Potentially constant statements
A potentially constant statement is one of the following:
false
, it’s a compile-time error. The second operand is only evaluated for non-const evaluation.)this.x = e
andthrow e
, otherwise a potentially constant expression statement would be useless.);
. (Unless we remove this from the language. Which we should. Just use{}
for “do nothing”.)if
statement where the condition is a potentially constant expression and each included branch is a potentially constant statement. (Can omit anelse
branch.)var counter = 0;
,Object? o;
,const cr = 0x0d;
orvar x = 0, y = 0;
.)label: stmt
wherestmt
is a potentially constant statement.break
statement, with or without label. Can be used with switch, or just to break out of deeply nestedif
s.var <pattern> = <expr>
(declaration pattern).<pattern> = <expr>
(assignment pattern).if
statement with acase
condition with a potentially constant pattern and potentially constantwhen
clause, if present.if … else if … else if … else
.There are no loops. Constant evaluation should not diverge, and a constant constructor doing, effectively
while (true);
should not block the compiler, or have it need to time-out.That’s also why there is no
continue
allowing aswitch
case to go to another switch case. You can implement loops using ”goto”.It might be possible to have a
for
/in
loop over a constant platformList
instance, since it’s known to terminate. I’d suggest starting without it, and see if it becomes a priority. It would be the only way to destructure and inspect a constant list’s elements. A spread likeconst [1, …list, 3]
does not get to inspect the list elements, only include them all, and it only works in constant expressions, not potentially constant.Potentially constant expressions
The following expression forms are added as potentially constant expressions:
throw <expression>
is a potentially constant expression, the<expression>
does not have to be. If evaluated as a constant, it immediately causes a compile-time error without trying to evaluate<expression>
.Identifiers denoting local variables or instance variables declared by the current class. (Extended from just variables of the initializer list scope, or constant variables).
Instance variable reads,
this.id
, for variables declared by the current class. Must not be read before it’s assigned.Variable assignments,
id = expr
, including composite assignment, likeid += expr
orid++
, whereid
is either a local writable variable in scope, or an instance variable of the surrounding class (for initializing).Instance variable assignment:
this.id = expr
,this.id += expr
,this.id++
for variables declared by the surrounding class.expr.id
whereexpr
evaluates to a record andid
is a record field getter for that record.expr!
whereexpr
is potentially constant and it’s a compile-time error ifexpr
isnull
during constant evaluation. We allowexpr as Object
, but notexpr!
. There is no reason for that omission. If we allowp!
as a pattern, it should be allowed as an expression as well.A switch expression whose patterns are potentially constant patterns and branch expressions are potentially constant expressions.
There are still no fancy composite object allocations. List, map and set literals must be constant, which precludes having any references to local variables or fields. Object creation expressions must also be
const
, invoking a constant constructor with constant arguments, not potentially constant. Function literals are right out, as long as we don’t have “constant function literals”, in which case they wouldn’t be able to reference any non-global state. You can allocate records in potentially constant expressions, but not much else (we ignoreint
,String
,Type
, etc. values here, those are “simple” non-composite values).During evaluation of a constructor, all instance variables declared by the surrounding class are treated like unassigned local variables. It’s a compile-time error to read them unless they’re definitely assigned, it’s a compile-time error to assign to them if they’re final and not definitely unassigned. If the variable is
late
, that doesn’t affect initialization, which doesn’t go through getters. Even in non-constant constructors, you cannot capturethis
until the super-constructor has been invoked, so you cannot create a closure which accesses instance variables. The “this
variable” is considered unassigned until the superconstructor has been called, and accessing instance variables is special cased to be allowed anyway, with thatthis
.Potentially constant patterns
Pattern sub-expressions are always constant, so there is no issue with having them inside constant execution. The issues are with whether the pattern matching can be allowed.
A pattern is potentially constant if it’s one of the following.
_
,var id
,final id
, whereid
includes_
.SomeType id
,final SomeType id
,SomeType()
,p as SomeType
,p?
,p!
whereid
includes_
, the object pattern has no property patterns,p
is a potentially constant pattern andSomeType
is a constant type. If a type-check is trivially satisfied by the matched value type, then it’s really a catch-all. Makes no difference for whether it’s allowed.const expr
, if and only if the value has constant equality.== expr
,!= expr
,< expr
, …,>= expr
. During constant evaluation, it’s a compile-time error if the matched value is not a constant valuev
wherev == expr
, …,v >= expr
would be a valid constant expression (has constant equality for==
and!=
, isint
ordouble
for<
, …>=
).(p1, ..., pn)
wherep1
…pn
are potentially constant patterns.The missing patterns are: List patterns, map patterns and object patterns with field extraction patterns, which would be destructuring a constructed (non-record) object. Record objects are treated specially, as if they’re just bundles of single values, and we allow constructing records as potentially constant expressions, so we can also allow destructuring.
(We can allow
String(length: p)
sinceString.length
is the one named property that is allowed as a constant. Not sure it’s worth specifying, and allowingString(length: 0)
but notString(isEmpty: true)
isn’t helping anyone. Maybe if we’d also allowList.length
, it might be useful.)If a pattern has a
when
clause, that expression must also be a potentially constant expression.The variables introduced by pattern matching are normal local variables and can be used as such.
Potentially constant types
A type variable in scope is a potentially constant type. A type literal is a constant type if it doesn’t contain a type variable, and a potentially constant type if it does.
Since
T
orList<T>
whereT
denotes a type variable is allowed as potentially constant expressions, an invocation of a constant constructor can create a newType
object for eachconst
allocation.Summary
This generalizes constant NRG constructor initializer lists to sequences of statements, but allows local variables and
if
/switch
statements for better control flow.The goal is still to reach the end of initialization, the call to the super-constructor, with all instance variables definitely assigned, and without ever giving access to any uninitialized state.
You can simulate having local variables today, by chaining redirecting generative constructors to introduce new variables each time a new value is needed. I don’t expect there is anything allowed by the potentially constant statements here, that you can’t do with such a constructor chain if you really want to. It’s just much more convenient to do it in a single constructor body.
We can require that all local variables are declared as final. That just forces the author to write things in static-single-assignment form. Might as well let the compiler do that. With no loops, variable assignment should be easy to track.
This proposal also extends what you can do with potentially constant expressions. It’s mainly for working with the new local variables, but that does fall through to the existing syntax,
Foo(int x) : this.v1 = x++, this.v2 = x++, this.v3 = x;
becomes valid. That’s probably not an issue.Allowing
switche
andif
/case
with patterns is an improvement over writing the same thing using?
/:
chains. Without the ability to destructure anything other than records, theswitch
/if
's will just be about selecting a branch.Future extensions
If Dart wants to generalize “potentially constant evaluation” to, fx, constant getters and functions which can be invoked during constant evaluation, it makes sense to have a potentially constant sub-language that is not just expressions.
Constant/stable functions
A constant instance getter or method would be one that can be invoked as a constant on a constant receiver, and is guaranteed to return the same value every time with no side-effects if evaluated as a constant.
That getter or method has to be declared as such in the interface (to ensure that it’s not overridden by a non-constant implementation in a subclass). A constant getter can be implemented using a final variable, but to preserve getter/field symmetry, it can also be a getter with a potentially constant function body, either
=> expr
whereexpr
is a potentially constant expression, or{…}
which contains nothing but potentially constant statements, withreturn expr;
being added as a potentially constant statement (which is still not allowed in an NRG constructor body). The body can then refer to the class’s type argument and to other constant getters or methods of thethis
object, or to constant static helper functions.A constant static/top-level function or getter would only be able to refer to its own parameters (and constant global state).
(It’s tricky if we want to abstract over being a constant evaluation, like
List.any
would be a constant computation if the argument function is a function with a potentially constant implementation, but not otherwise, so it abstracting overconst
, which seems about as tricky as abstracting over asynchrony. So probably no higher level const-ness.)Allowing more constant expressions
We can, safely, allow constant expressions to invoke most methods on
int
,double
andString
objects, and even onList
/Set
/Map
objects if we require them to be platformList
/Set
/Map
constant instances. That will immediately give access to the individual elements/entries just from using[]
andlength
onList
, but without a way to loop, it will still be “constant time”.It’s a little inconsistent that we allow
String.length
, but notString.isEmpty
, but a line has to be drawn somewhere, and moving it anywhere other than “as many members of platform types as possible” feels arbitrary. And I’m not sure we’re ready for all the members of platform types being available (even though it’s probably safe as long as there is constant no iteration and no methods with callbacks like fxIterable.any
).Usage in const non-redirecting factory constructors
Dart currently doesn't allow const non-redirecting factory constructors, which it should.
Having potentially constant statements could allow those constructors' bodies to be blocks, not just
=>
bodies.The text was updated successfully, but these errors were encountered: