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

@@ -0,0 +1,24 @@
// Error-set declarations + `error.X` tag values + enum-like `==` typing
// (ERR step E1.1). A declared `error { ... }` set is a real type with a u32
// runtime layout; `error.X` is its tag value — the named set when context
// provides one (membership-checked), else the raw global u32 id. Tags compare
// with an `error.X` literal or another error-set value. The rejections live in
// `examples/218-error-set-typing.sx`.
#import "modules/std.sx";
ParseErr :: error { BadDigit, Overflow, Empty }
main :: () -> s32 {
c : ParseErr = error.BadDigit;
d : ParseErr = error.Overflow;
r : s32 = 0;
if c == error.BadDigit { r = r + 1; } // true -> +1
if c == error.Overflow { r = r + 2; } // false
if c == d { r = r + 4; } // false (BadDigit != Overflow)
if d == error.Overflow { r = r + 8; } // true -> +8
tag : u32 = error.Empty; // u32 context -> raw global tag id
if tag != 0 { r = r + 16; } // tag ids are >= 1 -> +16
print("error-set result: {}\n", r); // -> 25
return r;
}

View File

@@ -0,0 +1,16 @@
// Error-set value + `==` typing rejections (ERR step E1.1):
// - an `error.X` literal must name a tag that is in the destination set,
// - an error-set value compares only with an `error.X` tag or another
// error-set value; comparing to a raw integer is a type error
// (coerce with `xx` to compare the raw id).
// The positive cases live in `examples/217-error-sets.sx`.
#import "modules/std.sx";
ParseErr :: error { BadDigit, Overflow }
main :: () -> s32 {
c : ParseErr = error.NotInSet; // error: NotInSet not in ParseErr
if c == 42 { return 1; } // error: error-set value vs raw integer
return 0;
}

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. // 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. // This ensures null gets the same LLVM type as the value being compared.
if (bop.op == .eq or bop.op == .neq) { 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 { 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 // Pack-arity intercept: `<pack_name>.len` in a pack-fn mono's
// body resolves to the comptime-known N. The mono doesn't // body resolves to the comptime-known N. The mono doesn't
// materialise the `[]Any` slice that the inline path used, so // 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); 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 } /// Lower a tagged enum construction: .Variant.{ field_inits }
/// The struct literal provides the payload fields; we wrap them in an enum_init. /// The struct literal provides the payload fields; we wrap them in an enum_init.
fn lowerTaggedEnumLiteral( 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 { fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 {
return switch (op) { return switch (op) {
.add => "+", .add => "+",

View File

@@ -0,0 +1 @@
25

View File

@@ -0,0 +1 @@
error-set result: 25

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,11 @@
error: error tag 'error.NotInSet' is not in error set 'ParseErr'
--> /Users/agra/projects/sx/examples/218-error-set-typing.sx:13:20
|
13 | c : ParseErr = error.NotInSet; // error: NotInSet not in ParseErr
| ^^^^^^^^^^^^^^
error: an error-set value compares only with an `error.X` tag or another error-set value; coerce with `xx` to compare the raw id
--> /Users/agra/projects/sx/examples/218-error-set-typing.sx:14:8
|
14 | if c == 42 { return 1; } // error: error-set value vs raw integer
| ^