Why can't rustc separately compile const functions without restrictions?

Why can't rustc separately compile const functions without restrictions in function body?

I thought all the examples below would be compiled. I thought there were requirements for the function arguments and the return type to implement Sized and Copy, in the first example UB is called, but not in the second and third. But it doesn't compile, it's not available in rust.

from stackoverflow question:

While technically possible, the Rust team decided against it. The primary reason being semantic versioning issues. Changing the body of a function, without changing the signature, could make that function no longer const-compatible and break uses in a const-context. const fn is a contract, and breaking that contract is an explicit breaking change.

Why did the rust team refuse this? Are there any links to this discussion and decision?

I have a view for the third example that rustc can separately, as a library, compile the do_something function, call it separately in some rust-sandbox and substitute the result into a constant SOMETHING.

It seems to me that marking all const is terrible and unnecessary. It is necessary to simply reduce the restrictions that const imposes.

Example 1:

const fn do_something(count: isize) -> isize {
    struct Something {
        x: isize,
        y: isize,
    }

    let mut something = Box::new(Something { x: 1, y: 1 });

    for i in 0..count {
        something.x += i;
        something.y -= i;
    }

    let Something { x, y } = *something;
    x * x + y * y
}

const SOMETHING: isize = do_something(10);

fn main() {
    println!("{SOMETHING}"); // SAFETY: OK
}

Example 2:

#[derive(Clone, Copy)]
pub struct Foo {
    ptr: usize,
}

impl Foo {
    fn new() -> Self {
        let data = Box::leak(Box::new(1i32)) as *mut i32;
        Self { ptr: data as usize }
    }

    /// do action
    ///
    /// # Safety
    ///
    /// The caller must ensure that the context in which Foo was created was non-const (runtime)
    fn do_action(self) {
        // SAFETY: ptr is leak from Box<i32> from runtime
        let data = unsafe { Box::from_raw(self.ptr as *mut i32) };
        println!("{}", data);
    }
}

const fn do_something() -> Foo {
    Foo::new() 
}

const FOO: Foo = do_something();

fn main() {
    let foo = Foo::new();
    foo.do_action(); // SAFETY: OK
    FOO.do_action(); // NOT SAFETY: UB
}

Example 3:

const fn do_something(count: isize) -> isize {
    struct Something {
        x: isize,
        y: isize,
    }

    let mut something = Box::new(Something { x: 1, y: 1 });

    for i in 0..count {
        something.x += i;
        something.y -= i;
    }

    let Something { x, y } = *something;
    x * x + y * y
}

const SOMETHING: isize = do_something(10);

fn main() {
    println!("{SOMETHING}"); // SAFETY: OK
}

This would not work when cross-compiling unless a virtual machine for the target was available, because const code executes with all the memory layout and cfg appropriate to the target platform, not the host platform (that is running the compiler).

But in the current rust, const funcs are compiled and executed, don't they have cross-compilng available? Then how do const functions work now if they are executed on the host platform? I don't want to make const functions platform dependent, but it might be better to tie it to cfg in some cases.

They are executed by a dedicated const interpreter that works on Rust MIR code rather than machine code, which is roughly the same thing as Miri. In general, const evaluation restrictions are one of:

  • The operation can't be handled by the interpreter, perhaps because it depends on the actual processor or OS (e.g. asm blocks or arbitrary system calls)
  • The operation has effects that would have to happen at run time, and no workaround is available (e.g. heap allocation)
  • There are semantic problems that a decision hasn't been made on yet (e.g. floating point computations)
4 Likes

Why isn't rustc used directly? Is it because of problems with the non-determinism of certain operations? Why doesn't the rust community want to make the const functions of the platform dependent, in some parts, such as the file system, if it were available?

It looks as if the rust community wants to create its own intrepretor for const functions so that all the code is executed in it, so that its result is the same everywhere. But it seems to me that this approach can kill some extreme optimizations that could be developed in const functions

rustc without the const interpreter can only compile code, not run it. const evaluation needs to be able to run code that isn't, and possibly can’t be, compiled for the host, so that it is consistent with what will happen if and when the same code is run on the target in the compiled program.

Const code can be platform dependent, in the sense that it can have parameters that are different depending on the target, e.g. the size_of() some type, or what cfgs are set.

But taking input from the environment is different β€” it's important for const generics, where const values enter the type system, that const code can be deterministic, in the sense that the compiler can run it twice and get the same answer both times. So, const code must not be able to perform arbitrary IO.

