feat: fold field_count/size_of/align_of as comptime constants

The int-returning type-query builtins now fold to a compile-time constant in
const-required positions (`inline for` bound, array dimension), like a plain
`K :: 3` const — previously they evaluated only as runtime values, so
`[field_count(S)]T` and `inline for 0..field_count(S)` were rejected as "not a
compile-time integer". This is what lets a `($T) -> Type` builder loop
`inline for 0..field_count(T)` to assemble a member list from a type's fields
(the `race` result synthesis).

The shared comptime-int folder `evalConstIntExpr` (program_index.zig) gained a
`.call => ctx.evalConstCallInt(node)` arm. The body-lowering ctx (`Lowering`)
implements it — resolve the type arg via `resolveTypeArg`, return
`memberCount orelse 0` / `typeSizeBytes` / `typeAlignBytes`, matching the runtime
value path in lower/call.zig exactly. `SourceConstCtx` delegates to its wrapped
Lowering; the stateless ctxs (`ModuleConstCtx`, `StatelessInner`, test `DimCtx`)
stub null (they cannot resolve a type-expr arg). A non-type-query call / wrong
arg count / unresolved type arg folds to null (not a comptime integer).

Adversarially reviewed (SHIP): the fold matches the value path across every type
kind, ctx coverage is complete, recursion is AST-depth bounded, no speculative
spurious diagnostics, `orelse 0` is field_count's definitional value for
non-aggregates (not a silent default). Locked by
examples/comptime/0648-comptime-typequery-const-fold.sx. Suite green (820/0).
This commit is contained in:
agra
2026-06-26 13:13:16 +03:00
parent f1d298764f
commit 2a6ef39829
8 changed files with 112 additions and 0 deletions

View File

@@ -119,6 +119,11 @@ pub const SourceConstCtx = struct {
pub fn lookupConstStructField(self: SourceConstCtx, name: []const u8, field: []const u8) ?i64 {
return self.lowering.foldConstStructField(name, field, self.frame);
}
// Type-query builtin folds (`field_count`/`size_of`/`align_of`) — delegate to
// the wrapped Lowering, which can resolve the type-expr arg.
pub fn evalConstCallInt(self: SourceConstCtx, node: *const Node) ?i64 {
return self.lowering.evalConstCallInt(node);
}
pub fn lookupQualifiedConst(self: SourceConstCtx, ns: []const u8, field: []const u8) ?i64 {
return self.lowering.foldQualifiedConstInt(ns, field, self.frame);
}
@@ -850,6 +855,41 @@ pub const Lowering = struct {
return self.comptimeIntNamed(name);
}
/// Fold a pure int-returning type-query builtin CALL to its compile-time
/// constant: `field_count(T)` / `size_of(T)` / `align_of(T)`. This is what
/// lets a reflection-derived count drive an `inline for` bound / array dim
/// exactly like a plain `K :: 3` const — e.g. a `($T) -> Type` builder that
/// loops `inline for 0..field_count(T)` to assemble a variant list (the
/// `race` result synthesis). (Reaches the self-ctx fold paths — array dim,
/// inline-for bound; a Vector LANE in a plain type annotation resolves through
/// the stateless type-bridge ctx, which stubs this to null.) Resolves the type
/// arg through the same `resolveTypeArg` + accessors the value path uses, so
/// the folded constant always matches the call's runtime value. Returns null
/// for any non-type-query call, a wrong arg count, or an unresolved type arg
/// → the shared folder then treats it as not-a-compile-time-integer.
pub fn evalConstCallInt(self: *Lowering, node: *const Node) ?i64 {
const c = switch (node.data) {
.call => |call| call,
else => return null,
};
const name = switch (c.callee.data) {
.identifier => |id| id.name,
else => return null,
};
if (c.args.len != 1) return null;
const is_fc = std.mem.eql(u8, name, "field_count");
const is_sz = std.mem.eql(u8, name, "size_of");
const is_al = std.mem.eql(u8, name, "align_of");
if (!is_fc and !is_sz and !is_al) return null;
const ty = self.resolveTypeArg(c.args[0]);
if (ty == .unresolved) return null;
// `field_count` of a resolved non-aggregate is 0 (matches the value path
// in lower/call.zig), NOT a fold failure.
if (is_fc) return self.module.types.memberCount(ty) orelse 0;
if (is_sz) return @intCast(self.typeSizeBytes(ty));
return @intCast(self.typeAlignBytes(ty));
}
/// Pack-length leaf for the shared integer-expression evaluator: a pack
/// name's monomorphised arity (e.g. an `inline for 0..xs.len` bound).
/// Resolves through `pack_param_count`, which is populated when a comptime