@amritk/nish 0.11.0 → 0.13.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.
@@ -33,20 +33,21 @@
33
33
  * runner measured about 1.34x this box on the same suite, so the shape
34
34
  * transfers and the absolute seconds do not.
35
35
  */
36
- import { spawn } from "node:child_process";
36
+ import { spawn } from "node:child_process"
37
37
 
38
- const argv = process.argv.slice(2);
39
- const asJson = argv.includes("--json");
40
- const topAt = argv.indexOf("--top");
41
- const TOP = topAt >= 0 && Number.isFinite(Number(argv[topAt + 1])) ? Number(argv[topAt + 1]) : 40;
38
+ const argv = process.argv.slice(2)
39
+ const asJson = argv.includes("--json")
40
+ const topAt = argv.indexOf("--top")
41
+ const TOP = topAt >= 0 && Number.isFinite(Number(argv[topAt + 1])) ? Number(argv[topAt + 1]) : 40
42
42
 
43
43
  /**
44
44
  * Everything after `--` is the command to profile. The default is the suite as
45
45
  * CI runs it, minus the build: `npm test` would rebuild `dist/` first, and a
46
46
  * `tsc` run is not what this is measuring.
47
47
  */
48
- const dashdash = argv.indexOf("--");
49
- const command = dashdash >= 0 && argv.length > dashdash + 1 ? argv.slice(dashdash + 1) : ["node", "tests/run.js"];
48
+ const dashdash = argv.indexOf("--")
49
+ const command =
50
+ dashdash >= 0 && argv.length > dashdash + 1 ? argv.slice(dashdash + 1) : ["node", "tests/run.js"]
50
51
 
51
52
  /**
52
53
  * One row per line the child printed, with `dt` the gap since the line before
@@ -54,25 +55,26 @@ const command = dashdash >= 0 && argv.length > dashdash + 1 ? argv.slice(dashdas
54
55
  * check in the log, and `t` is kept as well as `dt` because "which minute of
55
56
  * the run was this" is how a reader locates a slow block.
56
57
  */
57
- const rows = [];
58
- const t0 = process.hrtime.bigint();
59
- let last = 0;
60
- let buffered = "";
58
+ const rows = []
59
+ const t0 = process.hrtime.bigint()
60
+ let last = 0
61
+ let buffered = ""
61
62
 
62
63
  const takeLine = (line) => {
63
- const t = Number(process.hrtime.bigint() - t0) / 1e6;
64
- rows.push({ t, dt: t - last, text: line });
65
- last = t;
66
- };
64
+ const t = Number(process.hrtime.bigint() - t0) / 1e6
65
+ rows.push({ t, dt: t - last, text: line })
66
+ last = t
67
+ }
67
68
 
68
69
  const onData = (data) => {
69
- buffered += data;
70
- let i;
71
- while ((i = buffered.indexOf("\n")) >= 0) {
72
- takeLine(buffered.slice(0, i));
73
- buffered = buffered.slice(i + 1);
70
+ buffered += data
71
+ let i = buffered.indexOf("\n")
72
+ while (i >= 0) {
73
+ takeLine(buffered.slice(0, i))
74
+ buffered = buffered.slice(i + 1)
75
+ i = buffered.indexOf("\n")
74
76
  }
75
- };
77
+ }
76
78
 
77
79
  /**
78
80
  * A heartbeat on stderr, because the tables below cannot be printed until the
@@ -82,27 +84,29 @@ const onData = (data) => {
82
84
  * the oracles working or the run wedged.
83
85
  */
84
86
  const heartbeat = setInterval(() => {
85
- const latest = rows.length > 0 ? rows[rows.length - 1].text.slice(0, 72) : "nothing printed yet";
86
- const elapsed = (Number(process.hrtime.bigint() - t0) / 1e9).toFixed(0);
87
- process.stderr.write(`[ci-profile] ${elapsed}s, ${rows.length} lines: ${latest}\n`);
88
- }, 30_000);
89
- heartbeat.unref();
90
-
91
- const child = spawn(command[0], command.slice(1), { stdio: ["ignore", "pipe", "pipe"] });
92
- child.stdout.setEncoding("utf8");
93
- child.stderr.setEncoding("utf8");
94
- child.stdout.on("data", onData);
95
- child.stderr.on("data", onData);
87
+ const latest = rows.length > 0 ? rows[rows.length - 1].text.slice(0, 72) : "nothing printed yet"
88
+ const elapsed = (Number(process.hrtime.bigint() - t0) / 1e9).toFixed(0)
89
+ process.stderr.write(`[ci-profile] ${elapsed}s, ${rows.length} lines: ${latest}\n`)
90
+ }, 30_000)
91
+ heartbeat.unref()
92
+
93
+ const child = spawn(command[0], command.slice(1), { stdio: ["ignore", "pipe", "pipe"] })
94
+ child.stdout.setEncoding("utf8")
95
+ child.stderr.setEncoding("utf8")
96
+ child.stdout.on("data", onData)
97
+ child.stderr.on("data", onData)
96
98
 
