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:
agra
2026-06-01 11:11:56 +03:00
parent 1d6e26f011
commit 6f77c55613
16 changed files with 214 additions and 41 deletions

29
library/modules/log.sx Normal file
View 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);"; }

View File

@@ -116,3 +116,39 @@ find_executable :: (name: [:0]u8) -> ?string {
}
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);
}
}