@amritk/nish-aarch64-darwin 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/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
+ };