Introduce a couple of new potential constants and allow a non-redirecting factory constructor to be constant
This issue is a proposal that constructors like const factory A(int i, String s) => B(i, s); should be supported.
(See also https://github.com/dart-lang/language/issues/3356, which is a subset of this proposal.)
The sublanguage of constant expressions in Dart currently does not include abstraction over instance creation for anything other than records. For example:
class A {
final Record r;
const A(int i, String s): r = (i ,s); // OK.
}
This is allowed because (i, s) is a potentially constant expression and, the result of substituting the actual arguments for the formal parameters at a call site which is a constant expression is a constant expression (e.g., if we call it using const A(1, "") then there is no compile-time error because the resulting record (1, "") is a constant expression).
However, an instance creation expression like const A(...) cannot contain formal parameters because it can only receive actual arguments which are constant expressions. For example:
class B {
final A a;
const B(int i, String s): a = const A(i, s); // Error!
}
It is also an error when const is omitted from const A(i, s) because A(i, s) is not a constant expression, and it is required that every invocation of the constructor B is such that a substitution of the actual arguments for the formal parameters yields constant expressions in every initializer element. In the example, the substitution of an invocation like const B(1, "") yields A(1, ""), and that is not a constant expression.
Proposal
An instance creation C<T1 .. Ts>(a1, .. ak) (where <T1 .. Ts> may be absent) that occurs in a declaration of a constructor k of a class D is a potentially constant expression when each of a1 .. ak is a potentially constant expression.
In Dart without this feature, invocations of a constant constructor k during constant expression evaluation is required to be such that a substitution of the actual arguments for the formal parameters yields constant expressions in every initializer element. This rule is enhanced such that the substitution on an instance creation where not all actual arguments are constant (that is, some of the actual arguments are only potentially constant) will add the modifier const on said instance creation.
This is the step in the example in the previous section where A(i, s) by substitution is turned into A(1, ""). This expression is then turned into const A(1, ""), which is constant.
Similarly, a list literal or set literal is potentially constant if it only contains elements that are potentially constant, and a map literal is potentially constant if it only contains keys and values that are potentially constant.
The substitution rule is further enhanced such that the substitution on a collection literal where not all elements are constant (that is, some of the elements are only potentially constant) will add the modifier const on said collection literal.
For example, we might have an expression <int>[i, j] in the initializer list which is turned into <int>[2, 3] by substitution, and it is then turned into const <int>[2, 3], which is constant.
An error occurs if a constant expression evaluation more than once evaluates an expression which is obtained by substitution and addition of const on the same potentially constant but not constant expression.
That is, a constant expression evaluation cannot recursively evaluate any of the potentially constant expressions that are introduced by this proposal. This prevents constant expression evaluation from entering an infinite loop. If a more permissive rule can be found that still prevents non-termination then we are free to use it, and this will be a non-breaking change.
Finally, a constant factory constructor declaration of the form const factory D(args1) => e; is allowed when e is a constant expression, and when it is a potentially constant expression. In the latter case, an invocation of the constructor is an error unless substitution of the actual arguments for the formal parameters turns e into a constant expression.
Examples
This allows factories to perform a similar kind of abstraction as redirecting generative constructors (that is, they can change the actual argument list). For example:
class const Point(final int x, final int y) {
const new gen(int both): this(both, both); // A redirecting generative constructor.
const factory fac(int both) => Point(both, both); // A factory.
}
extension on Point {
// Extension constructors must be factories.
const factory fac2(int both) => Point(both, both); // An extension factory.
}
It also allows existing constant constructors to perform actions which were previously not supported:
class A {
final List<int> list;
const A(int i, int j): list = [i, j];
}
In a class with a constant constructor, the initializing expression of each instance variable must be a constant expression. This is not true today with [i, j], and const [i, j] is an error because i and j are not constant. However, with this proposal [i, j] is a potentially constant expression and an invocation like const A(2, 3) is checked in two steps: [i, j] is turned into [2, 3] by the substitution, then [2, 3] is turned into const [2, 3] because the original expression [i, j] contains some expressions that are potentially constant and not constant. The result expression const [2, 3] is a constant expression, so the invocation is accepted (that is, it is not an error).
Here is an example where a potential non-termination danger gives rise to a compile-time error during constant expression evaluation:
class A {
A._();
const factory(int k) => k <= 0 ? A._() : A(k - 1);
}
void main() {
A(2); // Not constant, no problem.
// const A(2); // Compile-time error.
}
The error occurs because the evaluation of const A(2) will give rise to an evaluation of const A(1) which is an evaluation of the expression A(k - 1) after substitution and addition of const, which gives rise to an evaluation of const A(0), which is an evaluation of the expression A(k - 1) after substitution and addition of const. This is the second time, and that is an error.
Versions
-
Nov 21 2025, version 1.1: Corrected the rule that prevents non-termination and filled in more detail about collection literals and substitution checks.
-
Nov 20 2025, version 1.0.
For each invocation of k as a constant expression, it is required that none of the actual arguments passed to k is such that k is invoked during the evaluation of that argument (those arguments are themselves constant expressions, which implies that the entire evaluation is known at compile time).
A couple of comments here.
First, from discussion IRL, I don't think this does what you want. That is, I believe that you intend the following code to be rejected:
class C implements D {
const C(D d);
}
class D {
const factory D(C c);
}
const c = C(D());
const d = D(c);
But it seems to me to be ambiguous whether evaluating the argument to D.new here involves invoking D.new or not. It certainly requires that D.new have been invoked to produce c, but it is not clear to me why this rejects the code above given that it is both valid and desired that implementations not have to fully transitively re-evaluate every constant at every use site in such a call (which I think is what is required for the above error, unless maybe you record as part of every constant the set of invocations that happened during its evaluation). This seems messy to me.
Second, I don't think this error actually accomplishes what you want. My understanding is that you are trying to prevent constant evaluation from recursively building up arbitrarily big things, but I don't think this does that, since you still allow arbitrary recursion. So the following I think would be allowed?
// This is a currently legal Dart class for a simple linked list using null as the nil value
class LL {
final int x;
final LL? next;
const LL(this.x, this.next);
}
// I think this class would be legal under this proposal. The conditional expression in the body of the const factory
// constructor is potentially constant, I think? And once values are substituted into it, it becomes constant?
class Rec {
final LL? ll;
const Rec._(this.ll);
const factory Rec(int x, LL? l) =>
(x == 0) ? Rec._(l) : Rec(x - 1, LL(x, l));
}
void main() {
// This produces a linked list of length 30
const a = Rec(30, null);
}
const factory D(args1) => e;
where e is potentially constant - I want this. (I always did, but I still do: #3356).
For each invocation of k as a constant expression, it is required that none of the actual arguments passed to k is such that k is invoked during the evaluation of that argument (those arguments are themselves constant expressions, which implies that the entire evaluation is known at compile time).
I believe this only applies to the non-const object creation expressions in the body of the constructor k.
And then it's still both too harsh and too lenient.
No need to disallow
const factory Cons.([T? value]) => value == null ? const Cons._nil() : Cons._cons(car: value, cdr: const Cons(null));
The invocation of const Cons(null) is a separate evaluation that can be completed first. That one doesn't call itself back.
So it's about whether a entiry-potentially-constant evaluation of a constructor will end up calling the same constructor again. That's not what it says, this is only about arguments. I can diverge without that.
enum Collatz {
invalid;
const factory Collatz([int cursor = 2, int? start]) =>
cursor < 2 ? const Collatz(2) :
cursor == start ? invalid : // Cycle detected.
cursor == 1 ? Collatz(start + 1, null) :
cursor & 1 == 0
? Collatz(cursor ~/ 2, start ?? cursor)
: Collatz(cursor * 3 + 1, start ?? cursor);
}
A const Collatz() will be Collatz.invalid if the Collatz conjecture is false, and never complete if it's true. (Or if it's false, but there is a diverging sequence starting at a number smaller than the first number that's part of a cycle.)
All arguments are integers.
All the recursive constructor invocations are tail calls, so only allowing them in tail position won't even help.
Allowing any subexpression of e to be a potentially constant constructor invocation breaks with one existing principle of constant expressions: The number of created constant objects is linear in the number of constant object creation expressions in the program. Each const Something(...) invocation creates one unique object. If that object contains another object, that was created by other const Whatnot(...) expression.
That doesn't apply to records, or integers for that matter. They don't have identity anyway, so we can't say how many there are, but we can create more than one inside a single constructor invocation. The saving grace is that records don't have constructors that can recurse, so it's still limited to the number of record expressions in that one constructor initializer list. Each recursion to another constructor is required to be properly constant and create precisely one object.
Avoiding cycles currently only means that evaluation of a constant expression must not depend on the value of that constant expression.
If we properly disallow a non-constant constructor invocation recursing to itself, then we can stil lget expoential behavior.
sealed class Tree {
const factory Tree0(int count) => count <= 1 ? Leaf(0) : Node(Tree1(count ~/ 2, 0), Tree1((count + 1)~/2, count ~/2));
const factory Tree1(int count, int offset) =>
count <= 1 ? Leaf(offset) : Node(Tree2(count ~/ 2, offset), Tree2((count + 1)~/2, offset + count ~/2));
const factory Tree2(int count, int offset) =>
count <= 1 ? Leaf(offset) : Node(Tree3(count ~/ 2, offset), Tree3((count + 1)~/2, offset + count ~/2));
...
const factory Tree20(int count, int offset) =>
count <= 1 ? Leaf(offset) : Node(Tree21(count ~/ 2, offset), Tree21((count + 1)~/2, offset + count ~/2));
const factory Tree21(int count, int offset) =>
Node(offset, offset + node); // Omit recursion.
}
class Leaf(final int value) implements Tree;
class Node(final Tree left, final Tree right) implements Tree;
Here a const Tree0(1048576) should still build a mega-tree using only linear constructors and one invocation.
So, how can we prevent that?
The problem is not the self-reference.
A problem is that we call two new constructors per constructor invocation. When we can do that, then we can get exponential execution from a linear number of constructors, even without self-refernce.
So, what if a potentially constant object creation expression could only have arguments/elements that does not contain any further non-constant object creation expressions? (And still no transitive self-reference.)
Since you always need to return one object, and you can't really do member invocations on created objects, this would effectively be the same as only allowing non-const object creation expressions in tail position.
(But might be easier to specify than having to define what "tail position" is.)
Except that you can put operations into conditions, say compute two values and compare them for identity, before computing a third value to return. Still bad for time/space complexity.1
If we require non-const object creation expressions to only be in tail position (which is easier to define for constant expressions than in general), then we should avoid creating more than one object. Then we get at most a number of constructor invocations linear in the number of redirects, each only calling one other constructor.
I think that's my proposal:
- Allow
=> expras a constant factory body whenexpris potenially constant. #3356 - Allow that expression to contain non-
constobject creation expressions, with potentially constant arguments/elements but only in tail position. - It's a compile-time error if potentially constant executation of a constructor recursively invokes the same constructor again. (It's OK if it goes through any
constinvocation first, then it's just referring to an existing value). - A potentially constant expression
Eis in tail position of another potentially constant expressionCif:CisEEis in tail position ofTand, for any expressionsE'andE'':Cis(T)CisE' ? T : E''orE' ? E'' : TCisE' ?? T(That should be enough for constant expressions. There are no cascades or assignments.)
@leafpetersen wrote:
I believe that you intend the following code to be rejected:
class C implements D { const C(D d); } class D { const factory D(C c); } const c = C(D()); const d = D(c);
I just wanted to maintain a property that we've always had with constant expressions: Constant expression evaluation should be guaranteed to terminate.
In the example, the factory constructor would have to return something, and we can't invoke D() without passing an argument, so we'd need to adjust the code slightly. However, I do think you're right that the rule I mentioned doesn't work as intended.
Whatever it takes to rule out non-termination would be a relevant candidate for the rule. We would want to get the greatest possible expressive power within those boundaries, and we also want to keep the expressive power limited because every single generalization of the constant expression sublanguage always turns out to be costly in terms of implementation and maintenance costs.
Similarly for the next issue:
So the following I think would be allowed?
... // Code containing a constructor that runs `k` times where `k` is an `int` argument.
Yes, I would not expect the Dart static analysis to attempt to prove termination of a recursive function based on a proof by induction (and an assumption that the argument can't possible be negative, or we're willing to wait for MIN_INT and wrapping around. ;-). So the idea was that a limitation that prohibits this example would be needed, and it would probably eliminate a huge amount of expressive power.
So we definitely need to improve and correct the non-termination rule. On the other hand, I think that's basically a pluggable add-on to the design: When we have a good rule, we can add it. Until then we'll just have a design that may allow for some constant expressions that may cause the compiler/analyzer to enter an infinite loop.
[Edit: I modified the original posting to use a better non-termination prevention rule.]
@lrhn wrote:
I want this. (I always did, but I still do: https://github.com/dart-lang/language/issues/3356).
Oh, I forgot about that proposal! I added a link to it in the original posting of this issue.
From proposal #3356:
Constant non-redirecting-const-factory return value
The proposal in this issue supports the given example (which is presented along with the assumption that constantExpression is a constant expression whose type is a subtype of ConstructorName, or ConstructorName<X1 .. Xk> if ConstructorName is generic):
const factory ConstructorName(args) => constantExpression;
Next, #3356 has a section about the case where the body is potentially constant:
Potentially constant non-redirecting-const-factory return value
This is also covered by the proposal in this issue. The core of the example is this declaration:
const factory Formatter({bool pretty = false}) => pretty ? _prettyFormatter : _denseFormatter;
where _prettyFormatter and _denseFormatter are references to constant variables (in particular, they are constant expressions).
As the next section 'Workaround (to become possible!)' in #3356 explains, this is already expressible by means of extension types.
This proposal goes a couple of steps further because it allows a constant factory constructor body to contain expressions that are constant constructor invocations and collection literals which are potentially constant according to the enhanced definition of 'potentially constant' which is proposed here.
For example, the following is supported in this proposal (as mentioned in the OP), but it is not supported in #3356:
class A {
final List<int> list;
const A(int i, int j): list = [i, j];
}
With primary constructors we'd need to generalize the proposal slightly such that the transformation that adds const during constant expression checking is also performed on the initializing expressions of non-late instance variables. With that, we can express the same thing slightly more smoothly:
class const A(int i, int j) {
final List<int> list = [i, j];
}