language icon indicating copy to clipboard operation
language copied to clipboard

Constant constructor call should be legal in another constant constructor definition.

Open DartBot opened this issue 9 years ago • 36 comments

This issue was originally filed by @Cat-sushi


The code below is illegal with the current language specification.

  class C {     final x;     const C(this.x);   }

  class D {     final C c;     const D(x) : this.c = const C(x); // compile-time error!   }

I understand that 'x' in 'const C(x);' is not a compile-time constant, but a potentially constant expression, and this is the reason why the constructor call 'call C(x);' is illegal. I also understand the reason why the 'x' is potentially constant, the constantivity of 'x' is depends on the call site which calls the constructor whether with 'const' or with 'new'.

On the other hand, with my understanding, the compiler evaluates the result of constant expressions at compile-time, and the compiler can check constantivity at compile-time with call site sequence. At the same time, in run-time context, constructors are less constrained, and the language processor can simply regard 'const' of 'const C(x);' as 'new', and can call the prepared run-time constructor. I think the disregard of keyword 'const' and the need for the run-time constructor of the constructor call 'const C(x);' in run-time context are kind of similar to those of constant constructor definition like 'const C(this.x);' or const 'D(x)...;'.

So, regardless of discussion in issue dart-lang/sdk#392 and issue dart-lang/sdk#19558, I think constant constructor calls in another constant constructor definition should be legal. And, I feel it is more natural.

In addition, I think this proposal is upper compatible of the current language specification.

DartBot avatar Sep 16 '14 11:09 DartBot

Treating "const" as "new" in some cases would probably be too confusing.

It is a problem that you can't create an object in a const constructor initializer list.

In the long run, a solution could be to make "new" optional, so you write:   const D(x) : this.c = C(x); and then let the implicit construction operator be new at runtime and const when used to create a const object. It beats introducing the new_or_const operator :)


Added Area-Language, Triaged labels.

lrhn avatar Sep 16 '14 18:09 lrhn

This comment was originally written by @Cat-sushi


Yes, treating "const" as "new" is confusing.

The villain of the confusion is newable constant constructor. So, hiding keyword "const" or "new" doesn't alleviate the confusion, even aggravates it.

Of course, newable const constructor is useful and kind of reasonable.

DartBot avatar Sep 16 '14 21:09 DartBot

This comment was originally written by @Cat-sushi


In addition, on the other hand, my proposal is completely consistent in compile-time constant context, like other initialization list.

I think, it is kind of reasonable.

// 'call C(x);' in my original post is typo of 'const C(x);'. (^_^)

DartBot avatar Sep 16 '14 23:09 DartBot

This comment was originally written by @Cat-sushi


I think the current implementation compiles constant constructor without call site sequence. Is it true? And why? Is it related to start up speed?

I think it is the bottle neck of solution of ambiguity in potentially constant expression. And constant constructor should be compiled with every call site.

DartBot avatar Sep 17 '14 22:09 DartBot

Added Customer-Flutter label. I'd like to be able to define const convenience constructors that forward properties to a nested object.

Example:

class Semantics extends StatelessWidget {
  // This is not supported today:
  const Semantics({
    bool checked: false,
    bool selected: false,
  }) : properties = const SemanticsProperties(
    checked: checked,
    selected: selected,
  );

  const Semantics.fromProperties(this.properties);

  final SemanticsProperties properties;
}

yjbanov avatar Nov 28 '17 22:11 yjbanov

FWIW, this is forcing Flutter to have a breaking API change.

Hixie avatar Dec 04 '17 22:12 Hixie

I think it can be closed with dart 2.0. Thank you. DartPad

Cat-sushi avatar Sep 22 '18 09:09 Cat-sushi

@Cat-sushi I'm still seeing the following analyzer error: "Initializer expressions in constant constructors must be constants."

yjbanov avatar Sep 23 '18 22:09 yjbanov

@yjbanov So am I. DartPad some time emit no error, or executes it with same error. DartVM always throw runtime exception "Not a constant expression".

