fix(ir): reject non-type args to the 7 type-introspection builtins [F0.8]
size_of, align_of, field_count, type_name, type_eq, type_is_unsigned,
and is_flags silently reinterpreted a value argument as a type:
type_is_unsigned(6) read 6 as a TypeId index (types[6] = u8 -> true),
size_of(6)/size_of(true) sized its typeof (8), type_name(6) returned
types[6]'s name. Per Agra's ruling, all 7 now strictly require a type
(compile-time): a value argument is a compile error.
One shared guard (Lowering.reflectionTypeArgGuard, run at the top of
tryLowerReflectionCall) classifies each arg via reflectionArgIsType: a
spelled / compile-time type or generic type parameter (isStaticTypeArg),
or a runtime Type value (static type .any -- type_of(x), a []Type
element list[i], a Type-typed local/field/param) is accepted; anything
else is rejected with "<builtin> expects a type, got '<type>'". The
runtime path for type_name / type_is_unsigned is preserved (the {}
formatter calls type_is_unsigned(type_of(val)) at runtime). The 5
comptime-only builtins stay comptime-only (runtime reflection deferred).
Regression: examples/1144-diagnostics-reflection-builtin-needs-type.sx
(reject cases across all 7, exit 1). Unit test: reflectionArgIsType in
lower.test.zig. specs.md / readme.md document the strict type
requirement (and add the previously-undocumented align_of, type_eq,
type_is_unsigned). issues/0090 RESOLVED banner updated.
This commit is contained in:
@@ -10499,6 +10499,13 @@ pub const Lowering = struct {
|
||||
/// Try to lower a call as a reflection builtin (expanded inline during lowering).
|
||||
/// Returns null if the call is not a recognized reflection builtin.
|
||||
fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref {
|
||||
// Strict `$T: Type` guard for the type-introspection builtins. A
|
||||
// value argument (`6`, `true`, `5.2`, a struct) is rejected with a
|
||||
// diagnostic instead of being silently reinterpreted as a TypeId
|
||||
// index / sized via its `typeof` (issue 0090). One shared
|
||||
// classification covers all 7; it runs before dispatch.
|
||||
if (self.reflectionTypeArgGuard(name, c)) |sentinel| return sentinel;
|
||||
|
||||
if (std.mem.eql(u8, name, "size_of")) {
|
||||
// size_of(T) → const_int(sizeof(T))
|
||||
const ty = self.resolveTypeArg(c.args[0]);
|
||||
@@ -10738,6 +10745,80 @@ pub const Lowering = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Strict `$T: Type` classification shared by the 7 type-introspection
|
||||
/// builtins. An argument denotes a type iff it is a spelled /
|
||||
/// compile-time type or generic type parameter (the `isStaticTypeArg`
|
||||
/// shapes), or a runtime `Type` value — which is `.any`-typed at
|
||||
/// runtime (`type_of(x)`, a `[]Type` element `list[i]`, a `Type`-typed
|
||||
/// local / field / param). Any other expression — a value of type
|
||||
/// s64 / f64 / bool / a struct — is NOT a type.
|
||||
pub fn reflectionArgIsType(self: *Lowering, arg: *const Node) bool {
|
||||
if (self.isStaticTypeArg(arg)) return true;
|
||||
return self.inferExprType(arg) == .any;
|
||||
}
|
||||
|
||||
/// Guard for the type-introspection builtins (`size_of`, `align_of`,
|
||||
/// `field_count`, `type_name`, `type_eq`, `type_is_unsigned`,
|
||||
/// `is_flags`): every argument must denote a type. A value argument is
|
||||
/// rejected with a diagnostic rather than silently reinterpreted as a
|
||||
/// TypeId index or sized via its `typeof` (issue 0090).
|
||||
///
|
||||
/// Returns null when `name` is not a guarded builtin OR every argument
|
||||
/// is a type (→ fall through to normal dispatch). Returns a harmless
|
||||
/// result-typed sentinel Ref when a violation was diagnosed; the
|
||||
/// emitted `.err` gates the build so the value is never observed.
|
||||
fn reflectionTypeArgGuard(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref {
|
||||
const arity: usize = if (std.mem.eql(u8, name, "type_eq"))
|
||||
2
|
||||
else if (std.mem.eql(u8, name, "size_of") or
|
||||
std.mem.eql(u8, name, "align_of") or
|
||||
std.mem.eql(u8, name, "field_count") or
|
||||
std.mem.eql(u8, name, "type_name") or
|
||||
std.mem.eql(u8, name, "type_is_unsigned") or
|
||||
std.mem.eql(u8, name, "is_flags"))
|
||||
1
|
||||
else
|
||||
return null;
|
||||
|
||||
var ok = true;
|
||||
if (c.args.len != arity) {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, c.callee.span, "{s} expects {d} type argument{s}, got {d}", .{
|
||||
name, arity, if (arity == 1) @as([]const u8, "") else "s", c.args.len,
|
||||
});
|
||||
}
|
||||
ok = false;
|
||||
} else {
|
||||
for (c.args) |a| {
|
||||
if (self.reflectionArgIsType(a)) continue;
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, a.span, "{s} expects a type, got '{s}'", .{
|
||||
name, self.formatTypeName(self.inferExprType(a)),
|
||||
});
|
||||
}
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok) return null;
|
||||
return self.reflectionErrorSentinel(name);
|
||||
}
|
||||
|
||||
/// Result-typed placeholder returned after `reflectionTypeArgGuard`
|
||||
/// diagnoses a non-type argument: a string for `type_name`, a bool for
|
||||
/// the predicate builtins, an int for the size / count builtins. Never
|
||||
/// observed at runtime — the diagnostic already fails the build — but
|
||||
/// keeps the IR well-typed so lowering can finish and report every
|
||||
/// error in one pass.
|
||||
fn reflectionErrorSentinel(self: *Lowering, name: []const u8) Ref {
|
||||
if (std.mem.eql(u8, name, "type_name"))
|
||||
return self.builder.constString(self.module.types.internString(""));
|
||||
if (std.mem.eql(u8, name, "type_eq") or
|
||||
std.mem.eql(u8, name, "type_is_unsigned") or
|
||||
std.mem.eql(u8, name, "is_flags"))
|
||||
return self.builder.constBool(false);
|
||||
return self.builder.constInt(0, .s64);
|
||||
}
|
||||
|
||||
/// Resolve a type argument from a call expression. Handles:
|
||||
/// - Type param bindings ($T → concrete type via type_bindings)
|
||||
/// - Direct type names (Vec4 → lookup in TypeTable)
|
||||
|
||||
Reference in New Issue
Block a user