Merge branch 'flow/sx-foundation/F3.3' into dist-foundation

This commit is contained in:
agra
2026-06-05 01:11:26 +03:00
6 changed files with 143 additions and 7 deletions

View File

@@ -0,0 +1,69 @@
// std.cli EXIT-CODE + `--json` contract (F3.3 — FOUNDATION MILESTONE CLOSE).
//
// The minimal contract `dist` (and any sx CLI front-end) relies on:
//
// 1. NAMED EXIT CODES — `EX_OK` (0) and `EX_USAGE` (64, the sysexits.h
// usage-error code) are public constants in `std.cli`.
// 2. TERMINATORS — `exit_ok()` / `exit_usage()` end the process with the
// matching code, routing through the canonical `process.exit(code: u8)`.
// 3. `--json` MODE — the reserved global `--json` flag surfaces as
// `parsed.json`: TRUE when `--json` is in the argv, FALSE when it is not.
// In json mode stdout carries ONLY the machine result; human text goes
// to stderr (here via `log.err`, which writes `ERROR: …` to fd 2).
//
// The run DELIBERATELY ends on the usage path: after the assertions it
// triggers an `UnknownCommand`, writes the human diagnostic to STDERR, and
// terminates via `exit_usage()` — so the process exits 64 (EX_USAGE),
// captured in expected/0718-…​.exit. (sx `print`/`out` are unbuffered, so the
// stdout assertion lines still appear despite the `_exit`.)
#import "modules/std.sx";
#import "modules/std/cli.sx";
log :: #import "modules/log.sx";
report :: (label: string, ok: bool) {
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
}
main :: () -> ! {
publish_flags : []FlagSpec = .[
FlagSpec.{ name = "out", takes_value = true, required = true },
];
cmds : []Command = .[
Command.{ group = "ci", command = "publish", flags = publish_flags },
];
// ── 1. Named exit-code constants (the dist contract) ──────────────
report("ex-ok-is-0", EX_OK == 0);
report("ex-usage-is-64", EX_USAGE == 64);
// ── 2. `--json` detection — true with the flag, false without ─────
d : Diag = .{};
with_json : []string = .["ci", "publish", "--out", "dist", "--json"];
without_json : []string = .["ci", "publish", "--out", "dist"];
pj := try parse(with_json, cmds, @d);
pn := try parse(without_json, cmds, @d);
report("json-set-true", pj.json);
report("json-set-false", !pn.json);
// ── 3. json mode keeps stdout machine-pure ────────────────────────
// When `parsed.json`, the front-end emits ONLY the machine result on
// stdout. `out` writes the bytes verbatim (no `{}` interpolation), so
// the JSON braces are literal.
if pj.json {
out("{\"out\":\"");
out(pj.value_of("out"));
out("\"}\n");
}
// ── 4. Usage error → human text to stderr → exit_usage() (= 64) ───
// A bad command raises a `CliError`. The front-end maps every usage
// error to `EX_USAGE`: it writes the human diagnostic to STDERR (stdout
// stays machine-clean) and terminates with the usage code via the
// canonical `process.exit`.
bad : []string = .["ci", "deploy", "--out", "x"]; // unknown command
_, e := parse(bad, cmds, @d);
report("usage-error-raised", e == error.UnknownCommand);
log.err("unknown command '{}' (argv index {})", d.token, d.index);
exit_usage(); // -> _exit(EX_USAGE = 64)
}

View File

@@ -0,0 +1 @@
64

View File

@@ -0,0 +1 @@
ERROR: unknown command 'deploy' (argv index 1)

View File

@@ -0,0 +1,6 @@
ex-ok-is-0: ok
ex-usage-is-64: ok
json-set-true: ok
json-set-false: ok
{"out":"dist"}
usage-error-raised: ok

View File

