#29393

comptime: evaluate `@[comptime]` calls in `const` initializers and emit the value as static data; today every such const is computed in `_vinit`, and enum values ignore the result's integer width

Openenghitaloopened 27d ago

Describe the feature

Evaluate a const initializer that calls @[comptime] functions at compile time, and emit the result as static C data, for the values V already has: integers, floats, strings, fixed arrays, []T, maps, and structs of these.

@[comptime]
fn crc32_table(poly u32) [256]u32 {
	mut t := [256]u32{}
	for i in 0 .. 256 {
		mut crc := u32(i)
		for _ in 0 .. 8 {
			crc = if crc & 1 == 1 { (crc >> 1) ^ poly } else { crc >> 1 }
		}
		t[i] = crc
	}
	return t
}

const ieee_table = crc32_table(0xedb88320)

Expected C: const u32 main__ieee_table[256] = {0x00000000, 0x77073096, …};, with no code in _vinit.

@[comptime] already exists (#26475, 2519498), and docs.md says: "Calls to this function in const and enum expressions can be evaluated at compile time, when all call arguments are compile-time constants." That works for enum values. For consts it doesn't (see Today).

Requirements

  • Static data, no startup work. The result becomes a C initializer: const u32 x[256] = {…}, string x = {"…", n, 1}, a struct literal. A []T becomes a static backing array with .nofree | .nogrow. A map becomes static storage, either in V's layout or as a perfect hash over the known keys. Nothing runs in _vinit, and nothing goes on the heap.
  • Same answers as at runtime. Integers wrap at their declared width. The value must be the one the function returns when called at runtime.
  • Failures are compile errors at the const. A panic, an or { panic(…) }, or an error return reached during evaluation is an error at the const. So is anything the evaluator can't run (C functions, os.getenv, time.now, rand, globals, I/O). Evaluation never falls back to _vinit silently. Calls to a @[comptime] function with runtime arguments, outside const and enum expressions, stay ordinary calls.
  • Target, not host. $if, sizeof, the width of int and endianness inside an evaluated function follow -os/-arch.

Use case

Today a table is either built at every startup or pasted as numbers. The examples below are from vlib.

  • Built at every startup:
    • hash.crc32 builds four &Crc32 objects with their 256-entry tables, plus crc32q_table, in the _vinit of every program that imports it.
    • net.http builds the HPACK Huffman table and its decode map (… or { panic('hpack: …') }); net.quic builds the QPACK one.
    • compress.deflate builds its fixed Huffman trees.
    • term.ui builds the 256-color palette (color_table = init_color_table()).
    • math.big heap-allocates 23 constants (one_int … c20).
  • Pasted: crypto/aes/const.v holds about 300 lines of hex (pow_x, s_box0, s_box1, te0, td0) next to the poly they derive from. A 20-line @[comptime] fn make_sbox() [256]u8 (GF(2⁸) inverse plus the affine map) reproduces s_box0 byte for byte, and a reviewer can check it.
  • Large const maps, rebuilt entry by entry: net.http.mime has 2,354 MimeType entries and 1,203 extension → type entries. A program whose only work is mime.get_mime_type('wasm') takes 1.48 ms of CPU at startup against 0.37 ms for a baseline, and its binary is 746 KB against 35 KB (-prod -gc none, perf stat -r 300). About 700 KB of the extra .text is the code that inserts the entries. Others: encoding.html named references (about 2,100 entries), and the compiler's own reserved-word and libc-symbol maps, paid on every v run.
  • Generated files: net/http/mime/build.vsh downloads mime-db's JSON and writes a 15,337-line db.v. With a @[comptime] parser over $embed_file('db.json'), the JSON would be the source, and the extension index would be derived from it instead of being generated next to it.
  • Validation: base58.btc_alphabet = new_alphabet('…') or { panic(impossible) }, the HPACK and QPACK builders and the edwards25519 point constants turn a bad literal into a crash at startup. Evaluated at compile time, the mistake would be a compile error.
  • Prebuilt bytes: const r = 'HTTP/1.1 404 …'.bytes() copies a literal that is already in .rodata to the heap in _vinit. enghitalo/vanilla had more than 200 of these, as prebuilt HTTP responses. This case only needs constant folding.

The full list, with measurements and outside-vlib cases (routers from attributes, math tables, sRGB/gamma curves, lexer byte classes), is in enghitalo/vanilla#239, section A.

Today

At 9d34aa2, checked in the generated C. I called @[comptime] functions from consts in six forms: mask(1, 4) u32, wrap8(250, 10) u8, a loop returning [256]u8, a string, a recursive fib(20), and the crc32_table above. Every one became a plain C global filled in _vinit:

u32 main__ieee_table[256];
…
memmove(main__ieee_table, (crc32_table(0xedb88320)).ret_arr, sizeof(main__ieee_table));
  • const n = mask(1, 3) used as [n]u8{} fails with error: non-constant array bound n.
  • Enum values are evaluated, but in int: with @[comptime] fn wrap8(a u8, b u8) u8 { return a + b }, enum Small as u32 { w = wrap8(250, 10) } emits #define Small__w ((Small)(260)), while wrap8(250, 10) returns 4 at runtime. The enum evaluator handles parameters, straight-line assignments and return over a map[string]int: no loops, no arrays or strings, and no integer widths.

Without @[comptime], only literals, arithmetic, literal concatenation, fixed arrays and plain struct literals are static. These all go to _vinit (emit_const; []T is always runtime):

initializeremitted as
'…'.bytes()string__bytes(), a heap copy
[1, 2, 3]new_array_from_c_array()
{'html': 'text/html', …}new_map(), then one map__set() per entry
'Content-Length: ' + s.len.str()int__str(), string_plus_many()
error('…')memdup() of the error object
any callthe call (plus memmove() for a fixed array)
[[u8(1), 2]!, [u8(3), 4]!]!memmove()

Proposed solution

Pointers are at 9d34aa2.

  1. Make the enum path correct. Wrap each @[comptime] result and parameter to its declared type (u8, u32, …) in comptime_static_enum_call_value and comptime_static_enum_local_expr_value. A value that doesn't fit the enum's type should be an error. This could be a separate bug report if preferred.
  2. Fold builtin methods over literals in const initializers. '…'.bytes() becomes a static Array over the literal's bytes (.nofree | .nogrow); .len, .str() of a const int, and literal interpolation become literals. No interpreter is needed. This overlaps with step 3 of #29378 (try_lower_string_method_call).
  3. Evaluate @[comptime] calls in const initializers with v.eval. It already interprets V: loops, arrays, maps, structs, closures. After type checking, run the call with its constant arguments and turn the resulting eval.Value into a C initializer in emit_const. Two prerequisites in v.eval:
    • Fixed-width integers. Value carries integers as i64, so u8(250) + u8(10) gives 260, and a u32 CRC-style generator (shifts and xors) returns 734460456773 where the compiled program returns 21049157.
    • Coverage of the builtin methods generators call. 'content-type'.to_upper() fails with v.eval: unsupported string method `to_upper` .
  4. Lower the values to C.
    • Scalars, strings, fixed arrays and plain structs map to C initializers directly, as literals already do.
    • A []T becomes a static backing array plus .nofree | .nogrow.
    • A map becomes static buckets in V's layout, or a perfect hash.
    • An IError becomes a static MessageError object, as builtin__error_sentinel already is.
    • &T values, sum types and interfaces need their payloads emitted as static objects.
    • Anything else is a compile error.
  5. Cache the results with the module cache, keyed by the source of the evaluated functions, so a 2,000-entry map isn't re-evaluated on every build.

Floating point needs one rule: math.pow, math.sin and similar functions, evaluated by the compiler, can differ in the last bit from the target's libm. Most tables tolerate that. A table that must match a runtime computation exactly needs correctly rounded functions in the evaluator.

Other information

  • #26475 and 2519498: @[comptime] for enum values (integers, straight-line bodies).
  • #29378: folding pure string functions on compile-time strings; its step 3 covers part of step 2 here.
  • enghitalo/vanilla#239: the use cases with measurements (section A), and the other proposals from the same router work.

Environment

  • V 0.5.2 9d34aa2 (9d34aa2ecc297f8896a4916195a1ba0424807f8f, 2026-10-03)
  • OS: Manjaro Linux, kernel 7.1.13, x86_64
  • C compiler: gcc 16.2.1 (20260810)

Activity

0

No activity yet.