comptime: empty-member types are valid for all kinds; keep never-defined declare rejected

A comptime-constructed type with NO members is now VALID for every kind
(empty struct, empty tuple, empty enum, empty tagged_union) — only a bare
`declare("X")` placeholder that is never completed by a matching `define`
stays rejected (it would panic codegen).

- comptime_vm.zig registerTypeVm: drop the blanket "a type with no members
  is never valid" rejection. The per-kind loops are vacuous for an empty
  member list and the dup-name checks stay correct.
- types.zig TaggedUnionInfo: add `defined: bool = true`. Every real
  construction (normal unions, error sets, register_type completion) is
  "defined" by default; only the two declare-PLACEHOLDER sites set it false:
  comptime_vm.declareNominal and lower/comptime.preregisterForwardTypes.
- lower/comptime.checkComptimeTypeResult: reject on `!defined` (never-defined
  placeholder) instead of `fields.len == 0`, so an explicitly-defined empty
  union passes through while a never-completed declare is still gated.
- types.zig typeSizeBytes(tagged_union): floor the payload area at 8 bytes
  when no field carries a payload, mirroring the LLVM lowering — fixes a
  verifySizes panic on an empty/all-void tagged_union (IR sized to tag-only,
  LLVM laid out tag + [8 x i8]).

Tests:
- examples/1179: repurposed from "empty enum rejected" (now valid) to the
  never-defined `declare` case (the remaining rejection); preserves its
  issue-0140 regression role.
- examples/1180 (duplicate variant): still rejected, unchanged output.
- examples/0641 (new): construct empty struct/tuple/enum/tagged_union via
  define/declare; instantiate the constructible ones; exit 0.
This commit is contained in:
agra
2026-06-19 21:41:07 +03:00
parent ccba704378
commit 538349611e
9 changed files with 90 additions and 18 deletions

View File

@@ -420,6 +420,7 @@ fn scanDeclareNames(self: *Lowering, node: *const Node, depth: u32) void {
.name = nid,
.fields = &.{},
.tag_type = .i64,
.defined = false, // forward placeholder — never-completed `declare` stays rejected
} }, 0);
// Bind the name as a type alias too: a `Name :: <ctor>()` decl
// makes `Name` a const_decl author, so a `*Name` self-reference
@@ -544,14 +545,16 @@ pub fn runComptimeTypeFunc(self: *Lowering, func_id: FuncId, span: ast.Span) ?Ty
/// Post-check a comptime type-construction result (shared by the VM and legacy
/// paths). A bare `declare("X")` never completed by a `define(handle, …)` leaves
/// a zero-FIELD nominal slot (an undefined enum); sizing / constructing / emitting
/// it panics at codegen (`verifySizes`: llvm_size != ir_size). Reject it loudly
/// here — a zero-variant enum is never a legitimate result (`defineEnum` rejects
/// an empty variant list too). Returns the type, or null after gating the build.
/// a forward `tagged_union` PLACEHOLDER (`defined == false`); sizing /
/// constructing / emitting it panics at codegen (`verifySizes`: llvm_size !=
/// ir_size). Reject it loudly here. An *explicitly* defined empty type (an empty
/// struct / tuple / enum / tagged_union — `defined == true`, possibly 0 fields)
/// is a legitimate result and passes through. Returns the type, or null after
/// gating the build.
fn checkComptimeTypeResult(self: *Lowering, tid: TypeId, span: ast.Span) ?TypeId {
if (!tid.isBuiltin()) {
const info = self.module.types.get(tid);
if (info == .tagged_union and info.tagged_union.fields.len == 0) {
if (info == .tagged_union and !info.tagged_union.defined) {
if (self.diagnostics) |d|
d.addFmt(.err, span, "type '{s}' is declared but never defined — complete it with define(handle, info)", .{self.module.types.getString(info.tagged_union.name)});
return null;