openlang

CTFE happens where the declaration asks for it

Had D's CTFE pictured wrong for a while. I assumed that once a function is fit for compile time evaluation, the compiler would just do it wherever the arguments are known. It does it only where a compile time value is required.

import std.stdio;

int sumSquares(int n)
{
    if (!__ctfe)
        writeln("sumSquares ran at run time");
    int total = 0;
    foreach (i; 0 .. n)
        total += i * i;
    return total;
}

enum a = sumSquares(4);       // interpreted by the compiler
immutable b = sumSquares(4);  // module scope, also compile time

void main()
{
    auto c = sumSquares(4);   // ordinary call, prints the line
    writeln(a, " ", b, " ", c);
}

This prints the run time line once, from c, then 14 14 14. The enum and the module level immutable both need their value during compilation, so sumSquares is interpreted there and __ctfe is true. The auto in main is an ordinary call, even though the same function with the same argument was just interpreted twice.

Now replace that auto with an enum during some tidy-up. Nothing at the call changes except one keyword, and the function has quietly taken on a new requirement, that the interpreter can run all of it. Is the declaration keyword really the only marker a reader gets?

The conversation continues 4

Follow the reasoning
  1. Permalink

    So the word that asks sits on the variable, not on the function. Then immutable d = sumSquares(4); inside main goes through CTFE as well, I take it. A value that can never change has nothing left to wait for, so the compiler might as well fill it in.

  2. Permalink

    No. immutable says the value never changes. It says nothing about when it is computed. A local immutable is initialized when main runs, and the run time line prints twice. Inside a function, write enum or static immutable. A static local needs a compile time initializer, so that one goes through CTFE.

  3. Permalink

    Fun way to watch the requirement appear. Make sumSquares add rand() % 2 from core.stdc.stdlib and compile again. Both the enum and the module level immutable now fail, because the interpreter can't run a C function it has no source for. The auto line in main keeps compiling and just calls it.

    So to your question, yes, the keyword is the marker. It also works the other way round. An enum is a cheap check that a function is still interpretable, and the compiler complains the moment someone breaks that.

  4. Permalink

    Thanks, that straightens out my reading. The real case was a lookup table, and it went in as static immutable rather than enum. An enum of array type is pasted in as a literal wherever it is named, so each use builds a fresh array at run time, while the static immutable table exists once in the binary.

Other sides of the same question

  1. Why would this CPS sum overflow only in Debug?

    Could someone check my reading of this? A continuation-passing sum, written as an exercise. dotnet run -c Release prints 1000…

    Replies 5fsharp
  2. Same Int, two targets, two answers

    Found this while running the same Gleam module on both targets, and it's a runtime fact more than a language one. gleam run p…

    Replies 3gleam
  3. Braces around expr decide what the bytecode compiler sees

    Unbraced expr is the one Tcl habit I'd ban if I could! It looks like a style nit and it isn't. The braced line prints 0. The…

    Replies 16tcl