feat(stdlib): add Os.* — the OS/environment interface (#21)
All checks were successful
docs / build-and-deploy (push) Successful in 2s

An Os.* namespace, Go-flavored and game-scoped, for the environment *around*
the game: the command line, environment variables, standard streams, process
exit, the host platform, and the per-user known folders a game writes into.
Rounds the bare System.* builtins (arg/getenv/exit) into one coherent surface.

  - args / arg_count / arg     the argument vector (args() -> []string)
  - env / env_or / has_env     read env vars (null-safe via env_or)
  - set_env / unset_env        mutate this process's environment
  - exit(code)                 terminate with a status code
  - platform() / arch()        host facts (uname sysname/machine)
  - stdout_write / stderr_write  raw writes to the standard streams
  - save_dir / config_dir / cache_dir / temp_dir   per-user known folders

Pure libc over NUL-terminated strings; C-free, no new runtime. arg_count/arg/
exit stay light (no prelude) as thin aliases of the existing intrinsics; the
rest share one Os runtime prelude emitted on demand (g_uses_osrt). platform()
is portable (uname system name is field 0 on every Unix); arch() and the
known-folder layout follow the macOS/BSD conventions — the fully supported
native target today. Linux/Windows/wasm folder resolution and a target-aware
arch() are documented follow-ups.

- examples/library/os.ludic: asserts the invariants that hold regardless of
  host — env round-trip, env_or fallback, unset, args()==arg_count(), non-empty
  platform/arch and known dirs. Wired into `x test` (now 54 passed).
- docs: a new Os section + 17 per-symbol pages; inventory updated; every fence
  passes check-docs (--fmt) and the site builds via docgen.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.

Closes #21

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 22:19:11 +03:00
parent 031b138f84
commit b205dd8dfd
27 changed files with 12109 additions and 10447 deletions

View file

@ -0,0 +1,13 @@
---
id: os
title: Os
order: 6
---
The operating-system interface — the environment *around* the game rather than the game itself: the command line, environment variables, the standard streams, the process exit code, the host platform, and the per-user folders a game writes its saves, config, and cache into. It rounds the bare <a href="ns-System"><code>System</code></a> builtins (<code>arg</code>, <code>getenv</code>, <code>exit</code>) into one coherent, Go-flavored namespace.
It is deliberately small and game-scoped: there is no process spawning, no signals, and no user/permission APIs. Reach for it in launchers, asset pipelines, and dev tools — parsing launch flags like <code>--level 3</code>, reading config from the environment, or resolving where a save file belongs — far more than in the simulation itself.
**Determinism:** <a href="os-args"><code>args</code></a>, the <a href="os-env"><code>env</code></a> family, and <a href="os-platform"><code>platform</code></a>/<a href="os-arch"><code>arch</code></a> are non-deterministic host input. Read them once at startup to configure the game, and keep them out of the replayable simulation so a shared seed still reproduces.
**Platform coverage:** this first version targets the native macOS/BSD host — the fully supported target today. <a href="os-platform"><code>platform</code></a> is portable (the <code>uname</code> system name is available on every Unix); <a href="os-arch"><code>arch</code></a> and the known-folder layout (<a href="os-save_dir"><code>save_dir</code></a>/<a href="os-config_dir"><code>config_dir</code></a>/<a href="os-cache_dir"><code>cache_dir</code></a>) follow the macOS conventions. Linux/Windows/wasm folder resolution and a target-aware <code>arch</code> are documented follow-ups.

View file

@ -0,0 +1,23 @@
---
id: os-arch
name: Os.arch
category: os
kind: namespace-method
tokens: Os.arch
sig: Os.arch() -> string
tip: The host CPU architecture.
order: 11
ns: Os
member: arch
---
The host CPU architecture as reported by the system, e.g. <code>"arm64"</code> or <code>"x86_64"</code>. This reads the <code>uname</code> machine field using the macOS/BSD layout; a target-aware value for other platforms is a documented follow-up.
```ludic
program Arch {
entry {
let a = Os.arch()
print(len(a))
}
}
```

View file

@ -0,0 +1,26 @@
---
id: os-arg
name: Os.arg
category: os
kind: namespace-method
tokens: Os.arg
sig: Os.arg(i) -> string
tip: The i-th command-line argument.
order: 3
ns: Os
member: arg
---
The argument at index <code>i</code>. Index <code>0</code> is the program path; <code>1</code> onward are the launch arguments. Reading past <a href="os-arg_count"><code>arg_count()</code></a> is out of bounds.
Parameters:
- `i` — the argument index (0 = the program path)
```ludic
program Arg {
entry {
let prog = Os.arg(0) # the program path
print(len(prog))
}
}
```

View file

@ -0,0 +1,22 @@
---
id: os-arg_count
name: Os.arg_count
category: os
kind: namespace-method
tokens: Os.arg_count
sig: Os.arg_count() -> int
tip: How many command-line arguments there are.
order: 2
ns: Os
member: arg_count
---
The number of command-line arguments, always at least <code>1</code> (the program path itself). Pair it with <a href="os-arg"><code>arg</code></a> for index-based access, or just use <a href="os-args"><code>args</code></a>.
```ludic
program Argc {
entry {
if Os.arg_count() >= 1 { print(1) }
}
}
```

View file

@ -0,0 +1,24 @@
---
id: os-args
name: Os.args
category: os
kind: namespace-method
tokens: Os.args
sig: Os.args() -> []string
tip: Every command-line argument, as a list.
order: 1
ns: Os
member: args
---
Returns the whole argument vector as a <a href="type-slice"><code>[]string</code></a>: element <code>0</code> is the program path, and the rest are the arguments the process was launched with. The list has exactly <a href="os-arg_count"><code>arg_count()</code></a> elements, so you can iterate it directly to parse launch flags.
```ludic
program Args {
entry {
let all = Os.args()
var i = 0
while i < len(all) { print(len(all[i])); i = i + 1 } # one length per argument
}
}
```

View file

@ -0,0 +1,26 @@
---
id: os-cache_dir
name: Os.cache_dir
category: os
kind: namespace-method
tokens: Os.cache_dir
sig: Os.cache_dir(app) -> string
tip: Per-user cache directory for an app.
order: 16
ns: Os
member: cache_dir
---
The per-user directory for an application's disposable cache — regenerable data the system may clear under disk pressure. On macOS this is under <code>~/Library/Caches/</code>. Never store anything here you cannot recreate.
Parameters:
- `app` — the application name (used as the folder name)
```ludic
program CacheDir {
entry {
let dir = Os.cache_dir("MyGame")
print(len(dir))
}
}
```

View file

@ -0,0 +1,26 @@
---
id: os-config_dir
name: Os.config_dir
category: os
kind: namespace-method
tokens: Os.config_dir
sig: Os.config_dir(app) -> string
tip: Per-user config directory for an app.
order: 15
ns: Os
member: config_dir
---
The per-user directory where an application should store configuration, named for the app. On macOS this resolves under <code>~/Library/Application Support/</code> (the platform keeps config and saves together there).
Parameters:
- `app` — the application name (used as the folder name)
```ludic
program ConfigDir {
entry {
let dir = Os.config_dir("MyGame")
print(len(dir))
}
}
```

View file

@ -0,0 +1,28 @@
---
id: os-env
name: Os.env
category: os
kind: namespace-method
tokens: Os.env
sig: Os.env(name) -> string
tip: Read an environment variable (null if unset).
order: 4
ns: Os
member: env
---
Reads an environment variable and returns its value, or <code>null</code> when the variable is not set. Use <a href="os-env_or"><code>env_or</code></a> when you want a fallback, or <a href="os-has_env"><code>has_env</code></a> to test presence without risking a null.
Parameters:
- `name` — the variable name
```ludic
program Env {
entry {
if Os.has_env("HOME") {
let home = Os.env("HOME")
print(len(home))
}
}
}
```

View file

@ -0,0 +1,27 @@
---
id: os-env_or
name: Os.env_or
category: os
kind: namespace-method
tokens: Os.env_or
sig: Os.env_or(name, fallback) -> string
tip: Read an environment variable, or a fallback.
order: 5
ns: Os
member: env_or
---
Reads an environment variable, returning <code>fallback</code> when it is not set — the null-safe way to read optional configuration. The result is always non-null, so it is safe to use directly.
Parameters:
- `name` — the variable name
- `fallback` — the value to use when the variable is unset
```ludic
program EnvOr {
entry {
let assets = Os.env_or("LUDIC_ASSETS", "assets/")
print(len(assets))
}
}
```

View file

@ -0,0 +1,29 @@
---
id: os-exit
name: Os.exit
category: os
kind: namespace-method
tokens: Os.exit
sig: Os.exit(code)
tip: Terminate the process with a status code.
order: 9
ns: Os
member: exit
---
Terminates the process immediately with the given status code — <code>0</code> for success, non-zero for failure, the convention every shell and CI system reads. Nothing after the call runs. Intended for tools and launchers; a game normally ends through its own lifecycle instead.
Parameters:
- `code` — the process exit status (0 = success)
```ludic
program Exit {
entry {
if Os.arg_count() < 2 {
Os.stderr_write("usage: tool <arg>\n")
Os.exit(1)
}
print(0)
}
}
```

View file

@ -0,0 +1,25 @@
---
id: os-has_env
name: Os.has_env
category: os
kind: namespace-method
tokens: Os.has_env
sig: Os.has_env(name) -> bool
tip: Is an environment variable set?
order: 6
ns: Os
member: has_env
---
Tests whether an environment variable is set, without reading its value. Useful as a feature/debug toggle, or to guard an <a href="os-env"><code>env</code></a> read.
Parameters:
- `name` — the variable name
```ludic
program HasEnv {
entry {
if Os.has_env("LUDIC_DEBUG") { print(1) } else { print(0) }
}
}
```

View file

@ -0,0 +1,22 @@
---
id: os-platform
name: Os.platform
category: os
kind: namespace-method
tokens: Os.platform
sig: Os.platform() -> string
tip: The host platform id.
order: 10
ns: Os
member: platform
---
The host platform as a short id: <code>"macos"</code>, <code>"linux"</code>, or the raw <code>uname</code> system name on other Unixes. Derived from the operating-system name, which is portable across every Unix host. Non-deterministic host input — read it to branch on platform, not inside the simulation.
```ludic
program Platform {
entry {
if Os.platform() == "macos" { print(1) } else { print(0) }
}
}
```

View file

@ -0,0 +1,26 @@
---
id: os-save_dir
name: Os.save_dir
category: os
kind: namespace-method
tokens: Os.save_dir
sig: Os.save_dir(app) -> string
tip: Per-user save directory for an app.
order: 14
ns: Os
member: save_dir
---
The per-user directory where an application should store save files, named for the app. On macOS this is under <code>~/Library/Application Support/</code>. The path is returned as a string; creating the directory and reading/writing files is the filesystem library's job (see the <a href="os-config_dir"><code>config_dir</code></a>/<a href="os-cache_dir"><code>cache_dir</code></a> companions).
Parameters:
- `app` — the application name (used as the folder name)
```ludic
program SaveDir {
entry {
let dir = Os.save_dir("MyGame")
print(len(dir))
}
}
```

View file

@ -0,0 +1,26 @@
---
id: os-set_env
name: Os.set_env
category: os
kind: namespace-method
tokens: Os.set_env
sig: Os.set_env(name, value) -> bool
tip: Set an environment variable.
order: 7
ns: Os
member: set_env
---
Sets an environment variable for this process (and any children it launches), overwriting any existing value. Returns <code>true</code> on success. Environment changes are process-local and non-deterministic host state — keep them out of the simulation.
Parameters:
- `name` — the variable name
- `value` — the value to set
```ludic
program SetEnv {
entry {
if Os.set_env("LUDIC_MODE", "fast") { print(1) }
}
}
```

View file

@ -0,0 +1,26 @@
---
id: os-stderr_write
name: Os.stderr_write
category: os
kind: namespace-method
tokens: Os.stderr_write
sig: Os.stderr_write(s)
tip: Write a string to standard error.
order: 13
ns: Os
member: stderr_write
---
Writes a string to standard error verbatim. Standard error is the right stream for diagnostics, warnings, and usage messages: it stays separate from a tool's real output on standard out, so a caller can capture one without the other.
Parameters:
- `s` — the string to write
```ludic
program StderrWrite {
entry {
Os.stderr_write("warning: using defaults\n")
print(0)
}
}
```

View file

@ -0,0 +1,26 @@
---
id: os-stdout_write
name: Os.stdout_write
category: os
kind: namespace-method
tokens: Os.stdout_write
sig: Os.stdout_write(s)
tip: Write a string to standard output.
order: 12
ns: Os
member: stdout_write
---
Writes a string to standard output verbatim — no trailing newline is added, unlike <code>print</code>. Use it for tool output where you control the exact bytes (progress, generated text, machine-readable lines).
Parameters:
- `s` — the string to write
```ludic
program StdoutWrite {
entry {
Os.stdout_write("hello")
Os.stdout_write("\n")
}
}
```

View file

@ -0,0 +1,23 @@
---
id: os-temp_dir
name: Os.temp_dir
category: os
kind: namespace-method
tokens: Os.temp_dir
sig: Os.temp_dir() -> string
tip: The system temporary directory.
order: 17
ns: Os
member: temp_dir
---
The system temporary directory (from <code>TMPDIR</code>, falling back to <code>/tmp</code>) — for short-lived scratch files that need not survive a restart.
```ludic
program TempDir {
entry {
let dir = Os.temp_dir()
print(len(dir))
}
}
```

View file

@ -0,0 +1,26 @@
---
id: os-unset_env
name: Os.unset_env
category: os
kind: namespace-method
tokens: Os.unset_env
sig: Os.unset_env(name) -> bool
tip: Remove an environment variable.
order: 8
ns: Os
member: unset_env
---
Removes an environment variable from this process. Returns <code>true</code> on success (including when the variable was already absent).
Parameters:
- `name` — the variable name
```ludic
program UnsetEnv {
entry {
Os.set_env("LUDIC_TMP", "1")
if Os.unset_env("LUDIC_TMP") { print(1) }
}
}
```