Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
1122 lines
53 KiB
Zig
1122 lines
53 KiB
Zig
const std = @import("std");
|
|
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 errors = @import("../../errors.zig");
|
|
const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis;
|
|
|
|
const TypeId = types.TypeId;
|
|
const Ref = inst_mod.Ref;
|
|
const BlockId = inst_mod.BlockId;
|
|
const FuncId = inst_mod.FuncId;
|
|
const Function = inst_mod.Function;
|
|
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), .i32),
|
|
self.builder.constInt(@intCast(loc.col), .i32),
|
|
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));
|
|
}
|