-
Notifications
You must be signed in to change notification settings - Fork 213
const parameters / type parameters #2776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Small note for an implementation: The FFI requires to know all constants that are flowing in to a constant parameter (error value for FFI callbacks) and constant type parameter (C types for a FFI calls and callbacks). Currently, the implementation (FFI transform) does a sort of inlining of the bodies so that the the constants are immediately in place. And the current implementation only inlines one call deep (and prohibits the rest). This feature will enable transitive const values. (As Slava mentions to enable users of dart:ffi to build abstractions around As an alternative implementation strategy we could consider doing some const-flow-analysis that propagates possible tuples of const values and const types that flow into such functions. (Doing it on a per parameter and per type parameter fashion will lose too much information: for FFI callbacks we need to know the const error-return-value together with the const type argument for the C signature.) [Though, maybe we should implement by inlining first, and optimize later.] cc @johnniwinther for CFE, @alexmarkov for TFA-like design, and @sstrickl for const-propagation logic. |
@dcharkes inlining does not work as general implementation strategy because invocations might be recursive: void foo(int level, const int token) {
if (level != 0) {
foo(level - 1, token);
}
} So we would need some form of TFA support to propagate this information across call-graph edges. |
Just wondering, how would the ability to know that a particular In general, it sounds like you want to perform partial evaluation of invocations of specific methods/functions, because the Dart language abstracts over distinctions that FFI needs to treat explicitly (for instance, FFI needs to generate code separately for each actual value of a given type argument, but Dart is happy to cover all those actual arguments by a single declaration of a type parameter whose actual value isn't restricted by anything other than the bound). But you do actually need to know the actual value arguments at compile time, not just the actual type arguments, in terms of their precise identity? |
I was just wondering the same. If we start allowing that, we could do: List<Pointer> foo(const int level) {
return [
Pointer.fromFunction<SimpleAdditionType>(simpleAddition, /*errorValue=*/level)),
if (level != 0)
... foo(level - 1),
];
}
typedef SimpleAdditionType = Int32 Function(Int32, Int32);
int simpleAddition(int x, int y) {
print("simpleAddition($x, $y)");
return x + y;
} Which would require us to compile many FFI callback trampolines, for every possible value of
Yes, the error values are compiled into the code. Even if we would store the error value somewhere else, we would still need a different callback-id and different entry-point in memory to distinguish the callbacks from native. |
Imposed restrictions help to perform global propagation of these values and establish which values (or which combination of values) can flow through to a particular interesting call-site. Knowing possible inputs is good enough for the described purposes.
I am not sure I understand the question - this proposal allows you to know the set of possible constant values that flow into a specific function. |
Discussed this with @mraleph IRL, I think I've got a better understanding of the needs now. So here's a possible interpretation of the feature. My starting point is that it is intended to establish a guarantee that a specific kind of partial evaluation can be performed: Every method class A {
void m(const int i, int j) => j > 0 ? m(i, j - 1) : n(i);
void n(const int i) { print(i); }
}
void main() {
A().m(1, 10);
A().m(2, -1);
}
// Partial evaluation of the above program yields the following:
class A {
void m$$1(int j) => j > 0 ? m$$1(j - 1) : n$$1(i);
void m$$2(int j) => j > 0 ? m$$2(j - 1) : n$$2(i);
void n$$1() { print(1); }
void n$$2() { print(2); }
}
void main() {
A().m$$1(10);
A().m$$2(-1);
} It is possible, and it may be useful (in order to save space), to avoid creating specialized variants of all the functions/methods all the way down to the call site, but there would then be a need to create some kind of dispatcher when A method accepting a An actual argument which is passed to a formal parameter I don't think we need to generalize this such that (For instance, we could call an unspecialized variant of the function/method, and it would have parameter type Types have a similar distinction: Some terms derived from Considering an invocation of a generic function or method This sounds like we might have a space explosion, but it also sounds like approximately what's needed. ;-) |
This is exactly what I tried to say earlier.
Exactly. :-) In order to make the language spec more precise, we would need to give examples with the combinations of const type params and const parameters instead of just a single parameter. Because if we lose the relation between possible values of the different parameters the dispatch becomes multiplicative. class A {
List<T> n<const T>(const int i, const int j) { print(i+j); return <T>[]; }
}
void main() {
A().n<int>(10, 20);
A().n<double>(11, 22);
}
// Partial evaluation of the above program yields the following:
class A {
// Only the 2 combinations of const type parameters and parameters.
List<int> n$$1() { print(30); }
List<double> n$$2() { print(33); }
// And not 8 combinations with [int,double] x [10,11] x [20,22].
}
void main() {
A().m$$1();
A().m$$2();
}
I believe we're on the same page. 👍 |
Right, we need to consider distinct tuples containing every |
That sounds like the definition of "potentially constant", except that we allow potentially constant expressions to do compile-time-computable computations. Should this do the same? That is, if we invoke a function with a constant argument, and it invokes another function with a constant argument, should that argument be allowed to be any potentially constant expression which refers only to constant parameters? That suggests that we're going to do compile-time evaluation of the arguments, but we need to do that anyway to ensure they are constant, so it's probably about the same. But if we do that, then we can also allow A larger issue is that potentially constant expressions work both with constants and with non-constants. The latter case just cannot create new constants. That is, the Requiring a value to be constant is at odds with how Dart treats constant values - they are just like normal values, except that they can be created eagerly and are canonicalized. After that, there doesn't need to be any way to distinguish Anyway, the proposal itself: Are parameters and type parameters the only place this would apply? Those are typical API boundaries, so it makes sense that this is where you'd put a restriction that apply to other people (if you want only constants yourself, you can just do it). Would we want to apply it to sub-components of a larger value? It's not easy to make usable, since extracting the value is not going to be a constant expression, but with patterns and the inevitable pattern parameters, we might want to allow something like: class C {
final int x, y;
C(const this.x, this.y);
C.fromPair((const this.x, this.y));
} The It'll get harder to track which values flow where if they can go through other objects, but records are really close to just being juxtapositions of individual values, not values of their own, so it might just work. The subtyping suggests that a typedef F2<R, T> = R Function(T);
....
F2<int, const int> f = ...; Is that a problem? So far, every time we've had a "type" that we couldn't abstract over, we've ended up allowing it. (Well, we did it for |
TL;DR: @lrhn has a good point. We should probably explore Dart macros and see what the user experience for this use case is. Detailed reply:
That would not solve the
In the
Wouldn't the result values be also automatically be canonicalized, isn't that observable? And maybe code size (canonicalized values), and performance (compile-time evaluation vs runtime evaluation) is important. (Otherwise, we'd drop
Aren't partial evaluation and macro evaluation different? Partial evaluation requires that the source code has the same semantics as the partially evaluated code, macros are more free-form. Our macro-proposals can add things to the outline, and create expressions and statements with arbitrary semantics. On the flip side, maybe macros are harder to reason about than partial evaluation.
Agreed, we should try and see if any of our macro prototype implementations would help here.
We could generalize the logic we have now with those type of annotations, but if we would want to enable users to build abstractions, they should be able to use the annotations as well. So that feature will likely leak into code directly involved with |
That's a philosophical rathole. 😁 I posit that you can't generally, in plain Dart, write code that takes an instance of a known class with a There is nothing in the object itself which distinguishes a What you want is not for the run-time values to be "constants", you want the expression values to be available at compile-time, because you are doing things to them at compile-time that you cannot do at run-time. Partial evaluation is basically what Dart constant evaluation does today. |
@lrhn I disagree, actually it is quite consistent. It makes sense. Parameter lists can be interpreted as specialized variable declarations. As a consequence, Widget myConstCanonicalizedWidget(const String text) => const MyWidget(..., child: Text(text), ...); Calling the above would partially evaluate the body at compile time. Doing so would create independent widget constants for each invocation's constant shape. Furthermore, by introducing a const Widget myConstCanonicalizedWidget(const String text) => const ...(..., child: Text(text), ...);
const helloWidget = myConstCanonicalizedWidget('Hello, World!'); As a quick aside, it could also lead to intuitive metaprogramming. Described here, to avoid gunking up this issue: https://gist.github.com/ds84182/12e12cbf69f2627b462a1308600f9da1 Something like FFI could be implemented on top of this, instead of having to integrate deeply with CFE. But it is complicated, so it is definitely a tool meant to be used by package authors. |
I don't think the analogy works. A parameter list declares local variables that are initialized at runtime. It's not the same thing - a parameter list declares both a parameter (which becomes part of the function type) and a local variable of the same name (which becomes available in the body scope). The caller doesn't know whether a parameter variable is Introducing So the local variable is not a But potentially constant expressions only work for constructors because we can distinguish We don't have If we do that, basically making every invocation that requires a But that's not what's described here. I'm not sure what's described here actually makes sense as a general feature, it only works for things where the compiler can interpret the arguments to special compiler-recognized functions.
How, if I do: Widget myApply(bool choose, Widget Function(const String) factory, const String trueText, const String falseText) =>
choose ? factory(trueText) : factory(falseText); Notice that the And what does it even mean to partially evaluate a function call that is not entirely constant. Will you evaluate both branches here? What will you actually do inside the body of Partial evaluation is something you usually do bottom-up. When all the leaves of an expression are constants, and the operation can be performed at compile-time, then do the operation at compile-time. |
@lrhn Some time ago I had some implementation problems because I couldn't declare a parameter as extension EnvironmentString on String {
// Get environment variable, if not set throw `EnvironmentVariableError`.
static String fromEnvironmentOrThrow(/*const*/ String name) => bool.hasEnvironment(name)
? const String.fromEnvironment(name) // compile error here, name must be const
: throw EnvironmentVariableError('Environment variable $name not set.');
} Note:
Would this be a good example for this issue? It does not have nothing to do with FFI, since it's a user usecase. Another solution was to implement a |
Yes, this is a good example of a function which can only work if the function itself can be invoked as part of constant evaluation. If it isn't then If we allowed such a feature, then the compiler could inline the function at each call point. |
@lrhn Partial evaluation is not supported. Lambdas can act as an escape hatch for partial evaluation, but it has to be opted into. In the An example of partial evaluation would be Anyways, this is an entirely separate feature. But it is an extension of const parameters as it relies on it. Const parameters create specialized versions of a function with those parameters inlined. Like a more limited form of templating in C++, or generics in Rust. It introduces the concept of const (type) parameters to the language. Const functions go a step further, utilizing the same precedent to allow for less code duplication. With the As for another example where const type parameters are useful, it allows writing generic code over typed data without sacrificing performance. Here's a strawman example: // Hypothetically, lets say typedef TypedList<T> = List<T> + TypedData;
int sum<const T: TypedList<int>>>(T typedData) {
var acc = 0;
for (var x in typedData) acc += x;
return acc;
}
sum<Uint8List>(...) // Calls specialized version of sum for Uint8List
sum<Int64List>(...) // Calls specialized version of sum for Int64List |
Background
dart:ffi
has a number of APIs which require arguments or type arguments to be compile time constants to facilitate static analysis and tree-shaking. For example,funcPtr.asFunction<DF>()
requiresDF
to be a compile time constant, so that compiler could generate appropriate specialised trampoline which handles Dart->C calls. Similar restriction (for the very same reason) applies to the dual APIPointer.fromFunction(dartFunc)
, wheredartFunc
is expected a compile-time constant (static function tear-off).Currently these restrictions are enforced using custom
dart:ffi
specific CFE/analyzer passes. These passes have two problems:dart:ffi
specific and can't be reused when a similar need arises in some other API. One example of an API which could benefit from this isPluginUtilities.getCallbackHandle
indart:ui
(see also PluginUtilities.getCallbackHandle and tree-shaking flutter/flutter#118608)We have previously experimented with building a feature like this in the linter: see internal
@mustBeConst
proposal.Proposal
Allow
const
modifier on parameter and type parameter declarations:Adding
const
to a parameter or type parameter declaration introduces the following restrictions:const
on it must have either a constant argument value or an argument which refers to anotherconst
parameter. Other arguments are invalid.const
on it must have either a constant fully instantiated type argument value or refer to another const type parameter. Other type arguments are invalid.A<int>
is a valid instantiation ofA<const T>
and so isA<T>
ifT
is a const-type-parameter, butA<List<T>>
is incorrect.const
on parameters and type parameters in an obvious way: given function typesA = R Function(const B)
andB = R Function(B)
B
is a subtype ofA
, but not the other way around. Consequently overriding a function might remove requirement for parameter to be constant, but can't add it.dynamic
calls to function which containsconst
parameters orconst
type parameters will result in an exception./cc @lrhn @leafpetersen @eernstg @munificent @dcharkes @mkustermann
The text was updated successfully, but these errors were encountered: