Skip to content

Conversation

esdrubal
Copy link

@esdrubal esdrubal commented Mar 10, 2025

@esdrubal esdrubal self-assigned this Mar 10, 2025
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.

Copy link
Contributor

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 constness rules.

And an explanation of when const functions can be const-evaluated or can be downgraded to runtime behavior.

Copy link
Author

@esdrubal esdrubal Mar 11, 2025

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.

@esdrubal esdrubal force-pushed the esdrubal/const_fn branch 2 times, most recently from 60a5635 to 689698d Compare March 11, 2025 15:51
@esdrubal esdrubal force-pushed the esdrubal/const_fn branch from 689698d to 5eae2b2 Compare March 11, 2025 15:52
@xunilrj
Copy link
Contributor

xunilrj commented Mar 18, 2025

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

@xunilrj
Copy link
Contributor

xunilrj commented Mar 18, 2025

This allows const fns to use heap types internally but restricts types in the data sections to non heap types.

And what about strings? We would not be able to concatenate strings, for example.

const NAME: str = "Daniel";
const MSG: str = "Hello" + NAME;

Copy link

@jjcnn jjcnn left a 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 fns? That sort of thing.


const EQUAL: bool = eq(3u64, 4u64);
```

Copy link

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.

Comment on lines +195 to +197
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.

Copy link

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except for initialization.


- No access to storage
- Cannot use certain asm opcodes
- Error out when `const context` is assigned to a heap type.
Copy link

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Comment on lines +350 to +351
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.
Copy link

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.

Comment on lines +355 to +357
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.

Copy link

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.

@esdrubal
Copy link
Author

esdrubal commented Mar 25, 2025

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

Added those as entries in the const contexts section.

This allows const fns to use heap types internally but restricts types in the data sections to non heap types.

And what about strings? We would not be able to concatenate strings, for example.

const NAME: str = "Daniel";
const MSG: str = "Hello" + NAME;

@xunilrj maybe non heap types is not the correct term I want to use. Instead we want to restrict the types in the data section to types that are pointer and reference free.

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.

@IGI-111 IGI-111 assigned xunilrj and unassigned esdrubal Jun 19, 2025
Copy link
Member

@ironcev ironcev left a 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 consts 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::Constants 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 fns 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 in const fn
  • ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants