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:
31
examples/248-log-and-comptime.sx
Normal file
31
examples/248-log-and-comptime.sx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// `log.sx` leveled logging + the `is_comptime()` builtin (ERR step E4.1,
|
||||||
|
// slice 1). `log.{warn,info,err,debug}` interpolate like `print` and write
|
||||||
|
// `LEVEL: <msg>` to stderr. `is_comptime()` is `true` under `#run` (the
|
||||||
|
// comptime interpreter) and folds to `false` in compiled code, so a gated
|
||||||
|
// branch dead-codes out of the runtime binary.
|
||||||
|
//
|
||||||
|
// (`log.error` is spelled `log.err` — `error` is a reserved keyword. The
|
||||||
|
// `process.exit` / `assert` part of E4.1 is blocked on `noreturn` codegen,
|
||||||
|
// issue 0058.)
|
||||||
|
//
|
||||||
|
// The test runner merges stderr+stdout; the log lines (stderr) precede the
|
||||||
|
// single stdout line. Expected exit code: 0.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
log :: #import "modules/log.sx";
|
||||||
|
|
||||||
|
probe :: () -> s32 {
|
||||||
|
if is_comptime() { return 1; } // comptime interpreter path
|
||||||
|
return 2; // compiled-code path
|
||||||
|
}
|
||||||
|
|
||||||
|
CT :: #run probe(); // folds to 1 (run in the interpreter)
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
log.warn("disk {}% full", 91);
|
||||||
|
log.info("user {} connected", "alice");
|
||||||
|
log.err("bad fd {}", 7);
|
||||||
|
log.debug("trace x={}", 42);
|
||||||
|
print("[stdout] is_comptime runtime={} comptime={}\n", probe(), CT);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
15
examples/249-process-exit.sx
Normal file
15
examples/249-process-exit.sx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// `process.exit` (ERR step E4.1): immediate process termination with an exit
|
||||||
|
// code. No `defer` / `onfail` cleanup runs and no error-trace frames are pushed
|
||||||
|
// — it's POSIX `_exit(2)`. Here a runtime call exits 42; the line after never
|
||||||
|
// runs. (sx `print` writes unbuffered via `write(2)`, so the "starting" line
|
||||||
|
// still appears despite `_exit` skipping the stdio flush.) Expected exit: 42.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
proc :: #import "modules/process.sx";
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
print("starting\n");
|
||||||
|
proc.exit(42);
|
||||||
|
print("unreachable\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
15
examples/250-assert.sx
Normal file
15
examples/250-assert.sx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// `assert` (ERR step E4.1): a false condition prints `ASSERTION FAILED: <msg>`
|
||||||
|
// and exits 1; a true condition is a no-op. Built on `process.exit`. (The
|
||||||
|
// caller's `file:line` in the message rides on `#caller_location` — E4.1b.)
|
||||||
|
// Expected exit code: 1.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
proc :: #import "modules/process.sx";
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
proc.assert(2 + 2 == 4, "arithmetic"); // passes → no-op
|
||||||
|
print("first assert passed\n");
|
||||||
|
proc.assert(2 + 2 == 5, "two plus two is not five"); // fails → abort
|
||||||
|
print("unreachable\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
29
library/modules/log.sx
Normal file
29
library/modules/log.sx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#import "std.sx";
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// log.sx — plain leveled logging (ERR step E4.1), orthogonal to the
|
||||||
|
// error channel. Each entry is written to stderr as `LEVEL: <msg>\n`,
|
||||||
|
// where <msg> is the formatted `fmt` + args (same `{}` interpolation as
|
||||||
|
// `print`). Sink is stderr (fd 2) so log output stays out of a program's
|
||||||
|
// stdout data stream.
|
||||||
|
//
|
||||||
|
// Note: PLAN-ERR §log sketches a `LEVEL ts msg` line with an ISO-8601
|
||||||
|
// UTC timestamp. The timestamp is deferred — it needs a clock binding
|
||||||
|
// and would make golden tests time-dependent; the level + message are
|
||||||
|
// the load-bearing parts. Add `ts` once a pinnable clock lands.
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
libc :: #library "c";
|
||||||
|
|
||||||
|
write :: (fd: s32, buf: [*]u8, count: usize) -> isize #foreign libc;
|
||||||
|
|
||||||
|
// Prefix the level, append a newline, write the whole line to stderr.
|
||||||
|
emit :: (level: string, msg: string) {
|
||||||
|
line := concat(concat(level, msg), "\n");
|
||||||
|
write(2, line.ptr, xx line.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn :: ($fmt: string, ..$args) { #insert build_format(fmt); #insert "emit(\"WARN: \", result);"; }
|
||||||
|
info :: ($fmt: string, ..$args) { #insert build_format(fmt); #insert "emit(\"INFO: \", result);"; }
|
||||||
|
debug :: ($fmt: string, ..$args) { #insert build_format(fmt); #insert "emit(\"DEBUG: \", result);"; }
|
||||||
|
err :: ($fmt: string, ..$args) { #insert build_format(fmt); #insert "emit(\"ERROR: \", result);"; }
|
||||||
@@ -116,3 +116,39 @@ find_executable :: (name: [:0]u8) -> ?string {
|
|||||||
}
|
}
|
||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Process termination (ERR step E4.1) ───────────────────────────────
|
||||||
|
|
||||||
|
// Bound to POSIX `_exit(2)` (immediate termination — no atexit, no stdio
|
||||||
|
// flush), NOT libc `exit(3)`. Two reasons: (1) it matches `process.exit`'s
|
||||||
|
// "immediate stop, no cleanup" contract; (2) sx's `print` writes unbuffered
|
||||||
|
// via `write(2)`, so skipping the stdio flush loses nothing. Binding the
|
||||||
|
// symbol `"exit"` would also collide with this module's own `exit` function
|
||||||
|
// at the link level.
|
||||||
|
clib_exit :: (code: s32) -> noreturn #foreign libc "_exit";
|
||||||
|
|
||||||
|
// Stop the process immediately with exit code `code`. Does NOT unwind:
|
||||||
|
// no `defer` / `onfail` cleanup runs, no error-trace frames are pushed —
|
||||||
|
// it's the POSIX `_exit(2)` syscall. At comptime (`#run`) it terminates the
|
||||||
|
// COMPILER with the same code after printing a short diagnostic; in compiled
|
||||||
|
// code the `is_comptime()` branch folds away to just the syscall.
|
||||||
|
//
|
||||||
|
// (PLAN-ERR E4.1 also specifies a `loc: Source_Location = #caller_location`
|
||||||
|
// parameter and an interpreter-frame dump in the comptime branch. Both ride
|
||||||
|
// on the `#caller_location` directive — deferred to E4.1b.)
|
||||||
|
exit :: (code: u8) -> noreturn {
|
||||||
|
if is_comptime() {
|
||||||
|
print("\nprocess.exit({}) called at comptime\n", code);
|
||||||
|
}
|
||||||
|
clib_exit(xx code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abort with a message when `cond` is false. Prints `ASSERTION FAILED: <msg>`
|
||||||
|
// then exits 1; a true condition is a no-op. (E4.1b adds the caller's
|
||||||
|
// `file:line` via `#caller_location`.)
|
||||||
|
assert :: (cond: bool, msg: string) {
|
||||||
|
if !cond {
|
||||||
|
print("ASSERTION FAILED: {}\n", msg);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1658,6 +1658,11 @@ pub const LLVMEmitter = struct {
|
|||||||
const llvm_val = c.LLVMConstInt(self.cached_i1, @intFromBool(val), 0);
|
const llvm_val = c.LLVMConstInt(self.cached_i1, @intFromBool(val), 0);
|
||||||
self.mapRef(llvm_val);
|
self.mapRef(llvm_val);
|
||||||
},
|
},
|
||||||
|
.is_comptime => {
|
||||||
|
// Compiled code is never the comptime interpreter → constant
|
||||||
|
// `false`. A `if is_comptime() { … }` branch becomes dead.
|
||||||
|
self.mapRef(c.LLVMConstInt(self.cached_i1, 0, 0));
|
||||||
|
},
|
||||||
.const_string => |str_id| {
|
.const_string => |str_id| {
|
||||||
const str = self.ir_mod.types.getString(str_id);
|
const str = self.ir_mod.types.getString(str_id);
|
||||||
const llvm_val = self.emitStringConstant(str);
|
const llvm_val = self.emitStringConstant(str);
|
||||||
@@ -2343,7 +2348,10 @@ pub const LLVMEmitter = struct {
|
|||||||
args[j] = self.coerceArg(args[j], param_types[j]);
|
args[j] = self.coerceArg(args[j], param_types[j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const call_label: [*:0]const u8 = if (instruction.ty == .void or callee_uses_sret) "" else "call";
|
// A `void`/`noreturn` call has no value, so it must stay
|
||||||
|
// unnamed (LLVM rejects a named void result).
|
||||||
|
const call_is_void_like = instruction.ty == .void or instruction.ty == .noreturn;
|
||||||
|
const call_label: [*:0]const u8 = if (call_is_void_like or callee_uses_sret) "" else "call";
|
||||||
var result = c.LLVMBuildCall2(self.builder, fn_ty, callee, args.ptr, arg_count, call_label);
|
var result = c.LLVMBuildCall2(self.builder, fn_ty, callee, args.ptr, arg_count, call_label);
|
||||||
if (callee_uses_sret) {
|
if (callee_uses_sret) {
|
||||||
// Mirror the function-decl `sret(<T>)` attribute on the call site so the
|
// Mirror the function-decl `sret(<T>)` attribute on the call site so the
|
||||||
@@ -2354,7 +2362,7 @@ pub const LLVMEmitter = struct {
|
|||||||
c.LLVMAddCallSiteAttribute(result, param1_idx, sret_attr);
|
c.LLVMAddCallSiteAttribute(result, param1_idx, sret_attr);
|
||||||
// Load the actual struct value the callee wrote into the slot.
|
// Load the actual struct value the callee wrote into the slot.
|
||||||
result = c.LLVMBuildLoad2(self.builder, callee_raw_ret, sret_slot, "sret.load");
|
result = c.LLVMBuildLoad2(self.builder, callee_raw_ret, sret_slot, "sret.load");
|
||||||
} else if (instruction.ty != .void and callee_func.is_extern) {
|
} else if (!call_is_void_like and callee_func.is_extern) {
|
||||||
// Coerce ABI return value (e.g. i64 / [2 x i64]) back to IR struct type if needed
|
// Coerce ABI return value (e.g. i64 / [2 x i64]) back to IR struct type if needed
|
||||||
const expected_ty = self.toLLVMType(instruction.ty);
|
const expected_ty = self.toLLVMType(instruction.ty);
|
||||||
result = self.coerceArg(result, expected_ty);
|
result = self.coerceArg(result, expected_ty);
|
||||||
@@ -2455,11 +2463,12 @@ pub const LLVMEmitter = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fn_ty = c.LLVMFunctionType(ret_ty, param_tys.ptr, arg_count, 0);
|
const fn_ty = c.LLVMFunctionType(ret_ty, param_tys.ptr, arg_count, 0);
|
||||||
var result = c.LLVMBuildCall2(self.builder, fn_ty, callee, args.ptr, arg_count, if (instruction.ty == .void) "" else "icall");
|
const icall_void_like = instruction.ty == .void or instruction.ty == .noreturn;
|
||||||
|
var result = c.LLVMBuildCall2(self.builder, fn_ty, callee, args.ptr, arg_count, if (icall_void_like) "" else "icall");
|
||||||
|
|
||||||
// Coerce call result to instruction's expected type
|
// Coerce call result to instruction's expected type
|
||||||
const expected_ty = self.toLLVMType(instruction.ty);
|
const expected_ty = self.toLLVMType(instruction.ty);
|
||||||
if (instruction.ty != .void and c.LLVMTypeOf(result) != expected_ty) {
|
if (!icall_void_like and c.LLVMTypeOf(result) != expected_ty) {
|
||||||
result = self.coerceArg(result, expected_ty);
|
result = self.coerceArg(result, expected_ty);
|
||||||
}
|
}
|
||||||
self.mapRef(result);
|
self.mapRef(result);
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ pub const Op = union(enum) {
|
|||||||
const_string: StringId,
|
const_string: StringId,
|
||||||
const_null,
|
const_null,
|
||||||
const_undef, // `---` undefined initializer
|
const_undef, // `---` undefined initializer
|
||||||
|
/// ERR E4.1 — `is_comptime()` builtin. The SAME lowered IR is run by both
|
||||||
|
/// the comptime interpreter and the compiled backend, so this can't fold at
|
||||||
|
/// lower time: the interp evaluates it to `true`, emit_llvm emits constant
|
||||||
|
/// `false`. Lets stdlib (`process.exit`, `assert`) take a comptime-only
|
||||||
|
/// diagnostic branch that dead-codes out of compiled binaries.
|
||||||
|
is_comptime,
|
||||||
/// Comptime-only Type value. Carried as a `Value.type_tag(TypeId)`
|
/// Comptime-only Type value. Carried as a `Value.type_tag(TypeId)`
|
||||||
/// in the interpreter. NEVER emitted to LLVM — types are erased
|
/// in the interpreter. NEVER emitted to LLVM — types are erased
|
||||||
/// after lowering. `emit_llvm` bails loudly if it sees one,
|
/// after lowering. `emit_llvm` bails loudly if it sees one,
|
||||||
|
|||||||
@@ -599,6 +599,7 @@ pub const Interpreter = struct {
|
|||||||
.const_string => |sid| return .{ .value = .{ .string = self.module.types.getString(sid) } },
|
.const_string => |sid| return .{ .value = .{ .string = self.module.types.getString(sid) } },
|
||||||
.const_null => return .{ .value = .null_val },
|
.const_null => return .{ .value = .null_val },
|
||||||
.const_undef => return .{ .value = .undef },
|
.const_undef => return .{ .value = .undef },
|
||||||
|
.is_comptime => return .{ .value = .{ .boolean = true } },
|
||||||
.const_type => |tid| return .{ .value = .{ .type_tag = tid } },
|
.const_type => |tid| return .{ .value = .{ .type_tag = tid } },
|
||||||
|
|
||||||
// ── Arithmetic ──────────────────────────────────────
|
// ── Arithmetic ──────────────────────────────────────
|
||||||
|
|||||||
@@ -1264,8 +1264,8 @@ pub const Lowering = struct {
|
|||||||
|
|
||||||
// Lower the function body (set target_type to return type for implicit returns)
|
// Lower the function body (set target_type to return type for implicit returns)
|
||||||
const saved_target = self.target_type;
|
const saved_target = self.target_type;
|
||||||
self.target_type = if (ret_ty != .void) ret_ty else null;
|
self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null;
|
||||||
if (ret_ty != .void) {
|
if (ret_ty != .void and ret_ty != .noreturn) {
|
||||||
const body_val = self.lowerBlockValue(fd.body);
|
const body_val = self.lowerBlockValue(fd.body);
|
||||||
if (!self.currentBlockHasTerminator()) {
|
if (!self.currentBlockHasTerminator()) {
|
||||||
if (body_val) |val| {
|
if (body_val) |val| {
|
||||||
@@ -1282,6 +1282,8 @@ pub const Lowering = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// void / noreturn: no value to return — lower as statements and
|
||||||
|
// let `ensureTerminator` close the block (ret void / unreachable).
|
||||||
self.lowerBlock(fd.body);
|
self.lowerBlock(fd.body);
|
||||||
self.ensureTerminator(ret_ty);
|
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
|
// Lower the function body, capturing the last expression's value for implicit return
|
||||||
const saved_target = self.target_type;
|
const saved_target = self.target_type;
|
||||||
self.target_type = if (ret_ty != .void) ret_ty else null;
|
self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null;
|
||||||
if (ret_ty != .void) {
|
if (ret_ty != .void and ret_ty != .noreturn) {
|
||||||
const body_val = self.lowerBlockValue(fd.body);
|
const body_val = self.lowerBlockValue(fd.body);
|
||||||
if (!self.currentBlockHasTerminator()) {
|
if (!self.currentBlockHasTerminator()) {
|
||||||
if (body_val) |val| {
|
if (body_val) |val| {
|
||||||
@@ -1438,6 +1440,8 @@ pub const Lowering = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// void / noreturn: no value to return — lower as statements and
|
||||||
|
// let `ensureTerminator` close the block (ret void / unreachable).
|
||||||
self.lowerBlock(fd.body);
|
self.lowerBlock(fd.body);
|
||||||
self.ensureTerminator(ret_ty);
|
self.ensureTerminator(ret_ty);
|
||||||
}
|
}
|
||||||
@@ -10316,6 +10320,12 @@ pub const Lowering = struct {
|
|||||||
.struct_type = ty,
|
.struct_type = ty,
|
||||||
} }, .string);
|
} }, .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")) {
|
if (std.mem.eql(u8, name, "error_tag_name")) {
|
||||||
// error_tag_name(e) → look the error-set value's runtime tag id up
|
// 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.
|
// 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_index")) return .s64;
|
||||||
if (std.mem.eql(u8, bare_name, "field_name")) return .string;
|
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, "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, "is_flags")) return .bool;
|
||||||
if (std.mem.eql(u8, bare_name, "type_of")) return .any;
|
if (std.mem.eql(u8, bare_name, "type_of")) return .any;
|
||||||
if (std.mem.eql(u8, bare_name, "field_value")) 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);
|
self.builder.switchToBlock(handle_bb);
|
||||||
const body_val = self.runCatchBody(ce, err_val, err_set, succ_ty);
|
const body_val = self.runCatchBody(ce, err_val, err_set, succ_ty);
|
||||||
if (!self.currentBlockHasTerminator()) {
|
if (!self.currentBlockHasTerminator()) {
|
||||||
// A non-diverging handler must produce a value of the success type.
|
self.finishCatchHandler(body_val, succ_ty, merge_bb, span);
|
||||||
// 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.builder.switchToBlock(merge_bb);
|
self.builder.switchToBlock(merge_bb);
|
||||||
@@ -15802,26 +15796,46 @@ pub const Lowering = struct {
|
|||||||
const tag = self.builder.blockParam(handle_bb, 0, err_set);
|
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);
|
const body_val = self.runCatchBody(ce, tag, err_set, if (has_value) succ_ty else null);
|
||||||
if (!self.currentBlockHasTerminator()) {
|
if (!self.currentBlockHasTerminator()) {
|
||||||
self.emitTraceClear();
|
self.finishCatchHandler(body_val, succ_ty, merge_bb, span);
|
||||||
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.builder.switchToBlock(merge_bb);
|
self.builder.switchToBlock(merge_bb);
|
||||||
return if (has_value) self.builder.blockParam(merge_bb, 0, succ_ty) else self.builder.constInt(0, .void);
|
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
|
/// 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 binding (if any). When `want_ty` is non-null (value-carrying
|
||||||
/// catch), returns the body's value (or null if the body diverged); when
|
/// 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 {
|
fn ensureTerminator(self: *Lowering, ret_ty: TypeId) void {
|
||||||
if (self.currentBlockHasTerminator()) return;
|
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();
|
self.builder.retVoid();
|
||||||
} else {
|
} else {
|
||||||
// Use const_undef for complex types (string, struct, etc.)
|
// Use const_undef for complex types (string, struct, etc.)
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write
|
|||||||
},
|
},
|
||||||
.const_null => try writer.writeAll("const null : "),
|
.const_null => try writer.writeAll("const null : "),
|
||||||
.const_undef => try writer.writeAll("const undef : "),
|
.const_undef => try writer.writeAll("const undef : "),
|
||||||
|
.is_comptime => try writer.writeAll("is_comptime : "),
|
||||||
.const_type => |tid| try writer.print("const type({s}) : ", .{tt.typeName(tid)}),
|
.const_type => |tid| try writer.print("const type({s}) : ", .{tt.typeName(tid)}),
|
||||||
|
|
||||||
// ── Arithmetic ──────────────────────────────────────────
|
// ── Arithmetic ──────────────────────────────────────────
|
||||||
|
|||||||
1
tests/expected/248-log-and-comptime.exit
Normal file
1
tests/expected/248-log-and-comptime.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
5
tests/expected/248-log-and-comptime.txt
Normal file
5
tests/expected/248-log-and-comptime.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
WARN: disk 91% full
|
||||||
|
INFO: user alice connected
|
||||||
|
ERROR: bad fd 7
|
||||||
|
DEBUG: trace x=42
|
||||||
|
[stdout] is_comptime runtime=2 comptime=1
|
||||||
1
tests/expected/249-process-exit.exit
Normal file
1
tests/expected/249-process-exit.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
42
|
||||||
1
tests/expected/249-process-exit.txt
Normal file
1
tests/expected/249-process-exit.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
starting
|
||||||
1
tests/expected/250-assert.exit
Normal file
1
tests/expected/250-assert.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
2
tests/expected/250-assert.txt
Normal file
2
tests/expected/250-assert.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
first assert passed
|
||||||
|
ASSERTION FAILED: two plus two is not five
|
||||||
Reference in New Issue
Block a user