97
99
  const status = await new Promise((resolve) => {
98
- child.on("close", (code) => resolve(code ?? 1));
100
+ child.on("close", (code) => resolve(code ?? 1))
99
101
  child.on("error", (err) => {
100
- console.error(`could not run ${command.join(" ")}: ${err.message}`);
101
- resolve(1);
102
- });
103
- });
104
- clearInterval(heartbeat);
105
- if (buffered.length > 0) takeLine(buffered);
102
+ console.error(`could not run ${command.join(" ")}: ${err.message}`)
103
+ resolve(1)
104
+ })
105
+ })
106
+ clearInterval(heartbeat)
107
+ if (buffered.length > 0) {
108
+ takeLine(buffered)
109
+ }
106
110
 
107
111
  /**
108
112
  * A check's name up to its first colon, which is how this suite names a family:
@@ -112,23 +116,23 @@ if (buffered.length > 0) takeLine(buffered);
112
116
  * keep the oracles — each of which prints one long line — apart.
113
117
  */
114
118
  const familyOf = (text) => {
115
- const body = text.replace(/^(PASS|FAIL|SKIP)\s+/, "");
116
- const colon = body.indexOf(":");
117
- return colon > 0 ? body.slice(0, colon) : body.slice(0, 40);
118
- };
119
+ const body = text.replace(/^(PASS|FAIL|SKIP)\s+/, "")
120
+ const colon = body.indexOf(":")
121
+ return colon > 0 ? body.slice(0, colon) : body.slice(0, 40)
122
+ }
119
123
 
120
- const families = new Map();
124
+ const families = new Map()
121
125
  for (const row of rows) {
122
- const key = familyOf(row.text);
123
- const seen = families.get(key) ?? { ms: 0, lines: 0 };
124
- seen.ms += row.dt;
125
- seen.lines += 1;
126
- families.set(key, seen);
126
+ const key = familyOf(row.text)
127
+ const seen = families.get(key) ?? { ms: 0, lines: 0 }
128
+ seen.ms += row.dt
129
+ seen.lines += 1
130
+ families.set(key, seen)
127
131
  }
128
132
 
129
- const total = rows.length > 0 ? rows[rows.length - 1].t : 0;
130
- const byCost = [...rows].sort((a, b) => b.dt - a.dt).slice(0, TOP);
131
- const byFamily = [...families].sort((a, b) => b[1].ms - a[1].ms).slice(0, TOP);
133
+ const total = rows.length > 0 ? rows[rows.length - 1].t : 0
134
+ const byCost = [...rows].sort((a, b) => b.dt - a.dt).slice(0, TOP)
135
+ const byFamily = [...families].sort((a, b) => b[1].ms - a[1].ms).slice(0, TOP)
132
136
 
133
137
  if (asJson) {
134
138
  console.log(
@@ -144,14 +148,14 @@ if (asJson) {
144
148
  null,
145
149
  2
146
150
  )
147
- );
148
- process.exit(status);
151
+ )
152
+ process.exit(status)
149
153
  }
150
154
 
151
- const secs = (ms) => `${(ms / 1000).toFixed(1)}s`.padStart(8);
155
+ const secs = (ms) => `${(ms / 1000).toFixed(1)}s`.padStart(8)
152
156
 
153
- console.log(`\n${command.join(" ")}`);
154
- console.log(`exit ${status} — ${secs(total).trim()} of wall clock over ${rows.length} printed lines\n`);
157
+ console.log(`\n${command.join(" ")}`)
158
+ console.log(`exit ${status} — ${secs(total).trim()} of wall clock over ${rows.length} printed lines\n`)
155
159
 
