ERR/E1.1 (slice 2): error.X value lowering + enum-like == typing

Completes E1.1. All in ir/lower.zig (the IR layer, per slice 1's finding).

- lowerFieldAccess intercepts `error.X` (parsed as field_access(identifier
  "error", X)) → lowerErrorTagLiteral: interns the tag; when target_type is a
  named error set, types the value as that set and validates X ∈ set (out-of-set
  → diagnostic); otherwise emits the raw u32 global tag id (the spec's
  context-free default — not a silent guess).
- tryLowerErrorSetEquality (early branch in lowerBinaryOp) + errorSetTypeOf /
  isErrorTagLiteralNode: an error-set value or `error.X` literal forces the other
  operand to be one too, else a diagnostic ("compares only with an error.X tag or
  another error-set value; coerce with `xx`"). Both sides lower under the set type
  as context (error.X resolves + membership-checks); two bare tag literals with no
  context compare as global u32 ids. Handles both operand orders.

First ERR examples (end-to-end): 217-error-sets.sx (declared set + error.X +
== true/false + u32 coercion → "error-set result: 25", exit 25) and
218-error-set-typing.sx (out-of-set literal + tag-vs-raw-int → 2 diagnostics).

Failable `!`/`!Named` signatures and raise/try/catch/onfail semantics remain
(E1.2+). zig build, zig build test, and 256/256 examples green.
This commit is contained in:
agra
2026-05-31 17:59:47 +03:00
parent 73232ce170
commit f5974e5846
7 changed files with 150 additions and 0 deletions

View File

@@ -2648,6 +2648,14 @@ pub const Lowering = struct {
}
}
// Error-set equality: an error-set value compares only with an
// `error.X` tag literal or another error-set value. Comparing to a raw
// integer is a type error (coerce with `xx`). `e == error.X` resolves
// X against e's set and validates membership.
if (bop.op == .eq or bop.op == .neq) {
if (self.tryLowerErrorSetEquality(bop)) |result| return result;
}
// Set target_type for null literals to match the other operand's type.
// This ensures null gets the same LLVM type as the value being compared.
if (bop.op == .eq or bop.op == .neq) {
@@ -4346,6 +4354,14 @@ pub const Lowering = struct {
}
fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref {
// `error.X` — an error-tag literal. The `error` keyword in expression
// position parses as identifier "error" (E0.2), so `error.X` is a
// field access we intercept here. `error` is reserved, so this is
// unambiguous (no struct/pack can be named `error`).
if (fa.object.data == .identifier and std.mem.eql(u8, fa.object.data.identifier.name, "error")) {
return self.lowerErrorTagLiteral(fa.field, span);
}
// Pack-arity intercept: `<pack_name>.len` in a pack-fn mono's
// body resolves to the comptime-known N. The mono doesn't
// materialise the `[]Any` slice that the inline path used, so
@@ -4718,6 +4734,35 @@ pub const Lowering = struct {
return self.builder.enumInit(tag, Ref.none, target);
}
/// Lower an `error.X` tag literal to its global tag id (a `u32`). When the
/// destination context (`target_type`) is a named error set, the value is
/// typed as that set and `X`'s membership is validated; otherwise the value
/// is the raw `u32` global tag id (per the spec's context rule).
fn lowerErrorTagLiteral(self: *Lowering, tag_name: []const u8, span: ast.Span) Ref {
const tag_id = self.module.types.internTag(tag_name);
if (self.target_type) |t| {
if (!t.isBuiltin()) {
const info = self.module.types.get(t);
if (info == .error_set) {
var in_set = false;
for (info.error_set.tags) |member| {
if (member == tag_id) {
in_set = true;
break;
}
}
if (!in_set) {
if (self.diagnostics) |diags| {
diags.addFmt(.err, span, "error tag 'error.{s}' is not in error set '{s}'", .{ tag_name, self.module.types.getString(info.error_set.name) });
}
}
return self.builder.constInt(@as(i64, @intCast(tag_id)), t);
}
}
}
return self.builder.constInt(@as(i64, @intCast(tag_id)), .u32);
}
/// Lower a tagged enum construction: .Variant.{ field_inits }
/// The struct literal provides the payload fields; we wrap them in an enum_init.
fn lowerTaggedEnumLiteral(
@@ -14838,6 +14883,57 @@ pub const Lowering = struct {
};
}
/// The named error-set TypeId of `node`'s type, or null if not an
/// error-set-typed expression.
fn errorSetTypeOf(self: *Lowering, node: *const Node) ?TypeId {
const t = self.inferExprType(node);
if (t.isBuiltin()) return null;
return if (self.module.types.get(t) == .error_set) t else null;
}
/// True when `node` is an `error.X` tag literal (`field_access` whose
/// object is the `error` keyword, parsed as identifier "error").
fn isErrorTagLiteralNode(node: *const Node) bool {
if (node.data != .field_access) return false;
const obj = node.data.field_access.object;
return obj.data == .identifier and std.mem.eql(u8, obj.data.identifier.name, "error");
}
/// Lower `==` / `!=` when an error-set value or `error.X` tag is involved.
/// Returns null when neither operand is error-related (general path runs).
/// Both operands must be a tag (an `error.X` literal or an error-set value);
/// otherwise it's a type error (e.g. comparing a tag to a raw integer).
fn tryLowerErrorSetEquality(self: *Lowering, bop: *const ast.BinaryOp) ?Ref {
const l_set = self.errorSetTypeOf(bop.lhs);
const r_set = self.errorSetTypeOf(bop.rhs);
const l_tag = isErrorTagLiteralNode(bop.lhs);
const r_tag = isErrorTagLiteralNode(bop.rhs);
if (l_set == null and r_set == null and !l_tag and !r_tag) return null;
const l_ok = l_set != null or l_tag;
const r_ok = r_set != null or r_tag;
if (!l_ok or !r_ok) {
if (self.diagnostics) |diags| {
diags.addFmt(.err, bop.lhs.span, "an error-set value compares only with an `error.X` tag or another error-set value; coerce with `xx` to compare the raw id", .{});
}
return self.builder.constBool(false);
}
// Lower both sides with the set type as context so an `error.X` literal
// resolves to it (and validates membership). Two bare tag literals with
// no set context lower to global u32 ids (cross-set comparison is OK).
const set_ty = l_set orelse r_set;
const saved = self.target_type;
if (set_ty) |st| self.target_type = st;
const lv = self.lowerExpr(bop.lhs);
const rv = self.lowerExpr(bop.rhs);
self.target_type = saved;
return if (bop.op == .eq)
self.builder.cmpEq(lv, rv)
else
self.builder.emit(.{ .cmp_ne = .{ .lhs = lv, .rhs = rv } }, .bool);
}
fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 {
return switch (op) {
.add => "+",