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

@@ -0,0 +1,39 @@
// Int-returning type-query builtins (`field_count` / `size_of` / `align_of`)
// fold as comptime CONSTANTS — usable as an `inline for` bound and an array
// dimension, exactly 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 variant list from a type's members — the `race` result synthesis.)
#import "modules/std.sx";
S :: struct { a: i64; b: bool; c: f64; }
E :: enum { X; Y; Z; W; }
main :: () -> i32 {
// field_count as an inline-for bound
s := 0;
inline for 0..field_count(S) (i) { s = s + i; } // 0+1+2 = 3
print("field_count(S) loop sum = {}\n", s);
// field_count as an array dimension; fill it in a folded loop
xs : [field_count(S)]i64 = ---;
inline for 0..field_count(S) (i) { xs[i] = i * 10; }
print("array[field_count(S)] len = {} xs[2] = {}\n", xs.len, xs[2]);
// field_count of an enum (4 variants) driving a loop
e := 0;
inline for 0..field_count(E) (i) { e = e + 1; }
print("field_count(E) = {}\n", e);
// size_of / align_of fold too
bytes : [size_of(i64)]u8 = ---;
print("size_of(i64) array len = {}\n", bytes.len);
print("align_of(f64) = {}\n", align_of(f64));
// composed const expression as a dim
ys : [field_count(S) + 1]i64 = ---;
print("[field_count(S) + 1] len = {}\n", ys.len);
return 0;
}

View File

@@ -0,0 +1,6 @@
field_count(S) loop sum = 3
array[field_count(S)] len = 3 xs[2] = 20
field_count(E) = 4
size_of(i64) array len = 8
align_of(f64) = 8
[field_count(S) + 1] len = 4

View File

@@ -119,6 +119,11 @@ pub const SourceConstCtx = struct {
pub fn lookupConstStructField(self: SourceConstCtx, name: []const u8, field: []const u8) ?i64 { pub fn lookupConstStructField(self: SourceConstCtx, name: []const u8, field: []const u8) ?i64 {
return self.lowering.foldConstStructField(name, field, self.frame); 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 { pub fn lookupQualifiedConst(self: SourceConstCtx, ns: []const u8, field: []const u8) ?i64 {
return self.lowering.foldQualifiedConstInt(ns, field, self.frame); return self.lowering.foldQualifiedConstInt(ns, field, self.frame);
} }
@@ -850,6 +855,41 @@ pub const Lowering = struct {
return self.comptimeIntNamed(name); 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 /// Pack-length leaf for the shared integer-expression evaluator: a pack
/// name's monomorphised arity (e.g. an `inline for 0..xs.len` bound). /// name's monomorphised arity (e.g. an `inline for 0..xs.len` bound).
/// Resolves through `pack_param_count`, which is populated when a comptime /// Resolves through `pack_param_count`, which is populated when a comptime

View File

@@ -169,6 +169,9 @@ const DimCtx = struct {
pub fn lookupConstStructField(_: DimCtx, _: []const u8, _: []const u8) ?i64 { pub fn lookupConstStructField(_: DimCtx, _: []const u8, _: []const u8) ?i64 {
return null; return null;
} }
pub fn evalConstCallInt(_: DimCtx, _: *const ast.Node) ?i64 {
return null;
}
pub fn lookupPackLen(_: DimCtx, name: []const u8) ?i64 { pub fn lookupPackLen(_: DimCtx, name: []const u8) ?i64 {
if (std.mem.eql(u8, name, "xs")) return 3; if (std.mem.eql(u8, name, "xs")) return 3;
return null; return null;

View File

@@ -125,6 +125,14 @@ const ModuleConstCtx = struct {
pub fn lookupPackLen(_: ModuleConstCtx, _: []const u8) ?i64 { pub fn lookupPackLen(_: ModuleConstCtx, _: []const u8) ?i64 {
return null; return null;
} }
// A type-query builtin call (`field_count`/`size_of`/`align_of`) needs to
// resolve a type EXPRESSION arg, which this stateless module-const ctx cannot
// do (no `resolveTypeArg` / type-param bindings). The body-lowering ctx folds
// these; here it is null (a module const `N :: field_count(S)` folds through
// the source-aware path, not this global-map ctx).
pub fn evalConstCallInt(_: ModuleConstCtx, _: *const Node) ?i64 {
return null;
}
// The GLOBAL-map fold carries no namespace-import facts (no `namespace_edges` // The GLOBAL-map fold carries no namespace-import facts (no `namespace_edges`
// / per-source const cache), so a qualified-member const `m.CAP` can only be // / per-source const cache), so a qualified-member const `m.CAP` can only be
// resolved by the SOURCE-AWARE path (`SourceConstCtx` / `Lowering`). Null // resolved by the SOURCE-AWARE path (`SourceConstCtx` / `Lowering`). Null
@@ -412,6 +420,13 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 {
const idx = evalConstIntExpr(ie.index, ctx) orelse break :blk null; const idx = evalConstIntExpr(ie.index, ctx) orelse break :blk null;
break :blk ctx.lookupConstArrayElem(name, idx, node.span); break :blk ctx.lookupConstArrayElem(name, idx, node.span);
}, },
// A pure int-returning type-query builtin call (`field_count(T)`,
// `size_of(T)`, `align_of(T)`) folds to its constant when the ctx can
// resolve the type arg — the body-lowering ctx (with type-param bindings)
// can; the stateless registration ctxs return null. This is what lets a
// reflection-derived count drive an `inline for` bound / array dim, the
// same as a plain `K :: 3` const.
.call => ctx.evalConstCallInt(node),
.unary_op => |u| switch (u.op) { .unary_op => |u| switch (u.op) {
.negate => { .negate => {
const v = evalConstIntExpr(u.operand, ctx) orelse return null; const v = evalConstIntExpr(u.operand, ctx) orelse return null;

View File

@@ -94,6 +94,13 @@ const StatelessInner = struct {
pub fn lookupPackLen(_: StatelessInner, _: []const u8) ?i64 { pub fn lookupPackLen(_: StatelessInner, _: []const u8) ?i64 {
return null; return null;
} }
// A type-query builtin call (`field_count`/`size_of`/`align_of`) needs to
// resolve a type-expr arg (and, for `field_count`, type-param bindings),
// which the registration-time path lacks. Folded on the body-lowering path
// (`Lowering`); null here → the clean unresolved-dim diagnostic.
pub fn evalConstCallInt(_: StatelessInner, _: *const Node) ?i64 {
return null;
}
// The registration-time path holds only the flat global const map — no // The registration-time path holds only the flat global const map — no
// namespace-import facts (`namespace_edges` / per-source cache) — so a // namespace-import facts (`namespace_edges` / per-source cache) — so a
// qualified-member const `m.CAP` is not a compile-time leaf here (issue // qualified-member const `m.CAP` is not a compile-time leaf here (issue