156
160
  /**
157
161
  * The run's own verdict, echoed verbatim.
@@ -163,25 +167,34 @@ console.log(`exit ${status} — ${secs(total).trim()} of wall clock over ${rows.
163
167
  * the exit status the thing to do). So the summary is printed before the
164
168
  * timings, where it cannot be missed.
165
169
  */
166
- const verdict = rows.filter((r) => r.text.trim().length > 0).slice(-3);
170
+ const verdict = rows.filter((r) => r.text.trim().length > 0).slice(-3)
167
171
  if (verdict.length > 0) {
168
- console.log("=== what the run itself reported ===");
169
- for (const row of verdict) console.log(` ${row.text}`);
170
- console.log("");
172
+ console.log("=== what the run itself reported ===")
173
+ for (const row of verdict) {
174
+ console.log(` ${row.text}`)
175
+ }
176
+ console.log("")
171
177
  }
172
178
 
173
- console.log(`=== the ${byCost.length} most expensive checks (cost = the gap before the line was printed) ===`);
174
- for (const row of byCost) console.log(`${secs(row.dt)} at ${secs(row.t)} ${row.text.slice(0, 104)}`);
179
+ console.log(`=== the ${byCost.length} most expensive checks (cost = the gap before the line was printed) ===`)
180
+ for (const row of byCost) {
181
+ console.log(`${secs(row.dt)} at ${secs(row.t)} ${row.text.slice(0, 104)}`)
182
+ }
175
183
 
176
- console.log(`\n=== the ${byFamily.length} most expensive families (cumulative) ===`);
177
- for (const [name, f] of byFamily) console.log(`${secs(f.ms)} ${String(f.lines).padStart(5)} line(s) ${name.slice(0, 84)}`);
184
+ console.log(`\n=== the ${byFamily.length} most expensive families (cumulative) ===`)
185
+ for (const [name, f] of byFamily) {
186
+ console.log(`${secs(f.ms)} ${String(f.lines).padStart(5)} line(s) ${name.slice(0, 84)}`)
187
+ }
178
188
 
179
189
  /**
180
190
  * The share the expensive tail accounts for, because "the top ten are 70% of
181
191
  * the run" is the sentence that decides whether to optimise a check or the
182
192
  * shape of the job around it.
183
193
  */
184
- const topTen = [...rows].sort((a, b) => b.dt - a.dt).slice(0, 10).reduce((sum, r) => sum + r.dt, 0);
185
- console.log(`\nthe ten most expensive checks are ${((topTen / total) * 100).toFixed(0)}% of the run.`);
194
+ const topTen = [...rows]
195
+ .sort((a, b) => b.dt - a.dt)
196
+ .slice(0, 10)
197
+ .reduce((sum, r) => sum + r.dt, 0)
198
+ console.log(`\nthe ten most expensive checks are ${((topTen / total) * 100).toFixed(0)}% of the run.`)
186
199
 
187
- process.exit(status);
200
+ process.exit(status)
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * The diagnostic-code registry, read back out of a `codes.ts`.
3
3
  *
4
- * `self/codes.ts` holds the table -- a fragment line, then the `NL####` line
4
+ * `src/codes.ts` holds the table -- a fragment line, then the `NL####` line
5
5
  * that names its rule. Three places in this repository read it back:
6
6
  * `scripts/gen-diagnostic-codes.mjs`, which checks the registry's shape, its
7
- * order and that no number is used twice; `tests/diagnostic_coverage.js`,
7
+ * order and that no number is used twice; `tests/diagnostic-coverage.js`,
8
8
  * which asks which codes the suite reaches; and the `codes:` checks in
9
9
  * `tests/run.js`. This module is that parse, once, so the copies cannot drift
