comptime enum value params: $o: EnumType binds+resolves variant tag

A comptime value param whose constraint is a plain enum ($o: Ord) now
binds its enum-literal argument to the variant tag during inlined
comptime-call lowering. The tag is recorded in comptime_value_bindings
(readable downstream via comptimeIntNamed / direct map lookup, and as an
array-dim style const-int leaf) AND the param is bound into scope as an
enum_init value so body comparisons like 'if o == .a' lower as ordinary
enum comparisons. Distinct ordering args monomorphize the inlined body
per value.

A non-constant argument or an unknown variant emits a loud diagnostic
and binds nothing — never a silent default.

Locked by examples/0627-comptime-enum-value-param.sx.
This commit is contained in:
agra
2026-06-20 09:19:18 +03:00
parent 64c7db5eb1
commit 3c4305f78f
6 changed files with 134 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
// Comptime ENUM value parameter: `$o: <EnumType>` binds the enum-literal
// argument to its variant tag, monomorphizes the inlined body per distinct
// ordering value, and resolves `o` in the body as a compile-time-known enum
// constant — usable both in `if o == .a` comparisons AND as a comptime-readable
// variant tag during lowering (a downstream lowerer reads it via
// `comptime_value_bindings`, exercised here as the `[o]i64` array dimension).
#import "modules/std.sx";
Ord :: enum { a; b; c; }
pick :: ($o: Ord) -> i64 {
if o == .a { return 10; }
if o == .b { return 20; }
return 30;
}
// `o` read as a compile-time integer (its variant tag) in a TYPE position.
tag_dim :: ($o: Ord) -> i64 {
arr : [o]i64 = ---;
return arr.len;
}
main :: () {
print("{}\n", pick(.b)); // 20
print("{} {} {}\n", pick(.a), pick(.b), pick(.c)); // 10 20 30
print("{} {} {}\n", tag_dim(.a), tag_dim(.b), tag_dim(.c)); // 0 1 2
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
20
10 20 30
0 1 2

View File

@@ -1572,6 +1572,7 @@ 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 lowerInlineComptime = lower_comptime.lowerInlineComptime;
pub const lowerInsertExpr = lower_comptime.lowerInsertExpr;
pub const lowerInsertExprValue = lower_comptime.lowerInsertExprValue;

View File

@@ -739,6 +739,21 @@ 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);
defer {
if (enum_value_bindings) |*evb| evb.deinit();
self.comptime_value_bindings = saved_enum_value_bindings;
}
// Install comptime param nodes and lower the function body inline
const saved_cpn = self.comptime_param_nodes;
self.comptime_param_nodes = cpn;
@@ -836,6 +851,92 @@ 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.
/// A non-constant argument, or one naming an unknown variant, emits a loud
/// diagnostic and binds nothing — never a silent default.
pub fn bindEnumValueParams(
self: *Lowering,
fd: *const ast.FnDecl,
cpn: std.StringHashMap(*const Node),
store: *?std.StringHashMap(i64),
) 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).
const constraint_ty = self.resolveParamTypeInSource(fd.body.source_file, &param);
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 });
}
}
}
/// 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