From e7f5bd7aaada5f3608807168397e8de20976b8e3 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 4 Jun 2026 03:21:41 +0300 Subject: [PATCH] =?UTF-8?q?F3.1:=20std.cli=20os=5Fargs=20=E2=80=94=20real?= =?UTF-8?q?=20OS=20argv=20accessor=20via=20#foreign=20=5FNSGetArgv=20(exam?= =?UTF-8?q?ples/0716)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ...` 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). --- examples/0716-modules-cli-argv.sx | 31 ++++++ examples/expected/0716-modules-cli-argv.exit | 1 + .../expected/0716-modules-cli-argv.stderr | 1 + .../expected/0716-modules-cli-argv.stdout | 3 + library/modules/std/cli.sx | 94 +++++++++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 examples/0716-modules-cli-argv.sx create mode 100644 examples/expected/0716-modules-cli-argv.exit create mode 100644 examples/expected/0716-modules-cli-argv.stderr create mode 100644 examples/expected/0716-modules-cli-argv.stdout create mode 100644 library/modules/std/cli.sx diff --git a/examples/0716-modules-cli-argv.sx b/examples/0716-modules-cli-argv.sx new file mode 100644 index 0000000..5e58fa7 --- /dev/null +++ b/examples/0716-modules-cli-argv.sx @@ -0,0 +1,31 @@ +// Real OS-argv accessor from `modules/std/cli.sx` (#foreign _NSGetArgv). +// +// Only DETERMINISTIC structural invariants are asserted — the actual arg +// contents depend on how the test is invoked (under `sx run` the process +// argv is the interpreter's: ["sx", "run", ""]), so we never +// pin exact strings: +// - argc >= 1 (every process has argv[0]) +// - argv[0] is non-empty (the executable path) +// - os_argc() agrees with the filled slice length (no truncation) +// +// `buf` is a stack `[64]string`; `os_args` fills it with zero-copy views +// over the C runtime's argv block — no heap, no per-arg allocation. + +#import "modules/std.sx"; +#import "modules/std/cli.sx"; + +main :: () { + buf : [64]string = ---; + args := os_args(buf[0..64]); + + if args.len >= 1 { print("argc>=1: ok\n"); } + else { print("argc>=1: FAIL ({})\n", args.len); } + + if args.len >= 1 { + if args[0].len > 0 { print("arg0-nonempty: ok\n"); } + else { print("arg0-nonempty: FAIL\n"); } + } + + if os_argc() == args.len { print("argc-consistent: ok\n"); } + else { print("argc-consistent: FAIL (os_argc={} len={})\n", os_argc(), args.len); } +} diff --git a/examples/expected/0716-modules-cli-argv.exit b/examples/expected/0716-modules-cli-argv.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0716-modules-cli-argv.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0716-modules-cli-argv.stderr b/examples/expected/0716-modules-cli-argv.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0716-modules-cli-argv.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0716-modules-cli-argv.stdout b/examples/expected/0716-modules-cli-argv.stdout new file mode 100644 index 0000000..272f8da --- /dev/null +++ b/examples/expected/0716-modules-cli-argv.stdout @@ -0,0 +1,3 @@ +argc>=1: ok +arg0-nonempty: ok +argc-consistent: ok diff --git a/library/modules/std/cli.sx b/library/modules/std/cli.sx new file mode 100644 index 0000000..304da5f --- /dev/null +++ b/library/modules/std/cli.sx @@ -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 ...` 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); + } + } +}