From d7a6857ee1a5478c165c4366ec5acab982d24fb0 Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 20 Jun 2026 09:39:10 +0300 Subject: [PATCH] comptime value params: generalize to tagged_union (+ aggregate hook) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `$s: ` now binds a constant variant-literal argument as a compile-time-known value and resolves it in the inlined body — the payload-bearing generalization of the enum value param (3c4305f). A bare variant (`.point`) or a payload variant (`.circle(5.0)`) both bind: * the variant TAG goes into `comptime_value_bindings` (i64), so `comptimeIntNamed`/`if s == .circle` keep working and the param is readable in a TYPE position (`[s]i64`); * the full materialized `enum_init(tag, payload)` value goes into a new `comptime_value_ref_bindings` (param -> Ref) AND is scoped, so a payload read off the bound value (`s.rect`) resolves. A new lowering-time accessor `comptimeValueRefNamed(param)` reads it. `bindEnumValueParams` is generalized to `bindComptimeValueParams`, which switches on the constraint kind: `.@"enum"` -> tag-only bind, `.tagged_union` -> tag + value bind. Other value kinds (struct/array aggregates) are left with an explicit `else` (no silent default) and a comment marking where the aggregate-const arm goes when a repro lands; a non-constant arg / unknown variant is a loud, well-spanned diagnostic. Locked by examples/0640-comptime-tagged-union-value-param.sx (bare + payload variants, tag comparison, tag-as-dimension, payload read). 0627 (enum) stays green. --- .../0640-comptime-tagged-union-value-param.sx | 43 +++ ...640-comptime-tagged-union-value-param.exit | 1 + ...0-comptime-tagged-union-value-param.stderr | 1 + ...0-comptime-tagged-union-value-param.stdout | 3 + src/ir/lower.zig | 19 +- src/ir/lower/comptime.zig | 293 +++++++++++++----- 6 files changed, 277 insertions(+), 83 deletions(-) create mode 100644 examples/0640-comptime-tagged-union-value-param.sx create mode 100644 examples/expected/0640-comptime-tagged-union-value-param.exit create mode 100644 examples/expected/0640-comptime-tagged-union-value-param.stderr create mode 100644 examples/expected/0640-comptime-tagged-union-value-param.stdout diff --git a/examples/0640-comptime-tagged-union-value-param.sx b/examples/0640-comptime-tagged-union-value-param.sx new file mode 100644 index 00000000..8e5a1682 --- /dev/null +++ b/examples/0640-comptime-tagged-union-value-param.sx @@ -0,0 +1,43 @@ +// Comptime TAGGED-UNION value parameter: `$s: ` generalizes the +// enum value-param mechanism (examples/0627) to a payload-bearing enum. The +// argument is a constant variant literal — either bare (`.point`) or with a +// payload (`.circle(5.0)`) — and binds the param so it resolves in the body: +// * `if s == .circle` lowers as an ordinary tag comparison, +// * the variant tag is comptime-readable in a TYPE position (`[s]i64`), +// * a payload read off the bound value (`s.rect`) resolves to the +// compile-time-known payload. +// Distinct variant args monomorphize the inlined body per value. +#import "modules/std.sx"; + +Shape :: enum { + circle: f64; + rect: i64; + point; +} + +// Tag comparison in the body — bare AND payload variants both bind. +classify :: ($s: Shape) -> i64 { + if s == .circle { return 1; } + if s == .rect { return 2; } + return 3; +} + +// Variant tag read as a compile-time integer in a TYPE position (the array +// dimension), exercising the `comptime_value_bindings` / `comptimeIntNamed` +// integration contract for a tagged union. +tag_dim :: ($s: Shape) -> i64 { + arr : [s]i64 = ---; + return arr.len; +} + +// Payload read off the bound comptime value. +rect_payload :: ($s: Shape) -> i64 { + if s == .rect { return s.rect; } + return -1; +} + +main :: () { + print("{} {} {}\n", classify(.circle(5.0)), classify(.rect(7)), classify(.point)); // 1 2 3 + print("{} {} {}\n", tag_dim(.circle(0.0)), tag_dim(.rect(0)), tag_dim(.point)); // 0 1 2 + print("{}\n", rect_payload(.rect(42))); // 42 +} diff --git a/examples/expected/0640-comptime-tagged-union-value-param.exit b/examples/expected/0640-comptime-tagged-union-value-param.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0640-comptime-tagged-union-value-param.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0640-comptime-tagged-union-value-param.stderr b/examples/expected/0640-comptime-tagged-union-value-param.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0640-comptime-tagged-union-value-param.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0640-comptime-tagged-union-value-param.stdout b/examples/expected/0640-comptime-tagged-union-value-param.stdout new file mode 100644 index 00000000..857cb74d --- /dev/null +++ b/examples/expected/0640-comptime-tagged-union-value-param.stdout @@ -0,0 +1,3 @@ +1 2 3 +0 1 2 +42 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index a9dca39e..f91e525b 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -309,7 +309,16 @@ pub const Lowering = struct { struct_instance_bindings: std.StringHashMap(std.StringHashMap(TypeId)), // mangled struct name → type param bindings struct_instance_template: std.StringHashMap([]const u8), // mangled struct name → template name struct_instance_author: std.StringHashMap(*const ast.StructDecl), // mangled struct name → authoring StructDecl (CP-2: body-author ≡ layout-author) - comptime_value_bindings: ?std.StringHashMap(i64) = null, // comptime value bindings ($N → integer value) + comptime_value_bindings: ?std.StringHashMap(i64) = null, // comptime value bindings ($N → integer value: int / enum-tag / tagged-union-tag) + /// Comptime value params bound to a NON-scalar materialized value (a + /// tagged-union literal, a struct/array aggregate). Keyed by param name → + /// the IR `Ref` of the materialized value (an `enum_init(tag, payload)` for + /// a tagged union, an aggregate const for a struct/array). The companion to + /// `comptime_value_bindings`: the i64 map carries the comptime-readable + /// scalar (the variant TAG for a tagged union, so `comptimeIntNamed` keeps + /// returning it); this map carries the full value Ref so a lowering-time + /// consumer can read the whole bound value (`comptimeValueRefNamed`). + comptime_value_ref_bindings: ?std.StringHashMap(Ref) = null, protocol_thunk_map: std.StringHashMap([]const FuncId), // "Proto\x00Type" → thunk FuncIds protocol_vtable_type_map: std.StringHashMap(TypeId), // protocol name → vtable struct TypeId protocol_vtable_global_map: std.StringHashMap(inst_mod.GlobalId), // "Proto\x00Type" → vtable GlobalId @@ -1572,7 +1581,13 @@ pub const Lowering = struct { pub const lowerComptimeGlobal = lower_comptime.lowerComptimeGlobal; pub const lowerComptimeSideEffect = lower_comptime.lowerComptimeSideEffect; pub const lowerComptimeCall = lower_comptime.lowerComptimeCall; - pub const bindEnumValueParams = lower_comptime.bindEnumValueParams; + pub const bindComptimeValueParams = lower_comptime.bindComptimeValueParams; + pub const recordComptimeTag = lower_comptime.recordComptimeTag; + pub const recordComptimeValueRef = lower_comptime.recordComptimeValueRef; + pub const bindEnumValueParam = lower_comptime.bindEnumValueParam; + pub const bindTaggedUnionValueParam = lower_comptime.bindTaggedUnionValueParam; + pub const enumHasVariant = lower_comptime.enumHasVariant; + pub const comptimeValueRefNamed = lower_comptime.comptimeValueRefNamed; pub const lowerInlineComptime = lower_comptime.lowerInlineComptime; pub const lowerInsertExpr = lower_comptime.lowerInsertExpr; pub const lowerInsertExprValue = lower_comptime.lowerInsertExprValue; diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index 8b4002f9..c7fb9ba9 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -739,19 +739,25 @@ pub fn lowerComptimeCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *con } } - // Bind ENUM-typed comptime value params (`$o: Ord`) to their argument's - // variant tag — both into `comptime_value_bindings` (so a downstream - // lowerer can read the constant tag for an identifier) AND as a scoped - // enum value (so `if o == .a` lowers as an ordinary enum comparison). - // Saved/restored around the body so nested comptime calls don't leak the - // outer call's tags. A non-constant / unknown-variant enum arg is a loud - // diagnostic (never a silent default) per the value-param rules. - const saved_enum_value_bindings = self.comptime_value_bindings; - var enum_value_bindings: ?std.StringHashMap(i64) = null; - self.bindEnumValueParams(fd, cpn, &enum_value_bindings); + // Bind VALUE-typed comptime params (`$o: Ord`, `$s: Shape`, ...) to their + // argument's materialized comptime value — both into + // `comptime_value_bindings` (the comptime-readable scalar: int / enum-tag / + // tagged-union-tag, so a downstream lowerer can read the constant for an + // identifier) AND as a scoped value (so `if o == .a` / `if s == .circle` + // lowers as an ordinary comparison). Saved/restored around the body so + // nested comptime calls don't leak the outer call's bindings. A + // non-constant / unknown-variant arg is a loud diagnostic (never a silent + // default) per the value-param rules. + const saved_value_bindings = self.comptime_value_bindings; + const saved_value_ref_bindings = self.comptime_value_ref_bindings; + var value_bindings: ?std.StringHashMap(i64) = null; + var value_ref_bindings: ?std.StringHashMap(Ref) = null; + self.bindComptimeValueParams(fd, cpn, &value_bindings, &value_ref_bindings); defer { - if (enum_value_bindings) |*evb| evb.deinit(); - self.comptime_value_bindings = saved_enum_value_bindings; + if (value_bindings) |*vb| vb.deinit(); + if (value_ref_bindings) |*vrb| vrb.deinit(); + self.comptime_value_bindings = saved_value_bindings; + self.comptime_value_ref_bindings = saved_value_ref_bindings; } // Install comptime param nodes and lower the function body inline @@ -851,92 +857,204 @@ pub fn lowerComptimeCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *con return self.builder.constInt(0, .void); } -/// Bind ENUM-typed comptime value params (`$o: Ord`) for an inlined comptime -/// call. For each comptime param whose declared constraint resolves to an -/// `.@"enum"` type and whose call-site argument is a constant enum literal -/// (`.b`), this: -/// 1. folds the literal to its variant tag (declaration-order index, or the -/// explicit value), -/// 2. records `param_name -> tag` in `self.comptime_value_bindings` (lazily -/// allocated into `*store` and activated on `self`), so a downstream -/// lowerer / `#builtin` recognizer can read the constant tag for the -/// param identifier via `comptimeIntNamed(param_name)` (or directly off -/// `comptime_value_bindings.get(param_name)`), and -/// 3. binds `param_name` into the active scope as an `enum_init(tag, …)` -/// value of the constraint enum type, so the body's `if o == .a` lowers -/// as an ordinary enum comparison. +/// Bind VALUE-typed comptime params for an inlined comptime call. For each +/// comptime param whose declared constraint resolves to a value type and whose +/// call-site argument is a constant literal of that type, this materializes the +/// argument as a compile-time-known value and binds the param so it resolves in +/// the body. Two stores are written: +/// - `int_store` (`comptime_value_bindings`, param → i64): the +/// comptime-readable SCALAR — the int for an `int` param, the variant TAG +/// for an `enum` or `tagged_union` param. Preserves the integration +/// contract: `comptimeIntNamed(param)` keeps returning the tag/int, and the +/// param remains usable in a type position (`[o]i64`). +/// - `ref_store` (`comptime_value_ref_bindings`, param → Ref): the full +/// materialized value Ref for a non-scalar param (the `enum_init(tag, +/// payload)` of a tagged-union, the aggregate const of a struct/array), so +/// a lowering-time consumer can read the WHOLE bound value via +/// `comptimeValueRefNamed(param)`. +/// Supported constraint kinds: +/// - `.@"enum"` (payload-less): bind the variant tag, scope an +/// `enum_init(tag, none)` so `if o == .a` lowers as an enum comparison. +/// - `.tagged_union` (payload-bearing enum): bind the variant tag (int_store) +/// AND the full `enum_init(tag, payload)` value (ref_store + scope), so a +/// bare variant (`.point`) or a payload variant (`.circle(5.0)`) both +/// resolve — `if s == .circle` lowers as a tag comparison and any payload +/// read works off the bound value. /// A non-constant argument, or one naming an unknown variant, emits a loud -/// diagnostic and binds nothing — never a silent default. -pub fn bindEnumValueParams( +/// diagnostic and binds nothing — never a silent default. Constraint kinds that +/// are not value types (a metatype / `type` constraint, a generic type param) +/// are left to the type-binding machinery and skipped here. +pub fn bindComptimeValueParams( self: *Lowering, fd: *const ast.FnDecl, cpn: std.StringHashMap(*const Node), - store: *?std.StringHashMap(i64), + int_store: *?std.StringHashMap(i64), + ref_store: *?std.StringHashMap(Ref), ) void { for (fd.params) |param| { if (!param.is_comptime or param.is_variadic) continue; const arg_node = cpn.get(param.name) orelse continue; // Resolve the param's declared constraint type in the function's own - // defining module (the enum may be bare-visible only there). + // defining module (the enum/union may be bare-visible only there). const constraint_ty = self.resolveParamTypeInSource(fd.body.source_file, ¶m); if (constraint_ty.isBuiltin()) continue; const info = self.module.types.get(constraint_ty); - if (info != .@"enum") continue; // only plain enums carry a bare tag - - // The argument must be a constant enum literal (`.b`). A non-literal - // arg (a runtime value, an arbitrary expression) cannot bind a - // compile-time variant tag — diagnose loudly rather than fabricate one. - if (arg_node.data != .enum_literal) { - if (self.diagnostics) |d| - d.addFmt(.err, arg_node.span, "comptime enum value parameter '{s}' must be a constant enum literal of '{s}'", .{ param.name, self.formatTypeName(constraint_ty) }); - continue; - } - const variant_name = arg_node.data.enum_literal.name; - - // The variant must exist on the constraint enum — otherwise this is a - // typo / wrong-enum literal, not a bindable tag. - const name_id = self.module.types.internString(variant_name); - var known = false; - for (info.@"enum".variants) |v| { - if (v == name_id) { - known = true; - break; - } - } - if (!known) { - self.emitBadEnumVariant(constraint_ty, info.@"enum", variant_name, arg_node.span); - continue; - } - - const tag = self.resolveVariantValue(constraint_ty, variant_name); - - // 1+2: record the constant tag, lazily creating the binding map and - // activating it on `self` (so `comptimeIntNamed`/the downstream lowerer - // can read it). Seed the map from any already-active bindings so an - // outer comptime call's value params stay visible in the inlined body. - if (store.* == null) { - var m = std.StringHashMap(i64).init(self.alloc); - if (self.comptime_value_bindings) |outer| { - var it = outer.iterator(); - while (it.next()) |e| m.put(e.key_ptr.*, e.value_ptr.*) catch {}; - } - store.* = m; - self.comptime_value_bindings = store.*; - } - store.*.?.put(param.name, @intCast(tag)) catch {}; - self.comptime_value_bindings = store.*; - - // 3: bind the param as a scoped enum value so body comparisons lower. - if (self.scope) |scope| { - const enum_val = self.builder.enumInit(tag, Ref.none, constraint_ty); - const slot = self.builder.alloca(constraint_ty); - self.builder.store(slot, enum_val); - scope.put(param.name, .{ .ref = slot, .ty = constraint_ty, .is_alloca = true }); + switch (info) { + .@"enum" => self.bindEnumValueParam(param.name, constraint_ty, info.@"enum", arg_node, int_store), + .tagged_union => self.bindTaggedUnionValueParam(param.name, constraint_ty, info.tagged_union, arg_node, int_store, ref_store), + // Other constraint kinds (`type`-metatype params, generic type + // params, structs/arrays) are not bound here. struct/array + // aggregate value params are not yet wired (no literal-shape repro + // in the corpus drives them); when one lands, add a `.@"struct"` / + // `.array` arm that lowers the literal to an aggregate const and + // writes `ref_store`. Until then we leave the param to whatever + // downstream resolution already applies — never a silent default. + else => {}, } } } +/// Record `param → tag` in `int_store` (lazily creating/activating it, seeded +/// from any outer-active bindings so a surrounding comptime call's value params +/// stay visible). Shared by the enum and tagged-union binders. +pub fn recordComptimeTag(self: *Lowering, store: *?std.StringHashMap(i64), name: []const u8, tag: i64) void { + if (store.* == null) { + var m = std.StringHashMap(i64).init(self.alloc); + if (self.comptime_value_bindings) |outer| { + var it = outer.iterator(); + while (it.next()) |e| m.put(e.key_ptr.*, e.value_ptr.*) catch {}; + } + store.* = m; + self.comptime_value_bindings = store.*; + } + store.*.?.put(name, tag) catch {}; + self.comptime_value_bindings = store.*; +} + +/// Record `param → value Ref` in `ref_store` (lazily creating/activating it, +/// seeded from any outer-active ref bindings). Companion to `recordComptimeTag` +/// for the full materialized value of a non-scalar comptime value param. +pub fn recordComptimeValueRef(self: *Lowering, store: *?std.StringHashMap(Ref), name: []const u8, ref: Ref) void { + if (store.* == null) { + var m = std.StringHashMap(Ref).init(self.alloc); + if (self.comptime_value_ref_bindings) |outer| { + var it = outer.iterator(); + while (it.next()) |e| m.put(e.key_ptr.*, e.value_ptr.*) catch {}; + } + store.* = m; + self.comptime_value_ref_bindings = store.*; + } + store.*.?.put(name, ref) catch {}; + self.comptime_value_ref_bindings = store.*; +} + +/// Bind a payload-less `.@"enum"` comptime value param. The arg must be a +/// constant enum literal (`.b`) naming a known variant; records the tag and +/// scopes an `enum_init(tag, none)`. +pub fn bindEnumValueParam( + self: *Lowering, + name: []const u8, + constraint_ty: TypeId, + enum_info: types.TypeInfo.EnumInfo, + arg_node: *const Node, + int_store: *?std.StringHashMap(i64), +) void { + // The argument must be a constant enum literal (`.b`). A non-literal arg + // (a runtime value, an arbitrary expression) cannot bind a compile-time + // variant tag — diagnose loudly rather than fabricate one. + if (arg_node.data != .enum_literal) { + if (self.diagnostics) |d| + d.addFmt(.err, arg_node.span, "comptime enum value parameter '{s}' must be a constant enum literal of '{s}'", .{ name, self.formatTypeName(constraint_ty) }); + return; + } + const variant_name = arg_node.data.enum_literal.name; + if (!self.enumHasVariant(enum_info.variants, variant_name)) { + self.emitBadEnumVariant(constraint_ty, enum_info, variant_name, arg_node.span); + return; + } + + const tag = self.resolveVariantValue(constraint_ty, variant_name); + self.recordComptimeTag(int_store, name, @intCast(tag)); + + if (self.scope) |scope| { + const enum_val = self.builder.enumInit(tag, Ref.none, constraint_ty); + const slot = self.builder.alloca(constraint_ty); + self.builder.store(slot, enum_val); + scope.put(name, .{ .ref = slot, .ty = constraint_ty, .is_alloca = true }); + } +} + +/// Bind a `.tagged_union` (payload-bearing enum) comptime value param. The arg +/// is one of two constant forms: +/// - a bare variant literal `.point` (no payload), an `.enum_literal` node, or +/// - a payload variant `.circle(5.0)`, a `.call` node whose callee is an +/// `.enum_literal`. +/// Both must name a known variant. Records the variant TAG in `int_store` (so +/// `comptimeIntNamed`/`if s == .circle` work) AND the full materialized +/// `enum_init(tag, payload)` value in `ref_store` + scope (so a payload read off +/// the bound value resolves). A non-constant arg (any other node shape) or an +/// unknown variant is a loud diagnostic. +pub fn bindTaggedUnionValueParam( + self: *Lowering, + name: []const u8, + constraint_ty: TypeId, + union_info: types.TypeInfo.TaggedUnionInfo, + arg_node: *const Node, + int_store: *?std.StringHashMap(i64), + ref_store: *?std.StringHashMap(Ref), +) void { + // Identify the variant name from either accepted literal shape. + const variant_name: []const u8 = switch (arg_node.data) { + .enum_literal => |el| el.name, + .call => |c| if (c.callee.data == .enum_literal) c.callee.data.enum_literal.name else { + if (self.diagnostics) |d| + d.addFmt(.err, arg_node.span, "comptime tagged-union value parameter '{s}' must be a constant variant literal of '{s}' (e.g. `.variant` or `.variant(payload)`)", .{ name, self.formatTypeName(constraint_ty) }); + return; + }, + else => { + if (self.diagnostics) |d| + d.addFmt(.err, arg_node.span, "comptime tagged-union value parameter '{s}' must be a constant variant literal of '{s}' (e.g. `.variant` or `.variant(payload)`)", .{ name, self.formatTypeName(constraint_ty) }); + return; + }, + }; + + if (self.findTaggedVariant(union_info, variant_name) == null) { + self.emitBadVariant(constraint_ty, union_info, variant_name, arg_node.span); + return; + } + + const tag = self.resolveVariantIndex(constraint_ty, variant_name); + self.recordComptimeTag(int_store, name, @intCast(tag)); + + // Materialize the full value by lowering the argument expression with the + // constraint as its target type — `.circle(5.0)` lowers to an + // `enum_init(tag, payload)`, `.point` to `enum_init(tag, none)`. This runs + // in the CALLER's scope/source context (the arg was authored there), which + // is exactly where its payload sub-expressions must resolve. + const saved_target = self.target_type; + self.target_type = constraint_ty; + const value = self.lowerExpr(arg_node); + self.target_type = saved_target; + + self.recordComptimeValueRef(ref_store, name, value); + + if (self.scope) |scope| { + const slot = self.builder.alloca(constraint_ty); + self.builder.store(slot, value); + scope.put(name, .{ .ref = slot, .ty = constraint_ty, .is_alloca = true }); + } +} + +/// True iff `variants` (interned enum variant name-ids) contains `variant_name`. +pub fn enumHasVariant(self: *Lowering, variants: []const types.StringId, variant_name: []const u8) bool { + const name_id = self.module.types.internString(variant_name); + for (variants) |v| { + if (v == name_id) return true; + } + return false; +} + /// True if `node` (a fn body) contains any top-level `return` statement. /// Used by inline-comptime lowering to decide whether to allocate a /// result slot — pure tail-expression bodies skip the slot. Walks past @@ -1104,6 +1222,19 @@ pub fn comptimeIntNamed(self: *Lowering, name: []const u8) ?i64 { return self.foldSourceConstInt(name, null); } +/// Lowering-time accessor for the full materialized value of a NON-scalar +/// comptime value param (a tagged-union literal, a struct/array aggregate): +/// returns the IR `Ref` of the bound value (e.g. the `enum_init(tag, payload)` +/// of a `$s: Shape` bound to `.circle(5.0)`), or null if `name` is not a +/// non-scalar comptime value binding. Companion to `comptimeIntNamed`, which +/// returns the comptime-readable SCALAR (the variant tag) for the same param. +pub fn comptimeValueRefNamed(self: *Lowering, name: []const u8) ?Ref { + if (self.comptime_value_ref_bindings) |crb| { + if (crb.get(name)) |r| return r; + } + return null; +} + /// Source-aware INTEGER fold of a module const `name` (E2/F2/R1). Select the /// SOURCE-AWARE author (own-wins; ≥2 flat-visible → ambiguous → null, the loud /// diagnostic is the reference site's job), then fold ITS RHS with nested const