Open eernstg opened 2 years ago
For contains
on Iterables
, the collection elements must have primitive equality. (Might restrict it to List
and Set
, instead of any Iterable
.)
For constainsValue
on Map
, the values must have primitive equality. (For containsKey
and operator[]
, the keys must too, but they already had to in a const map literal.)
For contains
and split
on String
, the argument must be a String
(not any Pattern
), as already stated.
The String.split
might have to be dropped because it returns a mutable list normally, and would have to return a const
list if evaluated as const, and we don't usually allow const evaluation to behave differently from non-const evaluation. (It's an optimization to actually do the constant evaluation at compile-time,) It would mean that "abba".split("b")
in a non-const context would not be constant. Or? (We currently ignore that something is a "constant expression" when it's not required to be one, and allow compilers to canonicalize strings, or not do so, as they see fit.)
Thx! I adjusted the table. I did not change the static receiver type for operations on iterables, because the restrictions on the actual receiver already restricts the operations to objects of type List
or Set
, and the static type could actually be Iterable
. I removed split
because it seems to be a step too far to introduce this semantic discrepancy (being mutable or not).
There has not been much discussion here about making more constructors const
.
I recently want a const String.fromCharCode
and String.fromCharCodes
.
@rakudrama, perhaps the request for constant constructors of String
should be made in the SDK repo as a core library issue?
I do recognize that it is not possible to have an external const factory
constructor in Dart without going outside the language, but I still think that "the language Dart" (and, in particular, the language specification) wouldn't need to know about those constant constructors, it just needs to know that an instance creation that invokes a constant constructor can be a constant expression.
With that in place, we can say that this issue is exclusively about extending the set of constant expressions (by adding .length
on lists etc.).
operator []
Would this allow tighter tree shaking somehow on maps ? If I recall correctly one of the test I tried a while ago const maps weren't tree shaken (on web) but I could be wrong. IE if only a subset of elements are accessed with constants values.
tighter tree shaking
You could say that there is a potential benefit there, because we could conceptually consider m[k]
to be a single reference when m
is a constant list or map and k
is a constant. In that sense, const xs = [1, 2];
would introduce the "references" xs[0]
and xs[1]
, and then we'd just tree shake xs[1]
if it is never used.
However, this stops working as soon as the program drops a bit of information: If we're passing xs
as an argument to a function call (with any suitable declared type, e.g., List<int>
or Object
) then we can't keep track of any variable being or not being that same map. So if we have ys[1]
somewhere (where ys
could have type List<num>
, dynamic
, etc), it might access xs[1]
. Similarly, if we have a run-time expression e
, and we're accessing xs[e]
or ys[e]
, then it might again access xs[1]
.
So there isn't much hope that we could remove any elements from a list, or any key/value pairs from a map, based on a static analysis. Tree shaking works a lot better when we have a declaration which is known to be unused because no identifier in the program resolves to that declaration.
I created a proposal which is somewhat relevant here: https://github.com/dart-lang/language/issues/371, about 'link time sets and maps'. The main idea for those is that they are populated by things that are reachable already. So if you have a class C
which is unused then it will be ignored, but if you are using it (e.g., by creating an instance of it) then it's already surviving the tree shaking process, and in that case it will be taken into account when the link time sets and maps are created. For instance, we might then have a link time set of factory functions, and we could add some constructors of C
to that set.
This basically means that we're tree-shaking some elements out of the set and similarly for a map (e.g., a constructor tear-off like C.new
), because some associated elements (like the class C
) can be tree-shaken out of the program already, so it is at least somewhat similar to tree-shaking on the map itself.
Consider adding toInt
and toDouble
for num
receivers.
Ultimately what's the limit here? Does it make the compiler more complex ? Is it human time ? Is it performances ?
would this work:
const MyClass(List<int> list) : assert(checkBlackListed(list));
static double checkBlackListed(List<int> value) => !(const [1,2].contains(value));
Personally, it would have a positive impact if some exceptions could be caught before runtime (especially in tests where the values are often hardcoded), and I'd assume this type of feature is on the low budget side of implementation time compared to static metaprogramming, so I hope it can make it soon.
Here is another candidate enhancement: If r
is a constant expression whose value is a record with a getter g
(could be $1
or someName
), r.g
is a constant expression.
Cf. https://github.com/dart-lang/language/issues/1296#issuecomment-1109428611, https://github.com/dart-lang/language/issues/1296#issuecomment-1110075607. Issue #1296 contains several other comments that are relevant to this discussion, but the topic is separate from that of #1296, so here is an issue where the addition of primitive members is the main topic.
This topic covers a number of requests for specific enhancements of the expressive power of constant expressions. In particular, the ability to perform sanity checks (in an
assert
) on constructor arguments in a constant constructor has been requested many times.The language specification currently specifies what it means to have a primitive operator
==
. Essentially, this means that the implementation is guaranteed to be provided by the system, which again makes it possible to guarantee that the corresponding behavior can also be provided during constant expression evaluation at compile time.We could introduce the corresponding notion of being primitive for any member whose implementation is guaranteed to be system provided, thus extending the set of constant expressions with things like
myList.length
,mySet.isNotEmpty
, etc. Note that even a member likeSet.contains
could be handled, in spite of the fact that it is not a getter, because the semantics of an invocation is known and can be performed during constant expression evaluation.Here is a table showing the rather large number of cases where we could do this (based on a table in this comment by @lrhn):
length
,isEmpty
,isNotEmpty
Iterable
,Map
contains
,first
,last
,single
Iterable
operator []
List
containsKey
,containsValue
,operator []
Map
isNaN
,isFinite
,sign
,ceil
,floor
,round
,ceilToDouble
,floorToDouble
,roundToDouble
num
double
,int
substring
,charCodeAt
,contains
,startsWith
,endsWith
String
String
We would need a number of extra constraints, including:
Pattern
arguments toString
operations must be strings.contains
on iterables andcontainsValue
on maps, the argument must have primitive equality.Of course, we could do this to a tiny extent (e.g., just add
length
for lists), or we could do all of it, and we could do it incrementally over time.