10
10
  * apart again ([issue #96](https://github.com/amritk/nish/issues/96)).
@@ -22,7 +22,7 @@
22
22
  * Two properties are the whole point of having it, and both are here because
23
23
  * they have failed:
24
24
  *
25
- * - **The indentation is not part of the contract.** `self/codes.ts` lost a
25
+ * - **The indentation is not part of the contract.** `src/codes.ts` lost a
26
26
  * level when WP22 stage C rewrote its tables as arrows with concise
27
27
  * bodies, and every reader keyed on four literal spaces then read it as
28
28
  * *empty* rather than as changed. `^\s+` is what a pair is recognised by.
@@ -31,17 +31,29 @@
31
31
  * codes" the same answer, which is what let the first instance of this
32
32
  * survive unnoticed. This one raises instead.
33
33
  */
34
- import fs from "node:fs";
35
- import path from "node:path";
34
+ import fs from "node:fs"
35
+ import path from "node:path"
36
36
 
37
- const root = path.resolve(import.meta.dirname, "..");
37
+ const root = path.resolve(import.meta.dirname, "..")
38
+
39
+ /**
40
+ * One string literal in either quote: the formatter writes a fragment that
41
+ * holds a `"` with single quotes, because that needs no escape.
42
+ */
43
+ export const STRING_LITERAL = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g
38
44
 
39
45
  /**
40
46
  * One entry of the emitted table: the quoted fragment on its own line, then
41
47
  * the code on the next. Anchored to the line rather than to a column, for the
42
48
  * reason in the header.
43
49
  */
44
- const PAIR = /^\s+("(?:[^"\\]|\\.)*"),\n\s+"(NL\d{4})",$/gm;
50
+ const PAIR = new RegExp(`^\\s+(${STRING_LITERAL.source}),\\n\\s+"(NL\\d{4})",$`, "gm")
51
+
52
+ /** The text of a string literal in either quote, as JavaScript reads it. */
53
+ const unquote = (literal) =>
54
+ literal.startsWith("'")
55
+ ? JSON.parse(`"${literal.slice(1, -1).replaceAll("\\'", "'").replaceAll('"', '\\"')}"`)
56
+ : JSON.parse(literal)
45
57
 
46
58
  /**
47
59
  * Every `{ fragment, code }` of one registry file, in the order the file holds
@@ -58,16 +70,16 @@ const PAIR = /^\s+("(?:[^"\\]|\\.)*"),\n\s+"(NL\d{4})",$/gm;
58
70
  * message names the text by.
59
71
  */
60
72
  export const parseCodesRegistry = (text, label) => {
61
- const pairs = [];
73
+ const pairs = []
62
74
  for (const m of text.matchAll(PAIR)) {
63
- pairs.push({ fragment: JSON.parse(m[1]), code: m[2] });
75
+ pairs.push({ fragment: unquote(m[1]), code: m[2] })
64
76
  }
65
77
  if (pairs.length === 0) {
66
- throw new Error(`${label}: no diagnostic codes parsed -- the registry's shape has moved`);
78
+ throw new Error(`${label}: no diagnostic codes parsed -- the registry's shape has moved`)
67
79
  }
68
- return pairs;
69
- };
80
+ return pairs
81
+ }
70
82
 
71
83
  /** The same, for a registry on disk. The three callers all read a file. */
72
84
  export const readCodesRegistry = (file) =>
73
- parseCodesRegistry(fs.readFileSync(file, "utf8"), path.relative(root, file));
85
+ parseCodesRegistry(fs.readFileSync(file, "utf8"), path.relative(root, file))
@@ -10,13 +10,14 @@
10
10
  * with one line broken to show that each rule below is really enforced.
11
11
  *
12
12
  * **The registry is kept by hand now.** It used to be generated: this script
13
- * scanned `src/` for every diagnostic message, cut each at its interpolations,
14
- * and wrote the same table into `src/codes.ts` and `self/codes.ts`. That scan
13
+ * scanned stage0's `src/` for every diagnostic message, cut each at its interpolations,
14
+ * and wrote the same table into stage0's `src/codes.ts` and this compiler's `self/codes.ts`
15
+ * (now `src/codes.ts`). That scan
15
16
  * read stage0's source, and stage0 is deleted (wp19 §5 R6), so the generator
16
- * was frozen with the table it last wrote and `self/codes.ts` became the
17
+ * was frozen with the table it last wrote and `src/codes.ts` became the
17
18
  * registry. A new diagnostic gets its code by hand: append a fragment and the
18
19
  * next free number in its band (the second mode above prints those), at the
19
- * position the ordering rule below puts it. `tests/diagnostic_coverage.js`
20
+ * position the ordering rule below puts it. `tests/diagnostic-coverage.js`
20
21
  * is what notices a diagnostic that has no code, by counting `NL0000`.
21
22
  *
22
23
  * What `--check` still holds, because each is what makes a code worth keying
@@ -39,7 +40,7 @@
39
40
  * - **No fragment too short to identify a rule** (ten characters, trimmed),
