zig icon indicating copy to clipboard operation
zig copied to clipboard

PROPOSAL: introduce a `final` keyword to differenciate between comptime constants and runtime "constants"

Open Lking03x opened this issue 1 year ago • 4 comments

Problems With The Current Behavior

Zig code mix comptime and runtime constructs but it's often hard to distinguish between them but with zig's emphasis on comptime constructs and manipulation it, I think, a necessity to ease this process.

const N = @import(...);
const T = @import(...).T;

const c1 = magicNumber();
var a: u8 = 120;

pub fn main() void {
	const b = getCurrentTime(); // ???
	const c2 = buildNo(); // ???
	N.doStuff(b);
	N.doStuff(c2);
	...
}

The final Proposal

const N = @import(...); // a namespace
const T = @import(...).T; // a type

const b = magicNumber(); // 'static' constant (comptime known)
var a: u8 = 120; // 'static' variable

pub fn main() void {
	final b1 = getCurrentTime();
	const b2 = buildNo(); // 'static' constant (comptime known)
	N.doStuff(b2);
	N.doStuff(b1);
	...
}
  1. All comptime known/only constructs should use const or comptime var and runtime known/only ones use var or final
  2. Mixed expressions result are resolved accordingly
    • comptime and comptime = comptime => const or comptime var
    • comptime and runtime = runtime => var or final
    • runtime and runtime = runtime => var or final
  3. Container level declarations cannot be final
  4. const also always means 'static'
fn foo(i: u32) void {
	const n = 1234;
	doStuff(n, i);
}

will always be like doing

const n = 1234;
fn foo(i: u32) void {
	doStuff(n, i);
}
  1. Runtime constants cannot be final
    • const c = expr; <=> const c = comptime expr;
    • final b = comptime expr; will be illegal
    • var a = comptime expr; remains legal

Lking03x avatar Aug 02 '24 21:08 Lking03x

what issue is this solving?

Zig code mix comptime and runtime constructs but it's often hard to distinguish between them but with zig's emphasis on comptime constructs and manipulation it,

I think, a necessity to ease this process.

can you provide more justification for this?

nektro avatar Aug 03 '24 00:08 nektro

I think the biggest issue with this proposal would be in generic code: final x = T.x*2; If T.x can refer to a variable for some T, we need to store it in a final. But in the cases that T.x refers to a constant, it's valid to use it at comptime - with this proposal we need to store it in a const to do that (probably under a second name other than x to avoid the name clash). So we would need to duplicate all code to get to the respective value (or move it into a separate function and call that function with and without comptime).

Additionally declarations often depend on others. If a -> b -> c -> d -> e, and we need e at comptime, all of them need to be const. But if a is runtime-known in one instance, they all need to be final. If both occur, we again have to duplicate/split our code.

I'm also not sure what particular goals you would want to achieve with this proposal. Personally I would probably switch my default in function scopes from const to final, and only use const where necessary. That means const now means "must be comptime", but final still means "could be comptime". I'm not sure how often / when that would be useful.

rohlem avatar Aug 03 '24 08:08 rohlem

Thanks for the reply.

I think the biggest issue with this proposal would be in generic code...

  1. The first remark is unfortunately valid. However these problems are actual. This proposal will only make some things resurface.

Additionally declarations often depend on others. If a -> b -> c -> d -> e, and we need e at comptime, all of them need to be const. But if a is runtime-known in one instance, they all need to be final.

  1. The second remark ("Additionally declarations often depend on others..."): these dependencies won't cause a problem as zig's is already doing it that way. Currently const means either "compile time know constant" or "runtime time know constant", never both at the same time. (except maybe for generic codes using "anytype" or "comptime T: type") As stated before a final identifier is only legal in function scope, and never as a container level declaration. This is semantically the current behavior in zig, as a var initial value expression can only be comptime
    • "If a -> b -> c -> d -> e, and we need e at comptime, all of them need to be const."

      1. At the initialization of a var, at function scope, we don't care about the comptimeness of the initial value:
        • In containers : var a = expr; is already equivalent to var a = comptime expr;
        • In functions : both are legal For instance the following code:
        	// Example 1.3
        	const std = @import("std");
        	var a : u64 = 12345;
        	var b = a + 777; // ERROR!
        	pub fn main() !void {
        	    std.debug.print("{d}\n", .{b});
        	}
        
        fails with the error: error: unable to evaluate comptime expression var b = a + 777;, note: operation is runtime due to this operand a But at function scope a var can be initialized by a runtime expression
        fn foo() void {
        	final now = getCurrentTime(); // runtime
        	var future = now + 1000; // runtime
        
        	const bestTimeEver = getBestTimeEver(); // comptime
        	var betterFuture = bestTimeEver + 0; // runtime
        	// it could have been // var betterFuture = getBestTimeEver(); // comptime
        	doTimeTravel(betterFuture); 
        }
        
      2. If a -> b -> c -> d -> e, and we need e at comptime, all of them need to be const. Consider the example

    	const a = b + 1; // `a` can be `var`
    	const b = c;
    	const d = e;
    	const e = comptimeOnlyStuff();
    

Lking03x avatar Aug 03 '24 19:08 Lking03x

There's a common misconception that "comptime" has something to do with constness, when they are completely orthogonal concepts. You can have runtime const or var, and you can have comptime const or var. This aligns with one of the goals of Zig, which is that code can run in comptime or runtime.

Your proposal seems to be renaming these things in order to break this symmetry, with the justification that it makes it clear whether something is runtime or comptime. e.g. you would make this change: comptime const -> const comptime var -> var runtime const -> final runtime var -> var This doesn't make sense to me.

Lastly, to your example, I would add the comptime keyword to the buildNo call. This ensure that buildNo is actually called at comptime, and makes it clear that one is a comptime value and one is a runtime value.

const b = getCurrentTime();
const c2 = comptime buildNo();

MatthiasPortzel avatar Aug 04 '24 22:08 MatthiasPortzel

  • I know the current differences between comptime constructs (comptime_int, type...) and const, their roles, those that can be used at runtime (comptime_int value) and those that cannot (type, although @typeInfo can be used to have "RunTime Type Information")
  • The proposal only affects const and the proposed final. comptime var will be the comptime only equivalent to var and const the same to final. This separate and make orthogonal:
    • immutability: once initialized the value cannot be changed, and,
    • "constness": data set in stone loaded from the data part of executable/library or inlined in the generated machine code
  • In your example, with const being used for both this separation is not clear. Moreover if buildNo() is a comptime only function without the comptime keyword it's ambiguous.
  • The reason final b = comptime expr; will be illegal is: If one knows that b will reference a value resolved at comptime making it const communicate the intent. And a final at comptime won't make sense anymore.
  • Just like now, if a runtime function is called at comptime it's vars and finals would be treated like their comptime equivalent.

The goal of this proposal is to clarify the symmetry currently hidden as const play two different roles depending on the context.

If it's accepted, the comptime keyword will only have three usage:

  • Run Standalone Expression at comptime,
    omptime init();
     = comptime checks();
    omptime {
    ...
    ;
    
  • Function Declaration: to mandate that an argument must be provided at comptime only,
  • As Part Of A Runtime Expression: to mandate that parts of an expression must be resolved at comptime, in the case the compiler may decide otherwise.
    n bar(x: T) T {
    const c = comptimeExpr...;
    
    var a = runtimeValue(x);
    a.normalize();
    final b = merge(foo(a), comptime foo(c));
    log(b);
    return b;
    
    
    Here merge, foo and bar can be used both at runtime and at comptime.

Note: This proposal is only 1/3 of a bigger one, affecting also pointer qualifiers... I will share the rest with examples... once I get them ready.

Lking03x avatar Aug 06 '24 14:08 Lking03x

I know the current differences between comptime constructs (comptime_int, type...) and const, their roles, those that can be used at runtime (comptime_int value) and those that cannot (type, although @typeInfo can be used to have "RunTime Type Information")

This demonstrates a fundamental misunderstanding of all the concepts you just referenced. comptime_int cannot be used at runtime, nor can the result of @typeInfo be used at runtime (and as such cannot be used as runtime type information).

The rest of your reply avoids the very pertinent point brought up by @MatthiasPortzel:

There's a common misconception that "comptime" has something to do with constness, when they are completely orthogonal concepts. You can have runtime const or var, and you can have comptime const or var. This aligns with one of the goals of Zig, which is that code can run in comptime or runtime.

Your proposal is in opposition to currently accepted & desired status quo of symmetry between runtime and comptime code. The "ambiguity" you reference is not an actual issue - if code can be run at comptime, there's no actual benefit in forcing the programmer to annotate the delineation. It would simply add another way to write the same thing, and possibly force needles code duplication or workarounds to avoid running into the arbitrary error that otherwise doesn't appear when running an otherwise runtime snippet of code at comptime.

InKryption avatar Aug 06 '24 15:08 InKryption

If I may opine a bit, while sometimes knowing if something is comptime const or runtime const is challenging, maintaining flexibility is critical. In the case of the code that I have in prod, I work with a .so library that I dynamically load using dlopen/dlsym (known runtime only) but for test runs I use shim functions which would obligatorily be comptime known.

const's flexibility in this case, saves my bacon.

ityonemo avatar Aug 12 '24 06:08 ityonemo