F3.1: std.cli os_args — real OS argv accessor via #foreign _NSGetArgv (examples/0716)

Add library/modules/std/cli.sx: a pure-sx command-line argument accessor
backed by the macOS C runtime (_NSGetArgv/_NSGetArgc), no compiler change.

  os_argc() -> s64
  os_args(buf: []string) -> []string

Zero heap, zero per-arg allocation: os_args fills a caller-provided buffer
(stack array) with string VIEWS over the process's own argv block, which
lives for the whole process. The returned slice header is a by-value stack
return; nothing touches context.allocator.

Documents the `sx run` reality: under `sx run <prog.sx> ...` the process
argv is the interpreter's argv (sx, run, prog.sx, ...), not a program's
logical args. This accessor reports the real process argv truthfully;
mapping to logical args is a later consumer concern (distribution P3.1).

Non-macOS platforms bail loudly (message + _exit) rather than returning a
silent empty.

examples/0716-modules-cli-argv.sx asserts only deterministic structural
invariants (argc >= 1, argv[0] non-empty, os_argc() == filled length).
This commit is contained in:
agra
2026-06-04 03:21:41 +03:00
parent 090bdd7cfa
commit e7f5bd7aaa
5 changed files with 130 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
// =====================================================================
// cli.sx — process command-line argument accessor (macOS), pure sx.
//
// `os_args(buf)` returns the real OS-level process argv as a `[]string`,
// each element a zero-copy VIEW over the C runtime's argv memory. The
// caller provides the `buf: []string` backing (typically a stack array);
// every element points straight into the process's own argv block, which
// lives for the whole process lifetime, so the views never dangle.
//
// Zero heap, zero per-arg allocation: nothing here touches
// `context.allocator`. The returned slice header is a by-value stack
// return whose `.ptr` is the caller's `buf` and whose elements are views
// into C argv — ladder rungs "by-value", "view", and "caller buffer".
//
// `sx run <prog.sx> ...` reality — READ THIS BEFORE CONSUMING:
// Under `sx run`, the process argv is the sx INTERPRETER's argv, e.g.
// ["sx", "run", "prog.sx", ...] — NOT a program's "own" logical args.
// (The interpreter also consumes trailing tokens as additional source
// files, so they don't reach the program as plain args anyway.) This
// accessor reports the real process argv truthfully and does NOT strip
// the interpreter prefix. Mapping process argv -> a program's logical
// args (dropping the `sx run prog.sx` prefix, or via an sx-run
// convention) is a CONSUMER concern handled later (distribution P3.1),
// NOT here.
//
// Platform: macOS only for now, via the C runtime's `_NSGetArgv()`
// (char***) and `_NSGetArgc()` (int*). On any other OS the accessors bail
// loudly (message + non-zero exit) rather than returning a silent empty.
// =====================================================================
#import "modules/std.sx";
#import "modules/compiler.sx";
libc :: #library "c";
// macOS C-runtime argv/argc accessors (crt_externs.h):
// extern char ***_NSGetArgv(void); extern int *_NSGetArgc(void);
// Each returns a pointer to the runtime's slot; dereference once for the
// `char**` / `int` the process was launched with. Declared as `*s64` /
// `*s32` since on 64-bit a `char***` is just a pointer to a pointer-sized
// slot.
ns_get_argv :: () -> *s64 #foreign libc "_NSGetArgv";
ns_get_argc :: () -> *s32 #foreign libc "_NSGetArgc";
// Bound to POSIX `_exit(2)`. Used only on the unsupported-platform path to
// terminate loudly instead of handing back a misleading empty slice.
cli_bail_exit :: (code: s32) -> noreturn #foreign libc "_exit";
// Number of process arguments (argc). >= 1 for any normally-launched
// process, since argv[0] is the executable path.
os_argc :: () -> s64 {
inline if OS == {
case .macos: { return cast(s64) ns_get_argc().*; }
else: {
out("std.cli: unsupported platform — only macOS is implemented (needs _NSGetArgv/_NSGetArgc).\n");
cli_bail_exit(70);
}
}
}
// Fill `buf` with VIEWS over the process argv and return the filled prefix
// `buf[0 .. min(argc, buf.len)]`. Zero heap, zero copy: each element's
// bytes live in the C runtime's argv block, valid for the whole process.
//
// The caller owns `buf` (typically a stack `[N]string`); the returned
// slice points into it and is valid for as long as `buf` is in scope. If
// the process has more than `buf.len` arguments only the first `buf.len`
// are returned — call `os_argc()` first and size `buf` accordingly when an
// exact count matters.
os_args :: (buf: []string) -> []string {
inline if OS == {
case .macos: {
argc := cast(s64) ns_get_argc().*;
argv : [*]s64 = xx ns_get_argv().*;
n := if argc > buf.len then buf.len else argc;
i := 0;
while i < n {
cstr : [*]u8 = xx argv[i];
len := 0;
while cstr[len] != 0 { len += 1; }
buf[i] = string.{ ptr = cstr, len = len };
i += 1;
}
result : []string = ---;
result.ptr = buf.ptr;
result.len = n;
return result;
}
else: {
out("std.cli: unsupported platform — only macOS is implemented (needs _NSGetArgv/_NSGetArgc).\n");
cli_bail_exit(70);
}
}
}