40
41
  * which would match half the suite.
41
42
  * - **Every string in a table is half of a pair.** `codeFor` in
42
- * `self/codes.ts` reads each table as one flat array and steps through it
43
+ * `src/codes.ts` reads each table as one flat array and steps through it
43
44
  * by two, so a fragment added without its code line -- or a code without
44
45
  * its fragment -- shifts every pairing after it, and from there on
45
46
  * messages get their neighbour's code. The pair reader cannot see that:
@@ -57,32 +58,36 @@
57
58
  * before the name, never the name, so `branding.ts` stays the only place it is
58
59
  * spelled (rule 5 in `.claude/orientation.md`). Keep to that when adding one.
59
60
  */
60
- import fs from "node:fs";
61
- import path from "node:path";
62
- import { fileURLToPath } from "node:url";
63
- import { parseCodesRegistry } from "./codes-registry.js";
61
+ import fs from "node:fs"
62
+ import path from "node:path"
63
+ import { fileURLToPath } from "node:url"
64
+ import { parseCodesRegistry, STRING_LITERAL } from "./codes-registry.js"
64
65
 
65
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
66
- const REGISTRY = process.argv.slice(2).find((arg) => !arg.startsWith("--")) ?? path.join(ROOT, "self", "codes.ts");
66
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
67
+ const REGISTRY =
68
+ process.argv.slice(2).find((arg) => !arg.startsWith("--")) ?? path.join(ROOT, "src", "codes.ts")
67
69
 
68
70
  /** The bands a table entry may use. Band 0 is constants, never a table row. */
69
- const BANDS = new Set(["1", "2", "3", "4", "9"]);
71
+ const BANDS = new Set(["1", "2", "3", "4", "9"])
70
72
 
71
73
  /** Shorter than this, a fragment would match half the suite. */
72
- const MIN_FRAGMENT = 10;
74
+ const MIN_FRAGMENT = 10
73
75
 
74
76
  /**
75
- * The text of one table in `self/codes.ts`: from its `name = (): string[] => [`
77
+ * The text of one table in `src/codes.ts`: from its `name = (): string[] => [`
76
78
  * to the `];` that closes it. Parsed per table, because the table a fragment
77
79
  * sits in is part of what it means -- a performance rule is matched only
78
80
  * against a performance message.
79
81
  */
80
82
  const tableText = (text, name) => {
81
- const open = text.indexOf(`export const ${name} = (): string[] => [`);
82
- if (open < 0) return null;
83
- const close = text.indexOf("\n];", open);
84
- return close < 0 ? null : text.slice(open, close + 1);
85
- };
83
+ const header = new RegExp(`^(?:export )?const ${name} = \\(\\): string\\[\\] => \\[`, "m").exec(text)
84
+ const open = header === null ? -1 : header.index
85
+ if (open < 0) {
86
+ return null
87
+ }
88
+ const close = text.indexOf("\n]", open)
89
+ return close < 0 ? null : text.slice(open, close + 1)
90
+ }
86
91
 
87
92
  /**
88
93
  * How many strings one table holds: every literal after the line that opens
@@ -90,67 +95,68 @@ const tableText = (text, name) => {
90
95
  * its own rather than read off the pairs -- a stray literal is exactly what
91
96
  * the pair reader skips.
92
97
  */
93
- const STRING = /"(?:[^"\\]|\\.)*"/g;
94
- const literalCount = (body) => body.slice(body.indexOf("\n")).match(STRING)?.length ?? 0;
98
+ const literalCount = (body) => body.slice(body.indexOf("\n")).match(STRING_LITERAL)?.length ?? 0
95
99
 
96
100
  /** The order the compilers rely on: longest fragment first, then `localeCompare`. */
97
- const inOrder = (a, b) => b.fragment.length - a.fragment.length || a.fragment.localeCompare(b.fragment);
101
+ const inOrder = (a, b) => b.fragment.length - a.fragment.length || a.fragment.localeCompare(b.fragment)
98
102
 
