@amritk/nish 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Ambient declarations for the Nish global surface.
3
+ *
4
+ * Every Nish program is legal TypeScript syntax — the compiler parses it
5
+ * with the official TypeScript parser and nothing else. This file is what
6
+ * makes it legal TypeScript *semantics* too: reference it from a `tsconfig.json`
7
+ * (or with `/// <reference path="..." />`) and `tsc --noEmit`, your editor and
8
+ * your language server all accept `Result<T, E>`, `Ok(...)`, `i32` and the rest
9
+ * without a red squiggle.
10
+ *
11
+ * { "include": ["src/**\/*.ts", "node_modules/nish/runtime/nish.d.ts"] }
12
+ *
13
+ * **`nish` is still the authority.** TypeScript's structural checker is
14
+ * weaker than this compiler's in the places where Nish is deliberately
15
+ * stricter, and it cannot see the flow rules at all:
16
+ *
17
+ * - the integer and float widths are aliases of `number` here, so `tsc`
18
+ * lets an `i32` and an `f64` mix where `nish` refuses;
19
+ * - `tsc` does not know that a `Result` may not be dropped, that
20
+ * `orReturn()` returns early, or that the enclosing function has to
21
+ * return a `Result` of its own for it to be legal at all;
22
+ * - everything Phase 0 forbids (`any`, `throw`, `try`, prototypes,
23
+ * arrow functions, ...) is ordinary TypeScript and passes `tsc` happily.
24
+ *
25
+ * So a program that `tsc` accepts may still be rejected by `nish`; the
26
+ * reverse should never happen, and a case where it does is a bug in this file.
27
+ * `docs/LANGUAGE.md` is the normative description.
28
+ */
29
+
30
+ // ---- Numeric widths (docs/LANGUAGE.md -> Types) ------------------------------
31
+ //
32
+ // Nish treats these as distinct types that never mix implicitly. There is
33
+ // no way to say that in TypeScript without branding them, and a brand would
34
+ // break the literal syntax the language relies on (`let x: i32 = 5`), so they
35
+ // are aliases and the width check is left to `nish`.
36
+
37
+ type i32 = number;
38
+ type i64 = number;
39
+ type u8 = number;
40
+ type u16 = number;
41
+ type u32 = number;
42
+ type u64 = number;
43
+ type f32 = number;
44
+ type f64 = number;
45
+
46
+ // ---- Result (docs/LANGUAGE.md -> Result and error handling) ------------------
47
+ //
48
+ // Modelled as the tagged union TypeScript would use anyway, intersected with
49
+ // the method surface. That is what lets `if (r.ok)` and `if (r.isOk())` narrow
50
+ // `r` in `tsc` for the same reason and in the same places they narrow in
51
+ // `nish`.
52
+
53
+ /** The success arm of a `Result`, as the discriminant proves it. */
54
+ type ResultOk<T> = { readonly ok: true; readonly value: T };
55
+ /** The failure arm. */
56
+ type ResultErr<E> = { readonly ok: false; readonly error: E };
57
+
58
+ interface ResultMethods<T, E> {
59
+ /** True when this is the success arm; narrows `r` where it is a condition. */
60
+ isOk(): this is ResultOk<T> & ResultMethods<T, E>;
61
+ /** True when this is the failure arm; narrows `r` the other way. */
62
+ isErr(): this is ResultErr<E> & ResultMethods<T, E>;
63
+ /**
64
+ * The success payload, or an early `return Err(error)` from the enclosing
65
+ * function — Rust's `?`. `nish` additionally requires that function to
66
+ * return a `Result` whose error arm accepts `E`; `tsc` cannot check that.
67
+ */
68
+ orReturn(): T;
69
+ /** The success payload, or `fallback` when this is the failure arm. */
70
+ unwrapOr(fallback: T): T;
71
+ /** The success payload, or `message` on stderr and exit 1. */
72
+ expect(message: string): T;
73
+ }
74
+
75
+ /**
76
+ * The outcome of an operation that can fail. `T` may be `void` for an
77
+ * operation with nothing to hand back; `E` may not be.
78
+ */
79
+ type Result<T, E> = (ResultOk<T> | ResultErr<E>) & ResultMethods<T, E>;
80
+
81
+ /** The success value. Its `Result` type comes from the context, as `null`'s does. */
82
+ declare function Ok<T, E>(value: T): Result<T, E>;
83
+ declare function Ok<E>(): Result<void, E>;
84
+ /** The failure value, likewise. */
85
+ declare function Err<T, E>(error: E): Result<T, E>;
86
+
87
+ // ---- console and process -----------------------------------------------------
88
+ //
89
+ // Declared here with *Nish's* signatures — one argument, no format string,
90
+ // statement position — rather than borrowed from `lib.dom` or `@types/node`,
91
+ // which describe something much wider. `Console` is an interface so that a
92
+ // project which does pull `lib.dom` in merges with it instead of colliding;
93
+ // `Process` is not that lucky, so a project that needs `@types/node` for other
94
+ // reasons should drop this file's `process` rather than fight it.
95
+
96
+ interface Console {
97
+ /** `x` and a newline to stdout. Statement position; exactly one argument. */
98
+ log(x: string | number | boolean): void;
99
+ /** The same, on stderr. */
100
+ error(x: string | number | boolean): void;
101
+ }
102
+ declare var console: Console;
103
+
104
+ interface Process {
105
+ /**
106
+ * Terminate with `code`. Statement position, and a terminator: `never` is
107
+ * how TypeScript spells that, so a function ending in `process.exit(c)`
108
+ * satisfies its return type in `tsc` as it does in `nish`.
109
+ */
110
+ exit(code: i32): never;
111
+ /** The command line; `argv[0]` is the program path, as in C. Read-only. */
112
+ readonly argv: string[];
113
+ /** The operating system the program runs on: `"linux"`, `"darwin"`, or `"unknown"`. */
114
+ readonly platform: string;
115
+ /** The architecture: `"x64"`, `"arm64"`, or `"unknown"`. */
116
+ readonly arch: string;
117
+ }
118
+ declare var process: Process;
119
+
120
+ // ---- Numeric conversions (there is no cast in Nish) ----------------------
121
+
122
+ declare function toI32(x: number | boolean): i32;
123
+ declare function toI64(x: number | boolean): i64;
124
+ declare function toU8(x: number | boolean): u8;
125
+ declare function toU16(x: number | boolean): u16;
126
+ declare function toU32(x: number | boolean): u32;
127
+ declare function toU64(x: number | boolean): u64;
128
+ declare function toF32(x: number | boolean): f32;
129
+ declare function toF64(x: number | boolean): f64;
130
+ /** Reinterpret the 64 bits of an `f64`, never convert the value. */
131
+ declare function f64ToBits(x: f64): i64;
132
+ declare function bitsToF64(bits: i64): f64;
133
+
134
+ // ---- Streams and files (globals: Nish has no package resolution) ---------
135
+
136
+ /** `s` to stdout with no trailing newline and no conversion. */
137
+ declare function write(s: string): void;
138
+ /** `s` to stderr, likewise. */
139
+ declare function writeError(s: string): void;
140
+ /**
141
+ * `message` and a newline to stderr, then exit 1. Terminates control flow, so
142
+ * it is `never`: that is what lets `tsc` agree that a function ending in a
143
+ * `panic` returns, and that `x` is not null after `if (x === null) { panic(...); }`
144
+ * — the guard-then-panic shape `self/` uses everywhere in place of an assert.
145
+ */
146
+ declare function panic(message: string): never;
147
+ /**
148
+ * The whole file as a string; a path that cannot be read as one — missing, a
149
+ * directory, a parent that cannot be searched — prints `nish: cannot read
150
+ * <path>` and exits 1.
151
+ */
152
+ declare function readFileSync(path: string): string;
153
+ /** The same read, answering `null` for every path the other exits over. */
154
+ declare function readFileSyncOrNull(path: string): string | null;
155
+ declare function writeFileSync(path: string, data: string): void;
156
+ declare function appendFileSync(path: string, data: string): void;
157
+ /** One directory, not recursive; whether a directory is there afterwards. */
158
+ declare function mkdirSync(path: string): boolean;
159
+ /** Whether a directory is at `path` right now. One `stat`, and never an exit. */
160
+ declare function isDirectorySync(path: string): boolean;
161
+ /**
162
+ * The directory's entries, sorted ascending by bytes and without `.` or `..`,
163
+ * or `null` when it cannot be read. An empty directory is an empty array, so
164
+ * the null check is about the directory and not about its contents.
165
+ */
166
+ declare function readdirSync(path: string): string[] | null;
167
+ /**
168
+ * `path` with every symbolic link resolved, as an absolute normalised path —
169
+ * or `null` when it does not resolve, a path that does not exist included.
170
+ */
171
+ declare function realpathSync(path: string): string | null;
172
+ /** Run `argv[0]` through `PATH` and wait: the exit status, `128 + n` for a signal, `-1` for a failure. */
173
+ declare function spawnSync(argv: string[]): number;
174
+ /**
175
+ * The same run with each non-empty path receiving that stream, created or
176
+ * truncated; an empty string leaves that stream inherited.
177
+ */
178
+ declare function spawnSyncTo(argv: string[], stdoutPath: string, stderrPath: string): number;
179
+ /** One environment variable, or `null` when it is unset (an empty value is a set variable). */
180
+ declare function getenv(name: string): string | null;
181
+ /**
182
+ * A monotonic clock in nanoseconds, for timing a region of a program. The
183
+ * origin is arbitrary, so only the difference between two reads is meaningful.
184
+ */
185
+ declare function monotonicNanos(): i64;
186
+
187
+ // ---- The builtin modules (`nish:`) ---------------------------------------------
188
+ //
189
+ // The same builtins, importable. `nish:` resolves to no file — the import
190
+ // renames a builtin rather than introducing one, and the compiler emits the
191
+ // same IR for either spelling — but `tsc` still has to be told the modules
192
+ // exist, or an editor reports every one of these imports as unresolved. The
193
+ // signatures are deliberately the globals' own, repeated rather than aliased:
194
+ // `export { readFileSync }` inside a `declare module` would re-export the
195
+ // global and lose the doc comment an editor shows at the call site.
196
+ //
197
+ // `docs/LANGUAGE.md` -> Builtins -> Builtin modules is the normative list.
198
+
199
+ declare module "nish:fs" {
200
+ export function readFileSync(path: string): string;
201
+ export function readFileSyncOrNull(path: string): string | null;
202
+ export function writeFileSync(path: string, data: string): void;
203
+ export function appendFileSync(path: string, data: string): void;
204
+ /** `true` when the directory was created, `false` when it already existed. */
205
+ export function mkdirSync(path: string): boolean;
206
+ export function isDirectorySync(path: string): boolean;
207
+ /**
208
+ * The directory's entries, sorted ascending by bytes and without `.` or
209
+ * `..`, or `null` when it cannot be read.
210
+ */
211
+ export function readdirSync(path: string): string[] | null;
212
+ export function realpathSync(path: string): string | null;
213
+ }
214
+
215
+ declare module "nish:process" {
216
+ /** The global `process.exit`: a terminator, which `never` is how `tsc` spells it. */
217
+ export function exit(code: i32): never;
218
+ export function getenv(name: string): string | null;
219
+ export function spawnSync(argv: string[]): number;
220
+ /**
221
+ * The same run with each non-empty path receiving that stream, created or
222
+ * truncated; an empty string leaves that stream inherited.
223
+ */
224
+ export function spawnSyncTo(argv: string[], stdoutPath: string, stderrPath: string): number;
225
+ /**
226
+ * A monotonic clock in nanoseconds. The origin is arbitrary, so only the
227
+ * difference between two reads is meaningful.
228
+ */
229
+ export function monotonicNanos(): i64;
230
+ /** The command line; `argv[0]` is the program path, as in C. Read-only. */
231
+ export const argv: readonly string[];
232
+ /** The operating system the program runs on: `"linux"`, `"darwin"`, or `"unknown"`. */
233
+ export const platform: string;
234
+ /** The architecture: `"x64"`, `"arm64"`, or `"unknown"`. */
235
+ export const arch: string;
236
+ }
237
+
238
+ declare module "nish:io" {
239
+ /** `s` to stdout with no trailing newline and no conversion. */
240
+ export function write(s: string): void;
241
+ /** The same, on stderr. */
242
+ export function writeError(s: string): void;
243
+ /** `message` to stderr, then exit 1. A terminator, as `process.exit` is. */
244
+ export function panic(message: string): never;
245
+ }
246
+
247
+ // ---- Arena (docs/LANGUAGE.md -> Arena) ---------------------------------------
248
+
249
+ declare const Arena: {
250
+ /** The current bump address. */
251
+ mark(): i64;
252
+ /** Free everything allocated since `m`. */
253
+ release(m: i64): void;
254
+ /** Recycle everything in O(1), keeping the newest chunk. */
255
+ reset(): void;
256
+ /** Bytes bumped in the current chunk. */
257
+ used(): i64;
258
+ };
259
+
260
+ /**
261
+ * `CPtr` (WP27 S2): the address a `declare function` hands back, opaque and
262
+ * eight bytes wide. `docs/LANGUAGE.md` has the rules — it may be written in a
263
+ * `declare function` signature and on a local, compared with `null` or with
264
+ * another `CPtr`, and passed back to C, and that is all.
265
+ *
266
+ * An empty interface with a private brand rather than `unknown` or a type
267
+ * alias: `tsc` has to refuse the arithmetic and the dereference `nish` refuses,
268
+ * and it has to refuse assigning any other value to one. The brand is what makes
269
+ * it nominal, so a `{}` does not satisfy it; the name is never written, so the
270
+ * declaration costs a reader nothing.
271
+ */
272
+ declare interface CPtr {
273
+ readonly __nishForeignPointer: unique symbol;
274
+ }
275
+
276
+ // ---- What this file cannot say ----------------------------------------------
277
+ //
278
+ // The typed-array aliases (`Int32Array`, `Float32Array`, `Float64Array`,
279
+ // `BigInt64Array`) are *not* declared here. In Nish each one names the
280
+ // element-typed array itself — `Int32Array` is `i32[]` — but redeclaring them
281
+ // would collide with `lib.es5.d.ts` and break every other type in the standard
282
+ // library. So `tsc` reads them as the JavaScript views it knows, which accept
283
+ // indexing and `.length` but not an array literal; write `i32[]` where you
284
+ // want both compilers to agree.
285
+ //
286
+ // `a.pop()` is `T` in Nish — an empty array panics, because there is no
287
+ // `undefined` to answer with — and `T | undefined` in `lib.es5.d.ts`. Narrowing
288
+ // the standard `Array<T>` would need a global augmentation that changed the
289
+ // method for every array in the project, including a host's, so this one is
290
+ // left as it is: `tsc` asks for a null check that `nish` does not need.
package/runtime/nish.h ADDED
@@ -0,0 +1,340 @@
1
+ /* Nish runtime ABI, public C header.
2
+ *
3
+ * Include this from C drivers, N-API shims, or any other host that links the C
4
+ * runtime next to compiled Nish modules. Everything here is a contract shared
5
+ * with `self/runtime.ts` (the IR side) and the implementation, which is
6
+ * two translation units: `runtime/runtime.c` holds the core every program
7
+ * touches — the arena, strings, arrays, number formatting, the panics — and
8
+ * `runtime/runtime_os.c` holds everything that wraps a system call: the file
9
+ * functions, the directory and subprocess calls, `nish_getenv`, the monotonic
10
+ * clock, `nish_platform` / `nish_arch`. They are separate so that each carries
11
+ * its own measured code-size ceiling (docs/wp7-runtime.md, "Runtime additions
12
+ * and budget"); a host links both, which `scripts/build.sh` does for you when
13
+ * handed either. `tests/run.js` checks that every runtime function declared in
14
+ * runtime.ts has a prototype in this file.
15
+ *
16
+ * Headers generated by `nish --emit-header` include this one, so keep
17
+ * it C11-clean under -Wall -Wextra -Werror and usable from C++.
18
+ */
19
+ #ifndef NISH_H
20
+ #define NISH_H
21
+
22
+ #include <stdbool.h>
23
+ #include <stddef.h>
24
+ #include <stdint.h>
25
+
26
+ #ifdef __cplusplus
27
+ extern "C" {
28
+ #endif
29
+
30
+ /* Bind a C declaration to an Nish symbol whose name C cannot spell (a
31
+ * keyword such as `double`): `int32_t double_(int32_t n) NISH_SYMBOL("double");`
32
+ * Generated headers use it; __USER_LABEL_PREFIX__ supplies the `_` Mach-O adds. */
33
+ #define NISH_STRINGIFY_(x) #x
34
+ #define NISH_STRINGIFY(x) NISH_STRINGIFY_(x)
35
+ #define NISH_SYMBOL(name) __asm__(NISH_STRINGIFY(__USER_LABEL_PREFIX__) name)
36
+
37
+ /* Thread-local storage for the arena and the RNG seed (WP20 T0). A host that
38
+ * includes this header must be compiled with the same `-DNISH_THREADS` the
39
+ * runtime was, because the storage class is part of the ABI: modules compiled
40
+ * with `nish --threads` reference `@nish_arena` as a `thread_local` global and
41
+ * ELF refuses to link that against a non-TLS definition. `scripts/build.sh
42
+ * --threads` passes the macro to every input, which is how `nish --threads
43
+ * --link` keeps the two halves in step. The same definition is in
44
+ * runtime/runtime.c and runtime/runtime_wasm.c. */
45
+ #ifdef NISH_THREADS
46
+ #define NISH_TLS _Thread_local
47
+ #else
48
+ #define NISH_TLS
49
+ #endif
50
+
51
+ /* ---- Strings ------------------------------------------------------------
52
+ * An Nish `string` is a pointer to this header: { u64 len, bytes[len], 0 }.
53
+ * `len` is the UTF-8 byte length (`s.length` in Nish). The bytes are
54
+ * NUL-terminated so `s->data` is a valid C string, but they may contain
55
+ * embedded NULs, so prefer `len` over strlen. Strings are immutable: a
56
+ * pointer can be shared freely and never needs copying.
57
+ *
58
+ * Literals live in the module's constant data; every other string is
59
+ * allocated in the arena and stays valid until `nish_reset_arena` or
60
+ * `nish_free_arena`. Never free a string yourself. */
61
+ typedef struct nish_str {
62
+ uint64_t len;
63
+ char data[];
64
+ } nish_str;
65
+
66
+ /* ---- Arena --------------------------------------------------------------
67
+ * One bump allocator per process, or one per thread under `-DNISH_THREADS`
68
+ * (WP20 T0; the layout and every function below are the same either way).
69
+ * Compiled modules read this struct directly
70
+ * (the fast path is inlined into the IR), so its layout is ABI:
71
+ * %struct.nish_arena = type { i8*, i64, i64, i8* }
72
+ * Fields: current chunk buffer, bump offset, chunk capacity, chunk list.
73
+ * `off` and `cap` are `uint64_t` and not `size_t` because that `i64` is what
74
+ * the inlined fast path bumps and compares on every target: under wasm32 a
75
+ * `size_t` pair would put them at bytes 4 and 8 while compiled code reads 8
76
+ * and 16. runtime.c and runtime_wasm.c static-assert these offsets. */
77
+ struct nish_arena {
78
+ char *buf;
79
+ uint64_t off;
80
+ uint64_t cap;
81
+ void *chunks;
82
+ };
83
+ extern NISH_TLS struct nish_arena nish_arena;
84
+
85
+ /* Bump allocation: 8-byte rounded and aligned, uninitialised. */
86
+ void *nish_alloc_struct(uint64_t size);
87
+ /* Slow path: push a new chunk (at least 64 KB) and bump from it. Called by
88
+ * the inlined fast path when the current chunk is full; rarely by hosts. */
89
+ void *nish_arena_grow(uint64_t size);
90
+ /* Recycle everything in O(1): keeps the newest chunk, frees the rest. Every
91
+ * arena string and object becomes invalid. Call it between batches. */
92
+ void nish_reset_arena(void);
93
+ /* Release every chunk. The arena is lazy, so it can be used again afterwards. */
94
+ void nish_free_arena(void);
95
+ /* Arena scopes (WP6). A mark is the current bump address (`buf + off`), or 0
96
+ * while the arena is empty; `nish_arena_release(mark)` frees everything
97
+ * allocated since that mark (chunks pushed after it are freed, a mark of 0
98
+ * behaves like `nish_reset_arena`). Releasing while an object, string or
99
+ * array allocated after the mark is still referenced is undefined behaviour.
100
+ * Compiled functions whose allocations provably die with them bracket their
101
+ * body with these two calls; `Arena.mark/release/used` expose them. */
102
+ uint64_t nish_arena_mark(void);
103
+ void nish_arena_release(uint64_t mark);
104
+ /* Call-site reclaim (WP9): release back to `mark` but keep the newest block,
105
+ * moving it down to `mark` and answering its new address. Only a string may be
106
+ * kept, because a string is one flat block with no interior pointers; `p` must
107
+ * be the last allocation the arena handed out. Anything the guards cannot
108
+ * prove (a `p` outside the current chunk, a stale or newer mark, no room below
109
+ * it) leaves the arena untouched and answers `p`. Compiled callers use it to
110
+ * reclaim the temporaries a string-returning callee left behind. */
111
+ void *nish_arena_keep(uint64_t mark, void *p);
112
+ /* Bytes bumped in the current chunk (`Arena.used()`); a steady-state loop keeps it flat. */
113
+ uint64_t nish_arena_used(void);
114
+
115
+ /* ---- String operations ------------------------------------------------- */
116
+ /* Copy `len` bytes into a new arena string (`bytes` need not be terminated). */
117
+ nish_str *nish_str_new(const char *bytes, uint64_t len);
118
+ /* `a + b` */
119
+ nish_str *nish_str_concat(const nish_str *a, const nish_str *b);
120
+ /* `a === b`: same bytes (pointer equality is a fast path, not a requirement). */
121
+ bool nish_str_eq(const nish_str *a, const nish_str *b);
122
+ /* `s.length`: the byte length. Compiled code loads the header directly. */
123
+ uint64_t nish_str_len(const nish_str *s);
124
+ /* Whether `sub` occurs at byte offset `at` (negative: never); `startsWith` / `endsWith`. */
125
+ bool nish_str_at(const nish_str *s, int64_t at, const nish_str *sub);
126
+ /* `s.indexOf(sub)`: the first byte offset where `sub` occurs, or -1. An empty
127
+ needle answers 0 and one longer than `s` answers -1, as in JavaScript. */
128
+ int64_t nish_str_index_of(const nish_str *s, const nish_str *sub);
129
+ /* `console.log(s)`: one write(2) of the bytes plus a newline to stdout. */
130
+ void nish_print(const nish_str *s);
131
+ /* `console.error` and the newline-free `write` / `writeError`: fd 1 or 2. */
132
+ void nish_write(const nish_str *s, int32_t fd, bool newline);
133
+ /* Number to string, as `${n}` does: decimal for integers, shortest round-trip (JS Number#toString) for f64. */
134
+ nish_str *nish_str_from_i32(int32_t v);
135
+ nish_str *nish_str_from_f64(double v);
136
+ nish_str *nish_str_from_i64(int64_t v);
137
+ /* Unsigned decimal (WP15). u8/u16/u32 are zero-extended by the caller, so one
138
+ symbol serves every unsigned width. */
139
+ nish_str *nish_str_from_u64(uint64_t v);
140
+
141
+ /* Process and file I/O (WP7). `nish_exit` never returns; the file functions
142
+ * print a message to stderr and exit(1) on a fatal error. Everything from here
143
+ * to the end of the clock section below is implemented in runtime_os.c, with
144
+ * `nish_random` and `nish_argv_init` the exceptions: neither asks the operating
145
+ * system anything (the clock only seeds the first, and the entry point hands the
146
+ * second its arguments), so both stay with the core. `nish_random` keeps one seed
147
+ * word, per process or — under `-DNISH_THREADS` — per thread. */
148
+ double nish_random(void);
149
+ void nish_exit(int32_t code);
150
+ nish_str *nish_read_file(const nish_str *path);
151
+ /* As above, but NULL instead of exiting when the file cannot be read. */
152
+ nish_str *nish_read_file_or_null(const nish_str *path);
153
+ void nish_write_file(const nish_str *path, const nish_str *data);
154
+ void nish_append_file(const nish_str *path, const nish_str *data);
155
+
156
+ /* ---- Arrays (WP4) -------------------------------------------------------
157
+ * An Nish `T[]` (also spelled `Int32Array` / `Float64Array` /
158
+ * `BigInt64Array` for i32 / f64 / i64 elements) is a pointer to this header:
159
+ * %struct.nish_array = type { i64 len, i64 cap, i8* data }
160
+ * `data` holds `cap` elements of one fixed size (int32_t 4, double 8,
161
+ * int64_t 8, bool 1, pointers 8), 8-byte aligned when the runtime allocated
162
+ * it.
163
+ *
164
+ * WP15 section 2a: when the element type is an Nish `interface` — a record,
165
+ * fields and nothing else — `data` holds the records *themselves*, end to end,
166
+ * at exactly the stride and alignment clang gives the matching C struct. A
167
+ * `Point[]` is therefore a `struct Point *` and `((struct Point *)a->data)[i]`
168
+ * is element `i`, which is what lets a C host walk one with no marshalling.
169
+ * A `class` element, and a `T | null` element of any kind, is still one
170
+ * pointer per slot. `--emit-header` names the element type above each
171
+ * prototype and says which of the two it is.
172
+ *
173
+ * A generated header spells a parameter the callee only reads as
174
+ * `const nish_array *` and one it writes through (`a[i] = v`, `push`) as
175
+ * `nish_array *`; the element type is in the comment above each prototype.
176
+ *
177
+ * Passing a host buffer: build the header yourself (`nish_array a = { n, n,
178
+ * (char *)buf }`) and pass `&a`. The callee treats it like any array, so a
179
+ * `push` that grows it copies the elements into the arena and leaves your
180
+ * buffer behind; the callee must not retain the pointer beyond the call.
181
+ * A returned array lives in the arena (valid until the next reset/release):
182
+ * copy `len` elements out of `data` before recycling. */
183
+ typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array;
184
+ /* A fresh arena array of `len` uninitialised elements (`len == cap`), for a
185
+ * host that wants the runtime to own the storage (the wasm loader does).
186
+ * `elem_size` is `sizeof` one element, so for a record element type it is
187
+ * `sizeof(struct Point)` and the block is a C array of them. */
188
+ nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len);
189
+ /* The cold paths compiled code calls: `push` when len == cap, a failed bounds
190
+ * check. `nish_array_grow` moves `cap * elem_size` bytes into a fresh arena
191
+ * block and writes `data`/`cap`, so for a record element type it relocates the
192
+ * elements themselves — which is why the checker refuses to let a program hold
193
+ * a pointer to one across a `push` (WP15 section 2a). */
194
+ void nish_array_grow(nish_array *a, uint64_t elem_size);
195
+ void nish_panic_index(uint64_t idx, uint64_t len);
196
+ /* The failed range check of `s.slice(start, end)` (WP15 section 4): prints the
197
+ half-open interval that was asked for and the byte length it left, then
198
+ exits 1. Signed, although the check itself compares unsigned, so that an
199
+ offset that went negative reads as `-1` and not as 2^64 - 1. `substring`
200
+ clamps instead and never reaches this. */
201
+ void nish_panic_slice(int64_t start, int64_t end, int64_t len);
202
+
203
+ /* `process.argv` (WP7): a `string[]` (elements are `nish_str *`) that the entry
204
+ * wrapper `main` builds once from argc/argv before calling the program; index 0
205
+ * is the executable path. Allocated with malloc, so arena resets never touch
206
+ * it. Hosts that call Nish code without a `main` never need it: a program
207
+ * without an entry point cannot read `process.argv` (compile error). */
208
+ extern nish_array *nish_argv;
209
+ void nish_argv_init(int32_t argc, char **argv);
210
+
211
+ /* ---- Directories and subprocesses (WP14 D4) -----------------------------
212
+ * The calls a self-hosted driver needs to find its inputs and link its own
213
+ * output. Every one of them answers a value instead of exiting, exactly as
214
+ * `nish_read_file_or_null` does: Nish has no exceptions, so the caller
215
+ * owns the diagnostic. */
216
+ /* `mkdirSync(path)`: create one directory, NOT recursive (mode 0777 & ~umask,
217
+ * like Node's `fs.mkdirSync(p)` with no options). True when a directory
218
+ * exists at `path` afterwards, whether this call created it or it was already
219
+ * there; false for every other failure, a plain file at `path` included. */
220
+ bool nish_mkdir(const nish_str *path);
221
+ /* `isDirectorySync(path)` (WP14 §7a): true when a directory exists at `path`
222
+ as this call runs, false for everything else — a missing path, a plain file,
223
+ a parent that cannot be searched. One `stat`, no allocation, no exit; it is
224
+ the `stat` half of `nish_mkdir`, which calls it. */
225
+ bool nish_is_dir(const nish_str *path);
226
+ /* `readdirSync(path)`: the entries of directory `path` as a `string[]` (the
227
+ * elements are `nish_str *`), **sorted ascending by bytes** (`strcmp`), with
228
+ * `.` and `..` dropped and every other dotfile kept. The runtime sorts because
229
+ * the language has no `sort` for the caller to reach for, and because a listing
230
+ * in the file system's own order differs between two machines running the same
231
+ * program. NULL when the directory cannot be read at all — the language's
232
+ * `string[] | null` — where a directory that exists and is empty answers an
233
+ * empty array, which is a different answer. The array and its strings live in
234
+ * the arena, so copy what you keep before the next reset or release. NULL
235
+ * under WASI: `fd_readdir` lists a preopened directory rather than a path, so
236
+ * porting this there is a different contract and not a translation. */
237
+ nish_array *nish_readdir(const nish_str *path);
238
+ /* `spawnSync(argv)`: run element 0 of `argv` (searched on `PATH`) with `argv`
239
+ * as its argument vector, wait for it, and answer its exit status, or
240
+ * `128 + n` when signal `n` killed it. -1 when `argv` is empty, when the
241
+ * child cannot be started or waited for, and always under WASI, which has no
242
+ * processes. The elements are `nish_str *`; the child receives their bytes,
243
+ * so an argument containing a NUL is truncated at it. */
244
+ int32_t nish_spawn(const nish_array *argv);
245
+ /* `spawnSyncTo(argv, stdoutPath, stderrPath)`: exactly `nish_spawn` — the same
246
+ * `PATH` search, the same wait, the same status, `128 + n` and -1 — except that
247
+ * each non-empty path receives that stream, created or truncated at 0644 as
248
+ * `nish_write_file` would leave it. An **empty** string leaves that stream
249
+ * inherited, so one call can capture stdout and let stderr through to the
250
+ * terminal. The child opens the files, so a failed open is a child that could
251
+ * not start and answers -1 rather than leaving this process redirected. The two
252
+ * paths must differ: each is opened separately with its own offset, so naming
253
+ * one file twice makes the streams overwrite each other instead of
254
+ * interleaving; capture them apart and concatenate to merge them. One `static`
255
+ * implementation in runtime_os.c backs both spawn builtins, which is what keeps
256
+ * the argument vector, the wait and the signal convention written once. */
257
+ int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_str *err);
258
+
259
+ /* ---- The environment (WP19 R1) ------------------------------------------
260
+ * `getenv(name)`: the value of environment variable `name`, copied into the
261
+ * arena (so a later `setenv` cannot change a string the program still holds),
262
+ * or NULL when it is unset — the language's `string | null`. An empty value
263
+ * is a set variable and answers a zero-length string, not NULL. `name` is
264
+ * read and never retained; a NUL inside it truncates the lookup, as it does
265
+ * for every other path-like argument here. */
266
+ nish_str *nish_getenv(const nish_str *name);
267
+
268
+ /* ---- Symlinks (WP19 §5a item 4) -----------------------------------------
269
+ * `realpathSync(path)`: `path` with every symbolic link resolved and every
270
+ * `.`, `..` and repeated separator removed, as an absolute path copied into
271
+ * the arena — or NULL when it does not resolve, which the language reads as
272
+ * `string | null`. A path that does not exist is the ordinary NULL case and
273
+ * not an error: the caller is asking whether it resolves, and every component
274
+ * but the last must exist for POSIX `realpath` to answer at all.
275
+ *
276
+ * `path` is read and never retained. The answer is a fresh arena string, so
277
+ * it outlives the call the way every other string here does. */
278
+ nish_str *nish_realpath(const nish_str *path);
279
+
280
+ /* ---- What machine this is (WP14 §7a) ------------------------------------
281
+ * `process.platform` and `process.arch`, spelled as Node spells them:
282
+ * "linux" or "darwin", "x64" or "arm64", and "unknown" for anything this
283
+ * compiler has no target triple for, a WASI build included. Both answer a
284
+ * pointer into this library's own constant data, settled when it was compiled
285
+ * (a cross build compiles it for the target), so the strings survive every
286
+ * arena reset, never change, and must not be freed. */
287
+ const nish_str *nish_platform(void);
288
+ const nish_str *nish_arch(void);
289
+
290
+ /* ---- The clock ----------------------------------------------------------
291
+ * `monotonicNanos()`: `CLOCK_MONOTONIC` in nanoseconds, which is what timing a
292
+ * run needs. Deliberately not the wall clock: a wall clock corrected mid-run
293
+ * can go backwards and make an elapsed time negative. The origin is arbitrary
294
+ * — only the difference between two reads means anything, and no reading is
295
+ * comparable across processes or machines — and an i64 of nanoseconds holds 292
296
+ * years of difference. Compiled code calls this as a *writing* function on
297
+ * purpose: `readnone` would let LLVM fold the two reads around a measured
298
+ * region into one and measure zero. */
299
+ int64_t nish_monotonic_nanos(void);
300
+
301
+ /* String to number (WP7), ASCII whitespace only. mode 0 is `parseFloat`
302
+ * (longest JS decimal literal or `Infinity`, else NaN), mode 1 is `Number`
303
+ * (the whole string, trimmed; blank is 0; `0x` hex accepted, as in JS),
304
+ * mode 2 is `parseInt` (base 10 via strtoll, 0 without digits) as a double
305
+ * that the compiler saturates into an i32 with `llvm.fptosi.sat`. */
306
+ double nish_parse_number(const nish_str *s, int32_t mode);
307
+
308
+ /* Checked integer division (Rust semantics): the failed-check path. */
309
+ void nish_panic_div(bool by_zero);
310
+
311
+ /* ---- Parallel work (WP20 T1 / wp29 stage P1), runtime/runtime_parallel.c ----
312
+ *
313
+ * One region of work, divided. `nish_parallel_range` calls `body(lo, hi, ctx)`
314
+ * once per chunk of a partition of `[0, len)`: contiguous chunks, at most one
315
+ * element apart in size, at most `nish_cpu_count()` of them and never more than
316
+ * `len / grain`, with chunk 0 running on the calling thread. It returns when
317
+ * every chunk has run.
318
+ *
319
+ * Under `-DNISH_THREADS` the other chunks run on their own threads, each with
320
+ * its own arena, which it frees before exiting. Without the macro -- and on
321
+ * WASI, which has no threads -- the whole range runs on the calling thread, so
322
+ * the entry point exists either way and the flag decides only whether the work
323
+ * is divided. A body that itself calls this runs its range on one thread: the
324
+ * nesting guard is thread-local and there is no scheduler here.
325
+ *
326
+ * Two preconditions the caller owns, because this file cannot check them and
327
+ * the language surface above it will:
328
+ * - chunks run concurrently, so a body may read what the caller owns and may
329
+ * write only through storage the caller lent it and no two chunks share;
330
+ * - a body's result may not point into the arena, because a worker's arena is
331
+ * freed when its thread exits while chunk 0's is the caller's own. */
332
+ typedef void (*nish_par_body)(int64_t lo, int64_t hi, void *ctx);
333
+ int64_t nish_cpu_count(void);
334
+ void nish_parallel_range(nish_par_body body, void *ctx, int64_t len, int64_t grain);
335
+
336
+ #ifdef __cplusplus
337
+ }
338
+ #endif
339
+
340
+ #endif /* NISH_H */