@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.
- package/INSTALL.md +468 -0
- package/LICENSE +21 -0
- package/bin/nish +0 -0
- package/package.json +31 -0
- package/runtime/nish.d.ts +290 -0
- package/runtime/nish.h +340 -0
- package/runtime/nish.mjs +143 -0
- package/runtime/runtime.c +1184 -0
- package/runtime/runtime_os.c +351 -0
- package/runtime/runtime_parallel.c +156 -0
- package/runtime/runtime_wasm.c +99 -0
- package/runtime/shim.mjs +672 -0
- package/scripts/build.sh +279 -0
- package/std/README.md +185 -0
- package/std/json.ts +402 -0
- package/std/pair.ts +28 -0
- package/std/testing.ts +347 -0
- package/std/text.ts +193 -0
package/scripts/build.sh
ADDED
|
@@ -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.
|