99
- /** Every rule of `self/codes.ts` it breaks, as a line each; empty when it holds. */
103
+ /** Every rule of `src/codes.ts` it breaks, as a line each; empty when it holds. */
100
104
  const problems = (text) => {
101
- const found = [];
102
- const tables = [];
105
+ const found = []
106
+ const tables = []
103
107
  for (const [name, perf] of [
104
108
  ["diagnosticRules", false],
105
109
  ["performanceRules", true],
106
110
  ]) {
107
- const body = tableText(text, name);
111
+ const body = tableText(text, name)
108
112
  if (body === null) {
109
- found.push(`self/codes.ts has no \`${name}\` table`);
110
- continue;
113
+ found.push(`src/codes.ts has no \`${name}\` table`)
114
+ continue
111
115
  }
112
116
  try {
113
- const pairs = parseCodesRegistry(body, `self/codes.ts ${name}`);
114
- const literals = literalCount(body);
117
+ const pairs = parseCodesRegistry(body, `src/codes.ts ${name}`)
118
+ const literals = literalCount(body)
115
119
  if (literals !== 2 * pairs.length) {
116
120
  found.push(
117
121
  `\`${name}\` holds ${literals} strings and ${pairs.length} fragment/code pairs: a string ` +
118
122
  "without its other half shifts every later pairing `codeFor` makes"
119
- );
123
+ )
120
124
  }
121
- tables.push({ name, perf, pairs });
125
+ tables.push({ name, perf, pairs })
122
126
  } catch (err) {
123
- found.push(err.message);
127
+ found.push(err.message)
124
128
  }
125
129
  }
126
- const all = tables.flatMap((t) => t.pairs);
130
+ const all = tables.flatMap((t) => t.pairs)
127
131
 
128
132
  // The tables are the whole registry: a pair the reader finds outside them is
129
133
  // one the compilers never match, and one of theirs the reader misses is one
130
- // `tests/diagnostic_coverage.js` never asks about.
131
- let whole = [];
134
+ // `tests/diagnostic-coverage.js` never asks about.
135
+ let whole = []
132
136
  try {
133
- whole = parseCodesRegistry(text, "self/codes.ts");
137
+ whole = parseCodesRegistry(text, "src/codes.ts")
134
138
  } catch (err) {
135
- found.push(err.message);
139
+ found.push(err.message)
136
140
  }
137
141
  if (whole.length !== all.length) {
138
- found.push(`self/codes.ts holds ${whole.length} pairs, ${all.length} of them inside the two tables`);
142
+ found.push(`src/codes.ts holds ${whole.length} pairs, ${all.length} of them inside the two tables`)
139
143
  }
140
144
 
141
145
  for (const { name, perf, pairs } of tables) {
142
146
  for (let i = 0; i < pairs.length; i++) {
143
- const { fragment, code } = pairs[i];
144
- const band = code[2];
145
- if (!BANDS.has(band)) found.push(`${code} is not in a table band (1, 2, 3, 4 or 9): ${JSON.stringify(fragment)}`);
147
+ const { fragment, code } = pairs[i]
148
+ const band = code[2]
149
+ if (!BANDS.has(band)) {
150
+ found.push(`${code} is not in a table band (1, 2, 3, 4 or 9): ${JSON.stringify(fragment)}`)
151
+ }
146
152
  if (perf !== (band === "9")) {
147
- found.push(`${code} is in \`${name}\`, which holds ${perf ? "only" : "no"} NL9xxx codes`);
153
+ found.push(`${code} is in \`${name}\`, which holds ${perf ? "only" : "no"} NL9xxx codes`)
148
154
  }
149
155
  if (fragment.trim().length < MIN_FRAGMENT) {
150
- found.push(`${code}'s fragment ${JSON.stringify(fragment)} is under ${MIN_FRAGMENT} characters`);
156
+ found.push(`${code}'s fragment ${JSON.stringify(fragment)} is under ${MIN_FRAGMENT} characters`)
151
157
  }
152
158
  if (i > 0 && inOrder(pairs[i - 1], pairs[i]) > 0) {
153
- found.push(`${code} is out of order in \`${name}\`: it has to come before ${pairs[i - 1].code}`);
159
+ found.push(`${code} is out of order in \`${name}\`: it has to come before ${pairs[i - 1].code}`)
154
160
  }
155
161
  }
156
162
  }
