Files
sx/library/modules/process.sx
agra bdd0e96d78 feat(lang): block value requires no trailing ; (Rust-style)
A block's value is now its last statement ONLY when that statement is a
trailing expression with no `;`. A trailing `;` discards the value,
leaving the block void. This makes value-vs-statement explicit and lets
the compiler reject "this block was supposed to produce a value".

Compiler:
- Parser records `Block.produces_value` (last stmt is a no-`;` trailing
  expression) + `Block.discarded_semi` (the `;` that discarded a value),
  via `expectSemicolonAfter`. A trailing expression before `}` may now
  omit its `;` (previously a parse error). Match-arm and else-arm bodies
  are built value-producing regardless of the arm `;` (arms are exempt —
  the `;` is an arm terminator).
- Lowering: `lowerBlockValue` / the block-expr path / `inferExprType`
  respect `produces_value`. A value-position block that discards its value
  is a hard error (`lowerValueBody` for function bodies; the value-context
  `.block` path for if/else branches, `catch` bodies, value bindings,
  match arms). Pure-failable `-> !` bodies (value rides the error channel)
  and a value-if whose branches are void are handled without false errors.
- `defer`/`onfail` cleanup bodies lower as statements (void), so a
  trailing `;` there is fine.

Migration (behavior-preserving — output unchanged):
- stdlib + ~210 examples: dropped the trailing `;` on value-position last
  expressions. `format` now ends with an explicit `#insert "return
  result;"` (it relied on `#insert`-as-block-value, which `;` discards).
- Two `main :: () -> s32` examples that relied on the old silent
  default-return got an explicit trailing `0`.
- Rejection snapshots 0412 / 1013 regenerated (their quoted source lines
  lost a `;`); the diagnostics themselves are unchanged.

Docs/tests: specs.md "Block values" section; examples 0040 (rules) + 0041
(rejection); 3 parser unit tests. Filed issue 0066 (pre-existing
match-arm negated-literal phi-width quirk, surfaced not caused here).

Gates: zig build, zig build test, run_examples.sh -> 343 passed,
cross_compile.sh -> 7 passed (also refreshed its stale example names).
2026-06-02 09:23:50 +03:00

154 lines
6.2 KiB
Plaintext

#import "std.sx";
trace :: #import "trace.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 diagnostic naming the call site
// (`loc` defaults to `#caller_location`); in compiled code the `is_comptime()`
// branch folds away to just the syscall.
exit :: (code: u8, loc: Source_Location = #caller_location) -> noreturn {
if is_comptime() {
print("\nprocess.exit({}) called from {} at {}:{}\n", code, loc.func, loc.file, loc.line);
trace.print_interpreter_frames();
}
clib_exit(xx code)
}
// Abort with a message when `cond` is false. Prints `ASSERTION FAILED at
// <file>:<line>: <msg>` (the caller's location, via `#caller_location`) then
// exits 1; a true condition is a no-op.
assert :: (cond: bool, msg: string, loc: Source_Location = #caller_location) {
if !cond {
print("ASSERTION FAILED at {}:{}: {}\n", loc.file, loc.line, msg);
exit(1);
}
}