diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 23af4cc..1dee575 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -33,6 +33,7 @@ const ErrorAnalysis = @import("error_analysis.zig").ErrorAnalysis; const ErrorFlow = @import("error_flow.zig").ErrorFlow; const ObjcLowering = @import("ffi_objc.zig").ObjcLowering; const semantic_diagnostics = @import("semantic_diagnostics.zig"); +const lower_error = @import("lower/error.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -2880,7 +2881,7 @@ pub const Lowering = struct { // ── Statement lowering ────────────────────────────────────────── - fn lowerBlock(self: *Lowering, node: *const Node) void { + pub fn lowerBlock(self: *Lowering, node: *const Node) void { switch (node.data) { .block => |blk| { // Create a child scope for block-level variable shadowing @@ -2928,7 +2929,7 @@ pub const Lowering = struct { } /// Lower a block and return the last expression's value (for implicit returns). - fn lowerBlockValue(self: *Lowering, node: *const Node) ?Ref { + pub fn lowerBlockValue(self: *Lowering, node: *const Node) ?Ref { // Set force_block_value so nested if-else expressions produce values const saved = self.force_block_value; self.force_block_value = true; @@ -3844,7 +3845,7 @@ pub const Lowering = struct { // ── Expression lowering ───────────────────────────────────────── - fn lowerExpr(self: *Lowering, node: *const Node) Ref { + pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { // Stamp this node's source span onto the instructions it emits (ERR // E3.0 — feeds DWARF line-info + comptime frame resolution). Save/ // restore so a parent's later emits keep the parent's span after a @@ -10040,7 +10041,7 @@ pub const Lowering = struct { /// error tag, bound to each `onfail e`'s binding. Does not truncate — the /// terminating `ret` + the unwinding block-scope `emitBlockDefers` (which /// then see the terminator and skip) leave the stack consistent. - fn emitErrorCleanup(self: *Lowering, base: usize, err_tag: Ref) void { + pub fn emitErrorCleanup(self: *Lowering, base: usize, err_tag: Ref) void { if (base > self.defer_stack.items.len) return; const tag_ty = self.builder.getRefType(err_tag); const stack = self.defer_stack.items; @@ -13375,11 +13376,11 @@ pub const Lowering = struct { // ── Block helpers ─────────────────────────────────────────────── - fn freshBlock(self: *Lowering, prefix: []const u8) BlockId { + pub fn freshBlock(self: *Lowering, prefix: []const u8) BlockId { return self.freshBlockWithParams(prefix, &.{}); } - fn freshBlockWithParams(self: *Lowering, prefix: []const u8, params: []const TypeId) BlockId { + pub fn freshBlockWithParams(self: *Lowering, prefix: []const u8, params: []const TypeId) BlockId { var buf: [64]u8 = undefined; const name = std.fmt.bufPrint(&buf, "{s}.{d}", .{ prefix, self.block_counter }) catch prefix; self.block_counter += 1; @@ -13387,7 +13388,7 @@ pub const Lowering = struct { return self.builder.appendBlock(name_id, params); } - fn currentBlockHasTerminator(self: *Lowering) bool { + pub fn currentBlockHasTerminator(self: *Lowering) bool { const func = self.builder.module.getFunctionMut(self.builder.func.?); const block_idx = self.builder.current_block orelse return true; const block = &func.blocks.items[block_idx.index()]; @@ -16018,70 +16019,6 @@ pub const Lowering = struct { return .{ .get = self.jni_env_tl_get_fid.?, .set = self.jni_env_tl_set_fid.? }; } - /// Lazily declare the `sx_trace_push(u64)` / `sx_trace_clear()` runtime - /// externs (ERR E3.1). Storage is a `_Thread_local` ring buffer in - /// `library/vendors/sx_trace_runtime/sx_trace.c` — kept OUT of the user's IR - /// module (same JIT-TLS reason as the JNI env slot). Setting - /// `needs_trace_runtime` signals Compilation to auto-link the .c for AOT. - /// Wired into the `raise` / `try` push sites and the absorbing clear sites - /// at ERR E3.2. - fn getTraceFids(self: *Lowering) struct { push: FuncId, clear: FuncId } { - self.needs_trace_runtime = true; - if (self.trace_push_fid == null) { - const name = self.module.types.internString("sx_trace_push"); - const frame_param = self.module.types.internString("frame"); - var params = std.ArrayList(inst_mod.Function.Param).empty; - params.append(self.alloc, .{ .name = frame_param, .ty = .u64 }) catch unreachable; - const fid = self.builder.declareExtern(name, params.toOwnedSlice(self.alloc) catch unreachable, .void); - self.module.getFunctionMut(fid).call_conv = .c; - self.trace_push_fid = fid; - } - if (self.trace_clear_fid == null) { - const name = self.module.types.internString("sx_trace_clear"); - const fid = self.builder.declareExtern(name, &.{}, .void); - self.module.getFunctionMut(fid).call_conv = .c; - self.trace_clear_fid = fid; - } - return .{ .push = self.trace_push_fid.?, .clear = self.trace_clear_fid.? }; - } - - /// Error return-traces are emitted in debug-ish builds and skipped in - /// release (ERR E3.2 build-mode gating). `sx run` defaults to `-O0` - /// (`.none`), the common dev path; `.default`/`.aggressive` are release. - /// The spec's `--release-traces` opt-in + a `BuildOptions.error_traces` - /// accessor are a later refinement; for now the opt level is the gate. - fn tracesEnabled(self: *Lowering) bool { - const tc = self.target_config orelse return true; // no target → treat as debug - return tc.opt_level == .none or tc.opt_level == .less; - } - - /// Emit a trace-buffer push of `frame` (an opaque u64) at a failure site. - /// No-op when traces are disabled (release). `frame` is a placeholder until - /// DWARF (E3.0) supplies real return-address PCs and E3.3 resolves them. - fn emitTracePush(self: *Lowering, frame: Ref) void { - if (!self.tracesEnabled()) return; - const fids = self.getTraceFids(); - const coerced = self.coerceToType(frame, self.builder.getRefType(frame), .u64); - const args = self.alloc.dupe(Ref, &.{coerced}) catch return; - _ = self.builder.emit(.{ .call = .{ .callee = fids.push, .args = args } }, .void); - } - - /// Emit a trace-buffer clear at an absorbing site (`catch` / `or value` / - /// destructure). No-op when traces are disabled. - fn emitTraceClear(self: *Lowering) void { - if (!self.tracesEnabled()) return; - const fids = self.getTraceFids(); - _ = self.builder.emit(.{ .call = .{ .callee = fids.clear, .args = &.{} } }, .void); - } - - /// The trace frame value for a failure site (ERR E3.0 slice 3a). Emits the - /// niladic `.trace_frame` op (span-stamped via `Builder.current_span`); each - /// backend resolves it to a real frame — `emit_llvm` to a `Frame*`, `interp` - /// to a packed `(func_id, offset)`. The result feeds `sx_trace_push`. - fn placeholderTraceFrame(self: *Lowering) Ref { - return self.builder.emit(.{ .trace_frame = {} }, .u64); - } - /// When a namespaced import (`Ns :: #import "..."`) contains foreign-class /// declarations, ALSO register them under their qualified name `Ns.Class` /// so receiver types like `*Ns.Class` can find the fcd. The recursive @@ -17295,7 +17232,7 @@ pub const Lowering = struct { /// Handles int widening/narrowing, float widening/narrowing, and int↔float. /// IMPLICIT coercion — the typed-binding initializer path. A compile-time /// float narrowing to an integer folds when integral, errors when not. - fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref { + pub fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref { return self.coerceMode(val, src_ty, dst_ty, .implicit); } @@ -17524,1044 +17461,6 @@ 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"). - pub 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); - } - - /// The declared return type of the function currently being lowered (the - /// inlined body's type wins while inlining a comptime call), or null when - /// there is no enclosing function. - fn effectiveReturnType(self: *Lowering) ?TypeId { - if (self.inline_return_target) |iri| return iri.ret_ty; - if (self.builder.func) |fid| return self.module.functions.items[@intFromEnum(fid)].ret; - return null; - } - - /// If `ret_ty` belongs to a failable function, the TypeId of its error - /// channel; else null. `-> !Named` / `-> !` resolve the error set directly; - /// `-> (T..., !)` carries it as the last tuple field (the locked ABI). - pub fn errorChannelOf(self: *Lowering, ret_ty: TypeId) ?TypeId { - if (ret_ty.isBuiltin()) return null; - switch (self.module.types.get(ret_ty)) { - .error_set => return ret_ty, - .tuple => |t| { - if (t.fields.len == 0) return null; - const last = t.fields[t.fields.len - 1]; - if (last.isBuiltin()) return null; - return if (self.module.types.get(last) == .error_set) last else null; - }, - else => return null, - } - } - - /// True for the bare-`!` inferred placeholder error set (reserved name "!"). - fn isInferredErrorSet(self: *Lowering, set: TypeId) bool { - if (set.isBuiltin()) return false; - const info = self.module.types.get(set); - if (info != .error_set) return false; - return std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!"); - } - - /// Diagnose every tag of `src` that is not also a member of `dst` (the - /// enclosing function's named error set). Both must be `.error_set` types. - fn checkErrorSetSubset(self: *Lowering, src: TypeId, dst: TypeId, span: ast.Span) void { - if (src.isBuiltin()) return; - const src_info = self.module.types.get(src); - if (src_info != .error_set) return; - self.diagTagsNotInSet(src_info.error_set.tags, dst, span); - } - - /// Diagnose every tag id in `src_tags` that is not a member of the named - /// error set `dst`. Shared by the named-set subset check and E1.4b's - /// inferred-callee widening (where the callee's tags come from the SCC, - /// not a `.error_set` TypeId). - fn diagTagsNotInSet(self: *Lowering, src_tags: []const u32, dst: TypeId, span: ast.Span) void { - if (dst.isBuiltin()) return; - const dst_info = self.module.types.get(dst); - if (dst_info != .error_set) return; - for (src_tags) |tag| { - var found = false; - for (dst_info.error_set.tags) |d| { - if (d == tag) { - found = true; - break; - } - } - if (!found) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "error tag 'error.{s}' is not in caller's error set '{s}'", .{ self.module.types.getTagName(tag), self.module.types.getString(dst_info.error_set.name) }); - } - } - } - } - - /// `raise EXPR;` — terminate the enclosing failable function via the error - /// channel. E1.3 lowers the **pure-failable** shape (`-> !` / `-> !Named`, - /// whose return type IS the error set): emit `ret(EXPR)`. The value-carrying - /// shape (`-> (T..., !)`) needs the value slots set to `undef` alongside the - /// error slot — that tuple ABI lands in E2.1/E2.2, so we bail loudly here - /// rather than ship a half-built return that silently corrupts value slots. - fn lowerRaise(self: *Lowering, rs: *const ast.RaiseStmt, span: ast.Span) void { - // (1) `raise` is legal only inside a failable function. - const ret_ty = self.effectiveReturnType() orelse { - self.diagRaiseNotFailable(span); - return; - }; - const err_set = self.errorChannelOf(ret_ty) orelse { - self.diagRaiseNotFailable(span); - return; - }; - const inferred = self.isInferredErrorSet(err_set); - - // (2) Set check. Lowering EXPR with the function's error set as the - // target type makes a literal `raise error.X` validate `X ∈ set` - // inside lowerErrorTagLiteral (the inferred placeholder accepts any - // tag). The variable form `raise e` is subset-checked below. - const saved_target = self.target_type; - self.target_type = err_set; - const tag_ref = self.lowerExpr(rs.tag); - self.target_type = saved_target; - - if (!inferred and !isErrorTagLiteralNode(rs.tag)) { - if (self.errorSetTypeOf(rs.tag)) |src_set| { - self.checkErrorSetSubset(src_set, err_set, span); - } - } - - // (3) Push a trace frame: `raise` always escapes the function (ERR E3.2). - // Before cleanup, so the frame records the raise site itself. - self.emitTracePush(self.placeholderTraceFrame()); - - // (4) Emit the failure return. Pure-failable: the return type IS the - // error set, so return the tag value directly. - if (ret_ty == err_set) { - const tag_ty = self.builder.getRefType(tag_ref); - const coerced = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref; - self.emitErrorCleanup(self.func_defer_base, coerced); - if (self.inline_return_target) |iri| { - self.builder.store(iri.slot, coerced); - self.builder.br(iri.done_bb, &.{}); - } else { - self.builder.ret(coerced, err_set); - } - } else { - // Value-carrying `-> (T..., !)`: the error path leaves the value - // slots undefined and carries the tag in the error slot (ERR E2.1). - const tag_ty = self.builder.getRefType(tag_ref); - const coerced_tag = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref; - self.emitErrorCleanup(self.func_defer_base, coerced_tag); - const fields = self.module.types.get(ret_ty).tuple.fields; - var slots = std.ArrayList(Ref).empty; - defer slots.deinit(self.alloc); - for (fields[0 .. fields.len - 1]) |vty| { - slots.append(self.alloc, self.builder.constUndef(vty)) catch unreachable; - } - const tup = self.buildFailableTuple(ret_ty, slots.items, coerced_tag); - self.emitTupleRet(ret_ty, tup); - } - } - - /// Return a value-carrying failable function's success tuple - /// `{value(s)..., 0}` from `ref` (the user-returned value part). Forwarding - /// a full failable tuple (`return other_failable()` / explicit `return - /// (v, e)`) returns it as-is. Single-value `-> (T, !)` takes `ref` as the - /// lone value; multi-value `-> (T1, ..., !)` takes `ref` as a value-tuple - /// `(T1, ...)` and re-assembles its slots alongside the success error slot. - fn lowerFailableSuccessReturn(self: *Lowering, ref: Ref, ret_ty: TypeId, span: ast.Span) void { - const fields = self.module.types.get(ret_ty).tuple.fields; - const err_ty = fields[fields.len - 1]; - const val_ty = self.builder.getRefType(ref); - if (val_ty == ret_ty) { - // The expression already IS the full failable tuple (forwarding). - self.emitTupleRet(ret_ty, ref); - return; - } - const n_vals = fields.len - 1; - if (n_vals == 1) { - const cv = self.coerceToType(ref, val_ty, fields[0]); - const tup = self.buildFailableTuple(ret_ty, &.{cv}, self.builder.constInt(0, err_ty)); - self.emitTupleRet(ret_ty, tup); - return; - } - // Multi-value: `ref` must be a value-tuple `(T1, ..., Tn)`. Extract - // each value slot, coerce to the declared field type, and re-assemble - // with the success error slot (0). - if (val_ty.isBuiltin() or self.module.types.get(val_ty) != .tuple or self.module.types.get(val_ty).tuple.fields.len != n_vals) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "a multi-value failable function (`-> (T1, ..., !)`) must `return` a {d}-tuple of its value types", .{n_vals}); - } - return; - } - const vfields = self.module.types.get(val_ty).tuple.fields; - var vals = std.ArrayList(Ref).empty; - defer vals.deinit(self.alloc); - for (0..n_vals) |i| { - const fv = self.builder.emit(.{ .tuple_get = .{ .base = ref, .field_index = @intCast(i), .base_type = val_ty } }, vfields[i]); - vals.append(self.alloc, self.coerceToType(fv, vfields[i], fields[i])) catch unreachable; - } - const tup = self.buildFailableTuple(ret_ty, vals.items, self.builder.constInt(0, err_ty)); - self.emitTupleRet(ret_ty, tup); - } - - /// Build a failable return tuple `{value_refs..., tag}` typed `ret_ty`. - fn buildFailableTuple(self: *Lowering, ret_ty: TypeId, value_refs: []const Ref, tag: Ref) Ref { - var fields = std.ArrayList(Ref).empty; - defer fields.deinit(self.alloc); - fields.appendSlice(self.alloc, value_refs) catch unreachable; - fields.append(self.alloc, tag) catch unreachable; - return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, fields.items) catch unreachable } }, ret_ty); - } - - /// The success (value-part) type of a value-carrying failable tuple - /// `op_ty` (`-> (T..., !)`): the lone value type for a single-value - /// failable, or a synthesized value-tuple `(T1, ..., Tn)` (error slot - /// dropped) for a multi-value one. Callers must pass a value-carrying - /// tuple — a pure `-> !`'s success type is `void`, handled separately. - pub fn failableSuccessType(self: *Lowering, op_ty: TypeId) TypeId { - const fields = self.module.types.get(op_ty).tuple.fields; - const n_vals = fields.len - 1; - if (n_vals == 1) return fields[0]; - return self.module.types.intern(.{ .tuple = .{ - .fields = self.alloc.dupe(TypeId, fields[0..n_vals]) catch unreachable, - .names = null, - } }); - } - - /// The `target_type` to lower a returned expression against. For a - /// value-carrying failable (`-> (T..., !)`) a BARE returned value resolves - /// against the success value type (so a bare enum literal gets its real - /// ordinal); an EXPLICIT full failable tuple literal (`return (v..., e)`, - /// arity == full-tuple field count) keeps the failable-tuple target so its - /// trailing error element resolves against the error set and is forwarded - /// as-is. Every other return type passes through unchanged. - fn failableReturnTarget(self: *Lowering, ret_ty: TypeId, value_node: ?*const Node) TypeId { - if (ret_ty.isBuiltin()) return ret_ty; - if (self.module.types.get(ret_ty) != .tuple) return ret_ty; - if (self.errorChannelOf(ret_ty) == null) return ret_ty; - if (value_node) |vn| { - if (vn.data == .tuple_literal and - vn.data.tuple_literal.elements.len == self.module.types.get(ret_ty).tuple.fields.len) - return ret_ty; - } - return self.failableSuccessType(ret_ty); - } - - /// Extract the success value from an evaluated value-carrying failable - /// tuple `result` (type `op_ty`): the lone value slot for single-value, - /// or an assembled value-tuple (typed `succ_ty`) for multi-value. - fn extractSuccessValue(self: *Lowering, result: Ref, op_ty: TypeId, succ_ty: TypeId) Ref { - const fields = self.module.types.get(op_ty).tuple.fields; - const n_vals = fields.len - 1; - if (n_vals == 1) { - return self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = op_ty } }, fields[0]); - } - var vals = std.ArrayList(Ref).empty; - defer vals.deinit(self.alloc); - for (0..n_vals) |i| { - vals.append(self.alloc, self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = @intCast(i), .base_type = op_ty } }, fields[i])) catch unreachable; - } - return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, vals.items) catch unreachable } }, succ_ty); - } - - /// Extract the error slot (always the last field) of an evaluated - /// value-carrying failable tuple `result`, typed as `err_set`. - fn extractErrorSlot(self: *Lowering, result: Ref, op_ty: TypeId, err_set: TypeId) Ref { - const fields = self.module.types.get(op_ty).tuple.fields; - return self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = @intCast(fields.len - 1), .base_type = op_ty } }, err_set); - } - - /// Emit a return of an already-assembled tuple, honoring inline-comptime - /// return targets (store + branch) vs a real function return. - fn emitTupleRet(self: *Lowering, ret_ty: TypeId, tup: Ref) void { - if (self.inline_return_target) |iri| { - self.builder.store(iri.slot, tup); - self.builder.br(iri.done_bb, &.{}); - } else { - self.builder.ret(tup, ret_ty); - } - } - - fn diagRaiseNotFailable(self: *Lowering, span: ast.Span) void { - if (self.diagnostics) |diags| { - if (self.in_lambda_body) { - diags.addFmt(.err, span, "lambda body raises; declare its return type explicitly with `-> (T, !)` or `-> (T, !Named)`", .{}); - } else { - diags.addFmt(.err, span, "`raise` is only valid inside a failable function (a return type with `!` or `!Named`)", .{}); - } - } - } - - /// True if `node`'s value is failable — a `try` (the result is its - /// operand's success value, but the expression itself routes an error) or - /// any expression whose type carries an error channel (a bare failable - /// call). Used to detect failable `or` chains (deferred to E1.4b). - pub fn exprIsFailable(self: *Lowering, node: *const Node) bool { - if (node.data == .try_expr) return true; - return self.errorChannelOf(self.inferExprType(node)) != null; - } - - /// `try X` — a fallible attempt (ERR step E1.4a: the STANDALONE form, whose - /// failure target is function-propagation). Evaluates X; on failure, runs - /// the function's defers and returns the error to the caller; on success, - /// continues with X's value. E1.4a lowers the pure-failable shape (callee - /// `-> !` / `-> !Named`, caller likewise pure-failable). Value-carrying - /// callees, propagation from a value-carrying caller, and `try` inside an - /// `or` chain need the error-channel tuple ABI / fallback routing — those - /// land in E1.4b/E2, so we bail loudly here. - /// Synthesize a `Source_Location` value for a `#caller_location` marker - /// (ERR E4.1b). The node's `span`/`source_file` are the CALL site (rewritten - /// by `expandCallDefaults`); resolve them to file / line:col against the - /// source text and stamp the enclosing (caller) function name. - fn lowerCallerLocation(self: *Lowering, node: *const Node) Ref { - const sl_tid = self.module.types.findByName(self.module.types.internString("Source_Location")) orelse { - if (self.diagnostics) |d| d.addFmt(.err, node.span, "`#caller_location` needs `Source_Location` (from std.sx) in scope", .{}); - return self.builder.constInt(0, .void); - }; - const file = node.source_file orelse self.current_source_file orelse (self.main_file orelse ""); - const src = self.sourceForFile(file); - const loc = errors.SourceLoc.compute(src, node.span.start); - const func_name = self.currentFunctionName(); - var fields = [_]Ref{ - self.builder.constString(self.module.types.internString(file)), - self.builder.constInt(@intCast(loc.line), .s32), - self.builder.constInt(@intCast(loc.col), .s32), - self.builder.constString(self.module.types.internString(func_name)), - }; - return self.builder.emit(.{ .struct_init = .{ .fields = self.alloc.dupe(Ref, &fields) catch unreachable } }, sl_tid); - } - - /// The source text for `file`, via the diagnostics' file→source map (which - /// includes the main file). Empty if unavailable — line:col then degrade to - /// 1:1 rather than crash. - fn sourceForFile(self: *Lowering, file: []const u8) []const u8 { - const diags = self.diagnostics orelse return ""; - if (diags.import_sources) |is| { - if (is.get(file)) |s| return s; - } - return diags.source; - } - - /// Name of the function currently being lowered (the caller, at a - /// `#caller_location` site), or "" outside any function. - fn currentFunctionName(self: *Lowering) []const u8 { - const fid = self.builder.func orelse return ""; - return self.module.types.getString(self.module.functions.items[@intFromEnum(fid)].name); - } - - fn lowerTry(self: *Lowering, operand: *const Node, span: ast.Span) Ref { - // (1) `try` is legal only inside a failable function. - const caller_ret = self.effectiveReturnType() orelse { - self.diagTryNotFailable(span); - return self.builder.constInt(0, .void); - }; - const caller_set = self.errorChannelOf(caller_ret) orelse { - self.diagTryNotFailable(span); - return self.builder.constInt(0, .void); - }; - - // (2) The operand must be failable. This is the sole failable-operand - // check (the parser imposes none — see E0.2). - const op_ty = self.inferExprType(operand); - const callee_set = self.errorChannelOf(op_ty) orelse { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "`try` requires a failable expression; operand has type '{s}'", .{self.formatTypeName(op_ty)}); - } - return self.builder.constInt(0, .void); - }; - - // A value-carrying callee (`-> (T..., !)`) returns a tuple - // `{v..., err}`; a pure-failable callee (`-> !`) returns the bare - // error tag. - const callee_value_carrying = op_ty != callee_set; - - // (3) Widening: the callee's escape set must be ⊆ the caller's named - // set. For an inferred caller (`!`) the absorption happens in the - // whole-program SCC (E1.4b) — no check here. - self.checkEscapeWidening(operand, callee_set, caller_set, span); - - // (4) Lower: evaluate the operand, then branch on its error tag (which - // is the bare result for a pure callee, or the last tuple slot for - // a value-carrying one). - const result = self.lowerExpr(operand); - const err_val = if (callee_value_carrying) - self.extractErrorSlot(result, op_ty, callee_set) - else - result; - const err_ty = self.builder.getRefType(err_val); - const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool); - - const prop_bb = self.freshBlock("try.prop"); - const ok_bb = self.freshBlock("try.ok"); - self.builder.condBr(is_err, prop_bb, &.{}, ok_bb, &.{}); - - // Propagation: push a trace frame (this `try` failure escapes to the - // caller — ERR E3.2), run the function's cleanups (defers + onfails, - // since this is an error exit), then return the caller's failure - // carrying this tag (pure caller → `ret(tag)`; value-carrying → - // `ret {undef…, tag}`). - self.builder.switchToBlock(prop_bb); - self.emitTracePush(self.placeholderTraceFrame()); - self.emitErrorCleanup(self.func_defer_base, err_val); - self.emitErrorReturn(caller_ret, caller_set, err_val); - - // Success: a value-carrying callee yields its value part (the lone - // value, or a value-tuple); a pure-failable callee has no value (void). - self.builder.switchToBlock(ok_bb); - if (callee_value_carrying) { - const succ_ty = self.failableSuccessType(op_ty); - return self.extractSuccessValue(result, op_ty, succ_ty); - } - return self.builder.constInt(0, .void); - } - - /// Return the enclosing function's failure carrying error tag `err`. A - /// pure-failable caller (`-> !`) returns the tag directly; a value-carrying - /// caller (`-> (T..., !)`) returns `{undef value slots..., tag}`. Honors - /// inline-comptime return targets. The caller emits defers first. - fn emitErrorReturn(self: *Lowering, caller_ret: TypeId, caller_set: TypeId, err: Ref) void { - const ety = self.builder.getRefType(err); - const coerced = if (ety != caller_set) self.coerceToType(err, ety, caller_set) else err; - if (caller_ret == caller_set) { - if (self.inline_return_target) |iri| { - self.builder.store(iri.slot, coerced); - self.builder.br(iri.done_bb, &.{}); - } else { - self.builder.ret(coerced, caller_set); - } - } else { - const fields = self.module.types.get(caller_ret).tuple.fields; - var undefs = std.ArrayList(Ref).empty; - defer undefs.deinit(self.alloc); - for (fields[0 .. fields.len - 1]) |vty| { - undefs.append(self.alloc, self.builder.constUndef(vty)) catch unreachable; - } - const tup = self.buildFailableTuple(caller_ret, undefs.items, coerced); - self.emitTupleRet(caller_ret, tup); - } - } - - fn diagTryNotFailable(self: *Lowering, span: ast.Span) void { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "`try` is only valid inside a failable function (a return type with `!` or `!Named`)", .{}); - } - } - - /// `expr catch [e] BODY` — inline failure handler (ERR step E1.5, - /// pure-failable slice). Evaluates `expr`; on failure, binds the tag to - /// `e` (if present) and runs BODY; on success, the value is `void` (a - /// pure-failable LHS has no success value). BODY either diverges (via - /// `noreturn` — E1.4c) or falls through. `catch` consumes the error - /// locally, so — unlike `try` / `raise` — it needs no failable *enclosing* - /// function. Value-carrying LHS (binding the success value / a - /// value-producing body unifying with the success tuple) needs the - /// error-channel tuple ABI and lands in E2 — bail loudly here. - fn lowerCatch(self: *Lowering, ce: *const ast.CatchExpr, span: ast.Span) Ref { - // A failable `or` chain operand (`(try a or try b) catch e …`) routes - // its total failure to the catch handler — not the function — via the - // chain-fail target (ERR E2.4). A chain's value type is non-failable - // `T`, so it wouldn't pass the `errorChannelOf` check below. - if (ce.operand.data == .binary_op and ce.operand.data.binary_op.op == .or_op and - self.orIsFailableChain(&ce.operand.data.binary_op)) - { - return self.lowerCatchOverChain(ce, span); - } - - const op_ty = self.inferExprType(ce.operand); - const err_set = self.errorChannelOf(op_ty) orelse { - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "`catch` requires a failable expression; operand has type '{s}'", .{self.formatTypeName(op_ty)}); - } - return self.builder.constInt(0, .void); - }; - // Pure-failable LHS (`-> !`): no success value. Run the body on the - // error path; both paths fall through to a value-less merge. - if (op_ty == err_set) { - const err_val = self.lowerExpr(ce.operand); - const err_ty = self.builder.getRefType(err_val); - const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool); - const handle_bb = self.freshBlock("catch.handle"); - const merge_bb = self.freshBlock("catch.merge"); - self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{}); - self.builder.switchToBlock(handle_bb); - _ = self.runCatchBody(ce, err_val, err_set, null); - // The handler can inspect the trace (`trace.print_current()`); the - // absorption clear fires once it completes WITHOUT re-raising (a - // fall-through). A diverging body (`raise` / `return`) keeps / - // discards the buffer on its own path (ERR E3.2; reconciles - // PLAN-ERR §clear-points "cleared before body" with §catch-over-or - // "frames still in the buffer when the body runs"). - if (!self.currentBlockHasTerminator()) { - self.emitTraceClear(); - self.builder.br(merge_bb, &.{}); - } - self.builder.switchToBlock(merge_bb); - return self.builder.constInt(0, .void); - } - - // Value-carrying LHS (`-> (T..., !)`): on success the catch yields the - // value part (the lone value, or a value-tuple); on error it yields - // the handler body's value. The paths merge through a block-parameter - // (phi). - const succ_ty = self.failableSuccessType(op_ty); - const result = self.lowerExpr(ce.operand); - const err_val = self.extractErrorSlot(result, op_ty, err_set); - const succ_val = self.extractSuccessValue(result, op_ty, succ_ty); - const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_set) } }, .bool); - - const handle_bb = self.freshBlock("catch.handle"); - const merge_bb = self.freshBlockWithParams("catch.merge", &.{succ_ty}); - // Success → merge with the value slot; error → run the handler. - self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{succ_val}); - - self.builder.switchToBlock(handle_bb); - const body_val = self.runCatchBody(ce, err_val, err_set, succ_ty); - if (!self.currentBlockHasTerminator()) { - self.finishCatchHandler(body_val, succ_ty, merge_bb, span); - } - - self.builder.switchToBlock(merge_bb); - return self.builder.blockParam(merge_bb, 0, succ_ty); - } - - /// `(failable or-chain) catch [e] BODY` (ERR E2.4). The chain's operands - /// route per the chain rules; its TOTAL failure (the final operand failing) - /// is redirected to the catch handler via `chain_fail_target` rather than - /// propagating to the function. `e` binds the final error tag; the handler's - /// value (or divergence) joins the chain's success value at the merge. - fn lowerCatchOverChain(self: *Lowering, ce: *const ast.CatchExpr, span: ast.Span) Ref { - const chain = &ce.operand.data.binary_op; - - // The error tag reaching the handler is the final operand's (left-assoc - // chain → the top-level rhs). A value-terminator last operand means the - // chain can't fail — nothing for `catch` to absorb. - const last = unwrapTryNode(chain.rhs); - const last_ty = self.inferExprType(last); - const err_set = self.errorChannelOf(last_ty) orelse { - if (self.diagnostics) |d| d.addFmt(.err, span, "`catch` here is redundant — the `or` chain already absorbs every failure via its value terminator", .{}); - return self.builder.constInt(0, .void); - }; - - const succ_ty = self.orChainSuccessType(chain); - const has_value = succ_ty != .void; - - const handle_bb = self.freshBlockWithParams("catch.handle", &.{err_set}); - const merge_bb = if (has_value) - self.freshBlockWithParams("catch.merge", &.{succ_ty}) - else - self.freshBlock("catch.merge"); - - // Lower the chain with its total failure routed to the handler. - const saved = self.chain_fail_target; - self.chain_fail_target = .{ .bb = handle_bb, .set = err_set }; - const chain_val = self.lowerExpr(ce.operand); - self.chain_fail_target = saved; - // Chain success → merge with its value (the buffer was already cleared - // at the succeeding operand inside the chain). - if (has_value) { - const cv = self.coerceToType(chain_val, self.builder.getRefType(chain_val), succ_ty); - self.builder.br(merge_bb, &.{cv}); - } else { - self.builder.br(merge_bb, &.{}); - } - - // Handler: bind the final tag, run the body. The buffer still holds the - // chain's frames (handler may inspect them); absorb on non-diverging exit. - self.builder.switchToBlock(handle_bb); - const tag = self.builder.blockParam(handle_bb, 0, err_set); - const body_val = self.runCatchBody(ce, tag, err_set, if (has_value) succ_ty else null); - if (!self.currentBlockHasTerminator()) { - self.finishCatchHandler(body_val, succ_ty, merge_bb, span); - } - - self.builder.switchToBlock(merge_bb); - return if (has_value) self.builder.blockParam(merge_bb, 0, succ_ty) else self.builder.constInt(0, .void); - } - - /// Close a non-terminated `catch` handler block. `succ_ty` is the catch's - /// result type (`.void` for a pure-failable / void-chain catch — the merge - /// block then has no parameter). A `body_val` typed `noreturn` (e.g. a - /// `process.exit` / other noreturn call, which is NOT an IR terminator) - /// diverges: close with `unreachable` and skip the merge edge so its - /// "value" never reaches a phi. Otherwise clear the absorbed trace and - /// branch to the merge (coercing the body value, or diagnosing a missing / - /// void value for a value-carrying catch). - fn finishCatchHandler(self: *Lowering, body_val: ?Ref, succ_ty: TypeId, merge_bb: BlockId, span: ast.Span) void { - if (body_val) |v| { - if (self.builder.getRefType(v) == .noreturn) { - self.builder.emitUnreachable(); - return; - } - } - self.emitTraceClear(); - if (succ_ty == .void) { - self.builder.br(merge_bb, &.{}); - return; - } - const bv: Ref = blk: { - if (body_val) |v| { - const vty = self.builder.getRefType(v); - if (vty != .void) break :blk self.coerceToType(v, vty, succ_ty); - } - if (self.diagnostics) |diags| { - diags.addFmt(.err, span, "`catch` body must produce a value of type '{s}' (or diverge with `return` / `raise`)", .{self.formatTypeName(succ_ty)}); - } - break :blk self.builder.constUndef(succ_ty); - }; - self.builder.br(merge_bb, &.{bv}); - } - - /// Lower a `catch` body in a child scope that binds the error tag to the - /// catch binding (if any). When `want_ty` is non-null (value-carrying - /// catch), returns the body's value (or null if the body diverged); when - /// null (pure-failable catch), runs the body for effect and returns null. - fn runCatchBody(self: *Lowering, ce: *const ast.CatchExpr, err_val: Ref, err_set: TypeId, want_ty: ?TypeId) ?Ref { - var handle_scope = Scope.init(self.alloc, self.scope); - const saved_scope = self.scope; - self.scope = &handle_scope; - defer { - self.scope = saved_scope; - handle_scope.deinit(); - } - if (ce.binding) |name| { - handle_scope.put(name, .{ .ref = err_val, .ty = err_set, .is_alloca = false }); - } - if (want_ty == null) { - if (ce.body.data == .block) self.lowerBlock(ce.body) else _ = self.lowerExpr(ce.body); - return null; - } - const saved_fbv = self.force_block_value; - self.force_block_value = true; - defer self.force_block_value = saved_fbv; - return if (ce.body.data == .block) self.lowerBlockValue(ce.body) else self.lowerExpr(ce.body); - } - - /// `lhs or rhs` with a failable LHS (ERR step E2.4a — the value-terminator - /// form). On LHS success the result is its value part (the lone value, or a - /// value-tuple); on failure the LHS error is discarded and the result is - /// `rhs` (a plain value of the success type), so the whole expression is - /// non-failable. The CHAIN form (`... or try ...` / a failable RHS) needs - /// the fallback-target routing deferred from E1.4 — bail. - /// Widening at an escape (function-propagation) site: the escaping set must - /// be ⊆ the caller's named set. An inferred caller (`!`) absorbs everything - /// via the whole-program SCC (E1.4b) — no check. A bare-`!` callee carries - /// no tags on its placeholder TypeId, so check its SCC-converged set. - /// Shared by `try` propagation and a failable `or` chain's final operand. - fn checkEscapeWidening(self: *Lowering, callee_node: *const Node, callee_set: TypeId, caller_set: TypeId, span: ast.Span) void { - if (self.isInferredErrorSet(caller_set)) return; - if (!self.isInferredErrorSet(callee_set)) { - self.checkErrorSetSubset(callee_set, caller_set, span); - return; - } - // Bare-`!` callee: either a named top-level function (its converged set - // is name-keyed) or a closure/fn-type SLOT (its set is shape-keyed, - // shared program-wide by value-signature). - if (callTargetName(callee_node)) |nm| { - if (self.inferred_error_sets.get(nm)) |tags| { - self.diagTagsNotInSet(tags, caller_set, span); - return; - } - } - if (self.shapeKeyOfCallee(callee_node)) |key| { - if (self.shape_inferred_sets.get(key)) |tags| { - self.diagTagsNotInSet(tags, caller_set, span); - } - // Empty union (no closure of this shape ever raises) → silently - // allowed: the slot's `!` resolves to ∅ (ERR E5.1 sub-feature 6). - } - } - - /// Structural test: is this `or` a *failable* construct (value-terminator or - /// chain), rather than a boolean / optional-unwrap `or`? True when either - /// operand is failable-like — a `try`, an error-channel-typed expression, or - /// itself a nested failable `or` chain. Kept separate from `inferExprType`: - /// a `try`-chain's *value* type is its success type `T` (non-failable), so - /// the chain-ness is structural, not type-derived. - pub fn orIsFailableChain(self: *Lowering, bop: *const ast.BinaryOp) bool { - return self.operandIsFailableLike(bop.lhs) or self.operandIsFailableLike(bop.rhs); - } - - fn operandIsFailableLike(self: *Lowering, node: *const Node) bool { - if (node.data == .try_expr) return true; - if (node.data == .binary_op and node.data.binary_op.op == .or_op) { - return self.orIsFailableChain(&node.data.binary_op); - } - return self.errorChannelOf(self.inferExprType(node)) != null; - } - - /// The success (value) type of a failable `or` chain: descend to the - /// leftmost operand, unwrap any `try`, and take its failable success type - /// (`void` for a pure-`-> !` chain). All operands share this type. - pub fn orChainSuccessType(self: *Lowering, bop: *const ast.BinaryOp) TypeId { - var lhs = bop.lhs; - while (lhs.data == .binary_op and lhs.data.binary_op.op == .or_op and - self.orIsFailableChain(&lhs.data.binary_op)) - { - lhs = lhs.data.binary_op.lhs; - } - const ft = self.inferExprType(unwrapTryNode(lhs)); - const fset = self.errorChannelOf(ft) orelse return .unresolved; - return if (ft == fset) .void else self.failableSuccessType(ft); - } - - /// `try X` → `X` (the underlying failable); any other node unchanged. In an - /// `or` chain the `try` marker's routing IS the chain, so the chain lowers - /// the underlying failable directly rather than re-entering `lowerTry`. - fn unwrapTryNode(node: *const Node) *const Node { - return if (node.data == .try_expr) node.data.try_expr.operand else node; - } - - /// Flatten a left-associative failable `or` chain into its operands, - /// left-to-right. `a or b or c` parses as `(a or b) or c`; this collects - /// `[a, b, c]`. Walks the left spine only while it stays a failable - /// `or` chain (a parenthesized non-chain `or` on the left stops the walk). - fn flattenOrChain(self: *Lowering, bop: *const ast.BinaryOp, list: *std.ArrayList(*const Node)) void { - if (bop.lhs.data == .binary_op and bop.lhs.data.binary_op.op == .or_op and - self.orIsFailableChain(&bop.lhs.data.binary_op)) - { - self.flattenOrChain(&bop.lhs.data.binary_op, list); - } else { - list.append(self.alloc, bop.lhs) catch unreachable; - } - list.append(self.alloc, bop.rhs) catch unreachable; - } - - /// Lower a failable `or` (ERR E2.4): a value-terminator (`lhs or value`) or - /// a chain (`try a or try b or …`, possibly with a trailing value - /// terminator). Left-to-right, short-circuit: each failable operand's - /// failure routes to the next operand; the final operand either absorbs - /// (value terminator) or propagates to the enclosing function. Each failed - /// attempt pushes a trace frame; an absorbing resolution (any operand - /// succeeding, or the value terminator) clears the buffer; total failure - /// preserves the frames for the caller. - fn lowerFailableOr(self: *Lowering, bop: *const ast.BinaryOp) Ref { - const span = bop.lhs.span; - - var operands = std.ArrayList(*const Node).empty; - defer operands.deinit(self.alloc); - self.flattenOrChain(bop, &operands); - const last_idx = operands.items.len - 1; - const last_is_value = !self.operandIsFailableLike(operands.items[last_idx]); - - // The chain's total-failure routing. An absorbing consumer (`catch`) - // sets this so the final operand's failure reaches the handler; cleared - // while lowering operands so a nested operand doesn't inherit it. - const fail_target = self.chain_fail_target; - self.chain_fail_target = null; - defer self.chain_fail_target = fail_target; - - // Success type from the first operand (a failable; unwrap any `try`). - const first_ty = self.inferExprType(unwrapTryNode(operands.items[0])); - const first_set = self.errorChannelOf(first_ty) orelse { - if (self.diagnostics) |d| d.addFmt(.err, span, "the left operand of a failable `or` must be failable; got '{s}'", .{self.formatTypeName(first_ty)}); - return self.builder.constInt(0, .void); - }; - const has_value = first_ty != first_set; - const succ_ty = if (has_value) self.failableSuccessType(first_ty) else TypeId.void; - - // Pure-failable LHS (`-> !`) with a value terminator: nothing to fall - // back to. - if (!has_value and last_is_value) { - if (self.diagnostics) |d| d.addFmt(.err, span, "`or value` requires a value-carrying failable (`-> (T, !)`) — a `-> !` has no success value to fall back to; use `catch` to absorb the error", .{}); - return self.builder.constInt(0, .void); - } - - // Caller failability — only needed when the chain can propagate to the - // function (final operand is failable AND no absorbing consumer target). - var caller_ret: TypeId = .void; - var caller_set: TypeId = .void; - if (!last_is_value and fail_target == null) { - const cret = self.effectiveReturnType(); - const cset = if (cret) |r| self.errorChannelOf(r) else null; - if (cset == null) { - if (self.diagnostics) |d| d.addFmt(.err, span, "a failable `or` chain propagates on total failure, so it is only valid inside a failable function — add a value terminator (`… or value`) or wrap with `catch`", .{}); - return self.builder.constInt(0, .void); - } - caller_ret = cret.?; - caller_set = cset.?; - } - - const merge_bb = if (has_value) - self.freshBlockWithParams("orc.merge", &.{succ_ty}) - else - self.freshBlock("orc.merge"); - - for (operands.items, 0..) |operand, i| { - const is_last = i == last_idx; - - if (is_last and last_is_value) { - // Value terminator: absorbs every prior failure. - self.emitTraceClear(); - const saved = self.target_type; - self.target_type = succ_ty; - const v = self.lowerExpr(operand); - self.target_type = saved; - const vc = self.coerceToType(v, self.builder.getRefType(v), succ_ty); - self.builder.br(merge_bb, &.{vc}); - break; - } - - // Failable operand (`try X` marker or a bare failable). Lower the - // underlying failable; the `try` marker's routing IS the chain. - const underlying = unwrapTryNode(operand); - const op_ty = self.inferExprType(underlying); - const op_set = self.errorChannelOf(op_ty) orelse { - if (self.diagnostics) |d| d.addFmt(.err, operand.span, "operand of a failable `or` chain must be failable; got '{s}'", .{self.formatTypeName(op_ty)}); - return self.builder.constInt(0, .void); - }; - const op_value_carrying = op_ty != op_set; - - // Widening applies only when the final failure escapes to the - // function (no absorbing consumer); a `catch` target absorbs it. - if (is_last and fail_target == null) self.checkEscapeWidening(underlying, op_set, caller_set, operand.span); - - const result = self.lowerExpr(underlying); - const err_val = if (op_value_carrying) self.extractErrorSlot(result, op_ty, op_set) else result; - const err_ty = self.builder.getRefType(err_val); - const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool); - - const ok_bb = self.freshBlock("orc.ok"); - const fail_bb = self.freshBlock(if (is_last) "orc.prop" else "orc.next"); - self.builder.condBr(is_err, fail_bb, &.{}, ok_bb, &.{}); - - // Success: the chain resolved here — clear the buffer, merge value. - self.builder.switchToBlock(ok_bb); - self.emitTraceClear(); - if (has_value) { - const sv = self.extractSuccessValue(result, op_ty, succ_ty); - const svc = self.coerceToType(sv, self.builder.getRefType(sv), succ_ty); - self.builder.br(merge_bb, &.{svc}); - } else { - self.builder.br(merge_bb, &.{}); - } - - // Failure: push a trace frame, then either route to the next - // operand (same block — no function exit, so `onfail` does not - // fire) or, for the final operand, resolve the total failure: to an - // absorbing consumer (`catch`) if one set a target, else propagate - // to the caller. - self.builder.switchToBlock(fail_bb); - self.emitTracePush(self.placeholderTraceFrame()); - if (is_last) { - if (fail_target) |t| { - const ec = self.coerceToType(err_val, self.builder.getRefType(err_val), t.set); - self.builder.br(t.bb, &.{ec}); - } else { - self.emitErrorCleanup(self.func_defer_base, err_val); - self.emitErrorReturn(caller_ret, caller_set, err_val); - } - } - // else: fall through — the next operand is lowered in fail_bb. - } - - self.builder.switchToBlock(merge_bb); - return if (has_value) self.builder.blockParam(merge_bb, 0, succ_ty) else self.builder.constInt(0, .void); - } - - // ── ERR E1.4b: whole-program inferred-error-set convergence ────────── - - /// The bare callee name of a call expression (`g(...)` → "g"), or null if - /// the node isn't a direct call to a named function. E1.4b resolves only - /// the bare identifier (top-level functions); UFCS / mangled-local callees - /// aren't tracked by the SCC. - pub fn callTargetName(node: *const Node) ?[]const u8 { - if (node.data != .call) return null; - const callee = node.data.call.callee; - return if (callee.data == .identifier) callee.data.identifier.name else null; - } - - /// True when `rt` is a pure bare-`!` failable return (`-> !`, the inferred - /// set) — NOT `!Named` and NOT a value-carrying `-> (T..., !)` tuple. - pub fn astIsPureBareInferred(rt: ?*const Node) bool { - const n = rt orelse return false; - return n.data == .error_type_expr and n.data.error_type_expr.name == null; - } - - /// The named-set name of a pure `-> !Named` return (`"Named"`), or null for - /// bare-`!`, value-carrying, or non-failable returns. - pub fn astPureNamedSet(rt: ?*const Node) ?[]const u8 { - const n = rt orelse return null; - if (n.data != .error_type_expr) return null; - return n.data.error_type_expr.name; - } - - /// The declared tags of a named error set, by name; null if not a - /// registered error set. - pub fn namedSetTags(self: *Lowering, name: []const u8) ?[]const u32 { - const sid = self.module.types.internString(name); - const tid = self.module.types.findByName(sid) orelse return null; - if (tid.isBuiltin()) return null; - const info = self.module.types.get(tid); - return if (info == .error_set) info.error_set.tags else null; - } - - /// Whole-program inferred-error-set convergence. Thin delegation to the - /// canonical owner (`ErrorAnalysis`, `error_analysis.zig`); kept on - /// `Lowering` as a `pub` entry point because the lowering pipeline + the - /// E1.4b unit test call it. - pub fn convergeInferredErrorSets(self: *Lowering) void { - self.errorAnalysis().convergeInferredErrorSets(); - } - - pub fn containsTag(tags: []const u32, t: u32) bool { - for (tags) |x| if (x == t) return true; - return false; - } - - /// Whole-program closure-shape error-set convergence. Thin delegation to the - /// canonical owner (`ErrorAnalysis`, `error_analysis.zig`); kept on - /// `Lowering` as a `pub` entry point because the lowering pipeline calls it. - pub fn convergeClosureShapeSets(self: *Lowering) void { - self.errorAnalysis().convergeClosureShapeSets(); - } - - /// Record one closure literal's contribution to its value-signature shape's - /// inferred-`!` union. No-op unless the literal is a CONCRETE (non-generic) - /// bare-`!` failable closure; named-set / non-failable literals add no tags. - pub fn recordClosureShape(self: *Lowering, lam: *const ast.Lambda) void { - if (lam.type_params.len > 0) return; // generic shapes out of scope (sub-feature 8) - const rt_node = lam.return_type orelse return; // no annotation → non-failable infer - const ret = self.resolveType(rt_node); - const es = self.errorChannelOf(ret) orelse return; // not failable - if (!self.isInferredErrorSet(es)) return; // `!Named` → its own set, not the inferred union - - var ptys = std.ArrayList(TypeId).empty; - defer ptys.deinit(self.alloc); - for (lam.params) |p| { - if (p.is_variadic or p.is_pack or p.is_comptime) return; // not a plain fn-type slot - ptys.append(self.alloc, self.resolveType(p.type_expr)) catch return; - } - const key = self.closureShapeKey(ptys.items, self.returnValuePart(ret)); - - var tags = std.ArrayList(u32).empty; - defer tags.deinit(self.alloc); - var edges = std.ArrayList([]const u8).empty; - defer edges.deinit(self.alloc); - self.errorAnalysis().collectErrorSites(lam.body, &tags, &edges); - for (edges.items) |callee| { - for (self.calleeEscapeTags(callee)) |t| { - if (!containsTag(tags.items, t)) tags.append(self.alloc, t) catch {}; - } - } - self.unionShapeTags(key, tags.items); - } - - /// The escape tags of a callee referenced by name from a `try g()` edge: - /// a bare-`!` callee's converged set, or a `-> !Named` callee's declared set. - fn calleeEscapeTags(self: *Lowering, callee: []const u8) []const u32 { - if (self.inferred_error_sets.get(callee)) |t| return t; - if (self.program_index.fn_ast_map.get(callee)) |cfd| { - if (astPureNamedSet(cfd.return_type)) |nm| return self.namedSetTags(nm) orelse &.{}; - } - return &.{}; - } - - /// Merge `new_tags` into the shape node `key` (sorted, deduped). The map is - /// content-keyed (StringHashMap), so re-`put` with a fresh equal key string - /// overwrites the existing node's value in place. - fn unionShapeTags(self: *Lowering, key: []const u8, new_tags: []const u32) void { - var list = std.ArrayList(u32).empty; - defer list.deinit(self.alloc); - if (self.shape_inferred_sets.get(key)) |existing| list.appendSlice(self.alloc, existing) catch {}; - for (new_tags) |t| { - if (!containsTag(list.items, t)) list.append(self.alloc, t) catch {}; - } - const sorted = self.alloc.dupe(u32, list.items) catch return; - std.mem.sort(u32, sorted, {}, std.sort.asc(u32)); - self.shape_inferred_sets.put(key, sorted) catch {}; - } - - /// Canonical key for a callable VALUE-signature: param types + the value - /// part of the return (error slot excluded). Bare-`!` and non-failable - /// shapes of the same value-sig — and `.function` vs `.closure` of that - /// sig — collapse to one key, so all occurrences share one inferred node. - fn closureShapeKey(self: *Lowering, params: []const TypeId, value_ret: TypeId) []const u8 { - var buf = std.ArrayList(u8).empty; - buf.appendSlice(self.alloc, "shape") catch return "shape"; - for (params) |p| { - buf.append(self.alloc, '_') catch return "shape"; - buf.appendSlice(self.alloc, self.mangleTypeName(p)) catch return "shape"; - } - buf.appendSlice(self.alloc, "__") catch return "shape"; - buf.appendSlice(self.alloc, self.mangleTypeName(value_ret)) catch return "shape"; - return buf.items; - } - - /// The value part of a (possibly failable) return type, error slot dropped: - /// `(T, !)` → T (or a value-tuple); pure `-> !` → void; non-failable → self. - fn returnValuePart(self: *Lowering, ret: TypeId) TypeId { - const es = self.errorChannelOf(ret) orelse return ret; - if (ret == es) return .void; - return self.failableSuccessType(ret); - } - - /// Shape key of a call's callee expression when it's a closure/fn-type slot - /// (variable, field, index — anything with a `.closure`/`.function` type), - /// for the program-wide shape-union widening lookup. Null for non-callables. - fn shapeKeyOfCallee(self: *Lowering, node: *const Node) ?[]const u8 { - if (node.data != .call) return null; - const fty = self.inferExprType(node.data.call.callee); - if (fty.isBuiltin()) return null; - const info = self.module.types.get(fty); - const params: []const TypeId = switch (info) { - .closure => |c| c.params, - .function => |f| f.params, - else => return null, - }; - const ret: TypeId = switch (info) { - .closure => |c| c.ret, - .function => |f| f.ret, - else => return null, - }; - return self.closureShapeKey(params, self.returnValuePart(ret)); - } - /// Human-readable description of a typed module-const initializer, used in /// the issue-0088 type-mismatch diagnostic. A literal names its kind; a /// const-expression is described by its inferred type category, so the @@ -20122,6 +19021,61 @@ pub const Lowering = struct { self.builder.finalize(); } + + // --- moved to lower/error.zig (lower_error) --- + pub const getTraceFids = lower_error.getTraceFids; + pub const tracesEnabled = lower_error.tracesEnabled; + pub const emitTracePush = lower_error.emitTracePush; + pub const emitTraceClear = lower_error.emitTraceClear; + pub const placeholderTraceFrame = lower_error.placeholderTraceFrame; + pub const errorSetTypeOf = lower_error.errorSetTypeOf; + pub const isErrorTagLiteralNode = lower_error.isErrorTagLiteralNode; + pub const tryLowerErrorSetEquality = lower_error.tryLowerErrorSetEquality; + pub const effectiveReturnType = lower_error.effectiveReturnType; + pub const errorChannelOf = lower_error.errorChannelOf; + pub const isInferredErrorSet = lower_error.isInferredErrorSet; + pub const checkErrorSetSubset = lower_error.checkErrorSetSubset; + pub const diagTagsNotInSet = lower_error.diagTagsNotInSet; + pub const lowerRaise = lower_error.lowerRaise; + pub const lowerFailableSuccessReturn = lower_error.lowerFailableSuccessReturn; + pub const buildFailableTuple = lower_error.buildFailableTuple; + pub const failableSuccessType = lower_error.failableSuccessType; + pub const failableReturnTarget = lower_error.failableReturnTarget; + pub const extractSuccessValue = lower_error.extractSuccessValue; + pub const extractErrorSlot = lower_error.extractErrorSlot; + pub const emitTupleRet = lower_error.emitTupleRet; + pub const diagRaiseNotFailable = lower_error.diagRaiseNotFailable; + pub const exprIsFailable = lower_error.exprIsFailable; + pub const lowerCallerLocation = lower_error.lowerCallerLocation; + pub const sourceForFile = lower_error.sourceForFile; + pub const currentFunctionName = lower_error.currentFunctionName; + pub const lowerTry = lower_error.lowerTry; + pub const emitErrorReturn = lower_error.emitErrorReturn; + pub const diagTryNotFailable = lower_error.diagTryNotFailable; + pub const lowerCatch = lower_error.lowerCatch; + pub const lowerCatchOverChain = lower_error.lowerCatchOverChain; + pub const finishCatchHandler = lower_error.finishCatchHandler; + pub const runCatchBody = lower_error.runCatchBody; + pub const checkEscapeWidening = lower_error.checkEscapeWidening; + pub const orIsFailableChain = lower_error.orIsFailableChain; + pub const operandIsFailableLike = lower_error.operandIsFailableLike; + pub const orChainSuccessType = lower_error.orChainSuccessType; + pub const unwrapTryNode = lower_error.unwrapTryNode; + pub const flattenOrChain = lower_error.flattenOrChain; + pub const lowerFailableOr = lower_error.lowerFailableOr; + pub const callTargetName = lower_error.callTargetName; + pub const astIsPureBareInferred = lower_error.astIsPureBareInferred; + pub const astPureNamedSet = lower_error.astPureNamedSet; + pub const namedSetTags = lower_error.namedSetTags; + pub const convergeInferredErrorSets = lower_error.convergeInferredErrorSets; + pub const containsTag = lower_error.containsTag; + pub const convergeClosureShapeSets = lower_error.convergeClosureShapeSets; + pub const recordClosureShape = lower_error.recordClosureShape; + pub const calleeEscapeTags = lower_error.calleeEscapeTags; + pub const unionShapeTags = lower_error.unionShapeTags; + pub const closureShapeKey = lower_error.closureShapeKey; + pub const returnValuePart = lower_error.returnValuePart; + pub const shapeKeyOfCallee = lower_error.shapeKeyOfCallee; }; /// JNI param/return type resolution: user-declared types pass through diff --git a/src/ir/lower/error.zig b/src/ir/lower/error.zig new file mode 100644 index 0000000..bde7cb7 --- /dev/null +++ b/src/ir/lower/error.zig @@ -0,0 +1,1150 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ast = @import("../../ast.zig"); +const Node = ast.Node; +const types = @import("../types.zig"); +const inst_mod = @import("../inst.zig"); +const mod_mod = @import("../module.zig"); +const type_bridge = @import("../type_bridge.zig"); +const unescape = @import("../../unescape.zig"); +const parser_mod = @import("../../parser.zig"); +const interp_mod = @import("../interp.zig"); +const errors = @import("../../errors.zig"); +const jni_descriptor = @import("../jni_descriptor.zig"); +const program_index_mod = @import("../program_index.zig"); +const resolver_mod = @import("../resolver.zig"); +const imports_mod = @import("../../imports.zig"); +const ProgramIndex = program_index_mod.ProgramIndex; +const GlobalInfo = program_index_mod.GlobalInfo; +const StructTemplate = program_index_mod.StructTemplate; +const TemplateParam = program_index_mod.TemplateParam; +const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; +const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; +const ModuleConstInfo = program_index_mod.ModuleConstInfo; +const TypeResolver = @import("../type_resolver.zig").TypeResolver; +const ResolveEnv = @import("../type_resolver.zig").ResolveEnv; +const PackResolver = @import("../packs.zig").PackResolver; +const ExprTyper = @import("../expr_typer.zig").ExprTyper; +const CallResolver = @import("../calls.zig").CallResolver; +const GenericResolver = @import("../generics.zig").GenericResolver; +const ProtocolResolver = @import("../protocols.zig").ProtocolResolver; +const CoercionResolver = @import("../conversions.zig").CoercionResolver; +const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis; +const ErrorFlow = @import("../error_flow.zig").ErrorFlow; +const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; +const semantic_diagnostics = @import("../semantic_diagnostics.zig"); + +const TypeId = types.TypeId; +const StringId = types.StringId; +const Ref = inst_mod.Ref; +const BlockId = inst_mod.BlockId; +const FuncId = inst_mod.FuncId; +const Function = inst_mod.Function; +const Module = mod_mod.Module; +const Builder = mod_mod.Builder; + +const lower = @import("../lower.zig"); +const Lowering = lower.Lowering; +const Scope = lower.Scope; + +/// Lazily declare the `sx_trace_push(u64)` / `sx_trace_clear()` runtime +/// externs (ERR E3.1). Storage is a `_Thread_local` ring buffer in +/// `library/vendors/sx_trace_runtime/sx_trace.c` — kept OUT of the user's IR +/// module (same JIT-TLS reason as the JNI env slot). Setting +/// `needs_trace_runtime` signals Compilation to auto-link the .c for AOT. +/// Wired into the `raise` / `try` push sites and the absorbing clear sites +/// at ERR E3.2. +pub fn getTraceFids(self: *Lowering) struct { push: FuncId, clear: FuncId } { + self.needs_trace_runtime = true; + if (self.trace_push_fid == null) { + const name = self.module.types.internString("sx_trace_push"); + const frame_param = self.module.types.internString("frame"); + var params = std.ArrayList(inst_mod.Function.Param).empty; + params.append(self.alloc, .{ .name = frame_param, .ty = .u64 }) catch unreachable; + const fid = self.builder.declareExtern(name, params.toOwnedSlice(self.alloc) catch unreachable, .void); + self.module.getFunctionMut(fid).call_conv = .c; + self.trace_push_fid = fid; + } + if (self.trace_clear_fid == null) { + const name = self.module.types.internString("sx_trace_clear"); + const fid = self.builder.declareExtern(name, &.{}, .void); + self.module.getFunctionMut(fid).call_conv = .c; + self.trace_clear_fid = fid; + } + return .{ .push = self.trace_push_fid.?, .clear = self.trace_clear_fid.? }; +} + +/// Error return-traces are emitted in debug-ish builds and skipped in +/// release (ERR E3.2 build-mode gating). `sx run` defaults to `-O0` +/// (`.none`), the common dev path; `.default`/`.aggressive` are release. +/// The spec's `--release-traces` opt-in + a `BuildOptions.error_traces` +/// accessor are a later refinement; for now the opt level is the gate. +pub fn tracesEnabled(self: *Lowering) bool { + const tc = self.target_config orelse return true; // no target → treat as debug + return tc.opt_level == .none or tc.opt_level == .less; +} + +/// Emit a trace-buffer push of `frame` (an opaque u64) at a failure site. +/// No-op when traces are disabled (release). `frame` is a placeholder until +/// DWARF (E3.0) supplies real return-address PCs and E3.3 resolves them. +pub fn emitTracePush(self: *Lowering, frame: Ref) void { + if (!self.tracesEnabled()) return; + const fids = self.getTraceFids(); + const coerced = self.coerceToType(frame, self.builder.getRefType(frame), .u64); + const args = self.alloc.dupe(Ref, &.{coerced}) catch return; + _ = self.builder.emit(.{ .call = .{ .callee = fids.push, .args = args } }, .void); +} + +/// Emit a trace-buffer clear at an absorbing site (`catch` / `or value` / +/// destructure). No-op when traces are disabled. +pub fn emitTraceClear(self: *Lowering) void { + if (!self.tracesEnabled()) return; + const fids = self.getTraceFids(); + _ = self.builder.emit(.{ .call = .{ .callee = fids.clear, .args = &.{} } }, .void); +} + +/// The trace frame value for a failure site (ERR E3.0 slice 3a). Emits the +/// niladic `.trace_frame` op (span-stamped via `Builder.current_span`); each +/// backend resolves it to a real frame — `emit_llvm` to a `Frame*`, `interp` +/// to a packed `(func_id, offset)`. The result feeds `sx_trace_push`. +pub fn placeholderTraceFrame(self: *Lowering) Ref { + return self.builder.emit(.{ .trace_frame = {} }, .u64); +} + +/// The named error-set TypeId of `node`'s type, or null if not an +/// error-set-typed expression. +pub 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"). +pub 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). +pub 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); +} + +/// The declared return type of the function currently being lowered (the +/// inlined body's type wins while inlining a comptime call), or null when +/// there is no enclosing function. +pub fn effectiveReturnType(self: *Lowering) ?TypeId { + if (self.inline_return_target) |iri| return iri.ret_ty; + if (self.builder.func) |fid| return self.module.functions.items[@intFromEnum(fid)].ret; + return null; +} + +/// If `ret_ty` belongs to a failable function, the TypeId of its error +/// channel; else null. `-> !Named` / `-> !` resolve the error set directly; +/// `-> (T..., !)` carries it as the last tuple field (the locked ABI). +pub fn errorChannelOf(self: *Lowering, ret_ty: TypeId) ?TypeId { + if (ret_ty.isBuiltin()) return null; + switch (self.module.types.get(ret_ty)) { + .error_set => return ret_ty, + .tuple => |t| { + if (t.fields.len == 0) return null; + const last = t.fields[t.fields.len - 1]; + if (last.isBuiltin()) return null; + return if (self.module.types.get(last) == .error_set) last else null; + }, + else => return null, + } +} + +/// True for the bare-`!` inferred placeholder error set (reserved name "!"). +pub fn isInferredErrorSet(self: *Lowering, set: TypeId) bool { + if (set.isBuiltin()) return false; + const info = self.module.types.get(set); + if (info != .error_set) return false; + return std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!"); +} + +/// Diagnose every tag of `src` that is not also a member of `dst` (the +/// enclosing function's named error set). Both must be `.error_set` types. +pub fn checkErrorSetSubset(self: *Lowering, src: TypeId, dst: TypeId, span: ast.Span) void { + if (src.isBuiltin()) return; + const src_info = self.module.types.get(src); + if (src_info != .error_set) return; + self.diagTagsNotInSet(src_info.error_set.tags, dst, span); +} + +/// Diagnose every tag id in `src_tags` that is not a member of the named +/// error set `dst`. Shared by the named-set subset check and E1.4b's +/// inferred-callee widening (where the callee's tags come from the SCC, +/// not a `.error_set` TypeId). +pub fn diagTagsNotInSet(self: *Lowering, src_tags: []const u32, dst: TypeId, span: ast.Span) void { + if (dst.isBuiltin()) return; + const dst_info = self.module.types.get(dst); + if (dst_info != .error_set) return; + for (src_tags) |tag| { + var found = false; + for (dst_info.error_set.tags) |d| { + if (d == tag) { + found = true; + break; + } + } + if (!found) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "error tag 'error.{s}' is not in caller's error set '{s}'", .{ self.module.types.getTagName(tag), self.module.types.getString(dst_info.error_set.name) }); + } + } + } +} + +/// `raise EXPR;` — terminate the enclosing failable function via the error +/// channel. E1.3 lowers the **pure-failable** shape (`-> !` / `-> !Named`, +/// whose return type IS the error set): emit `ret(EXPR)`. The value-carrying +/// shape (`-> (T..., !)`) needs the value slots set to `undef` alongside the +/// error slot — that tuple ABI lands in E2.1/E2.2, so we bail loudly here +/// rather than ship a half-built return that silently corrupts value slots. +pub fn lowerRaise(self: *Lowering, rs: *const ast.RaiseStmt, span: ast.Span) void { + // (1) `raise` is legal only inside a failable function. + const ret_ty = self.effectiveReturnType() orelse { + self.diagRaiseNotFailable(span); + return; + }; + const err_set = self.errorChannelOf(ret_ty) orelse { + self.diagRaiseNotFailable(span); + return; + }; + const inferred = self.isInferredErrorSet(err_set); + + // (2) Set check. Lowering EXPR with the function's error set as the + // target type makes a literal `raise error.X` validate `X ∈ set` + // inside lowerErrorTagLiteral (the inferred placeholder accepts any + // tag). The variable form `raise e` is subset-checked below. + const saved_target = self.target_type; + self.target_type = err_set; + const tag_ref = self.lowerExpr(rs.tag); + self.target_type = saved_target; + + if (!inferred and !isErrorTagLiteralNode(rs.tag)) { + if (self.errorSetTypeOf(rs.tag)) |src_set| { + self.checkErrorSetSubset(src_set, err_set, span); + } + } + + // (3) Push a trace frame: `raise` always escapes the function (ERR E3.2). + // Before cleanup, so the frame records the raise site itself. + self.emitTracePush(self.placeholderTraceFrame()); + + // (4) Emit the failure return. Pure-failable: the return type IS the + // error set, so return the tag value directly. + if (ret_ty == err_set) { + const tag_ty = self.builder.getRefType(tag_ref); + const coerced = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref; + self.emitErrorCleanup(self.func_defer_base, coerced); + if (self.inline_return_target) |iri| { + self.builder.store(iri.slot, coerced); + self.builder.br(iri.done_bb, &.{}); + } else { + self.builder.ret(coerced, err_set); + } + } else { + // Value-carrying `-> (T..., !)`: the error path leaves the value + // slots undefined and carries the tag in the error slot (ERR E2.1). + const tag_ty = self.builder.getRefType(tag_ref); + const coerced_tag = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref; + self.emitErrorCleanup(self.func_defer_base, coerced_tag); + const fields = self.module.types.get(ret_ty).tuple.fields; + var slots = std.ArrayList(Ref).empty; + defer slots.deinit(self.alloc); + for (fields[0 .. fields.len - 1]) |vty| { + slots.append(self.alloc, self.builder.constUndef(vty)) catch unreachable; + } + const tup = self.buildFailableTuple(ret_ty, slots.items, coerced_tag); + self.emitTupleRet(ret_ty, tup); + } +} + +/// Return a value-carrying failable function's success tuple +/// `{value(s)..., 0}` from `ref` (the user-returned value part). Forwarding +/// a full failable tuple (`return other_failable()` / explicit `return +/// (v, e)`) returns it as-is. Single-value `-> (T, !)` takes `ref` as the +/// lone value; multi-value `-> (T1, ..., !)` takes `ref` as a value-tuple +/// `(T1, ...)` and re-assembles its slots alongside the success error slot. +pub fn lowerFailableSuccessReturn(self: *Lowering, ref: Ref, ret_ty: TypeId, span: ast.Span) void { + const fields = self.module.types.get(ret_ty).tuple.fields; + const err_ty = fields[fields.len - 1]; + const val_ty = self.builder.getRefType(ref); + if (val_ty == ret_ty) { + // The expression already IS the full failable tuple (forwarding). + self.emitTupleRet(ret_ty, ref); + return; + } + const n_vals = fields.len - 1; + if (n_vals == 1) { + const cv = self.coerceToType(ref, val_ty, fields[0]); + const tup = self.buildFailableTuple(ret_ty, &.{cv}, self.builder.constInt(0, err_ty)); + self.emitTupleRet(ret_ty, tup); + return; + } + // Multi-value: `ref` must be a value-tuple `(T1, ..., Tn)`. Extract + // each value slot, coerce to the declared field type, and re-assemble + // with the success error slot (0). + if (val_ty.isBuiltin() or self.module.types.get(val_ty) != .tuple or self.module.types.get(val_ty).tuple.fields.len != n_vals) { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "a multi-value failable function (`-> (T1, ..., !)`) must `return` a {d}-tuple of its value types", .{n_vals}); + } + return; + } + const vfields = self.module.types.get(val_ty).tuple.fields; + var vals = std.ArrayList(Ref).empty; + defer vals.deinit(self.alloc); + for (0..n_vals) |i| { + const fv = self.builder.emit(.{ .tuple_get = .{ .base = ref, .field_index = @intCast(i), .base_type = val_ty } }, vfields[i]); + vals.append(self.alloc, self.coerceToType(fv, vfields[i], fields[i])) catch unreachable; + } + const tup = self.buildFailableTuple(ret_ty, vals.items, self.builder.constInt(0, err_ty)); + self.emitTupleRet(ret_ty, tup); +} + +/// Build a failable return tuple `{value_refs..., tag}` typed `ret_ty`. +pub fn buildFailableTuple(self: *Lowering, ret_ty: TypeId, value_refs: []const Ref, tag: Ref) Ref { + var fields = std.ArrayList(Ref).empty; + defer fields.deinit(self.alloc); + fields.appendSlice(self.alloc, value_refs) catch unreachable; + fields.append(self.alloc, tag) catch unreachable; + return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, fields.items) catch unreachable } }, ret_ty); +} + +/// The success (value-part) type of a value-carrying failable tuple +/// `op_ty` (`-> (T..., !)`): the lone value type for a single-value +/// failable, or a synthesized value-tuple `(T1, ..., Tn)` (error slot +/// dropped) for a multi-value one. Callers must pass a value-carrying +/// tuple — a pure `-> !`'s success type is `void`, handled separately. +pub fn failableSuccessType(self: *Lowering, op_ty: TypeId) TypeId { + const fields = self.module.types.get(op_ty).tuple.fields; + const n_vals = fields.len - 1; + if (n_vals == 1) return fields[0]; + return self.module.types.intern(.{ .tuple = .{ + .fields = self.alloc.dupe(TypeId, fields[0..n_vals]) catch unreachable, + .names = null, + } }); +} + +/// The `target_type` to lower a returned expression against. For a +/// value-carrying failable (`-> (T..., !)`) a BARE returned value resolves +/// against the success value type (so a bare enum literal gets its real +/// ordinal); an EXPLICIT full failable tuple literal (`return (v..., e)`, +/// arity == full-tuple field count) keeps the failable-tuple target so its +/// trailing error element resolves against the error set and is forwarded +/// as-is. Every other return type passes through unchanged. +pub fn failableReturnTarget(self: *Lowering, ret_ty: TypeId, value_node: ?*const Node) TypeId { + if (ret_ty.isBuiltin()) return ret_ty; + if (self.module.types.get(ret_ty) != .tuple) return ret_ty; + if (self.errorChannelOf(ret_ty) == null) return ret_ty; + if (value_node) |vn| { + if (vn.data == .tuple_literal and + vn.data.tuple_literal.elements.len == self.module.types.get(ret_ty).tuple.fields.len) + return ret_ty; + } + return self.failableSuccessType(ret_ty); +} + +/// Extract the success value from an evaluated value-carrying failable +/// tuple `result` (type `op_ty`): the lone value slot for single-value, +/// or an assembled value-tuple (typed `succ_ty`) for multi-value. +pub fn extractSuccessValue(self: *Lowering, result: Ref, op_ty: TypeId, succ_ty: TypeId) Ref { + const fields = self.module.types.get(op_ty).tuple.fields; + const n_vals = fields.len - 1; + if (n_vals == 1) { + return self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = op_ty } }, fields[0]); + } + var vals = std.ArrayList(Ref).empty; + defer vals.deinit(self.alloc); + for (0..n_vals) |i| { + vals.append(self.alloc, self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = @intCast(i), .base_type = op_ty } }, fields[i])) catch unreachable; + } + return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, vals.items) catch unreachable } }, succ_ty); +} + +/// Extract the error slot (always the last field) of an evaluated +/// value-carrying failable tuple `result`, typed as `err_set`. +pub fn extractErrorSlot(self: *Lowering, result: Ref, op_ty: TypeId, err_set: TypeId) Ref { + const fields = self.module.types.get(op_ty).tuple.fields; + return self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = @intCast(fields.len - 1), .base_type = op_ty } }, err_set); +} + +/// Emit a return of an already-assembled tuple, honoring inline-comptime +/// return targets (store + branch) vs a real function return. +pub fn emitTupleRet(self: *Lowering, ret_ty: TypeId, tup: Ref) void { + if (self.inline_return_target) |iri| { + self.builder.store(iri.slot, tup); + self.builder.br(iri.done_bb, &.{}); + } else { + self.builder.ret(tup, ret_ty); + } +} + +pub fn diagRaiseNotFailable(self: *Lowering, span: ast.Span) void { + if (self.diagnostics) |diags| { + if (self.in_lambda_body) { + diags.addFmt(.err, span, "lambda body raises; declare its return type explicitly with `-> (T, !)` or `-> (T, !Named)`", .{}); + } else { + diags.addFmt(.err, span, "`raise` is only valid inside a failable function (a return type with `!` or `!Named`)", .{}); + } + } +} + +/// True if `node`'s value is failable — a `try` (the result is its +/// operand's success value, but the expression itself routes an error) or +/// any expression whose type carries an error channel (a bare failable +/// call). Used to detect failable `or` chains (deferred to E1.4b). +pub fn exprIsFailable(self: *Lowering, node: *const Node) bool { + if (node.data == .try_expr) return true; + return self.errorChannelOf(self.inferExprType(node)) != null; +} + +/// `try X` — a fallible attempt (ERR step E1.4a: the STANDALONE form, whose +/// failure target is function-propagation). Evaluates X; on failure, runs +/// the function's defers and returns the error to the caller; on success, +/// continues with X's value. E1.4a lowers the pure-failable shape (callee +/// `-> !` / `-> !Named`, caller likewise pure-failable). Value-carrying +/// callees, propagation from a value-carrying caller, and `try` inside an +/// `or` chain need the error-channel tuple ABI / fallback routing — those +/// land in E1.4b/E2, so we bail loudly here. +/// Synthesize a `Source_Location` value for a `#caller_location` marker +/// (ERR E4.1b). The node's `span`/`source_file` are the CALL site (rewritten +/// by `expandCallDefaults`); resolve them to file / line:col against the +/// source text and stamp the enclosing (caller) function name. +pub fn lowerCallerLocation(self: *Lowering, node: *const Node) Ref { + const sl_tid = self.module.types.findByName(self.module.types.internString("Source_Location")) orelse { + if (self.diagnostics) |d| d.addFmt(.err, node.span, "`#caller_location` needs `Source_Location` (from std.sx) in scope", .{}); + return self.builder.constInt(0, .void); + }; + const file = node.source_file orelse self.current_source_file orelse (self.main_file orelse ""); + const src = self.sourceForFile(file); + const loc = errors.SourceLoc.compute(src, node.span.start); + const func_name = self.currentFunctionName(); + var fields = [_]Ref{ + self.builder.constString(self.module.types.internString(file)), + self.builder.constInt(@intCast(loc.line), .s32), + self.builder.constInt(@intCast(loc.col), .s32), + self.builder.constString(self.module.types.internString(func_name)), + }; + return self.builder.emit(.{ .struct_init = .{ .fields = self.alloc.dupe(Ref, &fields) catch unreachable } }, sl_tid); +} + +/// The source text for `file`, via the diagnostics' file→source map (which +/// includes the main file). Empty if unavailable — line:col then degrade to +/// 1:1 rather than crash. +pub fn sourceForFile(self: *Lowering, file: []const u8) []const u8 { + const diags = self.diagnostics orelse return ""; + if (diags.import_sources) |is| { + if (is.get(file)) |s| return s; + } + return diags.source; +} + +/// Name of the function currently being lowered (the caller, at a +/// `#caller_location` site), or "" outside any function. +pub fn currentFunctionName(self: *Lowering) []const u8 { + const fid = self.builder.func orelse return ""; + return self.module.types.getString(self.module.functions.items[@intFromEnum(fid)].name); +} + +pub fn lowerTry(self: *Lowering, operand: *const Node, span: ast.Span) Ref { + // (1) `try` is legal only inside a failable function. + const caller_ret = self.effectiveReturnType() orelse { + self.diagTryNotFailable(span); + return self.builder.constInt(0, .void); + }; + const caller_set = self.errorChannelOf(caller_ret) orelse { + self.diagTryNotFailable(span); + return self.builder.constInt(0, .void); + }; + + // (2) The operand must be failable. This is the sole failable-operand + // check (the parser imposes none — see E0.2). + const op_ty = self.inferExprType(operand); + const callee_set = self.errorChannelOf(op_ty) orelse { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "`try` requires a failable expression; operand has type '{s}'", .{self.formatTypeName(op_ty)}); + } + return self.builder.constInt(0, .void); + }; + + // A value-carrying callee (`-> (T..., !)`) returns a tuple + // `{v..., err}`; a pure-failable callee (`-> !`) returns the bare + // error tag. + const callee_value_carrying = op_ty != callee_set; + + // (3) Widening: the callee's escape set must be ⊆ the caller's named + // set. For an inferred caller (`!`) the absorption happens in the + // whole-program SCC (E1.4b) — no check here. + self.checkEscapeWidening(operand, callee_set, caller_set, span); + + // (4) Lower: evaluate the operand, then branch on its error tag (which + // is the bare result for a pure callee, or the last tuple slot for + // a value-carrying one). + const result = self.lowerExpr(operand); + const err_val = if (callee_value_carrying) + self.extractErrorSlot(result, op_ty, callee_set) + else + result; + const err_ty = self.builder.getRefType(err_val); + const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool); + + const prop_bb = self.freshBlock("try.prop"); + const ok_bb = self.freshBlock("try.ok"); + self.builder.condBr(is_err, prop_bb, &.{}, ok_bb, &.{}); + + // Propagation: push a trace frame (this `try` failure escapes to the + // caller — ERR E3.2), run the function's cleanups (defers + onfails, + // since this is an error exit), then return the caller's failure + // carrying this tag (pure caller → `ret(tag)`; value-carrying → + // `ret {undef…, tag}`). + self.builder.switchToBlock(prop_bb); + self.emitTracePush(self.placeholderTraceFrame()); + self.emitErrorCleanup(self.func_defer_base, err_val); + self.emitErrorReturn(caller_ret, caller_set, err_val); + + // Success: a value-carrying callee yields its value part (the lone + // value, or a value-tuple); a pure-failable callee has no value (void). + self.builder.switchToBlock(ok_bb); + if (callee_value_carrying) { + const succ_ty = self.failableSuccessType(op_ty); + return self.extractSuccessValue(result, op_ty, succ_ty); + } + return self.builder.constInt(0, .void); +} + +/// Return the enclosing function's failure carrying error tag `err`. A +/// pure-failable caller (`-> !`) returns the tag directly; a value-carrying +/// caller (`-> (T..., !)`) returns `{undef value slots..., tag}`. Honors +/// inline-comptime return targets. The caller emits defers first. +pub fn emitErrorReturn(self: *Lowering, caller_ret: TypeId, caller_set: TypeId, err: Ref) void { + const ety = self.builder.getRefType(err); + const coerced = if (ety != caller_set) self.coerceToType(err, ety, caller_set) else err; + if (caller_ret == caller_set) { + if (self.inline_return_target) |iri| { + self.builder.store(iri.slot, coerced); + self.builder.br(iri.done_bb, &.{}); + } else { + self.builder.ret(coerced, caller_set); + } + } else { + const fields = self.module.types.get(caller_ret).tuple.fields; + var undefs = std.ArrayList(Ref).empty; + defer undefs.deinit(self.alloc); + for (fields[0 .. fields.len - 1]) |vty| { + undefs.append(self.alloc, self.builder.constUndef(vty)) catch unreachable; + } + const tup = self.buildFailableTuple(caller_ret, undefs.items, coerced); + self.emitTupleRet(caller_ret, tup); + } +} + +pub fn diagTryNotFailable(self: *Lowering, span: ast.Span) void { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "`try` is only valid inside a failable function (a return type with `!` or `!Named`)", .{}); + } +} + +/// `expr catch [e] BODY` — inline failure handler (ERR step E1.5, +/// pure-failable slice). Evaluates `expr`; on failure, binds the tag to +/// `e` (if present) and runs BODY; on success, the value is `void` (a +/// pure-failable LHS has no success value). BODY either diverges (via +/// `noreturn` — E1.4c) or falls through. `catch` consumes the error +/// locally, so — unlike `try` / `raise` — it needs no failable *enclosing* +/// function. Value-carrying LHS (binding the success value / a +/// value-producing body unifying with the success tuple) needs the +/// error-channel tuple ABI and lands in E2 — bail loudly here. +pub fn lowerCatch(self: *Lowering, ce: *const ast.CatchExpr, span: ast.Span) Ref { + // A failable `or` chain operand (`(try a or try b) catch e …`) routes + // its total failure to the catch handler — not the function — via the + // chain-fail target (ERR E2.4). A chain's value type is non-failable + // `T`, so it wouldn't pass the `errorChannelOf` check below. + if (ce.operand.data == .binary_op and ce.operand.data.binary_op.op == .or_op and + self.orIsFailableChain(&ce.operand.data.binary_op)) + { + return self.lowerCatchOverChain(ce, span); + } + + const op_ty = self.inferExprType(ce.operand); + const err_set = self.errorChannelOf(op_ty) orelse { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "`catch` requires a failable expression; operand has type '{s}'", .{self.formatTypeName(op_ty)}); + } + return self.builder.constInt(0, .void); + }; + // Pure-failable LHS (`-> !`): no success value. Run the body on the + // error path; both paths fall through to a value-less merge. + if (op_ty == err_set) { + const err_val = self.lowerExpr(ce.operand); + const err_ty = self.builder.getRefType(err_val); + const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool); + const handle_bb = self.freshBlock("catch.handle"); + const merge_bb = self.freshBlock("catch.merge"); + self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{}); + self.builder.switchToBlock(handle_bb); + _ = self.runCatchBody(ce, err_val, err_set, null); + // The handler can inspect the trace (`trace.print_current()`); the + // absorption clear fires once it completes WITHOUT re-raising (a + // fall-through). A diverging body (`raise` / `return`) keeps / + // discards the buffer on its own path (ERR E3.2; reconciles + // PLAN-ERR §clear-points "cleared before body" with §catch-over-or + // "frames still in the buffer when the body runs"). + if (!self.currentBlockHasTerminator()) { + self.emitTraceClear(); + self.builder.br(merge_bb, &.{}); + } + self.builder.switchToBlock(merge_bb); + return self.builder.constInt(0, .void); + } + + // Value-carrying LHS (`-> (T..., !)`): on success the catch yields the + // value part (the lone value, or a value-tuple); on error it yields + // the handler body's value. The paths merge through a block-parameter + // (phi). + const succ_ty = self.failableSuccessType(op_ty); + const result = self.lowerExpr(ce.operand); + const err_val = self.extractErrorSlot(result, op_ty, err_set); + const succ_val = self.extractSuccessValue(result, op_ty, succ_ty); + const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_set) } }, .bool); + + const handle_bb = self.freshBlock("catch.handle"); + const merge_bb = self.freshBlockWithParams("catch.merge", &.{succ_ty}); + // Success → merge with the value slot; error → run the handler. + self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{succ_val}); + + self.builder.switchToBlock(handle_bb); + const body_val = self.runCatchBody(ce, err_val, err_set, succ_ty); + if (!self.currentBlockHasTerminator()) { + self.finishCatchHandler(body_val, succ_ty, merge_bb, span); + } + + self.builder.switchToBlock(merge_bb); + return self.builder.blockParam(merge_bb, 0, succ_ty); +} + +/// `(failable or-chain) catch [e] BODY` (ERR E2.4). The chain's operands +/// route per the chain rules; its TOTAL failure (the final operand failing) +/// is redirected to the catch handler via `chain_fail_target` rather than +/// propagating to the function. `e` binds the final error tag; the handler's +/// value (or divergence) joins the chain's success value at the merge. +pub fn lowerCatchOverChain(self: *Lowering, ce: *const ast.CatchExpr, span: ast.Span) Ref { + const chain = &ce.operand.data.binary_op; + + // The error tag reaching the handler is the final operand's (left-assoc + // chain → the top-level rhs). A value-terminator last operand means the + // chain can't fail — nothing for `catch` to absorb. + const last = unwrapTryNode(chain.rhs); + const last_ty = self.inferExprType(last); + const err_set = self.errorChannelOf(last_ty) orelse { + if (self.diagnostics) |d| d.addFmt(.err, span, "`catch` here is redundant — the `or` chain already absorbs every failure via its value terminator", .{}); + return self.builder.constInt(0, .void); + }; + + const succ_ty = self.orChainSuccessType(chain); + const has_value = succ_ty != .void; + + const handle_bb = self.freshBlockWithParams("catch.handle", &.{err_set}); + const merge_bb = if (has_value) + self.freshBlockWithParams("catch.merge", &.{succ_ty}) + else + self.freshBlock("catch.merge"); + + // Lower the chain with its total failure routed to the handler. + const saved = self.chain_fail_target; + self.chain_fail_target = .{ .bb = handle_bb, .set = err_set }; + const chain_val = self.lowerExpr(ce.operand); + self.chain_fail_target = saved; + // Chain success → merge with its value (the buffer was already cleared + // at the succeeding operand inside the chain). + if (has_value) { + const cv = self.coerceToType(chain_val, self.builder.getRefType(chain_val), succ_ty); + self.builder.br(merge_bb, &.{cv}); + } else { + self.builder.br(merge_bb, &.{}); + } + + // Handler: bind the final tag, run the body. The buffer still holds the + // chain's frames (handler may inspect them); absorb on non-diverging exit. + self.builder.switchToBlock(handle_bb); + const tag = self.builder.blockParam(handle_bb, 0, err_set); + const body_val = self.runCatchBody(ce, tag, err_set, if (has_value) succ_ty else null); + if (!self.currentBlockHasTerminator()) { + self.finishCatchHandler(body_val, succ_ty, merge_bb, span); + } + + self.builder.switchToBlock(merge_bb); + return if (has_value) self.builder.blockParam(merge_bb, 0, succ_ty) else self.builder.constInt(0, .void); +} + +/// Close a non-terminated `catch` handler block. `succ_ty` is the catch's +/// result type (`.void` for a pure-failable / void-chain catch — the merge +/// block then has no parameter). A `body_val` typed `noreturn` (e.g. a +/// `process.exit` / other noreturn call, which is NOT an IR terminator) +/// diverges: close with `unreachable` and skip the merge edge so its +/// "value" never reaches a phi. Otherwise clear the absorbed trace and +/// branch to the merge (coercing the body value, or diagnosing a missing / +/// void value for a value-carrying catch). +pub fn finishCatchHandler(self: *Lowering, body_val: ?Ref, succ_ty: TypeId, merge_bb: BlockId, span: ast.Span) void { + if (body_val) |v| { + if (self.builder.getRefType(v) == .noreturn) { + self.builder.emitUnreachable(); + return; + } + } + self.emitTraceClear(); + if (succ_ty == .void) { + self.builder.br(merge_bb, &.{}); + return; + } + const bv: Ref = blk: { + if (body_val) |v| { + const vty = self.builder.getRefType(v); + if (vty != .void) break :blk self.coerceToType(v, vty, succ_ty); + } + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "`catch` body must produce a value of type '{s}' (or diverge with `return` / `raise`)", .{self.formatTypeName(succ_ty)}); + } + break :blk self.builder.constUndef(succ_ty); + }; + self.builder.br(merge_bb, &.{bv}); +} + +/// Lower a `catch` body in a child scope that binds the error tag to the +/// catch binding (if any). When `want_ty` is non-null (value-carrying +/// catch), returns the body's value (or null if the body diverged); when +/// null (pure-failable catch), runs the body for effect and returns null. +pub fn runCatchBody(self: *Lowering, ce: *const ast.CatchExpr, err_val: Ref, err_set: TypeId, want_ty: ?TypeId) ?Ref { + var handle_scope = Scope.init(self.alloc, self.scope); + const saved_scope = self.scope; + self.scope = &handle_scope; + defer { + self.scope = saved_scope; + handle_scope.deinit(); + } + if (ce.binding) |name| { + handle_scope.put(name, .{ .ref = err_val, .ty = err_set, .is_alloca = false }); + } + if (want_ty == null) { + if (ce.body.data == .block) self.lowerBlock(ce.body) else _ = self.lowerExpr(ce.body); + return null; + } + const saved_fbv = self.force_block_value; + self.force_block_value = true; + defer self.force_block_value = saved_fbv; + return if (ce.body.data == .block) self.lowerBlockValue(ce.body) else self.lowerExpr(ce.body); +} + +/// `lhs or rhs` with a failable LHS (ERR step E2.4a — the value-terminator +/// form). On LHS success the result is its value part (the lone value, or a +/// value-tuple); on failure the LHS error is discarded and the result is +/// `rhs` (a plain value of the success type), so the whole expression is +/// non-failable. The CHAIN form (`... or try ...` / a failable RHS) needs +/// the fallback-target routing deferred from E1.4 — bail. +/// Widening at an escape (function-propagation) site: the escaping set must +/// be ⊆ the caller's named set. An inferred caller (`!`) absorbs everything +/// via the whole-program SCC (E1.4b) — no check. A bare-`!` callee carries +/// no tags on its placeholder TypeId, so check its SCC-converged set. +/// Shared by `try` propagation and a failable `or` chain's final operand. +pub fn checkEscapeWidening(self: *Lowering, callee_node: *const Node, callee_set: TypeId, caller_set: TypeId, span: ast.Span) void { + if (self.isInferredErrorSet(caller_set)) return; + if (!self.isInferredErrorSet(callee_set)) { + self.checkErrorSetSubset(callee_set, caller_set, span); + return; + } + // Bare-`!` callee: either a named top-level function (its converged set + // is name-keyed) or a closure/fn-type SLOT (its set is shape-keyed, + // shared program-wide by value-signature). + if (callTargetName(callee_node)) |nm| { + if (self.inferred_error_sets.get(nm)) |tags| { + self.diagTagsNotInSet(tags, caller_set, span); + return; + } + } + if (self.shapeKeyOfCallee(callee_node)) |key| { + if (self.shape_inferred_sets.get(key)) |tags| { + self.diagTagsNotInSet(tags, caller_set, span); + } + // Empty union (no closure of this shape ever raises) → silently + // allowed: the slot's `!` resolves to ∅ (ERR E5.1 sub-feature 6). + } +} + +/// Structural test: is this `or` a *failable* construct (value-terminator or +/// chain), rather than a boolean / optional-unwrap `or`? True when either +/// operand is failable-like — a `try`, an error-channel-typed expression, or +/// itself a nested failable `or` chain. Kept separate from `inferExprType`: +/// a `try`-chain's *value* type is its success type `T` (non-failable), so +/// the chain-ness is structural, not type-derived. +pub fn orIsFailableChain(self: *Lowering, bop: *const ast.BinaryOp) bool { + return self.operandIsFailableLike(bop.lhs) or self.operandIsFailableLike(bop.rhs); +} + +pub fn operandIsFailableLike(self: *Lowering, node: *const Node) bool { + if (node.data == .try_expr) return true; + if (node.data == .binary_op and node.data.binary_op.op == .or_op) { + return self.orIsFailableChain(&node.data.binary_op); + } + return self.errorChannelOf(self.inferExprType(node)) != null; +} + +/// The success (value) type of a failable `or` chain: descend to the +/// leftmost operand, unwrap any `try`, and take its failable success type +/// (`void` for a pure-`-> !` chain). All operands share this type. +pub fn orChainSuccessType(self: *Lowering, bop: *const ast.BinaryOp) TypeId { + var lhs = bop.lhs; + while (lhs.data == .binary_op and lhs.data.binary_op.op == .or_op and + self.orIsFailableChain(&lhs.data.binary_op)) + { + lhs = lhs.data.binary_op.lhs; + } + const ft = self.inferExprType(unwrapTryNode(lhs)); + const fset = self.errorChannelOf(ft) orelse return .unresolved; + return if (ft == fset) .void else self.failableSuccessType(ft); +} + +/// `try X` → `X` (the underlying failable); any other node unchanged. In an +/// `or` chain the `try` marker's routing IS the chain, so the chain lowers +/// the underlying failable directly rather than re-entering `lowerTry`. +pub fn unwrapTryNode(node: *const Node) *const Node { + return if (node.data == .try_expr) node.data.try_expr.operand else node; +} + +/// Flatten a left-associative failable `or` chain into its operands, +/// left-to-right. `a or b or c` parses as `(a or b) or c`; this collects +/// `[a, b, c]`. Walks the left spine only while it stays a failable +/// `or` chain (a parenthesized non-chain `or` on the left stops the walk). +pub fn flattenOrChain(self: *Lowering, bop: *const ast.BinaryOp, list: *std.ArrayList(*const Node)) void { + if (bop.lhs.data == .binary_op and bop.lhs.data.binary_op.op == .or_op and + self.orIsFailableChain(&bop.lhs.data.binary_op)) + { + self.flattenOrChain(&bop.lhs.data.binary_op, list); + } else { + list.append(self.alloc, bop.lhs) catch unreachable; + } + list.append(self.alloc, bop.rhs) catch unreachable; +} + +/// Lower a failable `or` (ERR E2.4): a value-terminator (`lhs or value`) or +/// a chain (`try a or try b or …`, possibly with a trailing value +/// terminator). Left-to-right, short-circuit: each failable operand's +/// failure routes to the next operand; the final operand either absorbs +/// (value terminator) or propagates to the enclosing function. Each failed +/// attempt pushes a trace frame; an absorbing resolution (any operand +/// succeeding, or the value terminator) clears the buffer; total failure +/// preserves the frames for the caller. +pub fn lowerFailableOr(self: *Lowering, bop: *const ast.BinaryOp) Ref { + const span = bop.lhs.span; + + var operands = std.ArrayList(*const Node).empty; + defer operands.deinit(self.alloc); + self.flattenOrChain(bop, &operands); + const last_idx = operands.items.len - 1; + const last_is_value = !self.operandIsFailableLike(operands.items[last_idx]); + + // The chain's total-failure routing. An absorbing consumer (`catch`) + // sets this so the final operand's failure reaches the handler; cleared + // while lowering operands so a nested operand doesn't inherit it. + const fail_target = self.chain_fail_target; + self.chain_fail_target = null; + defer self.chain_fail_target = fail_target; + + // Success type from the first operand (a failable; unwrap any `try`). + const first_ty = self.inferExprType(unwrapTryNode(operands.items[0])); + const first_set = self.errorChannelOf(first_ty) orelse { + if (self.diagnostics) |d| d.addFmt(.err, span, "the left operand of a failable `or` must be failable; got '{s}'", .{self.formatTypeName(first_ty)}); + return self.builder.constInt(0, .void); + }; + const has_value = first_ty != first_set; + const succ_ty = if (has_value) self.failableSuccessType(first_ty) else TypeId.void; + + // Pure-failable LHS (`-> !`) with a value terminator: nothing to fall + // back to. + if (!has_value and last_is_value) { + if (self.diagnostics) |d| d.addFmt(.err, span, "`or value` requires a value-carrying failable (`-> (T, !)`) — a `-> !` has no success value to fall back to; use `catch` to absorb the error", .{}); + return self.builder.constInt(0, .void); + } + + // Caller failability — only needed when the chain can propagate to the + // function (final operand is failable AND no absorbing consumer target). + var caller_ret: TypeId = .void; + var caller_set: TypeId = .void; + if (!last_is_value and fail_target == null) { + const cret = self.effectiveReturnType(); + const cset = if (cret) |r| self.errorChannelOf(r) else null; + if (cset == null) { + if (self.diagnostics) |d| d.addFmt(.err, span, "a failable `or` chain propagates on total failure, so it is only valid inside a failable function — add a value terminator (`… or value`) or wrap with `catch`", .{}); + return self.builder.constInt(0, .void); + } + caller_ret = cret.?; + caller_set = cset.?; + } + + const merge_bb = if (has_value) + self.freshBlockWithParams("orc.merge", &.{succ_ty}) + else + self.freshBlock("orc.merge"); + + for (operands.items, 0..) |operand, i| { + const is_last = i == last_idx; + + if (is_last and last_is_value) { + // Value terminator: absorbs every prior failure. + self.emitTraceClear(); + const saved = self.target_type; + self.target_type = succ_ty; + const v = self.lowerExpr(operand); + self.target_type = saved; + const vc = self.coerceToType(v, self.builder.getRefType(v), succ_ty); + self.builder.br(merge_bb, &.{vc}); + break; + } + + // Failable operand (`try X` marker or a bare failable). Lower the + // underlying failable; the `try` marker's routing IS the chain. + const underlying = unwrapTryNode(operand); + const op_ty = self.inferExprType(underlying); + const op_set = self.errorChannelOf(op_ty) orelse { + if (self.diagnostics) |d| d.addFmt(.err, operand.span, "operand of a failable `or` chain must be failable; got '{s}'", .{self.formatTypeName(op_ty)}); + return self.builder.constInt(0, .void); + }; + const op_value_carrying = op_ty != op_set; + + // Widening applies only when the final failure escapes to the + // function (no absorbing consumer); a `catch` target absorbs it. + if (is_last and fail_target == null) self.checkEscapeWidening(underlying, op_set, caller_set, operand.span); + + const result = self.lowerExpr(underlying); + const err_val = if (op_value_carrying) self.extractErrorSlot(result, op_ty, op_set) else result; + const err_ty = self.builder.getRefType(err_val); + const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool); + + const ok_bb = self.freshBlock("orc.ok"); + const fail_bb = self.freshBlock(if (is_last) "orc.prop" else "orc.next"); + self.builder.condBr(is_err, fail_bb, &.{}, ok_bb, &.{}); + + // Success: the chain resolved here — clear the buffer, merge value. + self.builder.switchToBlock(ok_bb); + self.emitTraceClear(); + if (has_value) { + const sv = self.extractSuccessValue(result, op_ty, succ_ty); + const svc = self.coerceToType(sv, self.builder.getRefType(sv), succ_ty); + self.builder.br(merge_bb, &.{svc}); + } else { + self.builder.br(merge_bb, &.{}); + } + + // Failure: push a trace frame, then either route to the next + // operand (same block — no function exit, so `onfail` does not + // fire) or, for the final operand, resolve the total failure: to an + // absorbing consumer (`catch`) if one set a target, else propagate + // to the caller. + self.builder.switchToBlock(fail_bb); + self.emitTracePush(self.placeholderTraceFrame()); + if (is_last) { + if (fail_target) |t| { + const ec = self.coerceToType(err_val, self.builder.getRefType(err_val), t.set); + self.builder.br(t.bb, &.{ec}); + } else { + self.emitErrorCleanup(self.func_defer_base, err_val); + self.emitErrorReturn(caller_ret, caller_set, err_val); + } + } + // else: fall through — the next operand is lowered in fail_bb. + } + + self.builder.switchToBlock(merge_bb); + return if (has_value) self.builder.blockParam(merge_bb, 0, succ_ty) else self.builder.constInt(0, .void); +} + +// ── ERR E1.4b: whole-program inferred-error-set convergence ────────── + +/// The bare callee name of a call expression (`g(...)` → "g"), or null if +/// the node isn't a direct call to a named function. E1.4b resolves only +/// the bare identifier (top-level functions); UFCS / mangled-local callees +/// aren't tracked by the SCC. +pub fn callTargetName(node: *const Node) ?[]const u8 { + if (node.data != .call) return null; + const callee = node.data.call.callee; + return if (callee.data == .identifier) callee.data.identifier.name else null; +} + +/// True when `rt` is a pure bare-`!` failable return (`-> !`, the inferred +/// set) — NOT `!Named` and NOT a value-carrying `-> (T..., !)` tuple. +pub fn astIsPureBareInferred(rt: ?*const Node) bool { + const n = rt orelse return false; + return n.data == .error_type_expr and n.data.error_type_expr.name == null; +} + +/// The named-set name of a pure `-> !Named` return (`"Named"`), or null for +/// bare-`!`, value-carrying, or non-failable returns. +pub fn astPureNamedSet(rt: ?*const Node) ?[]const u8 { + const n = rt orelse return null; + if (n.data != .error_type_expr) return null; + return n.data.error_type_expr.name; +} + +/// The declared tags of a named error set, by name; null if not a +/// registered error set. +pub fn namedSetTags(self: *Lowering, name: []const u8) ?[]const u32 { + const sid = self.module.types.internString(name); + const tid = self.module.types.findByName(sid) orelse return null; + if (tid.isBuiltin()) return null; + const info = self.module.types.get(tid); + return if (info == .error_set) info.error_set.tags else null; +} + +/// Whole-program inferred-error-set convergence. Thin delegation to the +/// canonical owner (`ErrorAnalysis`, `error_analysis.zig`); kept on +/// `Lowering` as a `pub` entry point because the lowering pipeline + the +/// E1.4b unit test call it. +pub fn convergeInferredErrorSets(self: *Lowering) void { + self.errorAnalysis().convergeInferredErrorSets(); +} + +pub fn containsTag(tags: []const u32, t: u32) bool { + for (tags) |x| if (x == t) return true; + return false; +} + +/// Whole-program closure-shape error-set convergence. Thin delegation to the +/// canonical owner (`ErrorAnalysis`, `error_analysis.zig`); kept on +/// `Lowering` as a `pub` entry point because the lowering pipeline calls it. +pub fn convergeClosureShapeSets(self: *Lowering) void { + self.errorAnalysis().convergeClosureShapeSets(); +} + +/// Record one closure literal's contribution to its value-signature shape's +/// inferred-`!` union. No-op unless the literal is a CONCRETE (non-generic) +/// bare-`!` failable closure; named-set / non-failable literals add no tags. +pub fn recordClosureShape(self: *Lowering, lam: *const ast.Lambda) void { + if (lam.type_params.len > 0) return; // generic shapes out of scope (sub-feature 8) + const rt_node = lam.return_type orelse return; // no annotation → non-failable infer + const ret = self.resolveType(rt_node); + const es = self.errorChannelOf(ret) orelse return; // not failable + if (!self.isInferredErrorSet(es)) return; // `!Named` → its own set, not the inferred union + + var ptys = std.ArrayList(TypeId).empty; + defer ptys.deinit(self.alloc); + for (lam.params) |p| { + if (p.is_variadic or p.is_pack or p.is_comptime) return; // not a plain fn-type slot + ptys.append(self.alloc, self.resolveType(p.type_expr)) catch return; + } + const key = self.closureShapeKey(ptys.items, self.returnValuePart(ret)); + + var tags = std.ArrayList(u32).empty; + defer tags.deinit(self.alloc); + var edges = std.ArrayList([]const u8).empty; + defer edges.deinit(self.alloc); + self.errorAnalysis().collectErrorSites(lam.body, &tags, &edges); + for (edges.items) |callee| { + for (self.calleeEscapeTags(callee)) |t| { + if (!containsTag(tags.items, t)) tags.append(self.alloc, t) catch {}; + } + } + self.unionShapeTags(key, tags.items); +} + +/// The escape tags of a callee referenced by name from a `try g()` edge: +/// a bare-`!` callee's converged set, or a `-> !Named` callee's declared set. +pub fn calleeEscapeTags(self: *Lowering, callee: []const u8) []const u32 { + if (self.inferred_error_sets.get(callee)) |t| return t; + if (self.program_index.fn_ast_map.get(callee)) |cfd| { + if (astPureNamedSet(cfd.return_type)) |nm| return self.namedSetTags(nm) orelse &.{}; + } + return &.{}; +} + +/// Merge `new_tags` into the shape node `key` (sorted, deduped). The map is +/// content-keyed (StringHashMap), so re-`put` with a fresh equal key string +/// overwrites the existing node's value in place. +pub fn unionShapeTags(self: *Lowering, key: []const u8, new_tags: []const u32) void { + var list = std.ArrayList(u32).empty; + defer list.deinit(self.alloc); + if (self.shape_inferred_sets.get(key)) |existing| list.appendSlice(self.alloc, existing) catch {}; + for (new_tags) |t| { + if (!containsTag(list.items, t)) list.append(self.alloc, t) catch {}; + } + const sorted = self.alloc.dupe(u32, list.items) catch return; + std.mem.sort(u32, sorted, {}, std.sort.asc(u32)); + self.shape_inferred_sets.put(key, sorted) catch {}; +} + +/// Canonical key for a callable VALUE-signature: param types + the value +/// part of the return (error slot excluded). Bare-`!` and non-failable +/// shapes of the same value-sig — and `.function` vs `.closure` of that +/// sig — collapse to one key, so all occurrences share one inferred node. +pub fn closureShapeKey(self: *Lowering, params: []const TypeId, value_ret: TypeId) []const u8 { + var buf = std.ArrayList(u8).empty; + buf.appendSlice(self.alloc, "shape") catch return "shape"; + for (params) |p| { + buf.append(self.alloc, '_') catch return "shape"; + buf.appendSlice(self.alloc, self.mangleTypeName(p)) catch return "shape"; + } + buf.appendSlice(self.alloc, "__") catch return "shape"; + buf.appendSlice(self.alloc, self.mangleTypeName(value_ret)) catch return "shape"; + return buf.items; +} + +/// The value part of a (possibly failable) return type, error slot dropped: +/// `(T, !)` → T (or a value-tuple); pure `-> !` → void; non-failable → self. +pub fn returnValuePart(self: *Lowering, ret: TypeId) TypeId { + const es = self.errorChannelOf(ret) orelse return ret; + if (ret == es) return .void; + return self.failableSuccessType(ret); +} + +/// Shape key of a call's callee expression when it's a closure/fn-type slot +/// (variable, field, index — anything with a `.closure`/`.function` type), +/// for the program-wide shape-union widening lookup. Null for non-callables. +pub fn shapeKeyOfCallee(self: *Lowering, node: *const Node) ?[]const u8 { + if (node.data != .call) return null; + const fty = self.inferExprType(node.data.call.callee); + if (fty.isBuiltin()) return null; + const info = self.module.types.get(fty); + const params: []const TypeId = switch (info) { + .closure => |c| c.params, + .function => |f| f.params, + else => return null, + }; + const ret: TypeId = switch (info) { + .closure => |c| c.ret, + .function => |f| f.ret, + else => return null, + }; + return self.closureShapeKey(params, self.returnValuePart(ret)); +}