@@ -158,42 +164,53 @@ const problems = (text) => {
158
164
  const perfCodes = tables
159
165
  .filter((t) => t.perf)
160
166
  .flatMap((t) => t.pairs.map((p) => Number(p.code.slice(3))))
161
- .sort((a, b) => a - b);
162
- const gap = perfCodes.findIndex((n, i) => n !== i + 1);
167
+ .sort((a, b) => a - b)
168
+ const gap = perfCodes.findIndex((n, i) => n !== i + 1)
163
169
  if (gap >= 0) {
164
- found.push(`\`performanceRules\` has no NL${9000 + gap + 1} in its place: its codes run from NL9001 with no gap`);
170
+ found.push(
171
+ `\`performanceRules\` has no NL${9000 + gap + 1} in its place: its codes run from NL9001 with no gap`
172
+ )
165
173
  }
166
174
 
167
175
  const seen = (key) => {
168
- const counts = new Map();
169
- for (const pair of all) counts.set(pair[key], (counts.get(pair[key]) ?? 0) + 1);
170
- return [...counts].filter(([, n]) => n > 1).map(([value]) => value);
171
- };
172
- for (const code of seen("code")) found.push(`${code} names two rules`);
173
- for (const fragment of seen("fragment")) found.push(`${JSON.stringify(fragment)} has two entries`);
176
+ const counts = new Map()
177
+ for (const pair of all) {
178
+ counts.set(pair[key], (counts.get(pair[key]) ?? 0) + 1)
179
+ }
180
+ return [...counts].filter(([, n]) => n > 1).map(([value]) => value)
181
+ }
182
+ for (const code of seen("code")) {
183
+ found.push(`${code} names two rules`)
184
+ }
185
+ for (const fragment of seen("fragment")) {
186
+ found.push(`${JSON.stringify(fragment)} has two entries`)
187
+ }
174
188
 
175
- const count = /export const RULE_COUNT: i32 = (\d+);/.exec(text);
176
- if (count === null) found.push("self/codes.ts has no `RULE_COUNT`");
177
- else if (Number(count[1]) !== all.length) {
178
- found.push(`RULE_COUNT is ${count[1]} and the tables hold ${all.length} rules`);
189
+ const count = /export const RULE_COUNT: i32 = (\d+);?$/m.exec(text)
190
+ if (count === null) {
191
+ found.push("src/codes.ts has no `RULE_COUNT`")
192
+ } else if (Number(count[1]) !== all.length) {
193
+ found.push(`RULE_COUNT is ${count[1]} and the tables hold ${all.length} rules`)
179
194
  }
180
195
 
181
- return { found, all };
182
- };
196
+ return { found, all }
197
+ }
183
198
 
184
199
  /** The next number nobody has held, per band -- what a hand-added rule takes. */
185
200
  const nextFree = (pairs) => {
186
- const highest = new Map();
201
+ const highest = new Map()
187
202
  for (const { code } of pairs) {
188
- const band = code[2];
189
- highest.set(band, Math.max(highest.get(band) ?? 0, Number(code.slice(3))));
203
+ const band = code[2]
204
+ highest.set(band, Math.max(highest.get(band) ?? 0, Number(code.slice(3))))
190
205
  }
191
- return [...BANDS].map((band) => `NL${band}${String((highest.get(band) ?? 0) + 1).padStart(3, "0")}`);
192
- };
206
+ return [...BANDS].map((band) => `NL${band}${String((highest.get(band) ?? 0) + 1).padStart(3, "0")}`)
207
+ }
193
208
 
194
- const { found, all } = problems(fs.readFileSync(REGISTRY, "utf8"));
195
- for (const problem of found) console.error(`error: ${problem}`);
209
+ const { found, all } = problems(fs.readFileSync(REGISTRY, "utf8"))
210
+ for (const problem of found) {
211
+ console.error(`error: ${problem}`)
212
+ }
196
213
  if (found.length === 0 && !process.argv.includes("--check")) {
197
- console.log(`self/codes.ts: ${all.length} rules, well-formed; next free: ${nextFree(all).join(" ")}`);
214
+ console.log(`src/codes.ts: ${all.length} rules, well-formed; next free: ${nextFree(all).join(" ")}`)
198
215
  }
199
- process.exit(found.length > 0 ? 1 : 0);
216
+ process.exit(found.length > 0 ? 1 : 0)
@@ -1,3 +1,4 @@
1
+ # shellcheck shell=bash
1
2
  # Sourced, not run: how a shell script turns a compiler path into a command.
2
3
  #
3
4
  # . scripts/nish-compiler.sh
@@ -9,6 +10,7 @@
9
10
  # scripts/bootstrap.sh and tests/self/seed.js follows as `NODE_ENTRY`, so one
