@amritk/nish-x86_64-linux 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +468 -0
- package/LICENSE +21 -0
- package/bin/nish +0 -0
- package/package.json +31 -0
- package/runtime/nish.d.ts +290 -0
- package/runtime/nish.h +340 -0
- package/runtime/nish.mjs +143 -0
- package/runtime/runtime.c +1184 -0
- package/runtime/runtime_os.c +351 -0
- package/runtime/runtime_parallel.c +156 -0
- package/runtime/runtime_wasm.c +99 -0
- package/runtime/shim.mjs +672 -0
- package/scripts/build.sh +279 -0
- package/std/README.md +185 -0
- package/std/json.ts +402 -0
- package/std/pair.ts +28 -0
- package/std/testing.ts +347 -0
- package/std/text.ts +193 -0
package/std/testing.ts
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `std/testing` — a test runner for Nish programs, written in Nish.
|
|
3
|
+
*
|
|
4
|
+
* The repository's own suite is `tests/run.js`, a Node program: it compiles the
|
|
5
|
+
* corpus, assembles it, links it and diffs the result against a golden, so it
|
|
6
|
+
* has to be able to spawn a compiler and read a directory. This module answers
|
|
7
|
+
* the other half of the question — a *program* that checks its own behaviour
|
|
8
|
+
* and reports it — and it answers it in the language, with no Node anywhere.
|
|
9
|
+
*
|
|
10
|
+
* Five language rules shape the API, and each of them removes a shape that a
|
|
11
|
+
* JavaScript test framework would have reached for first:
|
|
12
|
+
*
|
|
13
|
+
* 1. **A function is not a value** ([LANGUAGE.md](../docs/LANGUAGE.md) §
|
|
14
|
+
* Functions), so there is no `test("name", () => { ... })`. A suite is an
|
|
15
|
+
* object the test program drives with straight-line calls, and the test
|
|
16
|
+
* names are arguments rather than closures. This is not a workaround: the
|
|
17
|
+
* whole-program pass cannot prove purity, termination or escape facts
|
|
18
|
+
* through an unknown callee, which is what keeps function types out of the
|
|
19
|
+
* language in the first place.
|
|
20
|
+
* 2. **There is no `try` / `catch`**, so an assertion cannot throw and be
|
|
21
|
+
* collected. Each one records its outcome and returns it, and `done`
|
|
22
|
+
* turns the tally into the process exit code.
|
|
23
|
+
* 3. **Every assertion answers a `boolean`** for the reason that follows from
|
|
24
|
+
* 2: an out-of-range index and an empty `pop` *panic*, which ends the
|
|
25
|
+
* process, so "check the length, then read the element" has to be a real
|
|
26
|
+
* branch — `if (!suite.eqI32("len", a.length, 3)) { return suite.done(); }`
|
|
27
|
+
* is how a test stops before the read that would abort the whole run.
|
|
28
|
+
* 4. **There are no generics**, so there is one assertion per type rather
|
|
29
|
+
* than one `eq` over all of them. The alternative — a single `eq` taking
|
|
30
|
+
* strings and asking every caller to interpolate — would report
|
|
31
|
+
* `expected "3", got "4"` for two numbers and lose the type in the
|
|
32
|
+
* message.
|
|
33
|
+
* 5. **There are no optional or default parameters**, so every assertion
|
|
34
|
+
* takes its name and its detail in full.
|
|
35
|
+
*
|
|
36
|
+
* The output is deliberately the shape `tests/run.js` prints — `PASS <name>`,
|
|
37
|
+
* `FAIL <name>` with an indented detail line, `SKIP <name> (<reason>)`, and a
|
|
38
|
+
* summary that ends `N passed, M failed, K skipped` — so a Nish test program
|
|
39
|
+
* reads like the rest of the suite and a skip is counted rather than merely
|
|
40
|
+
* mentioned. A green run that skipped half its checks should look different
|
|
41
|
+
* from one that proved everything.
|
|
42
|
+
*
|
|
43
|
+
* The widths are spelled explicitly (`i32`, `i64`, `f64`) rather than as
|
|
44
|
+
* `number`, so the module means the same thing under `--number-mode f64` as it
|
|
45
|
+
* does by default, exactly as `examples/arrays.ts` does — and every length this
|
|
46
|
+
* module reads from a builtin is converted with `toI32`, because `.length`
|
|
47
|
+
* answers `number` and spelling one's own widths is therefore necessary and not
|
|
48
|
+
* sufficient (`docs/wp26-stdlib.md` §4). Without the conversion the two
|
|
49
|
+
* comparisons against a length below would lower to `fcmp` on an `f64` in that
|
|
50
|
+
* mode and to `icmp` on an `i32` here, which is the same module compiling to
|
|
51
|
+
* two different programs.
|
|
52
|
+
*
|
|
53
|
+
* import { Suite } from "../std/testing";
|
|
54
|
+
*
|
|
55
|
+
* export const main = (): number => {
|
|
56
|
+
* const t = new Suite("arrays");
|
|
57
|
+
* const a: i32[] = [1, 2, 3];
|
|
58
|
+
* if (!t.eqI32("length", a.length, 3)) {
|
|
59
|
+
* return t.done(); // a[2] below would panic, not fail
|
|
60
|
+
* }
|
|
61
|
+
* t.eqI32("last", a[2], 3);
|
|
62
|
+
* return t.done(); // 0 when nothing failed, 1 otherwise
|
|
63
|
+
* };
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* How much of a haystack a failure detail quotes.
|
|
68
|
+
*
|
|
69
|
+
* The haystack in every real use of `contains` is a captured stream or a module
|
|
70
|
+
* of generated text, and a detail line forty thousand bytes long is not a
|
|
71
|
+
* diagnostic — it is the output the reader was already searching by hand.
|
|
72
|
+
*/
|
|
73
|
+
const TESTING_EXCERPT: i32 = 200;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `text` when it is short enough to read, and its first `TESTING_EXCERPT` bytes
|
|
77
|
+
* with the whole length named otherwise.
|
|
78
|
+
*
|
|
79
|
+
* The cut is at a byte, like every offset in this language, so a multi-byte
|
|
80
|
+
* character at the boundary is split — which is acceptable here and would not be
|
|
81
|
+
* anywhere else: this string is read by a person looking at a failure, not by a
|
|
82
|
+
* program.
|
|
83
|
+
*
|
|
84
|
+
* The name is deliberately not `excerpt`. A `std/` module is compiled into the
|
|
85
|
+
* program that imports it and the symbol namespace is flat, so a private helper
|
|
86
|
+
* called `excerpt` would stop any program that declares its own `excerpt` from
|
|
87
|
+
* compiling (`std/README.md`, "Name the private helpers as though they were
|
|
88
|
+
* exported").
|
|
89
|
+
*/
|
|
90
|
+
const testingExcerpt = (text: string): string => {
|
|
91
|
+
const length: i32 = toI32(text.length);
|
|
92
|
+
if (length <= TESTING_EXCERPT) {
|
|
93
|
+
return text;
|
|
94
|
+
}
|
|
95
|
+
return `${text.substring(0, TESTING_EXCERPT)}... (${length} bytes)`;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The index of the first line at which `actual` and `expected` differ, the
|
|
100
|
+
* shorter length when one is a prefix of the other, and `-1` when they are
|
|
101
|
+
* equal.
|
|
102
|
+
*
|
|
103
|
+
* `std/text` exports `firstDifference`, which is this loop, and this module
|
|
104
|
+
* deliberately does not import it: an import of a `std/` module is compiled into
|
|
105
|
+
* the program, so reaching for it here would put `trim`, `contains` and
|
|
106
|
+
* `replaceAll` into the namespace of every program that only wanted a `Suite`.
|
|
107
|
+
* Ten lines is the cheaper half of that trade.
|
|
108
|
+
*/
|
|
109
|
+
const testingFirstDifferentLine = (actual: string[], expected: string[]): i32 => {
|
|
110
|
+
const actualLength: i32 = toI32(actual.length);
|
|
111
|
+
const expectedLength: i32 = toI32(expected.length);
|
|
112
|
+
const shorter: i32 = actualLength < expectedLength ? actualLength : expectedLength;
|
|
113
|
+
let i: i32 = 0;
|
|
114
|
+
// Two bounds rather than `i < shorter`: they stop at the same index, and each
|
|
115
|
+
// is what proves one of the two reads below in range, where the minimum of
|
|
116
|
+
// the pair proves neither.
|
|
117
|
+
while (i < actualLength && i < expectedLength) {
|
|
118
|
+
if (actual[i] !== expected[i]) {
|
|
119
|
+
return i;
|
|
120
|
+
}
|
|
121
|
+
i += 1;
|
|
122
|
+
}
|
|
123
|
+
return actualLength === expectedLength ? -1 : shorter;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* One run of checks, and the tally it reports.
|
|
128
|
+
*
|
|
129
|
+
* A program may hold several — one per family of behaviour — and sum their exit
|
|
130
|
+
* codes, because `done` answers a code rather than ending the process: a suite
|
|
131
|
+
* that called `process.exit` itself would make the second suite unreachable.
|
|
132
|
+
*/
|
|
133
|
+
export class Suite {
|
|
134
|
+
/** Names the summary line, so several suites in one program stay apart. */
|
|
135
|
+
name: string;
|
|
136
|
+
// These three have initializers because the constructor does not assign them,
|
|
137
|
+
// and every field holds a value once it returns. `name` does not, and an
|
|
138
|
+
// initializer there would only be a store the constructor overwrites.
|
|
139
|
+
passed: i32 = 0;
|
|
140
|
+
failed: i32 = 0;
|
|
141
|
+
skipped: i32 = 0;
|
|
142
|
+
/**
|
|
143
|
+
* The names of the checks that failed, recapped by `done`. The detail of
|
|
144
|
+
* each was printed when it happened; this is the list a reader wants after
|
|
145
|
+
* a hundred lines of output have scrolled the failures off the screen.
|
|
146
|
+
*/
|
|
147
|
+
failures: string[];
|
|
148
|
+
|
|
149
|
+
constructor(name: string) {
|
|
150
|
+
this.name = name;
|
|
151
|
+
this.failures = [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Records a pass. Public because a program with a check of its own shape still wants the tally. */
|
|
155
|
+
pass(name: string): boolean {
|
|
156
|
+
this.passed += 1;
|
|
157
|
+
console.log(`PASS ${name}`);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Records a failure, with `detail` on its own indented line when there is
|
|
163
|
+
* one. An empty `detail` prints no second line, which is what a check whose
|
|
164
|
+
* name already says everything wants.
|
|
165
|
+
*/
|
|
166
|
+
fail(name: string, detail: string): boolean {
|
|
167
|
+
this.failed += 1;
|
|
168
|
+
this.failures.push(name);
|
|
169
|
+
console.log(`FAIL ${name}`);
|
|
170
|
+
if (toI32(detail.length) > 0) {
|
|
171
|
+
console.log(` ${detail}`);
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Records a check that did not run, and why. It answers `void` rather than a
|
|
178
|
+
* `boolean` because there is no outcome to branch on: the caller has already
|
|
179
|
+
* decided to skip, the way `tests/run.js` decides when `llvm-as` is missing.
|
|
180
|
+
*/
|
|
181
|
+
skip(name: string, reason: string): void {
|
|
182
|
+
this.skipped += 1;
|
|
183
|
+
console.log(`SKIP ${name} (${reason})`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
ok(name: string, condition: boolean): boolean {
|
|
187
|
+
if (condition) {
|
|
188
|
+
return this.pass(name);
|
|
189
|
+
}
|
|
190
|
+
return this.fail(name, "expected true, got false");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
eqBool(name: string, actual: boolean, expected: boolean): boolean {
|
|
194
|
+
if (actual === expected) {
|
|
195
|
+
return this.pass(name);
|
|
196
|
+
}
|
|
197
|
+
return this.fail(name, `expected ${expected}, got ${actual}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
eqI32(name: string, actual: i32, expected: i32): boolean {
|
|
201
|
+
if (actual === expected) {
|
|
202
|
+
return this.pass(name);
|
|
203
|
+
}
|
|
204
|
+
return this.fail(name, `expected ${expected}, got ${actual}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
eqI64(name: string, actual: i64, expected: i64): boolean {
|
|
208
|
+
if (actual === expected) {
|
|
209
|
+
return this.pass(name);
|
|
210
|
+
}
|
|
211
|
+
return this.fail(name, `expected ${expected}, got ${actual}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Exact `f64` equality, which is `fcmp oeq`: `NaN` is equal to nothing, so a
|
|
216
|
+
* check of a computation that can produce one fails rather than passing by
|
|
217
|
+
* accident. Use `nearF64` for anything that went through arithmetic.
|
|
218
|
+
*/
|
|
219
|
+
eqF64(name: string, actual: f64, expected: f64): boolean {
|
|
220
|
+
if (actual === expected) {
|
|
221
|
+
return this.pass(name);
|
|
222
|
+
}
|
|
223
|
+
return this.fail(name, `expected ${expected}, got ${actual}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* `f64` equality within `tolerance`, the absolute difference. The tolerance
|
|
228
|
+
* is a parameter rather than a constant here because the right epsilon
|
|
229
|
+
* belongs to the computation, not to the harness.
|
|
230
|
+
*/
|
|
231
|
+
nearF64(name: string, actual: f64, expected: f64, tolerance: f64): boolean {
|
|
232
|
+
let delta: f64 = actual - expected;
|
|
233
|
+
if (delta < 0) {
|
|
234
|
+
delta = -delta;
|
|
235
|
+
}
|
|
236
|
+
if (delta <= tolerance) {
|
|
237
|
+
return this.pass(name);
|
|
238
|
+
}
|
|
239
|
+
return this.fail(name, `expected ${expected} +/- ${tolerance}, got ${actual}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* String equality by content (`nish_str_eq`), which is what `===` on two
|
|
244
|
+
* strings already means. The values are quoted in the message so that a
|
|
245
|
+
* trailing space or an empty string is visible in the diff.
|
|
246
|
+
*/
|
|
247
|
+
eqStr(name: string, actual: string, expected: string): boolean {
|
|
248
|
+
if (actual === expected) {
|
|
249
|
+
return this.pass(name);
|
|
250
|
+
}
|
|
251
|
+
return this.fail(name, `expected "${expected}", got "${actual}"`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Whether `needle` occurs anywhere in `haystack`, which is the assertion a
|
|
256
|
+
* check over captured output makes: the message a compiler prints is pinned by
|
|
257
|
+
* the sentence it has to contain and not by the whole stream, because the rest
|
|
258
|
+
* of that stream is a version line and a file path that change for reasons the
|
|
259
|
+
* check is not about.
|
|
260
|
+
*
|
|
261
|
+
* The detail quotes the needle in full and the haystack only to
|
|
262
|
+
* `TESTING_EXCERPT` bytes: the needle is what a reader compares, and the
|
|
263
|
+
* haystack is what they have already got.
|
|
264
|
+
*/
|
|
265
|
+
contains(name: string, haystack: string, needle: string): boolean {
|
|
266
|
+
if (toI32(haystack.indexOf(needle)) >= 0) {
|
|
267
|
+
return this.pass(name);
|
|
268
|
+
}
|
|
269
|
+
return this.fail(name, `expected to contain "${needle}", got "${testingExcerpt(haystack)}"`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Whether every one of `needles` occurs in `haystack` — one check for an
|
|
274
|
+
* expectation file that holds a fragment per line, which is the shape
|
|
275
|
+
* `tests/cases/<name>.err` and `tests/link/<name>/expected.ir` both have.
|
|
276
|
+
*
|
|
277
|
+
* An empty needle is ignored, so the blank lines of such a file cost the caller
|
|
278
|
+
* no filtering, and a list with nothing in it passes: an expectation that says
|
|
279
|
+
* nothing is met by anything. The detail names the first fragment that is
|
|
280
|
+
* missing and how many are, because a detail that quoted all of them would bury
|
|
281
|
+
* the one a reader fixes first.
|
|
282
|
+
*/
|
|
283
|
+
containsAll(name: string, haystack: string, needles: string[]): boolean {
|
|
284
|
+
let looked: i32 = 0;
|
|
285
|
+
let missing: i32 = 0;
|
|
286
|
+
let first = "";
|
|
287
|
+
for (const needle of needles) {
|
|
288
|
+
if (toI32(needle.length) === 0) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
looked += 1;
|
|
292
|
+
if (toI32(haystack.indexOf(needle)) < 0) {
|
|
293
|
+
missing += 1;
|
|
294
|
+
if (toI32(first.length) === 0) {
|
|
295
|
+
first = needle;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (missing === 0) {
|
|
300
|
+
return this.pass(name);
|
|
301
|
+
}
|
|
302
|
+
if (missing === 1) {
|
|
303
|
+
return this.fail(name, `missing "${first}"`);
|
|
304
|
+
}
|
|
305
|
+
// `looked` and not `needles.length`, because the blank lines this ignored are
|
|
306
|
+
// not fragments anybody expected to find.
|
|
307
|
+
return this.fail(name, `missing ${missing} of ${looked}, first "${first}"`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Line-by-line equality of two texts already split into lines, reporting the
|
|
312
|
+
* first line that differs.
|
|
313
|
+
*
|
|
314
|
+
* It is `eqStr`'s answer for the case `eqStr` reports badly: a golden of four
|
|
315
|
+
* hundred lines against an emitted four hundred and one prints two walls of
|
|
316
|
+
* text under `expected ... got ...`, and the reader's next move is to diff them
|
|
317
|
+
* by hand. `<end of input>` is the side that ran out, which is what a missing
|
|
318
|
+
* or an extra line at the end looks like from here, and the line number is the
|
|
319
|
+
* one an editor takes.
|
|
320
|
+
*/
|
|
321
|
+
eqLines(name: string, actual: string[], expected: string[]): boolean {
|
|
322
|
+
const at: i32 = testingFirstDifferentLine(actual, expected);
|
|
323
|
+
if (at < 0) {
|
|
324
|
+
return this.pass(name);
|
|
325
|
+
}
|
|
326
|
+
const wanted = at < toI32(expected.length) ? `"${expected[at]}"` : "<end of input>";
|
|
327
|
+
const saw = at < toI32(actual.length) ? `"${actual[at]}"` : "<end of input>";
|
|
328
|
+
return this.fail(name, `line ${at + 1}: expected ${wanted}, got ${saw}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Prints the recap and the summary, and answers the exit code: 0 when
|
|
333
|
+
* nothing failed, 1 otherwise. A skip is not a failure, so it does not move
|
|
334
|
+
* the code — it moves the count on the summary line, which is the whole
|
|
335
|
+
* reason the count is printed.
|
|
336
|
+
*/
|
|
337
|
+
done(): i32 {
|
|
338
|
+
if (toI32(this.failures.length) > 0) {
|
|
339
|
+
console.log(`failed: ${this.failures.join(", ")}`);
|
|
340
|
+
}
|
|
341
|
+
console.log(`${this.name}: ${this.passed} passed, ${this.failed} failed, ${this.skipped} skipped`);
|
|
342
|
+
if (this.failed > 0) {
|
|
343
|
+
return 1;
|
|
344
|
+
}
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
}
|
package/std/text.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `std/text` — the string operations a program has to write itself.
|
|
3
|
+
*
|
|
4
|
+
* The language gives a string `length`, `charCodeAt`, `substring`, `indexOf`,
|
|
5
|
+
* `startsWith`, `endsWith` and `===` by content, and an array `join`. It does
|
|
6
|
+
* **not** give `split`, `trim`, `toLowerCase` or a regular expression, and that
|
|
7
|
+
* is a decision rather than a gap: each of those allocates, and several of them
|
|
8
|
+
* need a locale or a character table that this runtime has no room for
|
|
9
|
+
* (`docs/wp7-runtime.md` keeps the runtime under a measured byte budget).
|
|
10
|
+
*
|
|
11
|
+
* So a program that reads a file and wants its lines writes the loop. This
|
|
12
|
+
* module is that loop, written once. Every offset here is a **byte** offset,
|
|
13
|
+
* like `s.length` itself, and every function is ASCII-only where it inspects
|
|
14
|
+
* characters at all — a UTF-8 string passes through `splitLines` and `split`
|
|
15
|
+
* untouched, because a newline and a space cannot appear inside a multi-byte
|
|
16
|
+
* sequence.
|
|
17
|
+
*
|
|
18
|
+
* Every width this module declares is spelled (`i32`, never `number`) *and*
|
|
19
|
+
* every value a builtin hands it is converted at the read: `s.length`,
|
|
20
|
+
* `a.length`, `s.charCodeAt(i)` and `s.indexOf(t)` all answer `number`
|
|
21
|
+
* (LANGUAGE.md, *Arrays and strings as receivers*), and `number` is `f64` under
|
|
22
|
+
* `--number-mode f64`. Spelling the declarations is therefore necessary and not
|
|
23
|
+
* sufficient — without the `toI32` at each read this module does not compile in
|
|
24
|
+
* that mode at all (`docs/wp26-stdlib.md` §4). A length a loop tests on every
|
|
25
|
+
* iteration is read into an `i32` local once rather than converted per
|
|
26
|
+
* iteration, which is both cheaper and how the loop wants to read; a string is
|
|
27
|
+
* immutable, so its length cannot change underneath the local.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const NEWLINE: i32 = 10;
|
|
31
|
+
const CARRIAGE_RETURN: i32 = 13;
|
|
32
|
+
const SPACE: i32 = 32;
|
|
33
|
+
const TAB: i32 = 9;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether `code` is one of the four ASCII bytes this module treats as blank.
|
|
37
|
+
*
|
|
38
|
+
* The name is deliberately not `isBlank`. A `std/` module's private functions
|
|
39
|
+
* share the importing program's flat symbol namespace, so a program that
|
|
40
|
+
* declares its own `isBlank` could not compile against this module
|
|
41
|
+
* (`docs/wp26-stdlib.md` §3e); naming the module and the unit of inspection
|
|
42
|
+
* makes the collision unlikely instead.
|
|
43
|
+
*/
|
|
44
|
+
const isTextBlankByte = (code: i32): boolean =>
|
|
45
|
+
code === SPACE || code === TAB || code === NEWLINE || code === CARRIAGE_RETURN;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The lines of `text`, without their terminators.
|
|
49
|
+
*
|
|
50
|
+
* A trailing newline does **not** produce a final empty line, because a text
|
|
51
|
+
* file that ends in one has as many lines as it has newlines and a caller
|
|
52
|
+
* comparing two files line by line would otherwise see a phantom difference at
|
|
53
|
+
* the end. A `\r\n` line ending keeps its `\r`: stripping it would make this
|
|
54
|
+
* function guess at the file's provenance, and a caller that needs it gone can
|
|
55
|
+
* `trimEnd` each line.
|
|
56
|
+
*/
|
|
57
|
+
export const splitLines = (text: string): string[] => {
|
|
58
|
+
const length: i32 = toI32(text.length);
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
let start: i32 = 0;
|
|
61
|
+
let i: i32 = 0;
|
|
62
|
+
while (i < length) {
|
|
63
|
+
if (toI32(text.charCodeAt(i)) === NEWLINE) {
|
|
64
|
+
// `start` is never negative and never passes `i`, which is below
|
|
65
|
+
// `length`, so this test always holds. It is written because it is what
|
|
66
|
+
// proves `start` within `text`: the clamp on that bound is dead once it
|
|
67
|
+
// is proven, and the emitter drops it.
|
|
68
|
+
if (start >= 0 && start < length) {
|
|
69
|
+
lines.push(text.substring(start, i));
|
|
70
|
+
}
|
|
71
|
+
start = i + 1;
|
|
72
|
+
}
|
|
73
|
+
i += 1;
|
|
74
|
+
}
|
|
75
|
+
if (start < length) {
|
|
76
|
+
lines.push(text.substring(start, length));
|
|
77
|
+
}
|
|
78
|
+
return lines;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The runs of non-blank bytes in `text`, with every blank run as the separator.
|
|
83
|
+
* Leading, trailing and repeated blanks produce no empty elements, which is what
|
|
84
|
+
* splitting a line of command-line flags wants — and it is why this is not
|
|
85
|
+
* `split(text, " ")`, which would.
|
|
86
|
+
*/
|
|
87
|
+
export const splitWhitespace = (text: string): string[] => {
|
|
88
|
+
const length: i32 = toI32(text.length);
|
|
89
|
+
const parts: string[] = [];
|
|
90
|
+
let start: i32 = -1;
|
|
91
|
+
let i: i32 = 0;
|
|
92
|
+
while (i < length) {
|
|
93
|
+
if (isTextBlankByte(toI32(text.charCodeAt(i)))) {
|
|
94
|
+
// `start` is `-1` or a byte already passed, so the second half always
|
|
95
|
+
// holds when the first does; it proves the bound, as in `splitLines`.
|
|
96
|
+
if (start >= 0 && start < length) {
|
|
97
|
+
parts.push(text.substring(start, i));
|
|
98
|
+
start = -1;
|
|
99
|
+
}
|
|
100
|
+
} else if (start < 0) {
|
|
101
|
+
start = i;
|
|
102
|
+
}
|
|
103
|
+
i += 1;
|
|
104
|
+
}
|
|
105
|
+
if (start >= 0) {
|
|
106
|
+
parts.push(text.substring(start, length));
|
|
107
|
+
}
|
|
108
|
+
return parts;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** `text` without its leading blank bytes. */
|
|
112
|
+
export const trimStart = (text: string): string => {
|
|
113
|
+
const length: i32 = toI32(text.length);
|
|
114
|
+
let i: i32 = 0;
|
|
115
|
+
while (i < length && isTextBlankByte(toI32(text.charCodeAt(i)))) {
|
|
116
|
+
i += 1;
|
|
117
|
+
}
|
|
118
|
+
return text.substring(i, length);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** `text` without its trailing blank bytes. */
|
|
122
|
+
export const trimEnd = (text: string): string => {
|
|
123
|
+
let end: i32 = toI32(text.length);
|
|
124
|
+
while (end > 0 && isTextBlankByte(toI32(text.charCodeAt(end - 1)))) {
|
|
125
|
+
end -= 1;
|
|
126
|
+
}
|
|
127
|
+
return text.substring(0, end);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** `text` without blank bytes at either end. */
|
|
131
|
+
export const trim = (text: string): string => trimEnd(trimStart(text));
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Whether `needle` occurs anywhere in `haystack`. One `indexOf`, named, because
|
|
135
|
+
* `indexOf(...) >= 0` at a call site reads as arithmetic where the question is a
|
|
136
|
+
* yes or a no. `contains(s, "")` is `true`, as `indexOf("")` is `0`.
|
|
137
|
+
*/
|
|
138
|
+
export const contains = (haystack: string, needle: string): boolean => toI32(haystack.indexOf(needle)) >= 0;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `text` with every occurrence of `needle` replaced by `replacement`.
|
|
142
|
+
*
|
|
143
|
+
* The parts are collected and `join`ed once rather than concatenated in the loop:
|
|
144
|
+
* `s = s + t` inside a loop is quadratic in time *and* in arena, which is the one
|
|
145
|
+
* string mistake this project has measured the cost of — 180 MB of peak memory
|
|
146
|
+
* for 88 KB of output (`docs/wp14-selfhost.md` §3).
|
|
147
|
+
*
|
|
148
|
+
* An empty `needle` answers `text` unchanged, because every other answer is a
|
|
149
|
+
* choice about how many empty matches a string contains.
|
|
150
|
+
*/
|
|
151
|
+
export const replaceAll = (text: string, needle: string, replacement: string): string => {
|
|
152
|
+
const needleLength: i32 = toI32(needle.length);
|
|
153
|
+
if (needleLength === 0) {
|
|
154
|
+
return text;
|
|
155
|
+
}
|
|
156
|
+
const parts: string[] = [];
|
|
157
|
+
let rest = text;
|
|
158
|
+
while (true) {
|
|
159
|
+
const at: i32 = toI32(rest.indexOf(needle));
|
|
160
|
+
// `indexOf` never answers past `rest.length`, so the second half never
|
|
161
|
+
// holds; it is written because its negation is what proves `at` within
|
|
162
|
+
// `rest` for the `substring` below, as in `splitLines`.
|
|
163
|
+
if (at < 0 || at > toI32(rest.length)) {
|
|
164
|
+
parts.push(rest);
|
|
165
|
+
return parts.join(replacement);
|
|
166
|
+
}
|
|
167
|
+
parts.push(rest.substring(0, at));
|
|
168
|
+
rest = rest.substring(at + needleLength, toI32(rest.length));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The index of the first line at which `left` and `right` differ, or `-1` when
|
|
174
|
+
* one is a prefix of the other and they have the same length — that is, when
|
|
175
|
+
* they are equal. A caller comparing generated text against a golden wants the
|
|
176
|
+
* first differing line and not a boolean, because printing two whole files is
|
|
177
|
+
* not a diagnostic.
|
|
178
|
+
*/
|
|
179
|
+
export const firstDifference = (left: string[], right: string[]): i32 => {
|
|
180
|
+
const leftLength: i32 = toI32(left.length);
|
|
181
|
+
const rightLength: i32 = toI32(right.length);
|
|
182
|
+
let i: i32 = 0;
|
|
183
|
+
// Two tests rather than one against the shorter length: each proves `i`
|
|
184
|
+
// within one of the arrays, and a minimum taken with a ternary proves
|
|
185
|
+
// neither. The loop ends with `i` at the shorter length.
|
|
186
|
+
while (i < leftLength && i < rightLength) {
|
|
187
|
+
if (left[i] !== right[i]) {
|
|
188
|
+
return i;
|
|
189
|
+
}
|
|
190
|
+
i += 1;
|
|
191
|
+
}
|
|
192
|
+
return leftLength === rightLength ? -1 : i;
|
|
193
|
+
};
|