Cat-sushi avatar Sep 24 '18 03:09 Cat-sushi

I also see the error in DartPad and with dartanalyzer and the vm, but this is indeed the behavior that we should have.

Constant expressions in Dart were designed to be significantly less expressive than regular Dart. In particular, there's no notion of value directed recursion (which would allow for constant expression evaluation with high complexity, e.g., like the Ackermann function). A simple example would be the following:

class A {
  final A next;
  const A(int i): next = i > 0 ? const A(i - 1) : null;
}

As mentioned in earlier comments on this issue, we might be able to omit const and have it inferred based on the invocation of the constructor, and we might also allow it and just say that this particular constructor cannot be invoked except as part of a constant expression. That's not difficult to sort out.

But if we were to support this kind of recursion then there's basically no limit on the amount of computation that any given constant expression could give rise to, and that might well cause a perceived startup time optimization to turn into a program size nightmare (if we'd compute some huge object graphs at compile-time and make them part of the shipped program).

So the rules were kept simple: For a 'constant expression' there is a tight relationship between the syntax and the semantics (one syntactic expression corresponds to one constant object at run-time, and, by canonicalization, one constant object corresponds to all the syntactic expressions that have "the same value"). For a 'potentially constant expression' Dart only allows certain built-in operations (like a + b where a and/or b are arguments of the enclosing constructor: this must be a numeric addition or a string concatenation, not a user-defined operator +).

Even C++, whose template feature is known to be Turing complete, has a notion of constant expression evaluation (constexpr) that may or may not be Turing complete, so Dart is not the only language to hesitate in this area, even though C++ is definitely going further.

In summary, there are reasons why a constructor like const D(x) : this.c = const C(x) is not supported.

This issue should not be closed based on the updates in Dart 2 (Dart 2 doesn't differ from Dart 1 in any way that makes a difference for this issue). It could be maintained as a request for some enhancement in the area of constant expression evaluation, or it could be closed because such enhancements are not trivial to introduce (nor is it guaranteed that they have widespread support).

I basically think that the situation is unchanged, and if you, @Cat-sushi, or anyone, wish to maintain the pressure to get some enhancements in this area then this issue is a fine place to have that discussion.

eernstg avatar Sep 24 '18 08:09 eernstg

@eernstg thanks for explaining the background. I understand that there're a lot of cases where this could get nasty if implemented carelessly. However, I also think that there're a lot of valuable features that would be in the safe zone if we think carefully about it.

yjbanov avatar Sep 24 '18 16:09 yjbanov

I just checked whether I could find a "canonical" issue on enhancements to constant expression features, but it seems like there is no such thing. We have other requests like https://github.com/dart-lang/sdk/issues/29277 about functions that could be invoked during constant expression evaluation (so that's related, but much more radical).

I think it will be difficult to extend the expressive power of constant expressions without letting it explode into something which is essentially the whole language, but it could still be worthwhile to look for simple restrictions that would be strong enough to avoid that, and still useful in common, concrete scenarios.

eernstg avatar Sep 24 '18 17:09 eernstg

There's also my ancient DEP: https://github.com/yjbanov/dart-const-functions

yjbanov avatar Sep 24 '18 17:09 yjbanov

Right, I was actually looking for that, but searching issues, and didn't find it. Thanks! ;-)

eernstg avatar Sep 24 '18 18:09 eernstg

I'm not sure how relevant those motivating examples are any more (@matanlurey can bring us up to date), but there's plenty over in the Flutter land.

yjbanov avatar Sep 24 '18 19:09 yjbanov

I don't see any reason why this would be 'customer-flutter' only, but compelling examples from Flutter land would certainly be relevant.

eernstg avatar Sep 28 '18 08:09 eernstg

I don't believe the labels mean that a particular customer is the only one who's interested in a feature, only that the customer really really cares about the issue :)

yjbanov avatar Sep 28 '18 17:09 yjbanov

@eernstg Stumbled across this issue from "Flutter land" & am hoping highlight a use-case.

