comptime value params: generalize to tagged_union (+ aggregate hook)

`$s: <TaggedUnion>` 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.
This commit is contained in:
agra
2026-06-20 09:39:10 +03:00
parent 8144a88a21
commit d7a6857ee1
6 changed files with 277 additions and 83 deletions

View File

@@ -0,0 +1,43 @@
// Comptime TAGGED-UNION value parameter: `$s: <TaggedUnion>` 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
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
1 2 3
0 1 2
42

View File

@@ -309,7 +309,16 @@ pub const Lowering = struct {
struct_instance_bindings: std.StringHashMap(std.StringHashMap(TypeId)), // mangled struct name → type param bindings 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_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) 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_thunk_map: std.StringHashMap([]const FuncId), // "Proto\x00Type" → thunk FuncIds
protocol_vtable_type_map: std.StringHashMap(TypeId), // protocol name → vtable struct TypeId 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 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 lowerComptimeGlobal = lower_comptime.lowerComptimeGlobal;
pub const lowerComptimeSideEffect = lower_comptime.lowerComptimeSideEffect; pub const lowerComptimeSideEffect = lower_comptime.lowerComptimeSideEffect;
pub const lowerComptimeCall = lower_comptime.lowerComptimeCall; 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 lowerInlineComptime = lower_comptime.lowerInlineComptime;
pub const lowerInsertExpr = lower_comptime.lowerInsertExpr; pub const lowerInsertExpr = lower_comptime.lowerInsertExpr;
pub const lowerInsertExprValue = lower_comptime.lowerInsertExprValue; pub const lowerInsertExprValue = lower_comptime.lowerInsertExprValue;

View File

@@ -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 // Bind VALUE-typed comptime params (`$o: Ord`, `$s: Shape`, ...) to their
// variant tag — both into `comptime_value_bindings` (so a downstream // argument's materialized comptime value — both into
// lowerer can read the constant tag for an identifier) AND as a scoped // `comptime_value_bindings` (the comptime-readable scalar: int / enum-tag /
// enum value (so `if o == .a` lowers as an ordinary enum comparison). // tagged-union-tag, so a downstream lowerer can read the constant for an
// Saved/restored around the body so nested comptime calls don't leak the // identifier) AND as a scoped value (so `if o == .a` / `if s == .circle`
// outer call's tags. A non-constant / unknown-variant enum arg is a loud // lowers as an ordinary comparison). Saved/restored around the body so
// diagnostic (never a silent default) per the value-param rules. // nested comptime calls don't leak the outer call's bindings. A
const saved_enum_value_bindings = self.comptime_value_bindings; // non-constant / unknown-variant arg is a loud diagnostic (never a silent
var enum_value_bindings: ?std.StringHashMap(i64) = null; // default) per the value-param rules.
self.bindEnumValueParams(fd, cpn, &enum_value_bindings); 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 { defer {
if (enum_value_bindings) |*evb| evb.deinit(); if (value_bindings) |*vb| vb.deinit();
self.comptime_value_bindings = saved_enum_value_bindings; 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 // 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); return self.builder.constInt(0, .void);
} }
/// Bind ENUM-typed comptime value params (`$o: Ord`) for an inlined comptime /// Bind VALUE-typed comptime params for an inlined comptime call. For each
/// call. For each comptime param whose declared constraint resolves to an /// comptime param whose declared constraint resolves to a value type and whose
/// `.@"enum"` type and whose call-site argument is a constant enum literal /// call-site argument is a constant literal of that type, this materializes the
/// (`.b`), this: /// argument as a compile-time-known value and binds the param so it resolves in
/// 1. folds the literal to its variant tag (declaration-order index, or the /// the body. Two stores are written:
/// explicit value), /// - `int_store` (`comptime_value_bindings`, param → i64): the
/// 2. records `param_name -> tag` in `self.comptime_value_bindings` (lazily /// comptime-readable SCALAR — the int for an `int` param, the variant TAG
/// allocated into `*store` and activated on `self`), so a downstream /// for an `enum` or `tagged_union` param. Preserves the integration
/// lowerer / `#builtin` recognizer can read the constant tag for the /// contract: `comptimeIntNamed(param)` keeps returning the tag/int, and the
/// param identifier via `comptimeIntNamed(param_name)` (or directly off /// param remains usable in a type position (`[o]i64`).
/// `comptime_value_bindings.get(param_name)`), and /// - `ref_store` (`comptime_value_ref_bindings`, param → Ref): the full
/// 3. binds `param_name` into the active scope as an `enum_init(tag, …)` /// materialized value Ref for a non-scalar param (the `enum_init(tag,
/// value of the constraint enum type, so the body's `if o == .a` lowers /// payload)` of a tagged-union, the aggregate const of a struct/array), so
/// as an ordinary enum comparison. /// 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 /// A non-constant argument, or one naming an unknown variant, emits a loud
/// diagnostic and binds nothing — never a silent default. /// diagnostic and binds nothing — never a silent default. Constraint kinds that
pub fn bindEnumValueParams( /// 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, self: *Lowering,
fd: *const ast.FnDecl, fd: *const ast.FnDecl,
cpn: std.StringHashMap(*const Node), cpn: std.StringHashMap(*const Node),
store: *?std.StringHashMap(i64), int_store: *?std.StringHashMap(i64),
ref_store: *?std.StringHashMap(Ref),
) void { ) void {
for (fd.params) |param| { for (fd.params) |param| {
if (!param.is_comptime or param.is_variadic) continue; if (!param.is_comptime or param.is_variadic) continue;
const arg_node = cpn.get(param.name) orelse continue; const arg_node = cpn.get(param.name) orelse continue;
// Resolve the param's declared constraint type in the function's own // 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, &param); const constraint_ty = self.resolveParamTypeInSource(fd.body.source_file, &param);
if (constraint_ty.isBuiltin()) continue; if (constraint_ty.isBuiltin()) continue;
const info = self.module.types.get(constraint_ty); const info = self.module.types.get(constraint_ty);
if (info != .@"enum") continue; // only plain enums carry a bare tag switch (info) {
.@"enum" => self.bindEnumValueParam(param.name, constraint_ty, info.@"enum", arg_node, int_store),
// The argument must be a constant enum literal (`.b`). A non-literal .tagged_union => self.bindTaggedUnionValueParam(param.name, constraint_ty, info.tagged_union, arg_node, int_store, ref_store),
// arg (a runtime value, an arbitrary expression) cannot bind a // Other constraint kinds (`type`-metatype params, generic type
// compile-time variant tag — diagnose loudly rather than fabricate one. // params, structs/arrays) are not bound here. struct/array
if (arg_node.data != .enum_literal) { // aggregate value params are not yet wired (no literal-shape repro
if (self.diagnostics) |d| // in the corpus drives them); when one lands, add a `.@"struct"` /
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) }); // `.array` arm that lowers the literal to an aggregate const and
continue; // writes `ref_store`. Until then we leave the param to whatever
} // downstream resolution already applies — never a silent default.
const variant_name = arg_node.data.enum_literal.name; else => {},
// 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 });
} }
} }
} }
/// 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. /// True if `node` (a fn body) contains any top-level `return` statement.
/// Used by inline-comptime lowering to decide whether to allocate a /// Used by inline-comptime lowering to decide whether to allocate a
/// result slot — pure tail-expression bodies skip the slot. Walks past /// 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); 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 INTEGER fold of a module const `name` (E2/F2/R1). Select the
/// SOURCE-AWARE author (own-wins; ≥2 flat-visible → ambiguous → null, the loud /// 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 /// diagnostic is the reference site's job), then fold ITS RHS with nested const