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

@ -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

View file

@ -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
}

View file

@ -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
View 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")
}

File diff suppressed because it is too large Load diff