Example Looking to subclass BorderRadius and use a default constructor parameter (like BorderRadius.zero)...

class CustomWidget {
  // ...

  CustomWidget({
    this.borderRadius = CircularBorderRadius.zero, // <—— "error: Default values of an optional parameter must be constant."
  });

  // ...
}
class Radius {
  final double radius;
  const Radius(this.radius);
}

class BorderRadius {
  final Radius x;
  final Radius y;
  const BorderRadius(this.x, this.y);
}

class CircularBorderRadius extends BorderRadius {

  static const CircularBorderRadius zero = const CircularBorderRadius(0.0);

  const CircularBorderRadius(double radius):
  	super(const Radius(radius), const Radius(radius)); // <— ERROR "Arguments of a constant creation must be constant expressions."
}

mwalkerwells avatar Nov 01 '18 17:11 mwalkerwells

@mwalkerwells: Thanks for the input!

That is indeed an interesting case, because it may seem reasonable to be able to create two constant Radius instances based on the given radius.

But it's a tricky step to take! ;-)

The example fails now because of the nature of Dart constant constructors: There is nothing stopping them from being invoked at run time (passing arguments that are not known at compile-time), so there is no way you could ever be allowed to have const Radius(radius): There is no such constant object, and it's not going to help us that we could let one piece of syntax correspond to several different constant objects (that's already a non-trivial generalization), because it's an undecidable question which values radius might have.

So in order to get started on such a feature we would need to introduce the notion that "this particular const constructor can only be invoked in a constant object expression" (that is, a constructor invocation that starts with const explicitly, or that occurs in a constant context where it must be a constant, e.g., as the initializing value for a variable declared to be const).

That shouldn't be too hard to support, but it might not be considered to carry its own weight, weighed up against the notational and possibly semantic complexity of allowing developers to specify such a thing.

Even then, the ability to write things like const SomeConstructor(aParameterFromEnclosingConstructor) would not be a given, because the combination of that feature with the use of a conditional expression (b ? e1 : e2) immediately would allow us to express "wild" functions like the Ackermann function.

So we'd be back in a situation where we couldn't promise that constant objects are manageable for large projects, because they might suddenly explode in size (and it might be difficult to see why).

So even though that looks like a small step yielding an obviously useful feature, it's actually a feature that opens the door to all the complex stuff that we decided long ago we wouldn't allow in the context of constant computations.

So, if we are to go down this path, we'd at least need to confine such a feature very carefully in order to keep the generalization less powerful than general recursive functions.

eernstg avatar Nov 26 '18 16:11 eernstg

@eernstg Thank you so much for such a thoughtful explanation!

mwalkerwells avatar Nov 26 '18 19:11 mwalkerwells

I know this is an old issue, but I'd like to raise a point. I understand @eernstg where you are coming from when you point out that users can use recursive functions in their const declarations, but technically, they can do that (and mess up run-time just as bad) even without const. At least this way, these memory bloats can be caught at compile-time, not at run-time. Also, every feature has cases where optimal performance cannot be guaranteed, but should still be implemented because it can be helpful regardless. Especially down in "Flutter-land", there are many cases where a const can propogate through many widgets, and this could be helpful for those who want to keep their existing const calls. Again, when you consider that one non-const value can invalidate all other const values, the gains from this get better.

Unless if my understanding of const savings is wrong (which it probably is). Are the savings from const for simple data classes (such as TimeOfDay (this.hour, this.minute)) worth it?

Levi-Lesches avatar Jul 03 '19 03:07 Levi-Lesches

Also, as to how Dart would determine if the call is a const call or not, wouldn't it the same with const calls now? Like in:

class A {
  final int value;
  const A(this.value);
}

const A(5) is different than const A(someFunction()), and Dart knows that.

Levi-Lesches avatar Jul 03 '19 03:07 Levi-Lesches

@Levi-Lesches wrote:

they can do that (and mess up run-time just as bad) even without const

Right, run-time resource consumption generally cannot be bounded in a Turing complete language. But Dart puts a bound on constant expressions for several reasons, including the fact that this makes constants "safe" even in large programs, in the sense that the size of the set of constant objects won't explode.

