@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,357 @@
1
+ #!/usr/bin/env bash
2
+ # Build the self-hosted compiler: `self/`, compiled by `self/`.
3
+ #
4
+ # scripts/bootstrap.sh [-o <exe>] [--stages 1|2|3] [--profile speed|size|debug]
5
+ # [--work <dir>] [--verify] [--quiet]
6
+ #
7
+ # The chain is the one docs/wp14-selfhost.md §1 defines, with the seed as a
8
+ # parameter rather than a fixture (docs/wp19-stage0-retirement.md §3, G3):
9
+ #
10
+ # seed whatever compiles stage1 NISH_BOOTSTRAP, or build/seed
11
+ # stage1 self/, built by the seed
12
+ # stage2 self/, built by stage1 the default output
13
+ # stage3 self/, built by stage2 --verify only
14
+ #
15
+ # NISH_BOOTSTRAP=<path> names the seed, the way GOROOT_BOOTSTRAP names the Go
16
+ # that builds Go. It is either a released `nish`, executed directly, or a Node
17
+ # entry point (`.js`, `.mjs`, `.cjs`) wrapping one, executed as `node <path>`:
18
+ #
19
+ # NISH_BOOTSTRAP=~/nish-0.6.0-linux-x86_64 scripts/bootstrap.sh --verify
20
+ #
21
+ # Unset, the seed is build/seed/bin/nish, the last release as
22
+ # `scripts/fetch-seed.sh` unpacks it, and this runs that script first. When the
23
+ # fetch fails it stops and says so: a compiler has to come from somewhere, and
24
+ # the only place left is a release.
25
+ #
26
+ # stage2 is what this installs, because it is the first binary in the chain
27
+ # that no part of the seed emitted: the seed built the compiler that built it,
28
+ # and `--verify` is what says the two agree. `--stages 1` stops at the seed's
29
+ # own output, which is enough to *use* the self-hosted compiler and half the
30
+ # wait.
31
+ #
32
+ # --verify runs the equalities the proof is made of, byte for byte:
33
+ #
34
+ # IR(seed, self/) == IR(stage1, self/) reported, never asserted
35
+ # IR(stage1, self/) == IR(stage2, self/) the fixed point: self-hosted
36
+ # stage3 == stage2 as files, on ELF and on Mach-O
37
+ # alike; see below for what the
38
+ # Mach-O half took
39
+ #
40
+ # The last two are properties of the working tree and of nothing else: whatever
41
+ # built stage1, the compiler `self/` describes has to agree with itself and
42
+ # then reproduce itself. Both are asserted whatever the seed is.
43
+ #
44
+ # The third is a raw byte comparison, and on Mach-O two links of the same input
45
+ # did not produce the same bytes -- measured, at identical size, with every IR
46
+ # equality green (WP19 R2). WHICH bytes is measured too now, and so is what they
47
+ # vary with: LC_UUID, stable across two links to one output path and differing
48
+ # on a link to another, plus on arm64 the one code-directory slot that hashes
49
+ # the page it sits on. That is why `link_stage` below builds every comparable
50
+ # stage at one path, and with it the comparison holds as raw bytes on Mach-O as
51
+ # well -- measured end to end, whatever the UUID turns out to be a function of.
52
+ # Under that, on Darwin only, the script
53
+ # still asserts the size, strips what it can, and reports anything left over as
54
+ # *unattributed* rather than as a compiler difference -- narrower than raw bytes
55
+ # and wider than an exemption, which would accept a stage3 that is a different
56
+ # compiler. The comparison and the reasoning live in
57
+ # `scripts/verify-binaries.sh` -- a script rather than a block in here so that
58
+ # `tests/run.js` can drive both platforms' branches -- and its header has the
59
+ # measurement, byte offsets and all.
60
+ #
61
+ # The first one is reported and not asserted. With a released seed it is one
62
+ # implementation at two points in time: the IR HEAD emits for `self/` is the
63
+ # IR the last release emitted for it. Nothing in that sentence is about
64
+ # bootstrapping. It is a freeze on codegen between releases, and it fails on
65
+ # exactly the changes a release cycle exists to carry: the first improvement to
66
+ # land broke it, when a flow-sensitive bounds analysis proved 46 of `self/`'s
67
+ # 1,206 index checks redundant and 20 of 56 modules "differed" because the
68
+ # optimisation worked. So the difference is reported, and the report is
69
+ # information about this release rather than a verdict on the bootstrap.
70
+ #
71
+ # While `src/` existed, a stage0 seed made the same comparison the second half
72
+ # of Wheeler's diverse double-compiling, and it was asserted there
73
+ # (docs/wp19-stage0-retirement.md §1, G6). That claim went with `src/`.
74
+ #
75
+ # What the seeded run buys is not that equality. The rolling freeze — "a
76
+ # construct added in 0.N cannot be used by `self/` until 0.(N+1)" — is enforced
77
+ # by stage1 being built at all: a `self/` that reaches for something the seed
78
+ # has never heard of does not compile, does not link, and never gets as far as
79
+ # a comparison. That failure is loud here whatever the seed is, and it is the
80
+ # whole of what the seeded run proves about the freeze
81
+ # (docs/wp19-stage0-retirement.md §3, G3).
82
+ #
83
+ # `node tests/self/bootstrap.js` is the same check with the suite's reporting,
84
+ # and is what CI runs; this script is how the compiler gets built for use.
85
+ #
86
+ # The result is a complete compiler: it plans its own output, makes the
87
+ # directories and links through `scripts/build.sh` itself, so nothing has to
88
+ # stand between it and a build (docs/wp14-selfhost.md §7a).
89
+ #
90
+ # Needs a runnable seed, and clang + lld on PATH for the links
91
+ # (docs/INSTALL.md).
92
+ set -euo pipefail
93
+
94
+ # Read the seed before the `cd` below moves us: a relative NISH_BOOTSTRAP is
95
+ # relative to the directory the caller typed it in, not to the repository root.
96
+ seed_given="${NISH_BOOTSTRAP:-}"
97
+ case "$seed_given" in
98
+ ""|/*) ;;
99
+ *) seed_given="$PWD/$seed_given" ;;
100
+ esac
101
+
102
+ cd "$(dirname "$0")/.."
103
+
104
+ out=build/nish
105
+ work=build/selfhost
106
+ profile=speed
107
+ stages=2
108
+ verify=0
109
+ quiet=0
110
+
111
+ usage() {
112
+ cat <<'EOF'
113
+ usage: scripts/bootstrap.sh [-o <exe>] [--stages 1|2|3] [--profile speed|size|debug]
114
+ [--work <dir>] [--verify] [--quiet]
115
+
116
+ Builds the self-hosted compiler. The seed builds stage1, stage1 builds stage2
117
+ (the default output), stage2 builds stage3. --verify compares the IR each stage
118
+ emits for self/ and the stage2/stage3 binaries, byte for byte. The result is
119
+ the command line itself: -o, --link, --profile and the rest.
120
+
121
+ IR(seed) == IR(stage1) asks whether codegen has changed since the seed was
122
+ built, so it is reported and not asserted; IR(stage1) == IR(stage2) and
123
+ stage3 == stage2 are asserted whatever the seed is.
124
+
125
+ The seed is NISH_BOOTSTRAP=<path> when it is set — a released `nish` binary, or
126
+ a .js/.mjs entry point run under node — and build/seed/bin/nish, the release
127
+ scripts/fetch-seed.sh unpacks, when it is not:
128
+
129
+ scripts/bootstrap.sh
130
+ NISH_BOOTSTRAP=~/nish-0.6.0-linux-x86_64 scripts/bootstrap.sh --verify
131
+ EOF
132
+ exit "${1:-2}"
133
+ }
134
+
135
+ while [ $# -gt 0 ]; do
136
+ case "$1" in
137
+ -o|--output) out="${2:-}"; [ -n "$out" ] || usage; shift 2 ;;
138
+ --work) work="${2:-}"; [ -n "$work" ] || usage; shift 2 ;;
139
+ --profile) profile="${2:-}"; [ -n "$profile" ] || usage; shift 2 ;;
140
+ --stages) stages="${2:-}"; shift 2 ;;
141
+ --verify) verify=1; shift ;;
142
+ --quiet) quiet=1; shift ;;
143
+ -h|--help) usage 0 ;;
144
+ *) echo "bootstrap: unknown argument \`$1\`" >&2; usage ;;
145
+ esac
146
+ done
147
+
148
+ case "$stages" in
149
+ 1|2|3) ;;
150
+ *) echo "bootstrap: --stages takes 1, 2 or 3 (got \`$stages\`)" >&2; exit 2 ;;
151
+ esac
152
+ # `--stages` says which compiler to install; verifying the fixed point needs
153
+ # the stage that closes it built as well, whether or not it is the one kept.
154
+ install=$stages
155
+ build_to=$stages
156
+ [ "$verify" -eq 1 ] && build_to=3
157
+
158
+ # The published npm package ships bin/, runtime/, scripts/ and std/ but not
159
+ # self/ -- and, since 0.6.0, no compiler of its own at all -- so say which file
160
+ # is missing rather than failing inside the compiler.
161
+ if [ ! -f self/compile.ts ]; then
162
+ echo "bootstrap: self/compile.ts is missing; run this from a checkout of the repository" >&2
163
+ exit 3
164
+ fi
165
+
166
+ # One place that knows how to invoke the seed, so every stage below reads the
167
+ # same whichever kind of seed ran.
168
+ run_seed() {
169
+ if [ "$seed_kind" = node ]; then
170
+ node "$seed" "$@"
171
+ else
172
+ "$seed" "$@"
173
+ fi
174
+ }
175
+
176
+ seed_die() {
177
+ echo "bootstrap: $seed_origin $seed $1" >&2
178
+ echo "bootstrap: the seed is a released \`nish\` binary, or a .js/.mjs entry point run under node" >&2
179
+ exit 3
180
+ }
181
+
182
+ # Unset: the release scripts/fetch-seed.sh leaves in build/seed, fetched
183
+ # first when it is not there (a no-op, without the network, when it is).
184
+ if [ -z "$seed_given" ]; then
185
+ if ! sh scripts/fetch-seed.sh >&2 || [ ! -e build/seed/bin/nish ]; then
186
+ echo "bootstrap: no seed: NISH_BOOTSTRAP is unset and scripts/fetch-seed.sh could not" >&2
187
+ echo "bootstrap: fetch the last release into build/seed; set NISH_BOOTSTRAP=<nish>" >&2
188
+ exit 3
189
+ fi
190
+ seed_given="$PWD/build/seed/bin/nish"
191
+ seed_origin="the released seed"
192
+ else
193
+ seed_origin="NISH_BOOTSTRAP seed"
194
+ fi
195
+ seed=$seed_given
196
+ # The kind is decided by the extension, and deliberately not by the
197
+ # executable bit or by sniffing the bytes. The bit describes the download
198
+ # rather than the file — a binary unpacked from a release tarball or pulled
199
+ # out of a CI artifact can arrive without +x —
200
+ # whereas the suffix is the one thing whoever built the seed chose. Anything
201
+ # with no suffix is a binary, which is how a released `nish` arrives.
202
+ case "$seed" in
203
+ *.js|*.mjs|*.cjs) seed_kind=node; seed_label="$seed_origin (node $seed)" ;;
204
+ *) seed_kind=native; seed_label="$seed_origin ($seed)" ;;
205
+ esac
206
+ [ -e "$seed" ] || seed_die "does not exist"
207
+ [ -f "$seed" ] || seed_die "is not a file"
208
+ if [ "$seed_kind" = native ]; then
209
+ [ -x "$seed" ] || seed_die "is not executable"
210
+ else
211
+ [ -r "$seed" ] || seed_die "is not readable"
212
+ command -v node >/dev/null 2>&1 || seed_die "needs node on PATH, which is not there"
213
+ fi
214
+ # A seed that cannot answer `--version` cannot compile self/ either — a
215
+ # binary built for another platform, a .js that is not a compiler — and
216
+ # finding that out here names the variable and the path the caller set, where
217
+ # finding it out in the stage1 link names a temporary file three stages deep.
218
+ run_seed --version >/dev/null 2>&1 || seed_die "is not runnable (\`--version\` failed)"
219
+ if ! command -v clang >/dev/null 2>&1 && [ -z "${CC:-}" ]; then
220
+ echo "bootstrap: needs clang on PATH to link the stages (see docs/INSTALL.md)" >&2
221
+ exit 3
222
+ fi
223
+
224
+ say() { [ "$quiet" -eq 1 ] || printf '%s\n' "$*"; }
225
+
226
+ # `a` and `b` hold one `.ll` per module of `self/`. Both the module set and
227
+ # every byte of every module must match: a stage that emitted one module fewer
228
+ # has not agreed about the rest.
229
+ compare_ir() {
230
+ local a="$1" b="$2" label="$3" name
231
+ if ! diff <(cd "$a" && ls ./*.ll) <(cd "$b" && ls ./*.ll) >/dev/null; then
232
+ echo "bootstrap: $label: the two stages emitted different module sets" >&2
233
+ exit 1
234
+ fi
235
+ for name in "$a"/*.ll; do
236
+ if ! cmp -s "$name" "$b/$(basename "$name")"; then
237
+ echo "bootstrap: $label: $(basename "$name") differs" >&2
238
+ diff "$name" "$b/$(basename "$name")" | head -n 20 >&2
239
+ exit 1
240
+ fi
241
+ done
242
+ say " $label: $(ls "$a"/*.ll | wc -l | tr -d ' ') modules identical"
243
+ }
244
+
245
+ # IR(seed) vs IR(stage1), reported and never asserted. What it measures is
246
+ # whether codegen has moved since the seed was built, which is a fact about
247
+ # this release rather than a property of the bootstrap — the header says why.
248
+ # Nothing in here exits.
249
+ survey_ir() {
250
+ local a="$1" b="$2" total=0 differing=0 name
251
+ if ! diff <(cd "$a" && ls ./*.ll) <(cd "$b" && ls ./*.ll) >/dev/null; then
252
+ say " note: IR(seed) vs IR(stage1): the two emitted different module sets."
253
+ say " Not a bootstrap failure: this comparison is not asserted"
254
+ say " (see the header of this script)."
255
+ return 0
256
+ fi
257
+ for name in "$a"/*.ll; do
258
+ total=$((total + 1))
259
+ cmp -s "$name" "$b/$(basename "$name")" || differing=$((differing + 1))
260
+ done
261
+ if [ "$differing" -eq 0 ]; then
262
+ say " note: IR(seed) vs IR(stage1): all $total modules identical."
263
+ say " Not asserted: codegen simply has not moved for self/ since"
264
+ say " the seed was built."
265
+ else
266
+ say " note: IR(seed) vs IR(stage1): $differing of $total modules differ."
267
+ say " Not a bootstrap failure. Codegen has moved for self/ since the"
268
+ say " seed was built, which is what a release carries. See the header"
269
+ say " of this script and docs/wp19-stage0-retirement.md §3, G3."
270
+ fi
271
+ }
272
+
273
+ # Build one stage with another, at a path every stage shares, and move it into
274
+ # place afterwards.
275
+ #
276
+ # The shared path is what makes `stage3 == stage2` hold on Mach-O, and it is
277
+ # measured rather than tidy-mindedness: LC_UUID is stable across two links to
278
+ # one output path and differs on a link to another. Two links of one input to
279
+ # one path produce one UUID and byte-identical files; the same two to `.../one`
280
+ # and `.../two` differ in exactly those sixteen bytes. So stage2 at
281
+ # `$work/stage2` and stage3 at `$work/stage3` gave two different UUIDs to two
282
+ # compilers that had agreed about every other byte, and the comparison could
283
+ # only ever report that as unattributed. The output path is the leading reason
284
+ # and the probe cannot rule out an invocation counter -- docs/wp10-ci.md#ci-matrix
285
+ # has that, the offsets, and the remedy that did not work -- but the shared path
286
+ # was measured to work end to end either way, and build.sh's Darwin branch says
287
+ # why the flag that drops the load command is not the answer.
288
+ #
289
+ # It costs one rename per stage and weakens nothing: the comparison is still a
290
+ # raw `cmp` of the two files.
291
+ #
292
+ # stage1 is left out because nothing compares it to a binary -- `IR(seed) ==
293
+ # IR(stage1)` compares the .ll files its build wrote, and those are the same
294
+ # bytes wherever the executable landed.
295
+ link_stage() {
296
+ local by="$1" name="$2"
297
+ rm -rf "$work/stage" "$work/stage.modules"
298
+ "$by" self/compile.ts --link "$work/stage" --profile "$profile" >/dev/null
299
+ rm -rf "$work/$name" "$work/$name.modules"
300
+ mv "$work/stage" "$work/$name"
301
+ mv "$work/stage.modules" "$work/$name.modules"
302
+ }
303
+
304
+ mkdir -p "$work"
305
+ outdir=$(dirname "$out")
306
+ [ "$outdir" = "" ] || mkdir -p "$outdir"
307
+
308
+ # Every stage is built the same way, by the stage before it, with one
309
+ # `--link`: the compiler plans the output, makes `<exe>.modules/` and runs
310
+ # `scripts/build.sh` itself (docs/wp14-selfhost.md §7a). That leaves each
311
+ # stage's IR in `<exe>.modules/`, which is where the equalities read it, and
312
+ # means the chain exercises the same driver a user does.
313
+ say "stage1: self/ compiled by $seed_label"
314
+ rm -rf "$work/stage1.modules"
315
+ # This is the step the rolling freeze is enforced by, so it says so when it
316
+ # fails rather than leaving a reader with the compiler's own diagnostic and no
317
+ # idea which rule it just met.
318
+ if ! run_seed self/compile.ts --link "$work/stage1" --profile "$profile" >/dev/null; then
319
+ echo "bootstrap: $seed_label could not build stage1 from self/ (its output is above)" >&2
320
+ echo "bootstrap: if it refused the source, that is the freeze doing its job: a construct" >&2
321
+ echo "bootstrap: added in 0.N cannot be used by self/ until 0.(N+1) (docs/wp12-release.md," >&2
322
+ echo "bootstrap: \"The bootstrap seed\"; docs/wp19-stage0-retirement.md §3, G3)" >&2
323
+ exit 1
324
+ fi
325
+
326
+ if [ "$build_to" -ge 2 ]; then
327
+ say "stage2: self/ compiled by stage1"
328
+ link_stage "$work/stage1" stage2
329
+ # The seed equality, reported and not asserted.
330
+ [ "$verify" -eq 1 ] && survey_ir "$work/stage1.modules" "$work/stage2.modules"
331
+ fi
332
+
333
+ if [ "$build_to" -ge 3 ]; then
334
+ say "stage3: self/ compiled by stage2"
335
+ link_stage "$work/stage2" stage3
336
+ [ "$verify" -eq 1 ] && compare_ir "$work/stage2.modules" "$work/stage3.modules" "IR(stage1) == IR(stage2)"
337
+ # `stage3 == stage2`, in scripts/verify-binaries.sh: byte-identical, which
338
+ # both stages being linked at one path is what buys on Mach-O as well. Under
339
+ # that, on Darwin, it still asserts the size, strips debug information where
340
+ # a tool can strip it, and fails anything left over as *unattributed* -- a
341
+ # net now rather than the arm that decides a darwin row. The comparison lives
342
+ # in a script of its own so that tests/run.js can drive every branch of it
343
+ # from one machine -- its header is where the reasoning is, and NISH_UNAME_S
344
+ # is how the suite asks for the other platform's branch.
345
+ if [ "$verify" -eq 1 ]; then
346
+ if verdict="$(bash scripts/verify-binaries.sh "$work/stage2" "$work/stage3" 2>&1)"; then
347
+ printf '%s\n' "$verdict" | while IFS= read -r line; do say " $line"; done
348
+ else
349
+ printf '%s\n' "$verdict" >&2
350
+ exit 1
351
+ fi
352
+ fi
353
+ fi
354
+
355
+ cp "$work/stage$install" "$out"
356
+ say "bootstrap: wrote $out ($(wc -c < "$out" | tr -d ' ') bytes, stage$install, $profile profile)"
357
+ say " it takes -o <file.ll>, -o <dir>/, --link <exe> and --profile"
@@ -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"