fix(ir): reject non-type elements in tuple-literal-as-type (issue 0067)
`size_of((s32, 1))` treated the tuple literal as a tuple TYPE: for the non-type element `1` it emitted a `std.debug.print` and substituted `.s64` for that field, then compiled and printed a bogus size — a silent fabricated type (the forbidden silent-fallback pattern). Fix: - type_bridge.resolveTupleLiteralAsType: a non-type element now yields `.unresolved` (no `.s64`, no debug print) — it refuses to fabricate a tuple. type_bridge is stateless, so this is the binding-free backstop. - New stateful Lowering.resolveTupleLiteralTypeArg validates each element via isTypeShapedAstNode, emits a user-facing diagnostic at the offending element's span, and returns `.unresolved`. Wired into resolveTypeArg (size_of/align_of/…) and the resolveTypeWithBindings name-fallback; type_bridge builds the tuple only after validation passes. Regression: examples/1116-diagnostics-tuple-type-nontype-element-rejected.sx (exit 1 + diagnostic). Valid `(s32, s32)` still works (0115). Gate: zig build, zig build test, run_examples 351/0.
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
// A tuple literal used in a type position (`(s32, s32)` reinterpreted as a tuple
|
||||||
|
// type at a type-demanding site like `size_of`) must list only types. A non-type
|
||||||
|
// element — here the `1` in `(s32, 1)` — is rejected with a user-facing
|
||||||
|
// diagnostic instead of silently fabricating an `s64` field for that slot.
|
||||||
|
// Regression (issue 0067).
|
||||||
|
// Expected: a clean "tuple type element is not a type" error at the `1`; exit 1.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
print("bad tuple type size = {}\n", size_of((s32, 1)));
|
||||||
|
0
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: tuple type element is not a type (found `int_literal`); a tuple used as a type must list only types, e.g. `(s32, s32)`
|
||||||
|
--> examples/1116-diagnostics-tuple-type-nontype-element-rejected.sx:11:55
|
||||||
|
|
|
||||||
|
11 | print("bad tuple type size = {}\n", size_of((s32, 1)));
|
||||||
|
| ^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
92
issues/0067-tuple-literal-type-nontype-fallback.md
Normal file
92
issues/0067-tuple-literal-type-nontype-fallback.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# 0067 — tuple literal used as a type silently accepts non-type elements
|
||||||
|
|
||||||
|
> **RESOLVED** (2026-06-02).
|
||||||
|
> **Root cause:** `type_bridge.resolveTupleLiteralAsType` treated a tuple literal
|
||||||
|
> as a tuple TYPE and, for any element that wasn't type-shaped, emitted a
|
||||||
|
> `std.debug.print` and substituted `.s64` for that field — a silent fabricated
|
||||||
|
> type (the forbidden silent-fallback pattern). The stateful caller
|
||||||
|
> (`Lowering.resolveTypeArg`, used by `size_of`) delegated `.tuple_literal`
|
||||||
|
> straight to that path, so `size_of((s32, 1))` compiled and printed `16`.
|
||||||
|
> **Fix:**
|
||||||
|
> - `type_bridge.resolveTupleLiteralAsType` now returns `.unresolved` (no `.s64`,
|
||||||
|
> no debug print) when any element is not type-shaped — it refuses to fabricate
|
||||||
|
> a tuple. (type_bridge is stateless, so this is the binding-free backstop.)
|
||||||
|
> - New stateful `Lowering.resolveTupleLiteralTypeArg` validates each element via
|
||||||
|
> `type_bridge.isTypeShapedAstNode`, emits a user-facing diagnostic at the
|
||||||
|
> offending element's span, and returns `.unresolved`. It is wired into BOTH
|
||||||
|
> `resolveTypeArg` (size_of/align_of/…) and the `resolveTypeWithBindings`
|
||||||
|
> name-fallback; type_bridge builds the tuple only after validation passes.
|
||||||
|
> **Regression test:** `examples/1116-diagnostics-tuple-type-nontype-element-rejected.sx`
|
||||||
|
> (exit 1 + diagnostic). Valid `(s32, s32)` still works
|
||||||
|
> (`examples/0115-types-compound-type-in-expression.sx`). Suite 351/0.
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
`size_of((s32, 1))` treats the tuple literal as a tuple TYPE even though `1` is
|
||||||
|
not a type. The compiler prints an internal `type_bridge` debug line, then
|
||||||
|
silently substitutes `.s64` for that slot and compiles successfully.
|
||||||
|
|
||||||
|
Observed:
|
||||||
|
|
||||||
|
```text
|
||||||
|
type_bridge: tuple literal element is not a type (tag=int_literal) — cannot use as tuple type
|
||||||
|
bad tuple type size = 16
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: a user-facing compiler diagnostic rejecting the non-type tuple element,
|
||||||
|
with no fabricated tuple type and no successful run.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
print("bad tuple type size = {}\n", size_of((s32, 1)));
|
||||||
|
0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./zig-out/bin/sx run .sx-tmp/probe-tuple-literal-type-fallback.sx
|
||||||
|
```
|
||||||
|
|
||||||
|
The repro is standalone; the inline source above is sufficient to recreate the
|
||||||
|
scratch file under `.sx-tmp/`.
|
||||||
|
|
||||||
|
## Investigation prompt
|
||||||
|
|
||||||
|
Fix issue 0067: tuple literals reinterpreted as tuple types must reject non-type
|
||||||
|
elements instead of silently fabricating `.s64` fields.
|
||||||
|
|
||||||
|
Suspected area:
|
||||||
|
- `src/ir/type_bridge.zig`, `resolveTupleLiteralAsType`
|
||||||
|
- The current non-type branch does `std.debug.print(...)` and
|
||||||
|
`field_ids.append(alloc, .s64)`, which violates the compiler fallback rules.
|
||||||
|
- Related callers: `type_bridge.resolveAstType` for `.tuple_literal`, and
|
||||||
|
`Lowering.resolveTypeWithBindings` fallback paths that reach `type_bridge`.
|
||||||
|
|
||||||
|
Likely fix:
|
||||||
|
- Replace the `.s64` substitution with a real diagnostic path and an
|
||||||
|
unmistakable failure result (`.unresolved`, or a nullable/result return that
|
||||||
|
forces callers to handle the failure).
|
||||||
|
- Make the diagnostic user-facing via the lowering diagnostics plumbing, not
|
||||||
|
`std.debug.print`.
|
||||||
|
- Preserve the valid behavior pinned by `examples/0115-types-compound-type-in-expression.sx`,
|
||||||
|
where `(s32, s32)` in a type-demanding site resolves as a tuple type.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
- Add a focused diagnostics example in the `11xx` block for
|
||||||
|
`size_of((s32, 1))` expecting exit 1 and a clear diagnostic.
|
||||||
|
- Run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
zig build
|
||||||
|
zig build test
|
||||||
|
bash tests/run_examples.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result: the new invalid tuple-type repro fails with a diagnostic, the
|
||||||
|
valid `0115` tuple-type example still passes, and the full suite remains green.
|
||||||
@@ -11586,6 +11586,25 @@ pub const Lowering = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a tuple LITERAL used in a type position (`(s32, s32)` reinterpreted
|
||||||
|
/// as a tuple type at a type-demanding site such as `size_of`). Every element
|
||||||
|
/// must itself denote a type; a non-type element — e.g. the `1` in
|
||||||
|
/// `(s32, 1)` — is a user error. Emit a diagnostic pointing at the offending
|
||||||
|
/// element and return `.unresolved`; never fabricate a tuple with a bogus
|
||||||
|
/// field (issue 0067). type_bridge.resolveAstType builds the tuple only after
|
||||||
|
/// this validation passes.
|
||||||
|
fn resolveTupleLiteralTypeArg(self: *Lowering, node: *const Node) TypeId {
|
||||||
|
for (node.data.tuple_literal.elements) |el| {
|
||||||
|
if (!type_bridge.isTypeShapedAstNode(el.value, &self.module.types)) {
|
||||||
|
if (self.diagnostics) |diags| {
|
||||||
|
diags.addFmt(.err, el.value.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `(s32, s32)`", .{@tagName(el.value.data)});
|
||||||
|
}
|
||||||
|
return .unresolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map);
|
||||||
|
}
|
||||||
|
|
||||||
fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId {
|
fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId {
|
||||||
// Pack-index access in a type-arg slot (e.g. `type_name($args[0])`
|
// Pack-index access in a type-arg slot (e.g. `type_name($args[0])`
|
||||||
// or `type_eq($args[i], s64)`). Same shape as the
|
// or `type_eq($args[i], s64)`). Same shape as the
|
||||||
@@ -11662,13 +11681,13 @@ pub const Lowering = struct {
|
|||||||
// Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32))
|
// Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32))
|
||||||
return self.resolveTypeCallWithBindings(&cl);
|
return self.resolveTypeCallWithBindings(&cl);
|
||||||
},
|
},
|
||||||
|
.tuple_literal => return self.resolveTupleLiteralTypeArg(node),
|
||||||
.pointer_type_expr,
|
.pointer_type_expr,
|
||||||
.many_pointer_type_expr,
|
.many_pointer_type_expr,
|
||||||
.array_type_expr,
|
.array_type_expr,
|
||||||
.slice_type_expr,
|
.slice_type_expr,
|
||||||
.optional_type_expr,
|
.optional_type_expr,
|
||||||
.function_type_expr,
|
.function_type_expr,
|
||||||
.tuple_literal,
|
|
||||||
=> return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map),
|
=> return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map),
|
||||||
else => return .unresolved,
|
else => return .unresolved,
|
||||||
}
|
}
|
||||||
@@ -12854,6 +12873,10 @@ pub const Lowering = struct {
|
|||||||
switch (node.data) {
|
switch (node.data) {
|
||||||
.type_expr => |te| return self.typeResolver().resolveName(te.name),
|
.type_expr => |te| return self.typeResolver().resolveName(te.name),
|
||||||
.identifier => |id| return self.typeResolver().resolveName(id.name),
|
.identifier => |id| return self.typeResolver().resolveName(id.name),
|
||||||
|
// A non-spread tuple literal in a type position is a tuple-type
|
||||||
|
// literal (`(s32, s32)`); validate its elements are types and reject
|
||||||
|
// non-type elements loudly (issue 0067).
|
||||||
|
.tuple_literal => return self.resolveTupleLiteralTypeArg(node),
|
||||||
else => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map),
|
else => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,20 +276,23 @@ fn resolveTupleSpreadShape(tt: *const ast.TupleTypeExpr, table: *TypeTable, alia
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Treat a tuple value literal as the corresponding tuple TYPE — valid only when
|
// Treat a tuple value literal as the corresponding tuple TYPE — valid only when
|
||||||
// every element is itself a type expression. Non-type elements report a clear
|
// every element is itself a type expression. A non-type element (e.g. the `1`
|
||||||
// diagnostic and degrade to .s64 for that slot (which the snapshot will catch).
|
// in `(s32, 1)`) means this literal is NOT a type: refuse to fabricate a tuple
|
||||||
|
// and return the `.unresolved` sentinel (never `.s64`, which would silently lie
|
||||||
|
// about the size — issue 0067). type_bridge is stateless and has no diagnostics;
|
||||||
|
// the user-facing diagnostic is emitted by the stateful caller
|
||||||
|
// (`Lowering.resolveTupleLiteralTypeArg`), which validates before delegating
|
||||||
|
// here, so the valid path below builds the tuple and the invalid path never
|
||||||
|
// reaches it from lowering. The sentinel is the backstop for any other
|
||||||
|
// (binding-free) caller.
|
||||||
fn resolveTupleLiteralAsType(tl: *const ast.TupleLiteral, table: *TypeTable, alias_map: AliasMap) TypeId {
|
fn resolveTupleLiteralAsType(tl: *const ast.TupleLiteral, table: *TypeTable, alias_map: AliasMap) TypeId {
|
||||||
const alloc = table.alloc;
|
const alloc = table.alloc;
|
||||||
var field_ids = std.ArrayList(TypeId).empty;
|
var field_ids = std.ArrayList(TypeId).empty;
|
||||||
var name_ids_list = std.ArrayList(StringId).empty;
|
var name_ids_list = std.ArrayList(StringId).empty;
|
||||||
var any_named = false;
|
var any_named = false;
|
||||||
for (tl.elements) |el| {
|
for (tl.elements) |el| {
|
||||||
if (!isTypeShapedAstNode(el.value, table)) {
|
if (!isTypeShapedAstNode(el.value, table)) return .unresolved;
|
||||||
std.debug.print("type_bridge: tuple literal element is not a type (tag={s}) — cannot use as tuple type\n", .{@tagName(el.value.data)});
|
field_ids.append(alloc, resolveAstType(el.value, table, alias_map)) catch unreachable;
|
||||||
field_ids.append(alloc, .s64) catch unreachable;
|
|
||||||
} else {
|
|
||||||
field_ids.append(alloc, resolveAstType(el.value, table, alias_map)) catch unreachable;
|
|
||||||
}
|
|
||||||
if (el.name) |n| {
|
if (el.name) |n| {
|
||||||
any_named = true;
|
any_named = true;
|
||||||
name_ids_list.append(alloc, table.internString(n)) catch unreachable;
|
name_ids_list.append(alloc, table.internString(n)) catch unreachable;
|
||||||
|
|||||||
Reference in New Issue
Block a user