10
11
  # path names a compiler the same way to every tool. Bash, because the answer is
11
12
  # an array: a compiler path with a space in it stays one word.
13
+ # shellcheck disable=SC2034 # `compiler` is the answer, read by the script that sourced this
12
14
  nish_compiler() {
13
15
  case "$1" in
14
16
  *.js | *.mjs | *.cjs) compiler=(node "$1") ;;
@@ -22,41 +22,41 @@
22
22
  * say so when none was installed; two packages claiming one command name would
23
23
  * leave which binary wins up to npm's link order.
24
24
  */
25
- import fs from "node:fs";
26
- import path from "node:path";
27
- import { targetForAsset } from "../bin/packaging.js";
25
+ import fs from "node:fs"
26
+ import path from "node:path"
27
+ import { targetForAsset } from "../bin/packaging.js"
28
28
 
29
- const args = process.argv.slice(2);
29
+ const args = process.argv.slice(2)
30
30
  const flag = (name) => {
31
- const i = args.indexOf(name);
32
- return i < 0 ? null : args[i + 1];
33
- };
34
- const positional = args.filter((a, i) => !a.startsWith("--") && !(args[i - 1] ?? "").startsWith("--"));
35
- const [stageDir, asset] = positional;
31
+ const i = args.indexOf(name)
32
+ return i < 0 ? null : args[i + 1]
33
+ }
34
+ const positional = args.filter((a, i) => !a.startsWith("--") && !(args[i - 1] ?? "").startsWith("--"))
35
+ const [stageDir, asset] = positional
36
36
 
37
37
  if (stageDir === undefined || asset === undefined) {
38
- console.error("usage: platform-package.mjs <stage-dir> <asset> [--version <v>]");
39
- process.exit(2);
38
+ console.error("usage: platform-package.mjs <stage-dir> <asset> [--version <v>]")
39
+ process.exit(2)
40
40
  }
41
41
 
42
- const root = path.resolve(import.meta.dirname, "..");
43
- const main = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
44
- const version = flag("--version") ?? main.version;
42
+ const root = path.resolve(import.meta.dirname, "..")
43
+ const main = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"))
44
+ const version = flag("--version") ?? main.version
45
45
 
46
- const target = targetForAsset(asset);
46
+ const target = targetForAsset(asset)
47
47
  if (target === null) {
48
- console.error(`platform-package.mjs: ${asset} is not a platform this project builds for`);
49
- process.exit(1);
48
+ console.error(`platform-package.mjs: ${asset} is not a platform this project builds for`)
49
+ process.exit(1)
50
50
  }
51
51
 
52
- const binary = path.join(stageDir, "bin", "nish");
52
+ const binary = path.join(stageDir, "bin", "nish")
53
53
  if (!fs.existsSync(binary)) {
54
- console.error(`platform-package.mjs: ${binary} does not exist; stage the binary before packaging it`);
55
- process.exit(1);
54
+ console.error(`platform-package.mjs: ${binary} does not exist; stage the binary before packaging it`)
55
+ process.exit(1)
56
56
  }
57
57
 
58
- const pretty = { linux: "Linux", darwin: "macOS" }[target.os] ?? target.os;
59
- const chip = { x64: "x86_64", arm64: "ARM64" }[target.cpu] ?? target.cpu;
58
+ const pretty = { linux: "Linux", darwin: "macOS" }[target.os] ?? target.os
59
+ const chip = { x64: "x86_64", arm64: "ARM64" }[target.cpu] ?? target.cpu
60
60
 
61
61
  const manifest = {
62
62
  name: `${main.name}-${asset}`,
@@ -84,8 +84,8 @@ const manifest = {
84
84
  repository: main.repository,
85
85
  bugs: main.bugs,
86
86
  homepage: main.homepage,
87
- };
87
+ }
88
88
 
89
- const out = path.join(stageDir, "package.json");
90
- fs.writeFileSync(out, `${JSON.stringify(manifest, null, 2)}\n`);
91
- console.log(`${manifest.name}@${version} (os ${target.os}, cpu ${target.cpu}) -> ${out}`);
89
+ const out = path.join(stageDir, "package.json")
90
+ fs.writeFileSync(out, `${JSON.stringify(manifest, null, 2)}\n`)
91
+ console.log(`${manifest.name}@${version} (os ${target.os}, cpu ${target.cpu}) -> ${out}`)