diff --git a/examples/comptime/0648-comptime-typequery-const-fold.sx b/examples/comptime/0648-comptime-typequery-const-fold.sx new file mode 100644 index 00000000..d701a05e --- /dev/null +++ b/examples/comptime/0648-comptime-typequery-const-fold.sx @@ -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; +} diff --git a/examples/comptime/expected/0648-comptime-typequery-const-fold.exit b/examples/comptime/expected/0648-comptime-typequery-const-fold.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/comptime/expected/0648-comptime-typequery-const-fold.exit @@ -0,0 +1 @@ +0 diff --git a/examples/comptime/expected/0648-comptime-typequery-const-fold.stderr b/examples/comptime/expected/0648-comptime-typequery-const-fold.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/comptime/expected/0648-comptime-typequery-const-fold.stderr @@ -0,0 +1 @@ + diff --git a/examples/comptime/expected/0648-comptime-typequery-const-fold.stdout b/examples/comptime/expected/0648-comptime-typequery-const-fold.stdout new file mode 100644 index 00000000..c548a728 --- /dev/null +++ b/examples/comptime/expected/0648-comptime-typequery-const-fold.stdout @@ -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 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index ac4b90fe..800c50a8 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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 diff --git a/src/ir/program_index.test.zig b/src/ir/program_index.test.zig index 32308f44..32a8e8c5 100644 --- a/src/ir/program_index.test.zig +++ b/src/ir/program_index.test.zig @@ -169,6 +169,9 @@ const DimCtx = struct { pub fn lookupConstStructField(_: DimCtx, _: []const u8, _: []const u8) ?i64 { return null; } + pub fn evalConstCallInt(_: DimCtx, _: *const ast.Node) ?i64 { + return null; + } pub fn lookupPackLen(_: DimCtx, name: []const u8) ?i64 { if (std.mem.eql(u8, name, "xs")) return 3; return null; diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index e79701f4..c2270350 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -125,6 +125,14 @@ const ModuleConstCtx = struct { pub fn lookupPackLen(_: ModuleConstCtx, _: []const u8) ?i64 { 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` // / per-source const cache), so a qualified-member const `m.CAP` can only be // 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; 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) { .negate => { const v = evalConstIntExpr(u.operand, ctx) orelse return null; diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index ace5bc74..c6a404d2 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -94,6 +94,13 @@ const StatelessInner = struct { pub fn lookupPackLen(_: StatelessInner, _: []const u8) ?i64 { 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 // namespace-import facts (`namespace_edges` / per-source cache) — so a // qualified-member const `m.CAP` is not a compile-time leaf here (issue