Are the savings .. for simple data classes .. worth it?

I think the emphasis put on using constants by people who are keeping an eye on their performance indicate that it is worthwhile.

const A(5) is different than const A(someFunction()), and Dart knows that.

Yes, but that's exactly because 'constant expression' is defined inductively (using rules that generally have the form "e1+e2 is constant if e1 is constant and e2 is constant [plus any additional requirements]"), which means that it is a quite simple procedure to tell whether a given expression is constant. And someFunction() is never constant, the only syntax that gets close is identical(e1, e2).

eernstg avatar Jul 03 '19 07:07 eernstg

Thanks for responding. As for your first point, say the memory for the constant objects do explode. What benefit does the compiler get by forcing that memory bloat to run-time instead of compile-time (and recreating the objects each time)? I get that a long compile-time is a good warning that something is wrong, but most programmers know that so is a long delay on a button press.

As for determining what is and isn't a constant, my point still stands that this wouldn't introduce complications since Dart easily knows what constants are. By allowing more constructors to be const, the problem of determining when a value can or cannot be const doesn't change, as you pointed out.

Levi-Lesches avatar Jul 07 '19 19:07 Levi-Lesches

say the memory for the constant objects do explode

I agree that accepting a long compile step in return for better performance at run time is a perfectly meaningful choice—basically enabling non-trivial optimizations on any compiler is an example of doing that.

But this is not just about having a large program that suddenly takes a long time to compile. It's also about having many pieces of software which are brought together in order to create a large system. If constant evaluation complexity isn't bounded (e.g., constant computation could be Turing complete just like C++ template metaprogramming or constexpr) then you could easily get into a situation where version 1.27.1 of package foo worked fine, but when you switch to 1.27.2 your executable is suddenly much bigger.

Space consumption at run time is of course equally bad in several ways, but not all: With a big constant pool you'll have a bigger download size; also, with dynamically created objects you may create a big heap at some point, but garbage collection may be able to delete most of that as soon as the memory has been exhausted, and even if that is not possible it would not have helped to make them constant.

Besides, I mentioned that we've kept constants relatively simple for several reasons. One of them is that it's not simple at all ;-)

For instance, there are about 2400 github issues referring to const, 449 of them open, and constant expressions keep coming up as the tricky case (for instance, how do you handle bool.fromEnvironment() during separate compilation where the environment is not yet known?). So the question is also "do we want yet another tiny extension of constant expressions, or do we put the resources into non-nullable types", etc.

In any case, requests for more general constant expressions keep coming up, so we are certainly aware of the fact that it is an area of interest.

eernstg avatar Jul 08 '19 09:07 eernstg

So the question is also "do we want yet another tiny extension of constant expressions, or do we put the resources into non-nullable types", etc.

Now that we have NNBD, is this issue, along with https://github.com/dart-lang/language/issues/663, at sight?
This is very interesting for optimizations.

jodinathan avatar Oct 22 '21 10:10 jodinathan

Just to highlight another use-case, this doesn't work right now:

class MyIcon {
  const MyIcon._(int codePoint) :
      outlined = IconData(
        codePoint,
        fontFamily: 'MyIconsOutlinedRounded',
        fontPackage: 'this_package',
      ),
      filled = IconData(
        codePoint,
        fontFamily: 'MyIconsFilledRounded',
        fontPackage: 'this_package',
      );

  final IconData outlined;
  final IconData filled;
}

@immutable
class MyIcons {
  const MyIcons._();

  static const add = MyIcon._(0xf101);
  static const addressBook = MyIcon._(0xf101);
  // hundreds more of these
}

Instead, we're forced to do this:

class MyIcon {
  const MyIcon._(this.outlined, this.filled);

  final IconData outlined;
  final IconData filled;
}

@immutable
class MyIcons {
  const MyIcons._();

