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:
39
examples/comptime/0648-comptime-typequery-const-fold.sx
Normal file
39
examples/comptime/0648-comptime-typequery-const-fold.sx
Normal 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user