feat(stdlib): add Os.* — the OS/environment interface (#21)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
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:
parent
031b138f84
commit
b205dd8dfd
27 changed files with 12109 additions and 10447 deletions
13
docs/language/os/_section.md
Normal file
13
docs/language/os/_section.md
Normal 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.
|
||||
23
docs/language/os/os-arch.md
Normal file
23
docs/language/os/os-arch.md
Normal 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))
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/os/os-arg.md
Normal file
26
docs/language/os/os-arg.md
Normal 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))
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/os/os-arg_count.md
Normal file
22
docs/language/os/os-arg_count.md
Normal 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) }
|
||||
}
|
||||
}
|
||||
```
|
||||
24
docs/language/os/os-args.md
Normal file
24
docs/language/os/os-args.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/os/os-cache_dir.md
Normal file
26
docs/language/os/os-cache_dir.md
Normal 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))
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/os/os-config_dir.md
Normal file
26
docs/language/os/os-config_dir.md
Normal 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))
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/os/os-env.md
Normal file
28
docs/language/os/os-env.md
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
27
docs/language/os/os-env_or.md
Normal file
27
docs/language/os/os-env_or.md
Normal 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))
|
||||
}
|
||||
}
|
||||
```
|
||||
29
docs/language/os/os-exit.md
Normal file
29
docs/language/os/os-exit.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/os/os-has_env.md
Normal file
25
docs/language/os/os-has_env.md
Normal 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) }
|
||||
}
|
||||
}
|
||||
```
|
||||
22
docs/language/os/os-platform.md
Normal file
22
docs/language/os/os-platform.md
Normal 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) }
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/os/os-save_dir.md
Normal file
26
docs/language/os/os-save_dir.md
Normal 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))
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/os/os-set_env.md
Normal file
26
docs/language/os/os-set_env.md
Normal 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) }
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/os/os-stderr_write.md
Normal file
26
docs/language/os/os-stderr_write.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/os/os-stdout_write.md
Normal file
26
docs/language/os/os-stdout_write.md
Normal 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")
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/os/os-temp_dir.md
Normal file
23
docs/language/os/os-temp_dir.md
Normal 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))
|
||||
}
|
||||
}
|
||||
```
|
||||
26
docs/language/os/os-unset_env.md
Normal file
26
docs/language/os/os-unset_env.md
Normal 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) }
|
||||
}
|
||||
}
|
||||
```
|
||||
37
examples/library/os.ludic
Normal file
37
examples/library/os.ludic
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# os.ludic — Os.* environment interface. args/env/platform are non-deterministic
|
||||
# host input, so instead of fixed answers we assert the invariants that must hold
|
||||
# regardless of where it runs: an env var round-trips, env_or falls back when a
|
||||
# var is unset, unset removes it, args() agrees with arg_count(), and the host
|
||||
# facts and known folders are all non-empty. Running it prints: 1 2 3 4 5 6 7 8 9 10 11 12 13
|
||||
program Os {
|
||||
entry {
|
||||
# environment variables round-trip through set/get
|
||||
if Os.set_env("LUDIC_OS_TEST", "42") { print(1) }
|
||||
if Os.env("LUDIC_OS_TEST") == "42" { print(2) }
|
||||
if Os.has_env("LUDIC_OS_TEST") { print(3) }
|
||||
|
||||
# env_or returns the fallback for a variable that is not set
|
||||
if Os.env_or("LUDIC_OS_DEFINITELY_UNSET", "fallback") == "fallback" { print(4) }
|
||||
|
||||
# unset removes the variable
|
||||
if Os.unset_env("LUDIC_OS_TEST") { print(5) }
|
||||
if not Os.has_env("LUDIC_OS_TEST") { print(6) }
|
||||
|
||||
# the argument vector always carries at least the program path, and args()
|
||||
# is a []string of exactly arg_count() elements
|
||||
if Os.arg_count() >= 1 { print(7) }
|
||||
if len(Os.args()) == Os.arg_count() { print(8) }
|
||||
|
||||
# platform and arch are non-empty host facts
|
||||
if len(Os.platform()) > 0 { print(9) }
|
||||
if len(Os.arch()) > 0 { print(10) }
|
||||
|
||||
# per-user known folders resolve to non-empty paths
|
||||
if len(Os.save_dir("MyGame")) > 0 { print(11) }
|
||||
if len(Os.temp_dir()) > 0 { print(12) }
|
||||
|
||||
# diagnostics go to stderr, so they never disturb this program's stdout
|
||||
Os.stderr_write("os.ludic: hello from stderr\n")
|
||||
print(13)
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ var g_uses_cryptort: bool = false # Crypto.* was emitted -> emit the SHA-256 /
|
|||
var g_uses_uuidrt: bool = false # Uuid.* was emitted -> emit the UUID runtime (needs the crypto CSPRNG)
|
||||
var g_uses_noisert: bool = false # Noise.* was emitted -> emit the fixed-point noise runtime
|
||||
var g_uses_logrt: bool = false # Log.* was emitted -> emit the log level register + console sink
|
||||
var g_uses_osrt: bool = false # Os.* (prelude-backed methods) was emitted -> emit the Os runtime
|
||||
var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civil<->epoch conversions
|
||||
var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ function emit_program() -> void {
|
|||
if g_uses_uuidrt { emit_uuid_prelude() } # @fn_uuid_v4 / @fn_uuid_v7 / parse / equals (over the crypto CSPRNG)
|
||||
if g_uses_noisert { emit_noise_prelude() } # @fn_noise_value2/perlin2/simplex2/fbm2/cellular2 (Q16.16)
|
||||
if g_uses_logrt { emit_log_prelude() } # @L_log_level + @fn_log_emit (levelled stderr sink)
|
||||
if g_uses_osrt { emit_os_prelude() } # @fn_os_args/platform/arch/save_dir/... (libc env + uname)
|
||||
if g_uses_datert { emit_datetime_prelude() } # @fn_days_from_civil / @fn_civil_from_days conversions
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -247,6 +247,10 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if is_log_ns(meth) { return emit_log_ns(meth, e) }
|
||||
perr(`unknown builtin Log.{meth}`)
|
||||
}
|
||||
if (ns == "Os") {
|
||||
if is_os_ns(meth) { return emit_os_ns(meth, e) }
|
||||
perr(`unknown builtin Os.{meth}`)
|
||||
}
|
||||
if (ns == "Vector") {
|
||||
if is_vector_ns(meth) { return emit_vector_ns(meth, e) }
|
||||
perr(`unknown builtin Vector.{meth}`)
|
||||
|
|
|
|||
188
selfhost/emit_os.ludic
Normal file
188
selfhost/emit_os.ludic
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# emit_os.ludic — the Os.* namespace: 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. Go-flavored and
|
||||
# game-scoped: no process spawning, signals, or permission APIs — just the facts
|
||||
# a launcher, an asset pipeline, or a save system needs.
|
||||
#
|
||||
# Os.args() -> []string every command-line argument (argv[0..])
|
||||
# Os.arg_count() -> int how many arguments there are
|
||||
# Os.arg(i) -> string the i-th argument (0 = the program path)
|
||||
# Os.env(name) -> string an environment variable, or null if unset
|
||||
# Os.env_or(name, fb) -> string ...or `fb` when it is unset/empty-null
|
||||
# Os.has_env(name) -> bool is the variable set?
|
||||
# Os.set_env(name, val) -> bool set it (true on success)
|
||||
# Os.unset_env(name) -> bool remove it (true on success)
|
||||
# Os.exit(code) terminate the process with a status code
|
||||
# Os.platform() -> string "macos" | "linux" | ...(raw uname sysname)
|
||||
# Os.arch() -> string machine arch, e.g. "arm64" | "x86_64"
|
||||
# Os.stdout_write(s) write a string to standard output
|
||||
# Os.stderr_write(s) write a string to standard error
|
||||
# Os.save_dir(app) -> string per-user save directory for `app`
|
||||
# Os.config_dir(app) -> string per-user config directory for `app`
|
||||
# Os.cache_dir(app) -> string per-user cache directory for `app`
|
||||
# Os.temp_dir() -> string the system temporary directory
|
||||
#
|
||||
# Determinism: args/env/platform are non-deterministic host input — read them at
|
||||
# startup to configure the game, but keep them out of the replayable simulation.
|
||||
#
|
||||
# Platform coverage: this v1 targets the native (macOS/BSD) host, the only fully
|
||||
# supported target today. platform() is portable (uname sysname is field 0 on
|
||||
# every Unix); arch() and the known-folder layout assume the macOS/BSD utsname
|
||||
# and directory conventions. Linux/Windows/wasm known-folder resolution and a
|
||||
# target-aware arch() are documented follow-ups (see issue #21).
|
||||
|
||||
function is_os_ns(meth: pointer) -> bool {
|
||||
if (meth == "args") or (meth == "arg_count") or (meth == "arg") { return true }
|
||||
if (meth == "env") or (meth == "env_or") or (meth == "has_env") { return true }
|
||||
if (meth == "set_env") or (meth == "unset_env") { return true }
|
||||
if (meth == "exit") or (meth == "platform") or (meth == "arch") { return true }
|
||||
if (meth == "stdout_write") or (meth == "stderr_write") { return true }
|
||||
if (meth == "save_dir") or (meth == "config_dir") or (meth == "cache_dir") or (meth == "temp_dir") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
function emit_os_ns(meth: pointer, e: Node) -> Val {
|
||||
# arg_count / arg / exit stay light — they mirror the bare intrinsics and need
|
||||
# no Os runtime prelude, so a program using only these emits no extra IR.
|
||||
if (meth == "arg_count") { return val(emit_bind("load i32, ptr @L_argc"), "int") }
|
||||
if (meth == "arg") {
|
||||
let i = emit_expr(e.kids[0])
|
||||
let v = emit_bind("load ptr, ptr @L_argv")
|
||||
let q = emit_bind(`getelementptr ptr, ptr {v}, i32 {i.code}`)
|
||||
return val(emit_bind(`load ptr, ptr {q}`), "string")
|
||||
}
|
||||
if (meth == "exit") {
|
||||
let n = emit_expr(e.kids[0])
|
||||
emit(` call void @exit(i32 {n.code})\n`)
|
||||
emit(" unreachable\n")
|
||||
g_term = true
|
||||
return val("0", "void")
|
||||
}
|
||||
if (meth == "env") { # raw getenv: null when unset
|
||||
let nm = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call ptr @getenv(ptr {nm.code})`), "string")
|
||||
}
|
||||
if (meth == "has_env") {
|
||||
let nm = emit_expr(e.kids[0])
|
||||
let r = emit_bind(`call ptr @getenv(ptr {nm.code})`)
|
||||
let ne = emit_bind(`icmp ne ptr {r}, null`)
|
||||
return val(emit_bind(`zext i1 {ne} to i32`), "bool")
|
||||
}
|
||||
if (meth == "stdout_write") or (meth == "stderr_write") {
|
||||
var strm = "@__stdoutp"
|
||||
if (meth == "stderr_write") { strm = "@__stderrp" }
|
||||
let s = emit_expr(e.kids[0])
|
||||
let f = emit_bind(`load ptr, ptr {strm}`)
|
||||
let n = emit_bind(`call i64 @strlen(ptr {s.code})`)
|
||||
emit(` call i64 @fwrite(ptr {s.code}, i64 1, i64 {n}, ptr {f})\n`)
|
||||
return val("0", "void")
|
||||
}
|
||||
# everything below is served by the Os runtime prelude
|
||||
g_uses_osrt = true
|
||||
if (meth == "args") { return val(emit_bind("call ptr @fn_os_args()"), "[]string") }
|
||||
if (meth == "env_or") {
|
||||
let nm = emit_expr(e.kids[0]); let fb = emit_expr(e.kids[1])
|
||||
return val(emit_bind(`call ptr @fn_os_getenv_or(ptr {nm.code}, ptr {fb.code})`), "string")
|
||||
}
|
||||
if (meth == "set_env") {
|
||||
let nm = emit_expr(e.kids[0]); let v = emit_expr(e.kids[1])
|
||||
let r = emit_bind(`call i32 @setenv(ptr {nm.code}, ptr {v.code}, i32 1)`)
|
||||
let ok = emit_bind(`icmp eq i32 {r}, 0`)
|
||||
return val(emit_bind(`zext i1 {ok} to i32`), "bool")
|
||||
}
|
||||
if (meth == "unset_env") {
|
||||
let nm = emit_expr(e.kids[0])
|
||||
let r = emit_bind(`call i32 @unsetenv(ptr {nm.code})`)
|
||||
let ok = emit_bind(`icmp eq i32 {r}, 0`)
|
||||
return val(emit_bind(`zext i1 {ok} to i32`), "bool")
|
||||
}
|
||||
if (meth == "platform") { return val(emit_bind("call ptr @fn_os_platform()"), "string") }
|
||||
if (meth == "arch") { return val(emit_bind("call ptr @fn_os_arch()"), "string") }
|
||||
if (meth == "save_dir") {
|
||||
let a = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call ptr @fn_os_save_dir(ptr {a.code})`), "string")
|
||||
}
|
||||
if (meth == "config_dir") {
|
||||
let a = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call ptr @fn_os_config_dir(ptr {a.code})`), "string")
|
||||
}
|
||||
if (meth == "cache_dir") {
|
||||
let a = emit_expr(e.kids[0])
|
||||
return val(emit_bind(`call ptr @fn_os_cache_dir(ptr {a.code})`), "string")
|
||||
}
|
||||
# temp_dir
|
||||
return val(emit_bind("call ptr @fn_os_temp_dir()"), "string")
|
||||
}
|
||||
|
||||
# emit_os_prelude — the Os runtime, emitted once per program that uses the
|
||||
# prelude-backed Os.* methods (g_uses_osrt). Pure libc over NUL-terminated
|
||||
# strings; declares the few POSIX symbols the header does not already carry.
|
||||
function emit_os_prelude() -> void {
|
||||
emith("declare i32 @setenv(ptr, ptr, i32)\n")
|
||||
emith("declare i32 @unsetenv(ptr)\n")
|
||||
emith("declare i32 @uname(ptr)\n")
|
||||
|
||||
# string constants (escaped + length-counted by emit_str_const)
|
||||
let k_home = emit_str_const("HOME")
|
||||
let k_dot = emit_str_const(".")
|
||||
let k_appsp = emit_str_const("/Library/Application Support/")
|
||||
let k_cache = emit_str_const("/Library/Caches/")
|
||||
let k_tmpk = emit_str_const("TMPDIR")
|
||||
let k_tmp = emit_str_const("/tmp")
|
||||
let k_darw = emit_str_const("Darwin")
|
||||
let k_macos = emit_str_const("macos")
|
||||
let k_linux_k = emit_str_const("Linux")
|
||||
let k_linux = emit_str_const("linux")
|
||||
|
||||
# getenv(name) or a fallback when it is unset
|
||||
emith("define ptr @fn_os_getenv_or(ptr %name, ptr %fb) {\n")
|
||||
emith("entry:\n %r = call ptr @getenv(ptr %name)\n %z = icmp eq ptr %r, null\n br i1 %z, label %use, label %got\n")
|
||||
emith("use:\n ret ptr %fb\n")
|
||||
emith("got:\n ret ptr %r\n}\n")
|
||||
|
||||
# concatenate two NUL-terminated strings into a fresh malloc'd buffer
|
||||
emith("define ptr @fn_os_join2(ptr %a, ptr %b) {\n")
|
||||
emith("entry:\n %la = call i64 @strlen(ptr %a)\n %lb = call i64 @strlen(ptr %b)\n")
|
||||
emith(" %sum = add i64 %la, %lb\n %tot = add i64 %sum, 1\n %m = call ptr @malloc(i64 %tot)\n")
|
||||
emith(" call ptr @memcpy(ptr %m, ptr %a, i64 %la)\n")
|
||||
emith(" %m2 = getelementptr i8, ptr %m, i64 %la\n call ptr @memcpy(ptr %m2, ptr %b, i64 %lb)\n")
|
||||
emith(" %end = getelementptr i8, ptr %m, i64 %sum\n store i8 0, ptr %end\n ret ptr %m\n}\n")
|
||||
|
||||
emith("define ptr @fn_os_join3(ptr %a, ptr %b, ptr %c) {\n")
|
||||
emith("entry:\n %ab = call ptr @fn_os_join2(ptr %a, ptr %b)\n %r = call ptr @fn_os_join2(ptr %ab, ptr %c)\n ret ptr %r\n}\n")
|
||||
|
||||
# the user's home directory, or "." when HOME is unset
|
||||
emith(`define ptr @fn_os_home() {{\n %r = call ptr @fn_os_getenv_or(ptr {k_home}, ptr {k_dot})\n ret ptr %r\n}}\n`)
|
||||
|
||||
# per-user known folders (macOS/BSD layout)
|
||||
emith(`define ptr @fn_os_save_dir(ptr %app) {{\n %h = call ptr @fn_os_home()\n %r = call ptr @fn_os_join3(ptr %h, ptr {k_appsp}, ptr %app)\n ret ptr %r\n}}\n`)
|
||||
emith(`define ptr @fn_os_config_dir(ptr %app) {{\n %h = call ptr @fn_os_home()\n %r = call ptr @fn_os_join3(ptr %h, ptr {k_appsp}, ptr %app)\n ret ptr %r\n}}\n`)
|
||||
emith(`define ptr @fn_os_cache_dir(ptr %app) {{\n %h = call ptr @fn_os_home()\n %r = call ptr @fn_os_join3(ptr %h, ptr {k_cache}, ptr %app)\n ret ptr %r\n}}\n`)
|
||||
emith(`define ptr @fn_os_temp_dir() {{\n %r = call ptr @fn_os_getenv_or(ptr {k_tmpk}, ptr {k_tmp})\n ret ptr %r\n}}\n`)
|
||||
|
||||
# Os.args() -> a %LSlice of the argv strings (data = argv, len = cap = argc), a
|
||||
# snapshot the caller may iterate or index like any other []string.
|
||||
emith("define ptr @fn_os_args() {\n")
|
||||
emith("entry:\n %c = load i32, ptr @L_argc\n %v = load ptr, ptr @L_argv\n")
|
||||
emith(" %h = call ptr @malloc(i64 16)\n")
|
||||
emith(" %d0 = getelementptr inbounds %LSlice, ptr %h, i32 0, i32 0\n store ptr %v, ptr %d0\n")
|
||||
emith(" %d1 = getelementptr inbounds %LSlice, ptr %h, i32 0, i32 1\n store i32 %c, ptr %d1\n")
|
||||
emith(" %d2 = getelementptr inbounds %LSlice, ptr %h, i32 0, i32 2\n store i32 %c, ptr %d2\n")
|
||||
emith(" ret ptr %h\n}\n")
|
||||
|
||||
# platform(): uname sysname (field 0, portable) mapped to a short id
|
||||
emith("define ptr @fn_os_platform() {\n")
|
||||
emith("entry:\n %buf = call ptr @malloc(i64 8192)\n call i32 @uname(ptr %buf)\n")
|
||||
emith(` %cd = call i32 @strncmp(ptr %buf, ptr {k_darw}, i64 6)\n %isd = icmp eq i32 %cd, 0\n br i1 %isd, label %mac, label %chkl\n`)
|
||||
emith(`mac:\n ret ptr {k_macos}\n`)
|
||||
emith(`chkl:\n %cl = call i32 @strncmp(ptr %buf, ptr {k_linux_k}, i64 5)\n %isl = icmp eq i32 %cl, 0\n br i1 %isl, label %lin, label %other\n`)
|
||||
emith(`lin:\n ret ptr {k_linux}\n`)
|
||||
emith("other:\n ret ptr %buf\n}\n")
|
||||
|
||||
# arch(): the uname `machine` field. On macOS/BSD utsname each field is 256
|
||||
# bytes, so `machine` (index 4) sits at offset 1024. Documented BSD-layout
|
||||
# assumption (see the header note); other layouts are a follow-up.
|
||||
emith("define ptr @fn_os_arch() {\n")
|
||||
emith("entry:\n %buf = call ptr @malloc(i64 8192)\n call i32 @uname(ptr %buf)\n")
|
||||
emith(" %m = getelementptr i8, ptr %buf, i64 1024\n ret ptr %m\n}\n")
|
||||
}
|
||||
21860
selfhost/ludicc.seed.ll
21860
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -438,5 +438,24 @@
|
|||
"log-error",
|
||||
"log-set_level",
|
||||
"log-level"
|
||||
],
|
||||
"os": [
|
||||
"os-args",
|
||||
"os-arg_count",
|
||||
"os-arg",
|
||||
"os-env",
|
||||
"os-env_or",
|
||||
"os-has_env",
|
||||
"os-set_env",
|
||||
"os-unset_env",
|
||||
"os-exit",
|
||||
"os-platform",
|
||||
"os-arch",
|
||||
"os-stdout_write",
|
||||
"os-stderr_write",
|
||||
"os-save_dir",
|
||||
"os-config_dir",
|
||||
"os-cache_dir",
|
||||
"os-temp_dir"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/emit_uuid.ludic")
|
||||
push(f, "selfhost/emit_noise.ludic")
|
||||
push(f, "selfhost/emit_log.ludic")
|
||||
push(f, "selfhost/emit_os.ludic")
|
||||
push(f, "selfhost/emit_list.ludic")
|
||||
push(f, "selfhost/emit_ease.ludic")
|
||||
push(f, "selfhost/emit_collide.ludic")
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ function cmd_test() -> int {
|
|||
feat_case("library/uuid", "", "1 2 3 4 5 6 7 8 9 10", "uuid.ludic (Uuid v4/v7 format, version/variant, parse/equals)")
|
||||
feat_case("library/noise", "", "1 2 3 4 5 6 7 8 9 10 11", "noise.ludic (Noise value/perlin/simplex/fbm/cellular determinism + range)")
|
||||
feat_case("library/logging", "", "0 5 2 1", "logging.ludic (Log levels, set_level/level threshold, structured fields)")
|
||||
feat_case("library/os", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "os.ludic (Os args/env round-trip, platform/arch, known dirs)")
|
||||
|
||||
# issue #9: the Time/Date/Duration/Clock stdlib, driven from its own `entry`.
|
||||
net_case("lang/offline_rewards", "13 650 2026-08-30 0")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue