-
Notifications
You must be signed in to change notification settings - Fork 12
RFC for const fn. #44
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
base: master
Are you sure you want to change the base?
Conversation
Parsing needs to be changed so keyword `const` or `constexpr` can be used before | ||
all the places the keyword `fn` is used. | ||
This includes `fn`s in function, trait and impls. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is missing a section on how to enforce const
ness rules.
And an explanation of when const
functions can be const-evaluated or can be downgraded to runtime behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added section for when a const fn
is const-evaluated or downgraded to runtime behavior. And another section for constness enforcement rules.
60a5635
to
689698d
Compare
689698d
to
5eae2b2
Compare
Another "const context" is array size/string array size: let a = [0u8; N + 1]; // N + 1 must be a const
let a: str[N + 1] = "abc"; // N + 1 must be a const |
And what about strings? We would not be able to concatenate strings, for example.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally this is perfectly fine when it comes to the semantics of const fn
, but I wouldn't mind seeing a bit more focus on the UX. What is the user presented with if something goes wrong during compile-time evaulation of a const fn
? What constitutes unreachable code for const fn
s? That sort of thing.
|
||
const EQUAL: bool = eq(3u64, 4u64); | ||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As @xunilrj mentions there should be a description and an example of how const generics and const fn
work together.
When a `const fn` is called in a regular function and the `const fn` parameters are not | ||
known at compile time then the `const fn` also needs to be called at compile time. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned about the UX in this scenario.
It's clear that the user needs to be able to call a function at runtime even if it is declared const fn
. However, should we have some sort of warning when this happens, so that the user is aware that there is a runtime cost involved in the call?
Perhaps we could warn if a const fn
is never called in a constant context, and thus is always downgraded? Can we always warn in that situation?
|
||
To ensure that `const fn`s maintain predictable compile-time behavior, we enforce a set of constness rules that restrict what can and cannot be done inside a `const fn`. These rules prevent operations that rely on runtime state while allowing deterministic computation at compile time. | ||
|
||
- No access to storage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except for initialization.
rfcs/0016-const-fn.md
Outdated
|
||
- No access to storage | ||
- Cannot use certain asm opcodes | ||
- Error out when `const context` is assigned to a heap type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unclear to me what "Error out" means in this situation.
Does it result in a compilation error when this happens, or it the error supposed to be caught and handled by the compiler so that it can downgrade the call that resulted in the erroneous bytecode.
Support of `ref mut` in `const fn` is a requirement for properly | ||
handling heap type such as `Vec<T>` and function such as `fn push(ref mut self, value: T)`. | ||
|
||
A `const fn` with a `ref mut` parameters is callable inside other regular and `const fn`s but |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A `const fn` with a `ref mut` parameters is callable inside other regular and `const fn`s but | |
A `const fn` with a `ref mut` parameter is callable inside other regular and `const fn`s but |
A `const fn` with a `ref mut` parameters is callable inside other regular and `const fn`s but | ||
cannot be used directly in other `const contexts` such as constant items. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This suggests that there is a second type of const context
, something like mutable const context
or something. I think it would be helpful to spell out when that context applies and when it doesn't, and to give some examples of how it would work.
When a `const fn`is not called at runtime the final binary outputted by the compiler | ||
won't have any opcodes related to the computation of `const fn`s. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably this is handled by the dead code elimination phase? The UX for this scenario needs to go hand-in-hand with any warnings or whatever when a const fn
is never called in a const context.
Added those as entries in the const contexts section.
@xunilrj maybe I updated the RFC to reflect this. The given const string would be compiled into the intermediary const evaluation binary main function too: fn main() {
let NAME: str = "Daniel";
let MSG: str = "Hello" + NAME;
__log(raw_slice::from_parts::<u8>(__addr_of(&NAME), __size_of_val(NAME))); // Outputs const NAME value
__log(raw_slice::from_parts::<u8>(__addr_of(&MSG), __size_of_val(MSG))); // Outputs const MSG value
} Then the result of the logs can be stored in the binary data section of the final binary. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This review contains my major concerns regarding the proposed VM-based evaluation and writing the resulting content to the data section. The major concern is writing to the data section and skipping the IR representation of the constants.
No matter which eval-execution approach we take (VM, internal interpretation of AST, internal interpretation of IR), IMO, the first three remarks below are hard requirements for const fn
and Sway's const-eval in general.
const fn
/eval must be an integral part of the IR optimization pipeline
Writing the VM output to the data section (directly, or compacted or however, but to the data section, skipping the IR) is exactly what we don't want to have. We want to have the eval results as IR constants (ValueDatum::Constant
), to be able to use them in various IR optimizations, as it is the case now.
E.g., many evals will result in types like u64
. We ideally don't want to have those types in the data section. We want existing optimizations to put them into immediates or promote them to registers, etc.
Moreover, the RFC proposes that "When a const fn
is called with the same arguments in different places a unique data section const entry should be used." This is, again, exactly what we don't want to have. We want to support const deduplication as we have it now.
E.g., here is a more complex example. The real power of the const fn
feature is not in having more freedom in expressing declaring const
s but rather in eliminating runtime expressions in general.
It must be possible to do const evaluation several times in the IR pipeline, and not only once in the __const_entry
.
E.g., in this example:
fn some_non_const_fn(a: u8) {
// ... Some code. ...
let x = some_other_non_const_fn(a, 111);
// .... More code. ...
}
fn some_other_non_const_fn(x: u8, y: u8) -> u8 {
if some_const_fn(22, 33) > 100 {
// A lot of code.
} else {
some_other_const_fn(y)
}
}
Const-evaling some_const_fn(22, 33)
to something less then 100
will result in eliminating // A lot of code
branch, leaving only some_other_const_fn(y)
, which makes some_other_non_const_fn
eligible for inlining, which exposes it to the caller context where the y
argument is actually a constant 111
, which again allows us to const eval the inlined some_other_const_fn(111)
call, effectively completely eliminating the some_other_non_const_fn(a, 111);
call and replacing it with a constant. Which again can enable more optimizations.
How we structure the IR opt passes to get the best out of const fn eval with acceptable compile time effort is a separate topic. But the major requirement, that const eval results in a ValueDatum::Constant
that can be used in IR optimizations is, IMO, a hard requirement.
In short, if we just write the VM results to the data section, bypassing the IR and IR optimizations, we will end up with the const-eval mechanism that hinders even the current optimizations, instead of enabling powerful new ones.
const fn
must be compatible with storage configuration
Even if we accept the negative impact of writing VM results to the data section on the IR optimizations, as described above, that proposed approach will not work with storage configurations.
Storage configurations (RHS of the =
operator in the storage field declaration) are not compiled to bytecode at all, not to data section or anywhere else. Instead, they are used by the compiler only at compile time to generate slot JSON files. Slot JSON files are generated based on the const-evaled ValueDatum::Constant
s that are then assigned to storage slots by the compiler.
In other words, whatever we get as a result of const-evaling the storage configuration expression (RHS), from the VM or whichever evaluation approach we use, this result must be available to the compiler as ValueDatum::Constant
, to be able to generate slot JSON out of it.
Note that in the Storage 3.0, both the exact storage slots and the content will be const-evaluated from user defined code (currently it is only the configuration, RHS). Which means both must be available to the compiler and will never end up in the resulting bytecode. They will both be a temporary compile-time artifact.
Support for slices
Storage 3.0 was one of the major drivers for const fn
and there we will need support for const fn
evaluated slices. More specific, the ability to concatenate slices within a const-eval and to return them. The current proposal allows manipulation of heap types during the const-eval execution in VM, but not returning the heap types (types containing pointers and references). While we can have this restriction in general, we will need to support returning slices.
Although, even with the current proposal of VM-based execution, I don't see why continuous parts of memory couldn't be returned (and stored as ValueDatum::Constant
as argued above).
Error handling
My conviction was, that with const fn
we will get a better error handling around const evals. Currently, we just display "Could not evaluate initializer to a const declaration." without any hint what exactly in the chain, and where, went wrong.
I expected to have a backtracking in the const-evaluation that will show the compilation path that lead to the exact point where the error occurred.
If we outsource the evaluation to the VM, we can, after the VM reverts for whatever reason, know which of the __const_evaluated
calls has failed and which constant cannot be evaluated. But the only error we will be able to show will be the same as now - something went wrong and it's up to you to guess what. Which, in case of intensive use of const fn
s which we expect, can be very tricky.
Note that, because any of the const evaluation in the __const_entry
can fail and we need to know which one did, the actual structure of the __const_entry
will need to be:
fn __const_entry() {
let A = add(1,2);
__const_evaluated(0, __addr_of(A));
let B = sub(A, 2);
__const_evaluated(1, __addr_of(B));
}
Here is one personal experience, an infinite loop caused by not increasing the loop counter. It was difficult to figure it out even when having access to VM output. If it is somewhere in the const-eval chain, nailing it down would be mission impossible.
Complexity, performance, and tight coupling to VM compared to alternative
These are all obvious concerns.
The complexity of generating the __const_entry
and the overall mechanics of execution and callbacks. The transformations of the original tree after the __const_entry
is executed is also not trivial.
IMO we need to elaborate more on performance impact. Can we have the benchmark program attached as a part of this RFC in files
? Can we have a general PoC that will measure the overall performance impact and not only the one of starting the VM? E.g., we need to compile down the __const_entry
, get it run, get the data back via callbacks, etc.
Because of all this, I would love to see the alternative method of execution, inside of the compiler as we have it now, elaborated in more detail, even if we go with VM-based in the end.
In particular, what are the disadvantages of that approach and how relevant they are, e.g.,:
- no ASM
- encoding VM specific behavior for e.g. overflows
- ???
What are the advantages?
- performance (?)
- state of the art error handling
- simplicity of implementation
What is actually missing for e.g. Storage 3.0 which we see as the most complex case of using const fns?
- a lot of expressions already const-evals, we just need to formalize the
const fn
syntax, - support for slices (can be done with dedicated intrinsics, similar to ABI encoding).
- if
Vec
are based on slices they can also participate inconst fn
- ...
Rendered