So I understand correctly that these are all restrictions, because the rust community wants the code to always be executed in the same way and almost like on the target platform?

But why is IO bad? If the same project files are input, the compilation result should be the same.

When I said to give rustc directly, I meant that it not only compiles, but also runs code use something rust sandbox on tier 1 platform based on host platform. In cargo, we can specify which compilation platform is used, or specify through cfg.

Can you tell me what I'm thinking wrong about and why this approach is bad? As I understand it, if it was correct, it would have been implemented in rust a long time ago, thanks

Guaranteeing reproducible builds is a big deal. Google it if you haven't heard about this. If IO can be performed by const code during compilation, the compiler can't guarantee reproducible builds -- this would have to be guaranteed by the person implementing the const code and the person running the build scripts, and the person maintaining the build environment, which is not a guarantee at all.

1 Like

IO that is tracked to ensure that is possible today. This is valid code:

pub struct Foo {
    const fn new(s: &str) -> Self {...}
}

const Foo FOO = Foo::from_str(include_str!("input.txt"));

But if the compiler could not notice that the const evaluation is dependent on "input.txt", there would be trouble. So, arbitrary IO code must not be run.

When I said to give rustc directly, I meant that it not only compiles, but also runs code use something rust sandbox on tier 1 platform based on host platform. In cargo, we can specify which compilation platform is used, or specify through cfg.

Then const fns would behave differently depending on the host platform and whether they are being run as const-eval or at run time. If build-time code execution is what you want, then you can do that today with a build script or proc-macro. The point of const fns is that they do the same thing when called at run-time or compile-time, seamlessly (with some caveats; the produced value is not guaranteed to be identical).

1 Like

I agree with you. If I understand correctly, now it is implemented through build.rs and macros, wouldn't it be better if it was explicitly through the const function? Const keyword tells about the context where the function is executed and what access will have - compile time (host) or runtime (target).

1 Like

Are you proposing that const functions should be allowed to do IO, but in some sort of sandbox?

Can const fns execute as runtime functions? I thought that const fns is a complete analogue of consteval from c++.

What is the main benefit of limiting const fns and using build.rs and marcos instead. I'm just considering the possibility of language and its development. I'm not looking for a way to solve any problem now through const fns.

Yes.

My vision is that const fns are host platform functions that allow all available rust code, compile it and run it on the host platform, and place the result in a conditional storage to compile the main code for the target platform.

But as I already understood, the vision of developers and the rust community is that const fns are functions that are executed in compile time as if in runtime on the target platform.

It's just that if you perceive my idea, the restrictions practically disappear for const fns. There are simple restrictions on moving values from compile time to runtime and their lifetime.

const fns are functions that can be executed at compile time or at run time and produce equivalent results in either case. The use cases of const fns are largely to do things that are usually done at run time, but sometimes must be done at compile time to produce already-initialized data.

The original RFC for const fn laid out a specific motivation: adding the ability to initialize data structures in static items without making all of their fields public. Adding the const fn mechanism allowed the tidy solution of marking the relevant constructor functions const fns, so that they can be executed at compile time in addition to run time. If const fns were only usable at compile time, then using them for this purpose would require duplicate functions and two flavors of code.

2 Likes

This is basically a proc macro if you accept the result to be tokens representing the actual output.

2 Likes

I meant that the const fns body is not optimized and will run in the runtime as is (analogue to constexpr)? Or will it always be first called in compile time, and the result is placed at the call point in the runtime in a binary file (similar to consteval)?

They can also be executed on the target. So the goal is somewhat different that you assumed.

https://doc.rust-lang.org/reference/const_eval.html#const-functions

A const fn is a function that one is permitted to call from a const context. Declaring a function const has no effect on any existing uses, it only restricts the types that arguments and the return type may use, as well as prevent various expressions from being used within it. You can freely do anything with a const function that you can do with a regular function.

When called from a const context, the function is interpreted by the compiler at compile time. The interpretation happens in the environment of the compilation target and not the host. So usize is 32 bits if you are compiling against a 32 bit system, irrelevant of whether you are building on a 64 bit or a 32 bit system.

I'm sorry, I think I mixed up the terms.

Target platform is a platform where the code is compiled;

Host plaform is a platform where the code is executed.

Did I understand correctly?

That's backwards.