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.
155 lines
6.2 KiB
Plaintext
155 lines
6.2 KiB
Plaintext
#import "std.sx";
|
|
|
|
// =====================================================================
|
|
// process.sx — subprocess + environment stdlib (POSIX backend).
|
|
//
|
|
// Scope (Phase 1A): one entry point `run(cmd)` that shells out to
|
|
// /bin/sh, captures stdout, returns exit code + stdout. Plus
|
|
// `env(name)` / `find_executable(name)`. The bundler uses these to
|
|
// invoke `codesign`, `plutil`, `security`, `aapt2`, `javac`, `d8`,
|
|
// `keytool`, `apksigner`.
|
|
//
|
|
// Roadmap: phase 1B replaces `popen` with `posix_spawn` + pipes so
|
|
// we can capture stderr separately and pass argv without shell
|
|
// quoting. Until then, callers responsible for quoting + use 2>&1
|
|
// to fold stderr into the captured stream.
|
|
// =====================================================================
|
|
|
|
libc :: #library "c";
|
|
|
|
// ── Low-level libc bindings ─────────────────────────────────────────
|
|
|
|
popen :: (cmd: [:0]u8, mode: [:0]u8) -> *void #foreign libc;
|
|
pclose :: (stream: *void) -> s32 #foreign libc;
|
|
fread :: (ptr: [*]u8, size: usize, nmemb: usize, stream: *void) -> usize #foreign libc;
|
|
feof :: (stream: *void) -> s32 #foreign libc;
|
|
getenv :: (name: [:0]u8) -> *u8 #foreign libc;
|
|
strlen :: (s: *u8) -> usize #foreign libc;
|
|
system :: (cmd: [:0]u8) -> s32 #foreign libc;
|
|
|
|
// ── Public types ─────────────────────────────────────────────────────
|
|
|
|
ProcessResult :: struct {
|
|
/// Exit code as reported by `WEXITSTATUS(status)`. 0 = success.
|
|
/// Note: doesn't distinguish "killed by signal" from "exited
|
|
/// non-zero"; phase 1B will return a tagged union.
|
|
exit_code: s32;
|
|
stdout: string;
|
|
}
|
|
|
|
// ── Public API ───────────────────────────────────────────────────────
|
|
|
|
// Run a shell command, capture stdout, wait for exit. Returns null if
|
|
// the shell itself couldn't be spawned. A non-zero exit_code with
|
|
// valid stdout means the command ran and exited non-zero — distinct
|
|
// from spawn failure.
|
|
//
|
|
// `cmd` is interpreted by /bin/sh — callers MUST quote arguments
|
|
// containing spaces or shell metacharacters. To capture stderr along
|
|
// with stdout, append " 2>&1" to the command.
|
|
run :: (cmd: [:0]u8) -> ?ProcessResult {
|
|
f := popen(cmd, "r");
|
|
if cast(s64) f == 0 { return null; }
|
|
|
|
out := "";
|
|
buf := cstring(4096);
|
|
loop := true;
|
|
while loop {
|
|
n := fread(buf.ptr, 1, 4096, f);
|
|
if n == 0 { loop = false; }
|
|
if n > 0 {
|
|
chunk : string = ---;
|
|
chunk.ptr = buf.ptr;
|
|
chunk.len = cast(s64) n;
|
|
out = concat(out, chunk);
|
|
}
|
|
}
|
|
raw_status := pclose(f);
|
|
if raw_status < 0 { return null; }
|
|
// POSIX wait(2) status encoding: low byte = signal (if signaled),
|
|
// next byte = exit code (if normally exited). For our MVP we just
|
|
// surface the exit-code byte; the signal case is folded into the
|
|
// non-zero return.
|
|
exit_code := (raw_status >> 8) & 0xFF;
|
|
if exit_code == 0 {
|
|
if (raw_status & 0x7F) != 0 {
|
|
// Killed by signal — surface as a non-zero exit.
|
|
exit_code = 128 + (raw_status & 0x7F);
|
|
}
|
|
}
|
|
ProcessResult.{ exit_code = exit_code, stdout = out };
|
|
}
|
|
|
|
// Read an environment variable. Returns null if unset; an empty
|
|
// string if set to "".
|
|
env :: (name: [:0]u8) -> ?string {
|
|
p := getenv(name);
|
|
addr : s64 = xx p;
|
|
if addr == 0 { return null; }
|
|
n := strlen(p);
|
|
if n == 0 { return ""; }
|
|
buf := cstring(cast(s64) n);
|
|
memcpy(buf.ptr, xx p, cast(s64) n);
|
|
buf;
|
|
}
|
|
|
|
// Locate an executable by walking `$PATH`. Returns the absolute path
|
|
// to the first hit, or null if not found anywhere. Uses `command -v`
|
|
// under the shell; cheap and matches what the bundler ultimately
|
|
// shells out to anyway.
|
|
find_executable :: (name: [:0]u8) -> ?string {
|
|
// Compose `command -v <name>` — name is assumed shell-safe
|
|
// (executable names like `codesign`, `javac`, `aapt2`).
|
|
cmd := concat("command -v ", name);
|
|
// Need null-terminated for popen.
|
|
cmd_z := cstring(cmd.len);
|
|
memcpy(cmd_z.ptr, cmd.ptr, cmd.len);
|
|
if r := run(cmd_z) {
|
|
if r.exit_code != 0 { return null; }
|
|
// Strip the trailing newline that `command -v` emits.
|
|
out := r.stdout;
|
|
if out.len > 0 {
|
|
if out[out.len - 1] == 10 { out = substr(out, 0, out.len - 1); }
|
|
}
|
|
if out.len == 0 { return null; }
|
|
return out;
|
|
}
|
|
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);
|
|
}
|
|
}
|