From c23b76c7d62303f93392881f3d5060b03330a7f3 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 11 Jun 2026 12:44:43 +0300 Subject: [PATCH] lang: const-aggregate comptime folds (PLAN-CONST-AGG step 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An array const's '.len' and 'K[]' element reads, and a struct const's field ('LIT.r'), fold as compile-time integer leaves — usable in array dimensions and other constants' initializers. All source-aware (the SELECTED author's elements, folded in the author's context with the cyclic-definition frame); a const out-of-range index diagnoses at fold time, never wraps. - evalConstIntExpr gains the three ctx hooks (lookupConstAggLen / lookupConstArrayElem / lookupConstStructField) + an index_expr arm; all five ctx implementations extended (stateless tiers fold null). - Array consts dual-register in module_const_map (value = the literal node) so the folders see elements; bare reads still hit the GLOBAL arm first, so no double emission. - Untyped consts whose RHS is a const-aggregate leaf ('L :: K.len', 'E :: K[1]', 'R :: LIT.r') register in a pass 2b AFTER aggregates, gated on the receiver naming a const aggregate — a namespaced member ('F :: m.PI_ISH') is never mis-typed by the count placeholder. Examples: 0179 (folds in dims + const exprs), 1163 (OOB diagnostic). --- examples/0179-types-const-aggregate-folds.sx | 23 ++++++ .../1163-diagnostics-array-const-index-oob.sx | 12 +++ .../0179-types-const-aggregate-folds.exit | 1 + .../0179-types-const-aggregate-folds.stderr | 1 + .../0179-types-const-aggregate-folds.stdout | 2 + ...163-diagnostics-array-const-index-oob.exit | 1 + ...3-diagnostics-array-const-index-oob.stderr | 11 +++ ...3-diagnostics-array-const-index-oob.stdout | 1 + src/ir/lower.zig | 22 ++++++ src/ir/lower/comptime.zig | 74 +++++++++++++++++++ src/ir/lower/decl.zig | 29 ++++++++ src/ir/program_index.test.zig | 9 +++ src/ir/program_index.zig | 31 +++++++- src/ir/type_bridge.zig | 9 +++ 14 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 examples/0179-types-const-aggregate-folds.sx create mode 100644 examples/1163-diagnostics-array-const-index-oob.sx create mode 100644 examples/expected/0179-types-const-aggregate-folds.exit create mode 100644 examples/expected/0179-types-const-aggregate-folds.stderr create mode 100644 examples/expected/0179-types-const-aggregate-folds.stdout create mode 100644 examples/expected/1163-diagnostics-array-const-index-oob.exit create mode 100644 examples/expected/1163-diagnostics-array-const-index-oob.stderr create mode 100644 examples/expected/1163-diagnostics-array-const-index-oob.stdout diff --git a/examples/0179-types-const-aggregate-folds.sx b/examples/0179-types-const-aggregate-folds.sx new file mode 100644 index 0000000..eaadd3b --- /dev/null +++ b/examples/0179-types-const-aggregate-folds.sx @@ -0,0 +1,23 @@ +// Const aggregates fold at compile time: an array const's `.len` and +// `K[]` element reads, and a struct const's field (`LIT.r`), +// are compile-time integer leaves — usable in array dimensions and in +// other constants' initializer expressions, source-aware like every +// const fold. + +#import "modules/std.sx"; + +Color :: struct { r, g, b: s64; } +K : [4]s64 : .[11, 22, 33, 44]; +LIT :: Color.{ r = 5, g = 0, b = 0 }; + +N :: K[0] + K[3]; +L :: K.len; +R :: LIT.r; + +main :: () { + a : [K.len]u8 = ---; + b : [K[1]]u8 = ---; + c : [LIT.r]u8 = ---; + print("N={} L={} R={}\n", N, L, R); + print("dims={} {} {}\n", a.len, b.len, c.len); +} diff --git a/examples/1163-diagnostics-array-const-index-oob.sx b/examples/1163-diagnostics-array-const-index-oob.sx new file mode 100644 index 0000000..d20ba8d --- /dev/null +++ b/examples/1163-diagnostics-array-const-index-oob.sx @@ -0,0 +1,12 @@ +// A compile-time index into an array constant is bounds-checked at fold +// time — out of range is a diagnostic, never a wrap or a silent +// runtime read. + +#import "modules/std.sx"; + +K : [4]s64 : .[11, 22, 33, 44]; + +main :: () { + b : [K[9]]u8 = ---; + print("{}\n", b.len); +} diff --git a/examples/expected/0179-types-const-aggregate-folds.exit b/examples/expected/0179-types-const-aggregate-folds.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0179-types-const-aggregate-folds.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0179-types-const-aggregate-folds.stderr b/examples/expected/0179-types-const-aggregate-folds.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0179-types-const-aggregate-folds.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0179-types-const-aggregate-folds.stdout b/examples/expected/0179-types-const-aggregate-folds.stdout new file mode 100644 index 0000000..8dfd61c --- /dev/null +++ b/examples/expected/0179-types-const-aggregate-folds.stdout @@ -0,0 +1,2 @@ +N=55 L=4 R=5 +dims=4 22 5 diff --git a/examples/expected/1163-diagnostics-array-const-index-oob.exit b/examples/expected/1163-diagnostics-array-const-index-oob.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1163-diagnostics-array-const-index-oob.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1163-diagnostics-array-const-index-oob.stderr b/examples/expected/1163-diagnostics-array-const-index-oob.stderr new file mode 100644 index 0000000..be7d793 --- /dev/null +++ b/examples/expected/1163-diagnostics-array-const-index-oob.stderr @@ -0,0 +1,11 @@ +error: index 9 is out of bounds for constant 'K' (4 elements) + --> examples/1163-diagnostics-array-const-index-oob.sx:10:10 + | +10 | b : [K[9]]u8 = ---; + | ^^^^ + +error: array dimension must be a compile-time integer constant + --> examples/1163-diagnostics-array-const-index-oob.sx:10:10 + | +10 | b : [K[9]]u8 = ---; + | ^^^^ diff --git a/examples/expected/1163-diagnostics-array-const-index-oob.stdout b/examples/expected/1163-diagnostics-array-const-index-oob.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1163-diagnostics-array-const-index-oob.stdout @@ -0,0 +1 @@ + diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 0c2999a..4b1e16d 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -111,6 +111,15 @@ pub const SourceConstCtx = struct { pub fn nameIsFloatTyped(self: SourceConstCtx, name: []const u8) bool { return self.lowering.sourceConstIsFloatTyped(name, self.frame); } + pub fn lookupConstAggLen(self: SourceConstCtx, name: []const u8) ?i64 { + return self.lowering.foldConstAggLen(name); + } + pub fn lookupConstArrayElem(self: SourceConstCtx, name: []const u8, idx: i64, span: ?ast.Span) ?i64 { + return self.lowering.foldConstArrayElem(name, idx, span, self.frame); + } + pub fn lookupConstStructField(self: SourceConstCtx, name: []const u8, field: []const u8) ?i64 { + return self.lowering.foldConstStructField(name, field, self.frame); + } }; // ── Scope ─────────────────────────────────────────────────────────────── @@ -773,6 +782,16 @@ pub const Lowering = struct { return self.sourceConstIsFloatTyped(name, null); } + pub fn lookupConstAggLen(self: *Lowering, name: []const u8) ?i64 { + return self.foldConstAggLen(name); + } + pub fn lookupConstArrayElem(self: *Lowering, name: []const u8, idx: i64, span: ?ast.Span) ?i64 { + return self.foldConstArrayElem(name, idx, span, null); + } + pub fn lookupConstStructField(self: *Lowering, name: []const u8, field: []const u8) ?i64 { + return self.foldConstStructField(name, field, null); + } + /// Resolve a type node, checking type_bindings first for generic type params. pub fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId { // Pack-index in a type position: `$[]` resolves to the @@ -1524,6 +1543,9 @@ pub const Lowering = struct { pub const selectModuleConst = lower_comptime.selectModuleConst; pub const GlobalAuthor = lower_comptime.GlobalAuthor; pub const selectGlobalAuthor = lower_comptime.selectGlobalAuthor; + pub const foldConstAggLen = lower_comptime.foldConstAggLen; + pub const foldConstArrayElem = lower_comptime.foldConstArrayElem; + pub const foldConstStructField = lower_comptime.foldConstStructField; pub const resolveGlobalRef = lower_comptime.resolveGlobalRef; pub const sourceModuleConst = lower_comptime.sourceModuleConst; pub const pinConstAuthorSource = lower_comptime.pinConstAuthorSource; diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index 61afc3b..15e9095 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -898,6 +898,80 @@ pub fn selectModuleConst(self: *Lowering, name: []const u8) ConstAuthor { return .none; } +/// `.len` as a compile-time integer — the SELECTED author's +/// element count (E2/F2 source-aware, like every const fold). +pub fn foldConstAggLen(self: *Lowering, name: []const u8) ?i64 { + return switch (self.selectModuleConst(name)) { + .resolved => |sel| if (sel.info.value.data == .array_literal) + @intCast(sel.info.value.data.array_literal.elements.len) + else + null, + .own_opaque, .ambiguous, .none => null, + }; +} + +/// `K[]` over an ARRAY const: the element's compile-time integer +/// value, folded in the AUTHOR's context. Out-of-range diagnoses loudly — +/// never a wrap or a silent null-into-runtime. +pub fn foldConstArrayElem(self: *Lowering, name: []const u8, idx: i64, span: ?ast.Span, frame: ?*const ConstFoldFrame) ?i64 { + switch (self.selectModuleConst(name)) { + .resolved => |sel| { + if (sel.info.value.data != .array_literal) return null; + const elems = sel.info.value.data.array_literal.elements; + if (idx < 0 or idx >= elems.len) { + if (self.diagnostics) |d| + d.addFmt(.err, span, "index {d} is out of bounds for constant '{s}' ({d} elements)", .{ idx, name, elems.len }); + return null; + } + if (constFoldFrameContains(frame, name, sel.source)) return null; + var f = ConstFoldFrame{ .name = name, .source = sel.source, .parent = frame }; + const restore = self.pinConstAuthorSource(sel.source); + defer restore.unpin(); + return program_index_mod.evalConstIntExpr(elems[@intCast(idx)], SourceConstCtx{ .lowering = self, .frame = &f }); + }, + .own_opaque, .ambiguous, .none => return null, + } +} + +/// `.field` as a compile-time integer — the SELECTED author's +/// field initializer, matched by name (named inits) or position, folded in +/// the author's context. +pub fn foldConstStructField(self: *Lowering, name: []const u8, field: []const u8, frame: ?*const ConstFoldFrame) ?i64 { + switch (self.selectModuleConst(name)) { + .resolved => |sel| { + if (sel.info.value.data != .struct_literal) return null; + const sl = &sel.info.value.data.struct_literal; + const init_expr: ?*const Node = blk: { + const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; + if (has_names) { + for (sl.field_inits) |fi| { + if (fi.name) |n| if (std.mem.eql(u8, n, field)) break :blk fi.value; + } + break :blk null; + } + // Positional inits: index via the struct type's field order. + if (sel.info.ty.isBuiltin()) break :blk null; + const ti = self.module.types.get(sel.info.ty); + if (ti != .@"struct") break :blk null; + for (ti.@"struct".fields, 0..) |sf, i| { + if (std.mem.eql(u8, self.module.types.getString(sf.name), field)) { + if (i < sl.field_inits.len) break :blk sl.field_inits[i].value; + break :blk null; + } + } + break :blk null; + }; + const e = init_expr orelse return null; + if (constFoldFrameContains(frame, name, sel.source)) return null; + var f = ConstFoldFrame{ .name = name, .source = sel.source, .parent = frame }; + const restore = self.pinConstAuthorSource(sel.source); + defer restore.unpin(); + return program_index_mod.evalConstIntExpr(e, SourceConstCtx{ .lowering = self, .frame = &f }); + }, + .own_opaque, .ambiguous, .none => return null, + } +} + /// `source`'s per-source const cache entry for `name` (E0's /// `module_consts_by_source` write side), or null. pub fn sourceModuleConst(self: *Lowering, source: []const u8, name: []const u8) ?ModuleConstInfo { diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index 3c6e743..6b12852 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -785,6 +785,30 @@ pub fn scanDecls(self: *Lowering, decls: []const *const Node) void { else => {}, } } + // Pass 2b: untyped consts whose RHS is a CONST-AGGREGATE leaf + // (`L :: K.len`, `E :: K[1]`, `R :: LIT.r`) register with the count + // placeholder so the folders reach them. Runs AFTER pass 2 so the + // aggregate (array const / struct const) is registered regardless of + // declaration order; gated on the receiver naming a const aggregate so + // a namespaced member (`F :: m.PI_ISH`) is never mis-typed. + for (decls) |decl| { + if (decl.data != .const_decl) continue; + const cd = decl.data.const_decl; + if (cd.type_annotation != null) continue; + self.setCurrentSourceFile(decl.source_file); + const obj: *const Node = switch (cd.value.data) { + .field_access => |fa| fa.object, + .index_expr => |ie| ie.object, + else => continue, + }; + if (obj.data != .identifier) continue; + const recv_is_agg = switch (self.selectModuleConst(obj.data.identifier.name)) { + .resolved => |sel| sel.info.value.data == .array_literal or sel.info.value.data == .struct_literal, + .own_opaque, .ambiguous, .none => false, + }; + if (!recv_is_agg) continue; + self.putModuleConst(decl.source_file, cd.name, .{ .value = cd.value, .ty = .s64 }); + } } /// Register a typed module-level value constant (`AF_INET :s32: 2`). Run in @@ -966,6 +990,11 @@ pub fn registerConstArrayGlobal(self: *Lowering, cd: *const ast.ConstDecl) void .is_const = true, }); self.putGlobal(self.current_source_file, cd.name, .{ .id = gid, .ty = arr_ty }); + // ALSO register as a module const so the comptime folders see the + // elements (`K.len` / `K[]` in dims and const exprs). + // Bare value reads still hit the GLOBAL arm first (identifier arm + // order), so this never double-emits. + self.putModuleConst(self.current_source_file, cd.name, .{ .value = cd.value, .ty = arr_ty }); } /// Infer `[N]T` for an untyped array-literal constant. Element types unify: diff --git a/src/ir/program_index.test.zig b/src/ir/program_index.test.zig index 879119f..6ef0642 100644 --- a/src/ir/program_index.test.zig +++ b/src/ir/program_index.test.zig @@ -160,6 +160,15 @@ const DimCtx = struct { return null; } // `xs` stands in for a pack of arity 3; every other name has no pack length. + pub fn lookupConstAggLen(_: DimCtx, _: []const u8) ?i64 { + return null; + } + pub fn lookupConstArrayElem(_: DimCtx, _: []const u8, _: i64, _: ?ast.Span) ?i64 { + return null; + } + pub fn lookupConstStructField(_: DimCtx, _: []const u8, _: []const u8) ?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 e56f6c3..a2d37d1 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -113,6 +113,15 @@ const ModuleConstCtx = struct { pub fn lookupDimName(self: ModuleConstCtx, name: []const u8) ?i64 { return moduleConstIntFramed(self.consts, self.table, name, self.frame); } + pub fn lookupConstAggLen(_: ModuleConstCtx, _: []const u8) ?i64 { + return null; + } + pub fn lookupConstArrayElem(_: ModuleConstCtx, _: []const u8, _: i64, _: ?ast.Span) ?i64 { + return null; + } + pub fn lookupConstStructField(_: ModuleConstCtx, _: []const u8, _: []const u8) ?i64 { + return null; + } pub fn lookupPackLen(_: ModuleConstCtx, _: []const u8) ?i64 { return null; } @@ -329,17 +338,35 @@ pub fn evalConstIntExpr(node: *const Node, ctx: anytype) ?i64 { }; if (obj_name) |on| { // `.len` resolves to the monomorphised arity (e.g. an - // `inline for 0..xs.len` bound). - if (std.mem.eql(u8, fa.field, "len")) break :blk ctx.lookupPackLen(on); + // `inline for 0..xs.len` bound); `.len` to the + // const's element count. + if (std.mem.eql(u8, fa.field, "len")) { + if (ctx.lookupPackLen(on)) |n| break :blk n; + break :blk ctx.lookupConstAggLen(on); + } // `.min` / `.max` — the same fold the value path uses // (type_resolver), so `[u8.max]T` agrees with `u8.max` in // expression position. A `u64.max` (= -1 as i64) folds here too; // `foldDimU32` then rejects it as a negative array dimension. if (type_resolver.TypeResolver.integerLimitFor(on, fa.field)) |v| break :blk v; + // A struct const's integer field (`LIT.r`) folds to the + // SELECTED author's field value. + if (ctx.lookupConstStructField(on, fa.field)) |v| break :blk v; } // Any other field access is not a compile-time integer leaf. break :blk null; }, + // `K[]` over an ARRAY const folds to the element's value + // (bounds-checked at fold time — out of range diagnoses, never wraps). + .index_expr => |ie| blk: { + const on: ?[]const u8 = switch (ie.object.data) { + .identifier => |id| if (id.is_raw) null else id.name, + else => null, + }; + const name = on orelse break :blk null; + const idx = evalConstIntExpr(ie.index, ctx) orelse break :blk null; + break :blk ctx.lookupConstArrayElem(name, idx, node.span); + }, .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 23cf998..6ce173f 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -76,6 +76,15 @@ const StatelessInner = struct { /// registration-time path has no pack-arity information (packs are bound /// during body lowering), so a `.len` dimension is never a /// compile-time integer here → null → the clean unresolved-dim diagnostic. + pub fn lookupConstAggLen(_: StatelessInner, _: []const u8) ?i64 { + return null; + } + pub fn lookupConstArrayElem(_: StatelessInner, _: []const u8, _: i64, _: ?ast.Span) ?i64 { + return null; + } + pub fn lookupConstStructField(_: StatelessInner, _: []const u8, _: []const u8) ?i64 { + return null; + } pub fn lookupPackLen(_: StatelessInner, _: []const u8) ?i64 { return null; }