@amritk/nish-x86_64-linux 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,279 @@
1
+ #!/usr/bin/env bash
2
+ # Build an Nish .ll module (plus optional C sources) into a native binary.
3
+ #
4
+ # scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm]
5
+ #
6
+ # The C runtime is three translation units and is named as one: an input
7
+ # <dir>/runtime.c also compiles <dir>/runtime_os.c, the half that wraps the
8
+ # system calls (files, directories, subprocesses, the environment, the clock),
9
+ # and <dir>/runtime_parallel.c, the half that divides a range of work across
10
+ # threads. Each of those files says why they are compiled and measured apart.
11
+ #
12
+ # Profiles:
13
+ # debug clang defaults: no optimisation, symbols kept. The "before" number.
14
+ # speed -O3 + LTO + section GC + strip. Rust `--release` equivalent.
15
+ # size -Oz + LTO + section GC + strip + no unwind tables. Rust
16
+ # `opt-level="z"`, `panic="abort"`, `strip=true` equivalent.
17
+ # wasm wasm32 freestanding module exporting every non-internal function;
18
+ # load it from Node. Add runtime/runtime_wasm.c to the inputs when a
19
+ # function uses arrays (the arena and the array cold paths, no libc);
20
+ # strings and I/O still need a WASI runtime and are not available.
21
+ # wasm wasm32 freestanding module exporting every non-internal function
22
+ # (for modules that do not use the C runtime); load it from Node.
23
+ # wasi wasm32-wasi command module: runtime.c linked against wasi-libc, so
24
+ # string programs run under any WASI host (`_start` runs `main`;
25
+ # `node examples/wasi-host.mjs app.wasm args...`). Needs a WASI
26
+ # sysroot: WASI_SYSROOT=<dir>, or /usr/lib/wasi-sysroot,
27
+ # /opt/wasi-sdk/share/wasi-sysroot, /usr/share/wasi-sysroot.
28
+ # napi Node addon (<out>.node): the speed flags plus -shared -fPIC, built
29
+ # against the Node headers next to `node` (override: NODE_INCLUDE=<dir
30
+ # containing node_api.h>). Inputs: <modules.ll> runtime/runtime.c and
31
+ # the shim from `nish --emit-napi`.
32
+ #
33
+ # Profile-guided optimisation (WP9), for the speed, size and napi profiles:
34
+ # --pgo-generate instrumented build (-fprofile-generate); running the
35
+ # binary writes default_*.profraw into the current
36
+ # directory (or $LLVM_PROFILE_FILE)
37
+ # --pgo-use <profdata> optimise with a merged profile (-fprofile-use=<file>)
38
+ # Recipe:
39
+ # scripts/build.sh app.ll runtime/runtime.c -o app.instr --profile speed --pgo-generate
40
+ # ./app.instr <typical input> # one or more training runs
41
+ # llvm-profdata merge -o app.profdata default_*.profraw
42
+ # scripts/build.sh app.ll runtime/runtime.c -o app --profile speed --pgo-use app.profdata
43
+ # The instrumented link needs the compiler-rt profile runtime (Ubuntu:
44
+ # libclang-rt-<ver>-dev; it ships with Apple clang and Homebrew llvm).
45
+ # docs/wp9-optimisation.md reports what PGO buys on the benchmark suite.
46
+ #
47
+ # Threads (WP20 T0): `--threads` compiles every input with -DNISH_THREADS, which
48
+ # makes the arena and the RNG seed in runtime/runtime.c thread-local. Pass it
49
+ # exactly when the IR was compiled with `nish --threads` (`nish --threads
50
+ # --link` does it for you): compiled modules reference `@nish_arena` as a
51
+ # thread-local global, and ELF will not link that against a non-TLS definition,
52
+ # so a half-threaded build fails at the link rather than at run time.
53
+ #
54
+ # Debug info (WP10): `-g` compiles every input with -g and skips the strip
55
+ # step of the speed/size/napi profiles, so the DWARF that `nish -g`
56
+ # put in the .ll (line table, variables) reaches the binary. `nish
57
+ # --link -g` passes it through automatically.
58
+ #
59
+ # Works on Linux (clang + lld preferred, GNU ld tolerated) and macOS (Apple ld64
60
+ # or Homebrew llvm). Set CC to pick a compiler (default: clang on PATH).
61
+ set -euo pipefail
62
+
63
+ profile=speed
64
+ out=""
65
+ inputs=()
66
+ pgo=() # -fprofile-generate / -fprofile-use=<file>
67
+ debug=0 # -g: keep DWARF (nish -g emits it in the IR; runtime.c gets it here)
68
+ threads=0 # --threads: -DNISH_THREADS, the thread-local arena (WP20 T0)
69
+ while [ $# -gt 0 ]; do
70
+ case "$1" in
71
+ -o) out="$2"; shift 2 ;;
72
+ --profile) profile="$2"; shift 2 ;;
73
+ -g) debug=1; shift ;;
74
+ --threads) threads=1; shift ;;
75
+ --pgo-generate) pgo=(-fprofile-generate); shift ;;
76
+ --pgo-use)
77
+ [ -f "$2" ] || { echo "error: --pgo-use: profile '$2' not found (run the instrumented binary, then llvm-profdata merge)" >&2; exit 2; }
78
+ pgo=("-fprofile-use=$2"); shift 2 ;;
79
+ -h|--help) sed -n '2,41p' "$0"; exit 0 ;;
80
+ *) inputs+=("$1"); shift ;;
81
+ esac
82
+ done
83
+ [ ${#inputs[@]} -gt 0 ] || { echo "error: no input files" >&2; exit 2; }
84
+ [ -n "$out" ] || { echo "error: -o <out> is required" >&2; exit 2; }
85
+
86
+ # The runtime is three translation units, and a caller names one: whoever passes
87
+ # <dir>/runtime.c gets <dir>/runtime_os.c and <dir>/runtime_parallel.c compiled
88
+ # beside it. They were one file until the operating-system half was split out
89
+ # for its own size budget, and the parallel half followed for the same reason
90
+ # (each file's header comment says why), and a link line is where those splits
91
+ # would otherwise leak: `nish --link` builds its command line in
92
+ # self/compile.ts, the published package's recipe in every document and
93
+ # README names runtime.c, and a user's own clang line does too. Pairing them
94
+ # here keeps every one of those correct, and keeps "the runtime" one thing to
95
+ # name from the outside. A caller that names one itself is left alone, because
96
+ # naming one object twice is a duplicate-symbol error.
97
+ for i in ${inputs[@]+"${inputs[@]}"}; do
98
+ case "$i" in
99
+ */runtime.c|runtime.c)
100
+ for half in runtime_os.c runtime_parallel.c; do
101
+ side="${i%runtime.c}$half"
102
+ have=0
103
+ for j in "${inputs[@]}"; do
104
+ if [ "$j" = "$side" ]; then have=1; fi
105
+ done
106
+ if [ "$have" = 0 ] && [ -f "$side" ]; then inputs+=("$side"); fi
107
+ done ;;
108
+ esac
109
+ done
110
+
111
+ CC=${CC:-clang}
112
+ common=(-Wno-override-module) # our IR is target-neutral; clang fills the triple in
113
+ elf=() # flags that only make sense for ELF targets
114
+
115
+ # Platform-specific dead-stripping, symbol stripping and LTO linker selection.
116
+ case "$(uname -s)" in
117
+ Darwin)
118
+ # ld64: -dead_strip is the --gc-sections equivalent, -x drops local symbols
119
+ # (-s is deprecated on macOS). No -fuse-ld=lld: ld64 does LTO natively.
120
+ #
121
+ # NOT -no_uuid, however tempting it looks from the reproducibility side.
122
+ # LC_UUID is what made two links of one input differ here -- measured, on
123
+ # both darwin rows (docs/wp10-ci.md#ci-matrix) -- and dropping it does make
124
+ # them identical, and the binary then does not run: on arm64 dyld refuses
125
+ # an image with no LC_UUID outright, `missing LC_UUID load command`
126
+ # followed by SIGABRT, so the stage1 that linked went on to abort the
127
+ # moment the bootstrap ran it. Measured too, on the run after the one that
128
+ # named the UUID. An unloadable compiler is worse than any reproducibility,
129
+ # so the UUID stays. The ELF branch's --build-id=none below is the flag
130
+ # this would have been; ELF has no loader that insists on one.
131
+ #
132
+ # It costs nothing, because the UUID was measured stable across two links
133
+ # of one input to one output path, and differing on a link to another. That
134
+ # is what scripts/bootstrap.sh links every comparable stage at one path for,
135
+ # and it is why `stage3 == stage2` holds on Mach-O with the load command in.
136
+ gc=(-Wl,-dead_strip); strip_flag=(-Wl,-x) ;;
137
+ *)
138
+ gc=(-Wl,--gc-sections -Wl,--as-needed -Wl,-O2 -Wl,--build-id=none); strip_flag=(-s)
139
+ elf=(-fno-plt)
140
+ # GNU ld needs the gold plugin for LTO; prefer lld when clang can find it.
141
+ if command -v ld.lld >/dev/null 2>&1; then common+=(-fuse-ld=lld); fi ;;
142
+ esac
143
+
144
+ # -g: compile everything with debug info and never strip, whatever the profile,
145
+ # so the line table nish emitted survives into the binary.
146
+ if [ "$debug" = 1 ]; then common+=(-g); strip_flag=(); fi
147
+
148
+ # --threads: the storage class of the arena is ABI, so every input is compiled
149
+ # with the same macro -- C runtime and generated N-API shim alike.
150
+ #
151
+ # -ftls-model=initial-exec names, for the C side, the model the IR already
152
+ # names. Without it a -fPIC build (the napi profile) reaches the arena through
153
+ # a __tls_get_addr call, so the two halves of one inlined allocator would use
154
+ # two different models; it is also smaller (4,759 bytes of runtime.c `.text*`
155
+ # against 4,820 at -Oz -fPIC) and free where the model was local-exec anyway.
156
+ #
157
+ # `tls` is the same pair again for the wasm and wasi profiles, which build their
158
+ # own command line instead of using `common`; it is empty on every ordinary
159
+ # build, hence the bash 3.2 expansion spelling explained below.
160
+ #
161
+ # -pthread is for runtime_parallel.c, the translation unit that divides a range
162
+ # of work across threads: it is compiled in either configuration and only spawns
163
+ # under this macro, so this is the build where the flag has to be on the command
164
+ # line. On a current glibc the library half is already inside libc and the link
165
+ # would succeed without it; passing it is what makes that an implementation
166
+ # detail rather than something the build depends on.
167
+ #
168
+ # It is deliberately in `common` and not in `tls`: `tls` is the wasm and wasi
169
+ # command lines, which have no threads to link against, and where
170
+ # runtime_parallel.c compiles to its sequential fallback because it tests
171
+ # __wasi__ and __wasm__ as well as the macro.
172
+ tls=()
173
+ if [ "$threads" = 1 ]; then
174
+ common+=(-DNISH_THREADS=1 -ftls-model=initial-exec -pthread)
175
+ tls=(-DNISH_THREADS=1 -ftls-model=initial-exec)
176
+ fi
177
+
178
+ # The ${arr[@]+"${arr[@]}"} spelling below is not a style tic: macOS ships bash
179
+ # 3.2 (Apple will not ship GPLv3), where expanding an empty array as "${arr[@]}"
180
+ # under `set -u` is a fatal "unbound variable" -- bash 4.4 made it legal, which is
181
+ # why Linux never noticed. `pgo`, `elf`, `strip_flag` and `libs` are all empty on
182
+ # ordinary builds, so please do not simplify these back.
183
+ case "$profile" in
184
+ debug)
185
+ "$CC" "${common[@]}" "${inputs[@]}" -lm -o "$out" ;;
186
+ speed)
187
+ "$CC" "${common[@]}" -O3 -flto -DNDEBUG ${pgo[@]+"${pgo[@]}"} \
188
+ -ffunction-sections -fdata-sections -fomit-frame-pointer \
189
+ -fno-asynchronous-unwind-tables -fno-unwind-tables ${elf[@]+"${elf[@]}"} \
190
+ "${gc[@]}" ${strip_flag[@]+"${strip_flag[@]}"} "${inputs[@]}" -lm -o "$out" ;;
191
+ size)
192
+ "$CC" "${common[@]}" -Oz -flto -DNDEBUG ${pgo[@]+"${pgo[@]}"} \
193
+ -ffunction-sections -fdata-sections -fomit-frame-pointer \
194
+ -fno-asynchronous-unwind-tables -fno-unwind-tables ${elf[@]+"${elf[@]}"} \
195
+ -fno-stack-protector -fvisibility=hidden \
196
+ "${gc[@]}" ${strip_flag[@]+"${strip_flag[@]}"} "${inputs[@]}" -lm -o "$out" ;;
197
+ wasm)
198
+ # clang resolves wasm-ld next to its own binary first, then on PATH; ask it
199
+ # rather than probing PATH so a Homebrew llvm without a PATH entry still works.
200
+ wasm_ld=$("$CC" -print-prog-name=wasm-ld)
201
+ if [ ! -x "$wasm_ld" ]; then
202
+ echo "error: the wasm profile needs wasm-ld (install lld; on macOS: brew install llvm@18)" >&2
203
+ exit 2
204
+ fi
205
+ # -mbulk-memory lowers llvm.memset/memcpy (`new Array<T>(n)`, `push` growth) to the
206
+ # memory.fill/memory.copy instructions instead of libc calls the freestanding link lacks.
207
+ "$CC" -Wno-override-module --target=wasm32-unknown-unknown -Oz -nostdlib -mbulk-memory \
208
+ ${tls[@]+"${tls[@]}"} \
209
+ -Wl,--no-entry -Wl,--export-all -Wl,--strip-all -Wl,--gc-sections \
210
+ "${inputs[@]}" -o "$out" ;;
211
+ wasi)
212
+ sysroot=${WASI_SYSROOT:-}
213
+ for d in /usr/lib/wasi-sysroot /opt/wasi-sdk/share/wasi-sysroot /usr/share/wasi-sysroot; do
214
+ [ -n "$sysroot" ] || { [ -d "$d" ] && sysroot=$d; }
215
+ done
216
+ if [ -z "$sysroot" ] || [ ! -d "$sysroot/lib" ]; then
217
+ echo "error: the wasi profile needs a WASI sysroot (wasi-libc headers and libc.a) and none was found." >&2
218
+ echo " Install wasi-sdk's sysroot (https://github.com/WebAssembly/wasi-sdk/releases: wasi-sysroot-<ver>.tar.gz," >&2
219
+ echo " or apt install wasi-libc) and set WASI_SYSROOT=<dir> if it is not in one of the default locations" >&2
220
+ echo " (/usr/lib/wasi-sysroot, /opt/wasi-sdk/share/wasi-sysroot, /usr/share/wasi-sysroot). See docs/INSTALL.md." >&2
221
+ exit 2
222
+ fi
223
+ wasm_ld=$("$CC" -print-prog-name=wasm-ld)
224
+ if [ ! -x "$wasm_ld" ]; then
225
+ echo "error: the wasi profile needs wasm-ld (install lld; on macOS: brew install llvm@18)" >&2
226
+ exit 2
227
+ fi
228
+ # clang links compiler-rt's wasm32 builtins from its resource dir (wasi-sdk and Debian's
229
+ # libclang-rt-<ver>-dev-wasm32 put it there); wasi-libc's strtoll needs its __multi3.
230
+ # Otherwise look for wasi-sdk's separate libclang_rt.builtins-wasm32-wasi-<ver>.tar.gz
231
+ # unpacked next to the sysroot, or WASI_BUILTINS=<file>, and name libc explicitly
232
+ # (wasi-libc's libc.a includes libm) so clang stops looking for the archive itself.
233
+ libs=()
234
+ if [ ! -f "$("$CC" -print-resource-dir)/lib/wasi/libclang_rt.builtins-wasm32.a" ]; then
235
+ for f in "${WASI_BUILTINS:-}" "$sysroot/lib/wasm32-wasi/libclang_rt.builtins-wasm32.a" \
236
+ "$sysroot/lib/libclang_rt.builtins-wasm32.a" "$sysroot"/../libclang_rt.builtins-wasm32*/libclang_rt.builtins-wasm32.a; do
237
+ if [ -n "$f" ] && [ -f "$f" ]; then libs=(-nodefaultlibs -lc "$f"); break; fi
238
+ done
239
+ if [ ${#libs[@]} -eq 0 ]; then
240
+ echo "error: the wasi profile needs compiler-rt's wasm32 builtins (libclang_rt.builtins-wasm32.a) and clang has none." >&2
241
+ echo " Install libclang-rt-<ver>-dev-wasm32, or unpack wasi-sdk's libclang_rt.builtins-wasm32-wasi-<ver>.tar.gz" >&2
242
+ echo " into $sysroot/lib/wasm32-wasi/ or point WASI_BUILTINS at the .a file. See docs/INSTALL.md." >&2
243
+ exit 2
244
+ fi
245
+ fi
246
+ "$CC" -Wno-override-module --target=wasm32-wasi --sysroot="$sysroot" -Oz -DNDEBUG \
247
+ ${tls[@]+"${tls[@]}"} \
248
+ -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--strip-all \
249
+ "${inputs[@]}" ${libs[@]+"${libs[@]}"} -o "$out" ;;
250
+ napi)
251
+ # Node ships its C headers next to the binary: <prefix>/bin/node and
252
+ # <prefix>/include/node/node_api.h (official tarballs, nvm, fnm, volta).
253
+ # Distro packages put them in libnode-dev; NODE_INCLUDE overrides the guess.
254
+ node_bin=${NODE:-node}
255
+ node_inc=${NODE_INCLUDE:-}
256
+ if [ -z "$node_inc" ]; then
257
+ node_inc=$("$node_bin" -p "require('path').dirname(process.execPath) + '/../include/node'" 2>/dev/null || true)
258
+ fi
259
+ if [ -z "$node_inc" ] || [ ! -f "$node_inc/node_api.h" ]; then
260
+ echo "error: the napi profile needs the Node headers, but ${node_inc:-<node not found>}/node_api.h does not exist." >&2
261
+ echo " Install Node from nodejs.org/nvm (they ship include/node), or apt install libnode-dev and" >&2
262
+ echo " set NODE_INCLUDE=/usr/include/node (any directory that contains node_api.h)." >&2
263
+ exit 2
264
+ fi
265
+ shared=(-shared -fPIC)
266
+ # macOS: the napi_* symbols come from the node binary at load time, so the
267
+ # linker must not insist on resolving them. ELF shared objects allow this.
268
+ case "$(uname -s)" in Darwin) shared+=(-Wl,-undefined,dynamic_lookup) ;; esac
269
+ # runtime/nish.h is the public ABI header the generated shim includes.
270
+ runtime_inc="$(cd "$(dirname "$0")/../runtime" && pwd)"
271
+ "$CC" "${common[@]}" -O3 -flto -DNDEBUG ${pgo[@]+"${pgo[@]}"} -I"$node_inc" -I"$runtime_inc" \
272
+ -ffunction-sections -fdata-sections -fomit-frame-pointer \
273
+ -fno-asynchronous-unwind-tables -fno-unwind-tables ${elf[@]+"${elf[@]}"} \
274
+ "${shared[@]}" "${gc[@]}" ${strip_flag[@]+"${strip_flag[@]}"} "${inputs[@]}" -lm -o "$out" ;;
275
+ *) echo "error: unknown profile '$profile'" >&2; exit 2 ;;
276
+ esac
277
+
278
+ # `wc -c` pads with spaces on macOS; strip them so the number is clean.
279
+ printf '%s: %s bytes (%s)\n' "$out" "$(wc -c < "$out" | tr -d ' ')" "$profile"
package/std/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # `std/` — the standard library
2
+
3
+ Nish modules written in Nish, for Nish programs to import. There is no magic
4
+ here and nothing the compiler knows about: a module in this directory is an
5
+ ordinary Nish source file, compiled as part of whatever program imports it,
6
+ and subject to the same rules as `examples/` or `self/`
7
+ ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md) is the style guide).
8
+
9
+ | Module | What it is |
10
+ | --- | --- |
11
+ | [`testing.ts`](./testing.ts) | a test runner: a `Suite` a program drives with straight-line assertions, printing the `PASS` / `FAIL` / `SKIP` lines the repository's own harness prints, and answering the exit code |
12
+ | [`text.ts`](./text.ts) | the string operations a program would otherwise write inline: `splitLines`, `splitWhitespace`, `trim` and its halves, `contains`, `replaceAll`, and `firstDifference` over two arrays of lines |
13
+ | [`json.ts`](./json.ts) | `jsonField(object, name)`: the value of one field of one flat JSON object, which is the shape the compiler's own `--json` diagnostics have. A reader and not a parser — it answers text, answers `null` for a field that is not there, and does not validate |
14
+ | [`pair.ts`](./pair.ts) | `Pair<A, B>`: an interface with `first` and `second`, for a function that answers two values from one call. A type and nothing else — the caller writes an object literal at the return — and for returning two values rather than storing them side by side |
15
+
16
+ ## How a program imports it
17
+
18
+ By its package specifier:
19
+
20
+ ```ts
21
+ import { Suite } from "nish/testing";
22
+ ```
23
+
24
+ `nish/<module>` resolves to `<module>.ts` in this directory, found beside the
25
+ compiler that is running — not relative to the importing file, so the same
26
+ specifier works at any depth and from outside this repository.
27
+
28
+ **Source is the distribution format** ([`docs/wp21-packages.md`](../docs/wp21-packages.md)
29
+ §2), so an import of a `std/` module is not a link against a built library: the
30
+ module is compiled with the program that imports it, and the whole-program
31
+ attribute pass sees through it exactly as it sees through the program's own
32
+ functions. A `std/` function is inlined, specialised or dropped on the same
33
+ terms as a local one — and a module you import but never call is dropped
34
+ whole. Measured at the `speed` and `size` profiles, both of which link with
35
+ `-flto -Wl,--gc-sections`: a program importing three `std/text` functions and
36
+ calling none is **byte-identical** to the same program without the import.
37
+ (`debug` keeps them, which is what `debug` is for.)
38
+
39
+ A `std/` module is its own package (`nish`), so its symbols are scoped and a
40
+ program may declare a function one of these modules also exports
41
+ (`tests/link/std_package_scope`, `docs/wp21-packages.md` §5a). The package is
42
+ decided by the `nish/` specifier rather than by the directory the file is found
43
+ in — read off the path it would be `nish` from `node_modules/nish/std/` and the
44
+ root package from a checkout, and the same program would compile against an
45
+ installed compiler and be refused by a checkout of it.
46
+
47
+ A relative specifier still works and means the same thing —
48
+ `import { Suite } from "../std/testing"` — but it hard-codes the depth of the
49
+ importing file and only reaches an installed library by the path the install
50
+ put it at, so `nish/` is the form to write.
51
+
52
+ ### Why this is not the second module system this file used to warn about
53
+
54
+ An earlier version of this section said a bare specifier was "deliberately not
55
+ faked", because a resolver that special-cased this directory would be a second
56
+ module system and the one WP21 is bringing has to agree with Node's. That
57
+ reasoning still holds for third-party packages, which are still refused
58
+ (`docs/wp21-packages.md` §5b). It does not hold for *this* package, for two
59
+ reasons that are only true of it:
60
+
61
+ - There is exactly one right answer. `std/` ships inside the compiler's own
62
+ package and is versioned with it, so "the `std/` beside this binary" is not a
63
+ guess a resolver makes — it is the only `std/` that can be correct for the
64
+ compiler reading it. No version can be skewed against it.
65
+ - It **is** what Node resolves. `package.json` declares
66
+ `"./*": "./std/*.ts"` in `exports`, so `nish/text` is a package
67
+ self-reference: `import.meta.resolve("nish/text")` answers `std/text.ts`, and
68
+ `tsc` under `moduleResolution: node16` resolves it to the same file, which is
69
+ what gives an editor go-to-definition into the real source. The compiler
70
+ short-circuits to that answer rather than walking `node_modules` to reach it.
71
+
72
+ So this is WP21's first slice rather than a detour around it: the spelling is
73
+ the one WP21 specifies, and what is still missing is resolution for specifiers
74
+ that are *not* this package.
75
+
76
+ ## Writing a module here
77
+
78
+ - **Spell the widths — and then convert what the builtins hand you.** `i32`,
79
+ `i64`, `f64`, never `number`, so the module means the same thing under
80
+ `--number-mode f64` as it does by default (`examples/arrays.ts` says that half
81
+ for the same reason). It is necessary and **not sufficient**: `s.length`,
82
+ `a.length` and `s.charCodeAt(i)` answer `number`, which *is* `f64` in that
83
+ mode, so a module that declares every width of its own and still writes
84
+ `let end: i32 = text.length` or `while (i < text.length)` does not compile
85
+ there at all. Read each of those through `toI32` —
86
+ `const length: i32 = toI32(text.length)`, once per function rather than once
87
+ per iteration — and the module means one program in both modes. The default
88
+ mode pays nothing for it, because `toI32` on an `i32` is identity.
89
+ `tests/link/std_text_f64` is what keeps this from being prose;
90
+ [`docs/wp26-stdlib.md`](../docs/wp26-stdlib.md) §4 is the reasoning.
91
+ - **Name the private helpers as though they were exported.** A function name is
92
+ unique across the whole program whether or not it is exported, because the
93
+ whole-program attribute analysis is keyed by symbol name — and a `std/` module
94
+ is compiled *into* the program that imports it, so its private helpers are not
95
+ private to the namespace. A helper called `isBlank` would stop any program that
96
+ declares its own `isBlank` from compiling (`` Function `isBlank` is also
97
+ defined in main.ts ``), which is why `text.ts` calls it `isTextBlankByte`. Keep
98
+ the helpers few and their names distinctive; a module whose internals want
99
+ `compare`, `next` or `parse` is asking for package-scoped symbols, which do not
100
+ exist yet ([`docs/wp26-stdlib.md`](../docs/wp26-stdlib.md) §3e). Module
101
+ constants are exempt — `NEWLINE` and `SPACE` fold at their uses, so a program
102
+ may declare those names itself.
103
+ - **It is an Nish program**, so the constraints are the language's: a function is
104
+ an arrow bound to a module-level `const`, a function is never a value, there
105
+ is no `try` / `catch`, and there are no optional or default parameters. Those
106
+ three are what shape an API here more than any style preference — see the
107
+ header of `testing.ts` for what they did to that one, which was written before
108
+ WP18 added generic functions and classes
109
+ ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#generic-functions)) and still has
110
+ one assertion per type ([`docs/wp26-stdlib.md`](../docs/wp26-stdlib.md) §3c).
111
+ `pair.ts` is the first module here to export a generic type.
112
+ - **Ship it with a `tests/link/` case.** `tests/link/<name>/` is the only place a
113
+ multi-module program is exercised end to end, and it is also what puts the
114
+ module into the corpus the stage1 oracles read
115
+ ([`.claude/selfhost.md`](../.claude/selfhost.md)): a `std/` module with no
116
+ importer in `tests/link/` is compiled by neither compiler on any run.
117
+ `testing.ts` has two, one per outcome — `tests/link/std_testing` (exit 0) and
118
+ `tests/link/std_testing_fail` (exit 1, and the wording of every failure
119
+ message) — and `text.ts` and `json.ts` have `tests/link/std_text` and
120
+ `tests/link/std_json`, each of which uses `Suite` to check the module, the way a
121
+ user would. `pair.ts` has five, `tests/link/std_pair_*`, one per shape of
122
+ instantiation, each returning a `Pair` from a sibling module and pinned by its
123
+ stdout. `tests/link/std_text_f64` is the same corpus under
124
+ `--number-mode f64`, which is where a module that spelled its widths and forgot
125
+ a `toI32` is caught.
126
+ - **`std/` is not on the compiler's dependency list.** Nothing in `self/`
127
+ imports it, and nothing should: the compiler is the thing that has to
128
+ build before the library means anything.
129
+
130
+ ## Who reads the library, and what each reader proved
131
+
132
+ Two programs in this repository are written on top of `std/`, and between them
133
+ they are the reason the modules have the shape they do — a library with one
134
+ consumer is a guess.
135
+
136
+ | Program | What it does | What it uses |
137
+ | --- | --- | --- |
138
+ | [`tests/nish/run.ts`](../tests/nish/run.ts) | the golden cases and the `tests/link/` programs, compiled, assembled, linked, run and diffed | `Suite` (including `containsAll` for an `.err` file's fragments and `eqLines` for an IR golden), and all of `std/text` |
139
+ | [`tests/nish/cli.ts`](../tests/nish/cli.ts) | the command line's own contract — the streams, the exit-code bands, and one flat `--json` object per diagnostic — read by a program in the language the compiler compiles | `Suite`, `jsonField`, `splitLines` / `trim` / `contains` |
140
+
141
+ Three assertions exist because the first of those two had written them by hand:
142
+ `contains` for a fragment of a captured stream, `containsAll` for an expectation
143
+ file that holds one fragment per line, and `eqLines` for a generated text against
144
+ a golden, which reports the first differing line instead of printing both texts.
145
+ That is the rule the directory runs on — a `std/` function earns its place when a
146
+ program in this repository would otherwise write the loop, and the loop is
147
+ already written.
148
+
149
+ `std/json` is the one module admitted with a single importer, and the reason is
150
+ the *format* rather than the count: the object it reads is this project's own
151
+ published surface (`AGENTS.md`, [`docs/wp12-release.md`](../docs/wp12-release.md)),
152
+ so every Nish program that ever reads compiler output needs exactly this scan,
153
+ and the next one would copy it out of `tests/nish/cli.ts`. The module is also
154
+ where the format's one ambiguity is written down and tested —
155
+ [`docs/wp26-stdlib.md`](../docs/wp26-stdlib.md) §7 question 3 is where that
156
+ argument is recorded and where its second consumer is expected.
157
+
158
+ ## What `std/testing` is not, and what closed
159
+
160
+ `std/testing` is a test *library*: it is how a compiled program checks itself.
161
+ Driving the compiler — compiling a corpus, assembling it, linking it, running it
162
+ and diffing stdout against a golden — needed three things the language did not
163
+ have, and all three have since landed as builtins:
164
+
165
+ | Was missing | Now |
166
+ | --- | --- |
167
+ | a directory listing | `readdirSync(path): string[] \| null`, sorted by bytes, because there is no `sort` for a caller to reach for |
168
+ | a child's **output** — `spawnSync` answers a status and nothing else | `spawnSyncTo(argv, stdoutPath, stderrPath)`, each stream to a file, an empty path inheriting |
169
+ | a clock | `monotonicNanos(): i64` |
170
+
171
+ So the driver exists, in the language, and it is
172
+ [`tests/nish/run.ts`](../tests/nish/run.ts): it discovers the golden cases with
173
+ `readdirSync`, compiles each one by spawning the compiler, links and runs the ones
174
+ with a `.out`, diffs the IR against the golden line by line, and reports through a
175
+ `Suite`. It passes over the whole corpus — `npm run test:nish` — and `npm test`
176
+ runs it over a handful of cases so that the three builtins are exercised together
177
+ on a real workload on every run.
178
+
179
+ It is still not a replacement for `tests/run.js`, and the difference is worth
180
+ being precise about: it covers section A, the golden cases, and none of the
181
+ pipeline checks — no interop sidecars, no layout assertions, no wasm profiles, no
182
+ packaging and no self-hosting oracles. It also skips, by name and counted, the three
183
+ sidecars it does not implement (`.env`, `.argv`, `.stdout`). What it demonstrates
184
+ is that the language can host its own harness; what `tests/run.js` does is prove
185
+ the compiler.