@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.
- package/README.md +43 -23
- package/bin/launcher.js +45 -41
- package/bin/packaging.js +39 -33
- package/docs/AI.md +135 -11
- package/docs/INSTALL.md +29 -13
- package/package.json +10 -6
- package/runtime/nish.d.ts +22 -1
- package/runtime/nish.h +31 -11
- package/runtime/nish.mjs +16 -3
- package/runtime/{runtime_os.c → runtime-os.c} +1 -1
- package/runtime/{runtime_parallel.c → runtime-parallel.c} +112 -4
- package/runtime/{runtime_wasm.c → runtime-wasm.c} +6 -1
- package/runtime/runtime.c +5 -5
- package/runtime/shim.mjs +6 -6
- package/scripts/bootstrap.sh +29 -29
- package/scripts/build.sh +14 -9
- package/scripts/changelog-gen.mjs +260 -192
- package/scripts/ci-profile.mjs +81 -68
- package/scripts/codes-registry.js +25 -13
- package/scripts/gen-diagnostic-codes.mjs +86 -69
- package/scripts/nish-compiler.sh +2 -0
- package/scripts/platform-package.mjs +26 -26
- package/scripts/postinstall.mjs +46 -36
- package/scripts/size-report.sh +3 -3
- package/scripts/smoke.sh +1 -1
- package/std/README.md +16 -11
- package/std/collections.ts +221 -178
- package/std/json.ts +136 -136
- package/std/map.ts +39 -0
- package/std/pair.ts +2 -2
- package/std/testing.ts +67 -67
- package/std/text.ts +54 -54
- package/std/threads.ts +126 -38
package/scripts/ci-profile.mjs
CHANGED
|
@@ -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 =
|
|
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 (
|
|
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)
|
|
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)
|
|
170
|
-
|
|
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)
|
|
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)
|
|
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]
|
|
185
|
-
|
|
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
|
-
* `
|
|
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/
|
|
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.** `
|
|
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 =
|
|
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:
|
|
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
|
|
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 `
|
|
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/
|
|
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
|
-
* `
|
|
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 =
|
|
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 `
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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 `
|
|
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(`
|
|
110
|
-
continue
|
|
113
|
+
found.push(`src/codes.ts has no \`${name}\` table`)
|
|
114
|
+
continue
|
|
111
115
|
}
|
|
112
116
|
try {
|
|
113
|
-
const pairs = parseCodesRegistry(body, `
|
|
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/
|
|
131
|
-
let whole = []
|
|
134
|
+
// `tests/diagnostic-coverage.js` never asks about.
|
|
135
|
+
let whole = []
|
|
132
136
|
try {
|
|
133
|
-
whole = parseCodesRegistry(text, "
|
|
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(`
|
|
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))
|
|
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(
|
|
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)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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+)
|
|
176
|
-
if (count === null)
|
|
177
|
-
|
|
178
|
-
|
|
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)
|
|
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(`
|
|
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)
|
package/scripts/nish-compiler.sh
CHANGED
|
@@ -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}`)
|