@@ -25,11 +25,13 @@
//
// 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.
// loudly (message + `EX_UNAVAILABLE` exit via `process.exit`) rather than
// returning a silent empty.
// =====================================================================
#import "modules/std.sx";
#import "modules/compiler.sx";
proc :: #import "modules/process.sx";
libc :: #library "c";
@@ -42,9 +44,39 @@ libc :: #library "c";
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";
// =====================================================================
// EXIT-CODE & `--json` CONTRACT (F3.3) — the minimal surface `dist` (and
// any sx CLI front-end) relies on. Two pieces:
//
// 1. NAMED EXIT CODES (sysexits.h subset). A front-end ends with the
// code that matches the outcome: `EX_OK` on success, `EX_USAGE`
// after a `parse` failure (a `CliError` — bad command / flag /
// value), `EX_UNAVAILABLE` when the platform is unsupported.
// 2. TERMINATORS. `exit_ok()` / `exit_usage()` end the process with the
// matching code. They route through the canonical
// `process.exit(code: u8)` (modules/process.sx) — there is NO second
// hand-rolled `_exit` binding in this module; the unsupported-platform
// path below goes through `proc.exit(EX_UNAVAILABLE)` too.
//
// `--json` MODE needs no new code here: the parser already surfaces it as
// `parsed.json` (true iff `--json` appears in the argv — see `Parsed.json`).
// The convention a front-end follows: in json mode stdout carries ONLY the
// machine result, and human diagnostics go to stderr (e.g. via
// `modules/log.sx`'s `log.err`). Detect json mode by reading `parsed.json`.
// =====================================================================
EX_OK :u8: 0; // success
EX_USAGE :u8: 64; // command-line usage error (sysexits.h EX_USAGE)
EX_UNAVAILABLE :u8: 70; // service / platform unavailable (sysexits.h)
// End the process successfully (`EX_OK` = 0). Thin wrapper over the
// canonical `process.exit` terminator — immediate `_exit(2)`, no unwinding.
exit_ok :: () -> noreturn { proc.exit(EX_OK); }
// End the process with the usage-error code (`EX_USAGE` = 64). A CLI
// front-end calls this after `parse` raises a `CliError` and the human
// diagnostic has been written to stderr.
exit_usage :: () -> noreturn { proc.exit(EX_USAGE); }
// Number of process arguments (argc). >= 1 for any normally-launched
// process, since argv[0] is the executable path.
@@ -53,7 +85,7 @@ os_argc :: () -> s64 {
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);
proc.exit(EX_UNAVAILABLE);
}
}
}
@@ -88,7 +120,7 @@ os_args :: (buf: []string) -> []string {
}
else: {
out("std.cli: unsupported platform — only macOS is implemented (needs _NSGetArgv/_NSGetArgc).\n");
cli_bail_exit(70);
proc.exit(EX_UNAVAILABLE);
}
}
}
@@ -194,7 +226,7 @@ Parsed :: struct {
group: string;
command: string;
cmd_index: s64;
json: bool;
json: bool; // `--json` mode: true iff `--json` is in argv
rest: []string;
spec: []FlagSpec; // view of the matched command's flag specs
values: [16]FlagValue; // fixed inline storage, indexed by spec position

View File

@@ -425,6 +425,33 @@ The standard library (`modules/std.sx`) provides:
- **Math**: `sqrt`, `sin`, `cos`
- **Introspection**: `type_of`, `type_name`, `field_count`, `field_name`, `field_value`, `size_of`
### Command-line interface (`modules/std/cli.sx`)
`std.cli` builds command-line front-ends over an explicit logical argv
(`[]string`): `os_args(buf)` reads the real process argv, and
`parse(args, commands, diag) -> !Parsed` does subcommand dispatch + `--flag`
parsing. On top of that it defines the small **exit-code / `--json` contract**
a CLI program (e.g. `dist`) relies on:
```sx
#import "modules/std/cli.sx";
p, e := parse(args, cmds, @diag); // (Parsed, !CliError)
if e == error.UnknownCommand {
log.err("unknown command '{}'", diag.token); // human text -> stderr
exit_usage(); // usage error -> exit 64
}
if p.json { /* emit ONLY machine output on stdout */ }
```
- **Named exit codes** — `EX_OK` (0), `EX_USAGE` (64, the sysexits.h
command-line-usage code), `EX_UNAVAILABLE` (70, unsupported platform).
- **Terminators** — `exit_ok()` / `exit_usage()` end the process with the
matching code; both route through the canonical `process.exit(code: u8)`.
- **`--json` mode** — the reserved global `--json` flag surfaces as
`parsed.json` (true iff `--json` is in the argv). Convention: in json mode
stdout carries only the machine result; human diagnostics go to stderr.
## Cross-Compilation
```sh