ERR/E4.1 (slice 1): log + is_comptime + process.exit/assert (+ noreturn codegen)
Stdlib slice of Phase E4, plus the noreturn codegen fix that enables it. noreturn codegen (the enabling bug): E1.4c made `noreturn` type-system-only; this is its first backend consumer and it crashed LLVM verification. Fixed: - lower.zig: a `-> noreturn` body lowers as statements ending in `unreachable` (ensureTerminator emits unreachable; the two body-lowering sites no longer treat the last expr as a `ret`). - emit_llvm.zig: a `void`/`noreturn` call result stays unnamed (direct + foreign call sites) — LLVM rejects a named void value. - finishCatchHandler: a `noreturn` value-carrying catch body (which is not an IR terminator) closes the handler with `unreachable` instead of feeding a bad value into the merge phi. Shared by lowerCatch + lowerCatchOverChain. is_comptime(): new nullary `.is_comptime` IR op (inst/print/interp/emit_llvm) — interp evaluates true, emit_llvm emits constant false, so `if is_comptime()` dead-codes out of compiled binaries. Recognized by name in tryLowerReflectionCall + inferExprType (no std.sx decl, which would emit a spurious `declare @is_comptime` into every module). library/modules/log.sx: warn/info/debug/err — interpolate like print, write `LEVEL: <msg>` to stderr. (`error` is reserved → the level is `log.err`.) process.exit(code) -> noreturn + assert(cond, msg) in process.sx. `exit` is POSIX `_exit(2)` (immediate, no cleanup; sx print is unbuffered so nothing is lost), bound to "_exit" which also avoids a link-level clash with the sx `exit` function's own name. examples 248 (exit 0), 249 (exit 42), 250 (exit 1). #caller_location, the comptime-exit diagnostic flush, and trace.print_interpreter_frames deferred to E4.1b.
This commit is contained in:
@@ -1264,8 +1264,8 @@ pub const Lowering = struct {
|
||||
|
||||
// Lower the function body (set target_type to return type for implicit returns)
|
||||
const saved_target = self.target_type;
|
||||
self.target_type = if (ret_ty != .void) ret_ty else null;
|
||||
if (ret_ty != .void) {
|
||||
self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null;
|
||||
if (ret_ty != .void and ret_ty != .noreturn) {
|
||||
const body_val = self.lowerBlockValue(fd.body);
|
||||
if (!self.currentBlockHasTerminator()) {
|
||||
if (body_val) |val| {
|
||||
@@ -1282,6 +1282,8 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// void / noreturn: no value to return — lower as statements and
|
||||
// let `ensureTerminator` close the block (ret void / unreachable).
|
||||
self.lowerBlock(fd.body);
|
||||
self.ensureTerminator(ret_ty);
|
||||
}
|
||||
@@ -1420,8 +1422,8 @@ pub const Lowering = struct {
|
||||
|
||||
// Lower the function body, capturing the last expression's value for implicit return
|
||||
const saved_target = self.target_type;
|
||||
self.target_type = if (ret_ty != .void) ret_ty else null;
|
||||
if (ret_ty != .void) {
|
||||
self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null;
|
||||
if (ret_ty != .void and ret_ty != .noreturn) {
|
||||
const body_val = self.lowerBlockValue(fd.body);
|
||||
if (!self.currentBlockHasTerminator()) {
|
||||
if (body_val) |val| {
|
||||
@@ -1438,6 +1440,8 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// void / noreturn: no value to return — lower as statements and
|
||||
// let `ensureTerminator` close the block (ret void / unreachable).
|
||||
self.lowerBlock(fd.body);
|
||||
self.ensureTerminator(ret_ty);
|
||||
}
|
||||
@@ -10316,6 +10320,12 @@ pub const Lowering = struct {
|
||||
.struct_type = ty,
|
||||
} }, .string);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "is_comptime")) {
|
||||
// True under the comptime interpreter, false in compiled code — the
|
||||
// op decides per backend (it can't fold here, since the same IR
|
||||
// serves both). Lets stdlib gate a comptime-only diagnostic branch.
|
||||
return self.builder.emit(.{ .is_comptime = {} }, .bool);
|
||||
}
|
||||
if (std.mem.eql(u8, name, "error_tag_name")) {
|
||||
// error_tag_name(e) → look the error-set value's runtime tag id up
|
||||
// in the always-linked tag-name table. The value IS its u32 tag id.
|
||||
@@ -13915,6 +13925,7 @@ pub const Lowering = struct {
|
||||
if (std.mem.eql(u8, bare_name, "field_index")) return .s64;
|
||||
if (std.mem.eql(u8, bare_name, "field_name")) return .string;
|
||||
if (std.mem.eql(u8, bare_name, "error_tag_name")) return .string;
|
||||
if (std.mem.eql(u8, bare_name, "is_comptime")) return .bool;
|
||||
if (std.mem.eql(u8, bare_name, "is_flags")) return .bool;
|
||||
if (std.mem.eql(u8, bare_name, "type_of")) return .any;
|
||||
if (std.mem.eql(u8, bare_name, "field_value")) return .any;
|
||||
@@ -15731,24 +15742,7 @@ pub const Lowering = struct {
|
||||
self.builder.switchToBlock(handle_bb);
|
||||
const body_val = self.runCatchBody(ce, err_val, err_set, succ_ty);
|
||||
if (!self.currentBlockHasTerminator()) {
|
||||
// A non-diverging handler must produce a value of the success type.
|
||||
// A value-less (void) body is a type error — diagnose and feed an
|
||||
// undef placeholder so the merge phi stays well-typed (rather than
|
||||
// coercing `void` into a bad ref).
|
||||
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);
|
||||
};
|
||||
// Absorption clear on a non-diverging handler (see the pure-failable
|
||||
// path above): the body saw the trace, now it's consumed.
|
||||
self.emitTraceClear();
|
||||
self.builder.br(merge_bb, &.{bv});
|
||||
self.finishCatchHandler(body_val, succ_ty, merge_bb, span);
|
||||
}
|
||||
|
||||
self.builder.switchToBlock(merge_bb);
|
||||
@@ -15802,26 +15796,46 @@ pub const Lowering = struct {
|
||||
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.emitTraceClear();
|
||||
if (has_value) {
|
||||
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) |d| d.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});
|
||||
} else {
|
||||
self.builder.br(merge_bb, &.{});
|
||||
}
|
||||
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
|
||||
@@ -16349,7 +16363,12 @@ pub const Lowering = struct {
|
||||
|
||||
fn ensureTerminator(self: *Lowering, ret_ty: TypeId) void {
|
||||
if (self.currentBlockHasTerminator()) return;
|
||||
if (ret_ty == .void) {
|
||||
if (ret_ty == .noreturn) {
|
||||
// A `-> noreturn` function never returns; if control reaches the
|
||||
// end of the body it's genuinely unreachable (the body is expected
|
||||
// to diverge — call another noreturn, loop forever, etc.).
|
||||
self.builder.emitUnreachable();
|
||||
} else if (ret_ty == .void) {
|
||||
self.builder.retVoid();
|
||||
} else {
|
||||
// Use const_undef for complex types (string, struct, etc.)
|
||||
|
||||
Reference in New Issue
Block a user