  static const add = MyIcon._(
    IconData(
      0xf101,
      fontFamily: 'MyIconsOutlinedRounded',
      fontPackage: 'this_package',
    ),
    IconData(
      0xf101,
      fontFamily: 'MyIconsFilledRounded',
      fontPackage: 'this_package',
    ),
  );
  // ...
}

This increases the file size from a mere 1000 to over 8000 lines of code. Also, constant evaluation, in this case, is a necessity so that tree-shaking of the icon font works.

MarcelGarus avatar Feb 18 '22 10:02 MarcelGarus

See https://github.com/dart-lang/language/issues/2256 where this issue was first raised:

A constructor in an enum declaration (as of Dart 2.17, where enhanced enum declarations were introduced) is guaranteed to be invoked as part of constant expression evaluation. In other words, they will never be evaluated with new (explicitly or implicitly).

This means that some of the concerns stated above (involving instance creation at run time) do not apply to that kind of constructor, and hence it might be easier to support something like the proposal of this issue in that special case.

eernstg avatar May 24 '22 16:05 eernstg

I think it's appropriate to include construction of objects in a slightly broader sense than instance creation expressions (that is, expressions invoking a constructor, like const C() or C.named() in a constant context, where C is a class).

In particular, we could cover collection literals as well in this issue. Here is a motivating example:

class MyApi {}

class ClassProvider {
  final Type type;
  const ClassProvider(this.type);
}

class FactoryProvider {
  final String token;
  final String Function() func;
  const FactoryProvider.forToken(this.token, this.func);
}

class Module {
  final List<Object> providers;
  const Module(this.providers);
}
 
class MyApiModule extends Module {
  const MyApiModule(String Function() keyFunction) : super(const [
    ClassProvider(MyApi),
    FactoryProvider.forToken('key', keyFunction), // *** Error: `keyFunction` is not constant. ***
  ]);
}
 
void main() {
  const module = MyApiModule(myFunc);
  print(module.providers);
}

String myFunc() {
  return 'test';
}

In short, the fact that keyFunction is not a constant at the error (it's only potentially constant) prevents the use of common abstraction mechanisms. We can of course pull the construction of the constant objects out of the constructor, but it gets quite verbose at the call site, and it involves duplication of code at, potentially, a large number of call sites:

class MyApi {}

class ClassProvider {
  final Type type;
  const ClassProvider(this.type);
}

class FactoryProvider {
  final String token;
  final String Function() func;
  const FactoryProvider.forToken(this.token, this.func);
}

class Module {
  final List<Object> providers;
  const Module(this.providers);
}
 
class MyApiModule extends Module {
  const MyApiModule(super.providers);
}
 
void main() {
  // We have to provide the entire constant expression here:
  const module = MyApiModule(const [
      ClassProvider(MyApi),
      FactoryProvider.forToken('key', myFunc),
  ]);
  print(module.providers);
}

String myFunc() {
  return 'test';
}

eernstg avatar Aug 03 '22 08:08 eernstg

Allowing potentially constant/non-singleton object creation in a const constructor if we know, for certain, that the constructor will only ever be used as const, seems possible. We definitely know all the necessary values at compile-time.

It breaks with one rule that Dart has so far maintained: That a const expression always evaluates to the same value. This limits the number of const object creations happening at compile-time to be linear in the source code size.

If we allow such a potentially constant object creation, then we can't rely on our current, rather simple, cycle detection, where we check whether the evaluation of a const expression depends on its own value. If the same expression can have multiple values, we either have to say that a constructor cannot depend on itself, or we have to accept non-linear object creation.

Say:

class CFib {
  final List<CFib> value;
  const CFib(int n) : value = n <= 2 ? [] : [CFib(n - 1), CFib(n - 2)];
}
var x = const CFib(48);  

This constant evaluation terminates (eventually) and creates ... ~48 objects (after canonicalization, ~4G before).

(Could be worse. Could be the busy beaver function.)

I'm sure I can find a way to actually create the 4G different objects too. if necessary. Just do some string concatenation along the way.

lrhn avatar Aug 03 '22 12:08 lrhn