diff --git a/examples/217-error-sets.sx b/examples/217-error-sets.sx new file mode 100644 index 0000000..12ba87a --- /dev/null +++ b/examples/217-error-sets.sx @@ -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; +} diff --git a/examples/218-error-set-typing.sx b/examples/218-error-set-typing.sx new file mode 100644 index 0000000..442ae23 --- /dev/null +++ b/examples/218-error-set-typing.sx @@ -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; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index d5be070..54b7709 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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: `.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 => "+", diff --git a/tests/expected/217-error-sets.exit b/tests/expected/217-error-sets.exit new file mode 100644 index 0000000..7273c0f --- /dev/null +++ b/tests/expected/217-error-sets.exit @@ -0,0 +1 @@ +25 diff --git a/tests/expected/217-error-sets.txt b/tests/expected/217-error-sets.txt new file mode 100644 index 0000000..a18c8e6 --- /dev/null +++ b/tests/expected/217-error-sets.txt @@ -0,0 +1 @@ +error-set result: 25 diff --git a/tests/expected/218-error-set-typing.exit b/tests/expected/218-error-set-typing.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/218-error-set-typing.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/218-error-set-typing.txt b/tests/expected/218-error-set-typing.txt new file mode 100644 index 0000000..40e2cd8 --- /dev/null +++ b/tests/expected/218-error-set-typing.txt @@ -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 + | ^