@amritk/nish 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +553 -0
- package/bin/launcher.js +186 -0
- package/bin/nish +23 -0
- package/bin/packaging.js +220 -0
- package/docs/AI.md +1076 -0
- package/docs/INSTALL.md +468 -0
- package/llms.txt +49 -0
- package/package.json +87 -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/bootstrap.sh +357 -0
- package/scripts/build.sh +279 -0
- package/scripts/changelog-gen.mjs +528 -0
- package/scripts/changelog-section.sh +28 -0
- package/scripts/ci-profile.mjs +187 -0
- package/scripts/codes-registry.js +73 -0
- package/scripts/gen-diagnostic-codes.mjs +199 -0
- package/scripts/gen-pow5-tables.py +45 -0
- package/scripts/nish-compiler.sh +17 -0
- package/scripts/platform-package.mjs +91 -0
- package/scripts/postinstall.mjs +133 -0
- package/scripts/size-report.sh +72 -0
- package/scripts/smoke.sh +94 -0
- package/scripts/verify-binaries.sh +213 -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/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# `std/` — the standard library
|
|
2
|
+
|
|
3
|
+
Nish modules written in Nish, for Nish programs to import. There is no magic
|
|
4
|
+
here and nothing the compiler knows about: a module in this directory is an
|
|
5
|
+
ordinary Nish source file, compiled as part of whatever program imports it,
|
|
6
|
+
and subject to the same rules as `examples/` or `self/`
|
|
7
|
+
([`docs/LANGUAGE.md`](../docs/LANGUAGE.md) is the style guide).
|
|
8
|
+
|
|
9
|
+
| Module | What it is |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| [`testing.ts`](./testing.ts) | a test runner: a `Suite` a program drives with straight-line assertions, printing the `PASS` / `FAIL` / `SKIP` lines the repository's own harness prints, and answering the exit code |
|
|
12
|
+
| [`text.ts`](./text.ts) | the string operations a program would otherwise write inline: `splitLines`, `splitWhitespace`, `trim` and its halves, `contains`, `replaceAll`, and `firstDifference` over two arrays of lines |
|
|
13
|
+
| [`json.ts`](./json.ts) | `jsonField(object, name)`: the value of one field of one flat JSON object, which is the shape the compiler's own `--json` diagnostics have. A reader and not a parser — it answers text, answers `null` for a field that is not there, and does not validate |
|
|
14
|
+
| [`pair.ts`](./pair.ts) | `Pair<A, B>`: an interface with `first` and `second`, for a function that answers two values from one call. A type and nothing else — the caller writes an object literal at the return — and for returning two values rather than storing them side by side |
|
|
15
|
+
|
|
16
|
+
## How a program imports it
|
|
17
|
+
|
|
18
|
+
By its package specifier:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { Suite } from "nish/testing";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`nish/<module>` resolves to `<module>.ts` in this directory, found beside the
|
|
25
|
+
compiler that is running — not relative to the importing file, so the same
|
|
26
|
+
specifier works at any depth and from outside this repository.
|
|
27
|
+
|
|
28
|
+
**Source is the distribution format** ([`docs/wp21-packages.md`](../docs/wp21-packages.md)
|
|
29
|
+
§2), so an import of a `std/` module is not a link against a built library: the
|
|
30
|
+
module is compiled with the program that imports it, and the whole-program
|
|
31
|
+
attribute pass sees through it exactly as it sees through the program's own
|
|
32
|
+
functions. A `std/` function is inlined, specialised or dropped on the same
|
|
33
|
+
terms as a local one — and a module you import but never call is dropped
|
|
34
|
+
whole. Measured at the `speed` and `size` profiles, both of which link with
|
|
35
|
+
`-flto -Wl,--gc-sections`: a program importing three `std/text` functions and
|
|
36
|
+
calling none is **byte-identical** to the same program without the import.
|
|
37
|
+
(`debug` keeps them, which is what `debug` is for.)
|
|
38
|
+
|
|
39
|
+
A `std/` module is its own package (`nish`), so its symbols are scoped and a
|
|
40
|
+
program may declare a function one of these modules also exports
|
|
41
|
+
(`tests/link/std_package_scope`, `docs/wp21-packages.md` §5a). The package is
|
|
42
|
+
decided by the `nish/` specifier rather than by the directory the file is found
|
|
43
|
+
in — read off the path it would be `nish` from `node_modules/nish/std/` and the
|
|
44
|
+
root package from a checkout, and the same program would compile against an
|
|
45
|
+
installed compiler and be refused by a checkout of it.
|
|
46
|
+
|
|
47
|
+
A relative specifier still works and means the same thing —
|
|
48
|
+
`import { Suite } from "../std/testing"` — but it hard-codes the depth of the
|
|
49
|
+
importing file and only reaches an installed library by the path the install
|
|
50
|
+
put it at, so `nish/` is the form to write.
|
|
51
|
+
|
|
52
|
+
### Why this is not the second module system this file used to warn about
|
|
53
|
+
|
|
54
|
+
An earlier version of this section said a bare specifier was "deliberately not
|
|
55
|
+
faked", because a resolver that special-cased this directory would be a second
|
|
56
|
+
module system and the one WP21 is bringing has to agree with Node's. That
|
|
57
|
+
reasoning still holds for third-party packages, which are still refused
|
|
58
|
+
(`docs/wp21-packages.md` §5b). It does not hold for *this* package, for two
|
|
59
|
+
reasons that are only true of it:
|
|
60
|
+
|
|
61
|
+
- There is exactly one right answer. `std/` ships inside the compiler's own
|
|
62
|
+
package and is versioned with it, so "the `std/` beside this binary" is not a
|
|
63
|
+
guess a resolver makes — it is the only `std/` that can be correct for the
|
|
64
|
+
compiler reading it. No version can be skewed against it.
|
|
65
|
+
- It **is** what Node resolves. `package.json` declares
|
|
66
|
+
`"./*": "./std/*.ts"` in `exports`, so `nish/text` is a package
|
|
67
|
+
self-reference: `import.meta.resolve("nish/text")` answers `std/text.ts`, and
|
|
68
|
+
`tsc` under `moduleResolution: node16` resolves it to the same file, which is
|
|
69
|
+
what gives an editor go-to-definition into the real source. The compiler
|
|
70
|
+
short-circuits to that answer rather than walking `node_modules` to reach it.
|
|
71
|
+
|
|
72
|
+
So this is WP21's first slice rather than a detour around it: the spelling is
|
|
73
|
+
the one WP21 specifies, and what is still missing is resolution for specifiers
|
|
74
|
+
that are *not* this package.
|
|
75
|
+
|
|
76
|
+
## Writing a module here
|
|
77
|
+
|
|
78
|
+
- **Spell the widths — and then convert what the builtins hand you.** `i32`,
|
|
79
|
+
`i64`, `f64`, never `number`, so the module means the same thing under
|
|
80
|
+
`--number-mode f64` as it does by default (`examples/arrays.ts` says that half
|
|
81
|
+
for the same reason). It is necessary and **not sufficient**: `s.length`,
|
|
82
|
+
`a.length` and `s.charCodeAt(i)` answer `number`, which *is* `f64` in that
|
|
83
|
+
mode, so a module that declares every width of its own and still writes
|
|
84
|
+
`let end: i32 = text.length` or `while (i < text.length)` does not compile
|
|
85
|
+
there at all. Read each of those through `toI32` —
|
|
86
|
+
`const length: i32 = toI32(text.length)`, once per function rather than once
|
|
87
|
+
per iteration — and the module means one program in both modes. The default
|
|
88
|
+
mode pays nothing for it, because `toI32` on an `i32` is identity.
|
|
89
|
+
`tests/link/std_text_f64` is what keeps this from being prose;
|
|
90
|
+
[`docs/wp26-stdlib.md`](../docs/wp26-stdlib.md) §4 is the reasoning.
|
|
91
|
+
- **Name the private helpers as though they were exported.** A function name is
|
|
92
|
+
unique across the whole program whether or not it is exported, because the
|
|
93
|
+
whole-program attribute analysis is keyed by symbol name — and a `std/` module
|
|
94
|
+
is compiled *into* the program that imports it, so its private helpers are not
|
|
95
|
+
private to the namespace. A helper called `isBlank` would stop any program that
|
|
96
|
+
declares its own `isBlank` from compiling (`` Function `isBlank` is also
|
|
97
|
+
defined in main.ts ``), which is why `text.ts` calls it `isTextBlankByte`. Keep
|
|
98
|
+
the helpers few and their names distinctive; a module whose internals want
|
|
99
|
+
`compare`, `next` or `parse` is asking for package-scoped symbols, which do not
|
|
100
|
+
exist yet ([`docs/wp26-stdlib.md`](../docs/wp26-stdlib.md) §3e). Module
|
|
101
|
+
constants are exempt — `NEWLINE` and `SPACE` fold at their uses, so a program
|
|
102
|
+
may declare those names itself.
|
|
103
|
+
- **It is an Nish program**, so the constraints are the language's: a function is
|
|
104
|
+
an arrow bound to a module-level `const`, a function is never a value, there
|
|
105
|
+
is no `try` / `catch`, and there are no optional or default parameters. Those
|
|
106
|
+
three are what shape an API here more than any style preference — see the
|
|
107
|
+
header of `testing.ts` for what they did to that one, which was written before
|
|
108
|
+
WP18 added generic functions and classes
|
|
109
|
+
([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#generic-functions)) and still has
|
|
110
|
+
one assertion per type ([`docs/wp26-stdlib.md`](../docs/wp26-stdlib.md) §3c).
|
|
111
|
+
`pair.ts` is the first module here to export a generic type.
|
|
112
|
+
- **Ship it with a `tests/link/` case.** `tests/link/<name>/` is the only place a
|
|
113
|
+
multi-module program is exercised end to end, and it is also what puts the
|
|
114
|
+
module into the corpus the stage1 oracles read
|
|
115
|
+
([`.claude/selfhost.md`](../.claude/selfhost.md)): a `std/` module with no
|
|
116
|
+
importer in `tests/link/` is compiled by neither compiler on any run.
|
|
117
|
+
`testing.ts` has two, one per outcome — `tests/link/std_testing` (exit 0) and
|
|
118
|
+
`tests/link/std_testing_fail` (exit 1, and the wording of every failure
|
|
119
|
+
message) — and `text.ts` and `json.ts` have `tests/link/std_text` and
|
|
120
|
+
`tests/link/std_json`, each of which uses `Suite` to check the module, the way a
|
|
121
|
+
user would. `pair.ts` has five, `tests/link/std_pair_*`, one per shape of
|
|
122
|
+
instantiation, each returning a `Pair` from a sibling module and pinned by its
|
|
123
|
+
stdout. `tests/link/std_text_f64` is the same corpus under
|
|
124
|
+
`--number-mode f64`, which is where a module that spelled its widths and forgot
|
|
125
|
+
a `toI32` is caught.
|
|
126
|
+
- **`std/` is not on the compiler's dependency list.** Nothing in `self/`
|
|
127
|
+
imports it, and nothing should: the compiler is the thing that has to
|
|
128
|
+
build before the library means anything.
|
|
129
|
+
|
|
130
|
+
## Who reads the library, and what each reader proved
|
|
131
|
+
|
|
132
|
+
Two programs in this repository are written on top of `std/`, and between them
|
|
133
|
+
they are the reason the modules have the shape they do — a library with one
|
|
134
|
+
consumer is a guess.
|
|
135
|
+
|
|
136
|
+
| Program | What it does | What it uses |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| [`tests/nish/run.ts`](../tests/nish/run.ts) | the golden cases and the `tests/link/` programs, compiled, assembled, linked, run and diffed | `Suite` (including `containsAll` for an `.err` file's fragments and `eqLines` for an IR golden), and all of `std/text` |
|
|
139
|
+
| [`tests/nish/cli.ts`](../tests/nish/cli.ts) | the command line's own contract — the streams, the exit-code bands, and one flat `--json` object per diagnostic — read by a program in the language the compiler compiles | `Suite`, `jsonField`, `splitLines` / `trim` / `contains` |
|
|
140
|
+
|
|
141
|
+
Three assertions exist because the first of those two had written them by hand:
|
|
142
|
+
`contains` for a fragment of a captured stream, `containsAll` for an expectation
|
|
143
|
+
file that holds one fragment per line, and `eqLines` for a generated text against
|
|
144
|
+
a golden, which reports the first differing line instead of printing both texts.
|
|
145
|
+
That is the rule the directory runs on — a `std/` function earns its place when a
|
|
146
|
+
program in this repository would otherwise write the loop, and the loop is
|
|
147
|
+
already written.
|
|
148
|
+
|
|
149
|
+
`std/json` is the one module admitted with a single importer, and the reason is
|
|
150
|
+
the *format* rather than the count: the object it reads is this project's own
|
|
151
|
+
published surface (`AGENTS.md`, [`docs/wp12-release.md`](../docs/wp12-release.md)),
|
|
152
|
+
so every Nish program that ever reads compiler output needs exactly this scan,
|
|
153
|
+
and the next one would copy it out of `tests/nish/cli.ts`. The module is also
|
|
154
|
+
where the format's one ambiguity is written down and tested —
|
|
155
|
+
[`docs/wp26-stdlib.md`](../docs/wp26-stdlib.md) §7 question 3 is where that
|
|
156
|
+
argument is recorded and where its second consumer is expected.
|
|
157
|
+
|
|
158
|
+
## What `std/testing` is not, and what closed
|
|
159
|
+
|
|
160
|
+
`std/testing` is a test *library*: it is how a compiled program checks itself.
|
|
161
|
+
Driving the compiler — compiling a corpus, assembling it, linking it, running it
|
|
162
|
+
and diffing stdout against a golden — needed three things the language did not
|
|
163
|
+
have, and all three have since landed as builtins:
|
|
164
|
+
|
|
165
|
+
| Was missing | Now |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| a directory listing | `readdirSync(path): string[] \| null`, sorted by bytes, because there is no `sort` for a caller to reach for |
|
|
168
|
+
| a child's **output** — `spawnSync` answers a status and nothing else | `spawnSyncTo(argv, stdoutPath, stderrPath)`, each stream to a file, an empty path inheriting |
|
|
169
|
+
| a clock | `monotonicNanos(): i64` |
|
|
170
|
+
|
|
171
|
+
So the driver exists, in the language, and it is
|
|
172
|
+
[`tests/nish/run.ts`](../tests/nish/run.ts): it discovers the golden cases with
|
|
173
|
+
`readdirSync`, compiles each one by spawning the compiler, links and runs the ones
|
|
174
|
+
with a `.out`, diffs the IR against the golden line by line, and reports through a
|
|
175
|
+
`Suite`. It passes over the whole corpus — `npm run test:nish` — and `npm test`
|
|
176
|
+
runs it over a handful of cases so that the three builtins are exercised together
|
|
177
|
+
on a real workload on every run.
|
|
178
|
+
|
|
179
|
+
It is still not a replacement for `tests/run.js`, and the difference is worth
|
|
180
|
+
being precise about: it covers section A, the golden cases, and none of the
|
|
181
|
+
pipeline checks — no interop sidecars, no layout assertions, no wasm profiles, no
|
|
182
|
+
packaging and no self-hosting oracles. It also skips, by name and counted, the three
|
|
183
|
+
sidecars it does not implement (`.env`, `.argv`, `.stdout`). What it demonstrates
|
|
184
|
+
is that the language can host its own harness; what `tests/run.js` does is prove
|
|
185
|
+
the compiler.
|
package/std/json.ts
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `std/json` — the value of one field of one flat JSON object.
|
|
3
|
+
*
|
|
4
|
+
* It exists because the compiler's own machine-readable surface is JSON: under
|
|
5
|
+
* `--json` every diagnostic is one flat object on a line of stdout, whose `code`
|
|
6
|
+
* is a stable rule identifier (`AGENTS.md`, and `docs/wp12-release.md` for the
|
|
7
|
+
* exit-code bands). A Nish program that reads that surface — a wrapper, an
|
|
8
|
+
* editor plug-in, or `tests/nish/cli.ts`, which pins the contract — needs the
|
|
9
|
+
* value of a named field and nothing else, and the language has no `JSON.parse`
|
|
10
|
+
* to give it.
|
|
11
|
+
*
|
|
12
|
+
* So this is a **reader, not a parser**, and the difference is worth stating:
|
|
13
|
+
*
|
|
14
|
+
* - It answers **text**. A string field comes back with its escapes decoded
|
|
15
|
+
* and without its quotes; a number, a `true` or a `null` comes back as the
|
|
16
|
+
* bytes it was written with, for `parseInt` or `===` at the call site. A
|
|
17
|
+
* field whose value is `null` and one whose value is the *string* `"null"`
|
|
18
|
+
* therefore answer the same thing, which is the one ambiguity a program that
|
|
19
|
+
* needs to tell them apart cannot live with — and the sign that it wants a
|
|
20
|
+
* real parser rather than this.
|
|
21
|
+
* - It answers `null` for a field that is not there, so "absent" and "empty"
|
|
22
|
+
* stay apart the way they do in `getenv`.
|
|
23
|
+
* - It **does not validate**. A nested object or array is stepped over so that
|
|
24
|
+
* the fields after it are still reachable, and a truncated object still
|
|
25
|
+
* answers the fields that precede the truncation. A program that has to know
|
|
26
|
+
* whether the whole line was well-formed is asking a question this does not
|
|
27
|
+
* answer.
|
|
28
|
+
*
|
|
29
|
+
* There is one exported function on purpose. A `std/` module is compiled into
|
|
30
|
+
* the program that imports it and the symbol namespace is flat
|
|
31
|
+
* (`std/README.md`), so every name here is a name its importer cannot use: the
|
|
32
|
+
* helpers are `json`-prefixed and the reader is the only export. Splitting a
|
|
33
|
+
* stream into lines and picking the ones that start with `{` is two calls the
|
|
34
|
+
* caller already has — `splitLines` from `std/text` and `startsWith` — and
|
|
35
|
+
* duplicating them here would cost more names than it saves.
|
|
36
|
+
*
|
|
37
|
+
* import { jsonField } from "nish/json";
|
|
38
|
+
*
|
|
39
|
+
* for (const line of splitLines(stdout)) {
|
|
40
|
+
* if (!line.startsWith("{")) {
|
|
41
|
+
* continue;
|
|
42
|
+
* }
|
|
43
|
+
* const code = jsonField(line, "code");
|
|
44
|
+
* if (code !== null && code === "NL0003") {
|
|
45
|
+
* panic("the compiler crashed");
|
|
46
|
+
* }
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* Every offset here is a **byte** offset and every width is spelled, with each
|
|
50
|
+
* length and byte a builtin answers read through `toI32` — `.length` and
|
|
51
|
+
* `charCodeAt` answer `number`, which is `f64` under `--number-mode f64`, so the
|
|
52
|
+
* conversion is what makes this one module in both modes rather than two
|
|
53
|
+
* (`docs/wp26-stdlib.md` §4).
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const JSON_TAB: i32 = 9;
|
|
57
|
+
const JSON_NEWLINE: i32 = 10;
|
|
58
|
+
const JSON_CARRIAGE_RETURN: i32 = 13;
|
|
59
|
+
const JSON_SPACE: i32 = 32;
|
|
60
|
+
const JSON_QUOTE: i32 = 34;
|
|
61
|
+
const JSON_COMMA: i32 = 44;
|
|
62
|
+
const JSON_COLON: i32 = 58;
|
|
63
|
+
const JSON_BACKSLASH: i32 = 92;
|
|
64
|
+
const JSON_OPEN_BRACKET: i32 = 91;
|
|
65
|
+
const JSON_CLOSE_BRACKET: i32 = 93;
|
|
66
|
+
const JSON_OPEN_BRACE: i32 = 123;
|
|
67
|
+
const JSON_CLOSE_BRACE: i32 = 125;
|
|
68
|
+
/**
|
|
69
|
+
* The UTF-8 tags a `\u` escape is rebuilt out of: a two-byte lead is
|
|
70
|
+
* `110xxxxx`, a three-byte lead `1110xxxx`, a continuation byte `10xxxxxx`, and
|
|
71
|
+
* `LOW_SIX` is the six bits each continuation carries.
|
|
72
|
+
*
|
|
73
|
+
* They are named constants rather than the literals `192`, `224`, `128` and `63`
|
|
74
|
+
* for a reason that is the module's rule and not a style preference: a bare
|
|
75
|
+
* numeric literal is a `number`, which is an `f64` under `--number-mode f64`, so
|
|
76
|
+
* `192 + (code >> 6)` adds an `f64` to an `i32` there and does not compile at all
|
|
77
|
+
* (`docs/wp26-stdlib.md` §4, and `tests/link/std_text_f64` is what says so). A
|
|
78
|
+
* constant declared `i32` means the same thing in both modes.
|
|
79
|
+
*/
|
|
80
|
+
const JSON_UTF8_TWO_BYTE_LEAD: i32 = 192;
|
|
81
|
+
const JSON_UTF8_THREE_BYTE_LEAD: i32 = 224;
|
|
82
|
+
const JSON_UTF8_CONTINUATION: i32 = 128;
|
|
83
|
+
const JSON_UTF8_LOW_SIX: i32 = 63;
|
|
84
|
+
/** The first code point that needs two UTF-8 bytes, and the first that needs three. */
|
|
85
|
+
const JSON_UTF8_TWO_BYTE_FLOOR: i32 = 128;
|
|
86
|
+
const JSON_UTF8_THREE_BYTE_FLOOR: i32 = 2048;
|
|
87
|
+
/** The base `jsonHex4` accumulates in. */
|
|
88
|
+
const JSON_HEX_BASE: i32 = 16;
|
|
89
|
+
|
|
90
|
+
/** The four bytes JSON allows between tokens. */
|
|
91
|
+
const jsonIsBlankByte = (code: i32): boolean =>
|
|
92
|
+
code === JSON_SPACE || code === JSON_TAB || code === JSON_NEWLINE || code === JSON_CARRIAGE_RETURN;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The first index at or after `from` that is not blank, or the length.
|
|
96
|
+
*
|
|
97
|
+
* Every caller hands in an index it has already found, so `from` is never
|
|
98
|
+
* negative; the guard says so in a form the bounds proof reads, and is what
|
|
99
|
+
* lets the loop below read `text` without a check.
|
|
100
|
+
*/
|
|
101
|
+
const jsonSkipBlank = (text: string, from: i32): i32 => {
|
|
102
|
+
if (from < 0) {
|
|
103
|
+
return from;
|
|
104
|
+
}
|
|
105
|
+
const length: i32 = toI32(text.length);
|
|
106
|
+
let i: i32 = from;
|
|
107
|
+
while (i < length && jsonIsBlankByte(toI32(text.charCodeAt(i)))) {
|
|
108
|
+
i += 1;
|
|
109
|
+
}
|
|
110
|
+
return i;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The index just past the string literal whose opening quote is at `at`, or `-1`
|
|
115
|
+
* when the quote is never closed. A backslash takes the byte after it with it,
|
|
116
|
+
* which is all a scanner needs to know about escapes: `\"` cannot end the
|
|
117
|
+
* literal and `\\` cannot make the next quote an escape.
|
|
118
|
+
*/
|
|
119
|
+
const jsonEndOfString = (text: string, at: i32): i32 => {
|
|
120
|
+
const length: i32 = toI32(text.length);
|
|
121
|
+
if (at < 0) {
|
|
122
|
+
return -1;
|
|
123
|
+
}
|
|
124
|
+
let i: i32 = at + 1;
|
|
125
|
+
while (i < length) {
|
|
126
|
+
const code: i32 = toI32(text.charCodeAt(i));
|
|
127
|
+
if (code === JSON_BACKSLASH) {
|
|
128
|
+
i += 2;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (code === JSON_QUOTE) {
|
|
132
|
+
return i + 1;
|
|
133
|
+
}
|
|
134
|
+
i += 1;
|
|
135
|
+
}
|
|
136
|
+
return -1;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The index just past the value that starts at `at`, or `-1` when it does not
|
|
141
|
+
* end.
|
|
142
|
+
*
|
|
143
|
+
* A string ends at its quote and an object or array at the closer that brings
|
|
144
|
+
* the depth back to zero, with strings inside it skipped whole so that a `}` in
|
|
145
|
+
* a message cannot close it. Anything else — a number, `true`, `false`, `null` —
|
|
146
|
+
* ends at the first byte that cannot be part of it, which is the comma, the
|
|
147
|
+
* closer or the blank that follows.
|
|
148
|
+
*
|
|
149
|
+
* The string skip inside an object is a flag in the one loop rather than a call
|
|
150
|
+
* to `jsonEndOfString` that moves the cursor to wherever it answers: a cursor
|
|
151
|
+
* that only ever steps forward keeps the lower bound the bounds proof needs,
|
|
152
|
+
* and one assigned a callee's answer does not.
|
|
153
|
+
*/
|
|
154
|
+
const jsonEndOfValue = (text: string, at: i32): i32 => {
|
|
155
|
+
const length: i32 = toI32(text.length);
|
|
156
|
+
if (at < 0 || at >= length) {
|
|
157
|
+
return -1;
|
|
158
|
+
}
|
|
159
|
+
const first: i32 = toI32(text.charCodeAt(at));
|
|
160
|
+
if (first === JSON_QUOTE) {
|
|
161
|
+
return jsonEndOfString(text, at);
|
|
162
|
+
}
|
|
163
|
+
if (first === JSON_OPEN_BRACE || first === JSON_OPEN_BRACKET) {
|
|
164
|
+
let depth: i32 = 0;
|
|
165
|
+
let inString: boolean = false;
|
|
166
|
+
let i: i32 = at;
|
|
167
|
+
while (i < length) {
|
|
168
|
+
const code: i32 = toI32(text.charCodeAt(i));
|
|
169
|
+
if (inString) {
|
|
170
|
+
// `jsonEndOfString`'s rule: a backslash takes the byte after it along.
|
|
171
|
+
if (code === JSON_BACKSLASH) {
|
|
172
|
+
i += 2;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (code === JSON_QUOTE) {
|
|
176
|
+
inString = false;
|
|
177
|
+
}
|
|
178
|
+
i += 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (code === JSON_QUOTE) {
|
|
182
|
+
inString = true;
|
|
183
|
+
} else if (code === JSON_OPEN_BRACE || code === JSON_OPEN_BRACKET) {
|
|
184
|
+
depth += 1;
|
|
185
|
+
} else if (code === JSON_CLOSE_BRACE || code === JSON_CLOSE_BRACKET) {
|
|
186
|
+
depth -= 1;
|
|
187
|
+
if (depth === 0) {
|
|
188
|
+
return i + 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
i += 1;
|
|
192
|
+
}
|
|
193
|
+
return -1;
|
|
194
|
+
}
|
|
195
|
+
let i: i32 = at;
|
|
196
|
+
while (i < length) {
|
|
197
|
+
const code: i32 = toI32(text.charCodeAt(i));
|
|
198
|
+
if (code === JSON_COMMA || code === JSON_CLOSE_BRACE || code === JSON_CLOSE_BRACKET) {
|
|
199
|
+
return i;
|
|
200
|
+
}
|
|
201
|
+
if (jsonIsBlankByte(code)) {
|
|
202
|
+
return i;
|
|
203
|
+
}
|
|
204
|
+
i += 1;
|
|
205
|
+
}
|
|
206
|
+
return length;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** The value of one lower-case or upper-case hex digit, or `-1`. */
|
|
210
|
+
const jsonHexDigit = (code: i32): i32 => {
|
|
211
|
+
if (code >= 48 && code <= 57) {
|
|
212
|
+
return code - 48;
|
|
213
|
+
}
|
|
214
|
+
if (code >= 97 && code <= 102) {
|
|
215
|
+
return code - 87;
|
|
216
|
+
}
|
|
217
|
+
if (code >= 65 && code <= 70) {
|
|
218
|
+
return code - 55;
|
|
219
|
+
}
|
|
220
|
+
return -1;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** The four hex digits at `at` as one code point, or `-1` when they are not four hex digits. */
|
|
224
|
+
const jsonHex4 = (text: string, at: i32, end: i32): i32 => {
|
|
225
|
+
if (at + 4 > end) {
|
|
226
|
+
return -1;
|
|
227
|
+
}
|
|
228
|
+
let value: i32 = 0;
|
|
229
|
+
let i: i32 = 0;
|
|
230
|
+
while (i < 4) {
|
|
231
|
+
const digit: i32 = jsonHexDigit(toI32(text.charCodeAt(at + i)));
|
|
232
|
+
if (digit < 0) {
|
|
233
|
+
return -1;
|
|
234
|
+
}
|
|
235
|
+
value = value * JSON_HEX_BASE + digit;
|
|
236
|
+
i += 1;
|
|
237
|
+
}
|
|
238
|
+
return value;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* `code` as UTF-8, one to three bytes.
|
|
243
|
+
*
|
|
244
|
+
* A surrogate pair is **not** recombined: each half becomes its own three-byte
|
|
245
|
+
* sequence, which is what `😀` reads as here. Nothing this module
|
|
246
|
+
* exists to read produces one — `JSON.stringify`, and `jsonQuote` in
|
|
247
|
+
* `self/strings.ts` which matches it byte for byte, escape only the seven short
|
|
248
|
+
* forms and `\u00xx` below `0x20`, and pass every other byte through as itself —
|
|
249
|
+
* so the alternative would be code with no caller to keep it honest.
|
|
250
|
+
*/
|
|
251
|
+
const jsonUtf8 = (code: i32): string => {
|
|
252
|
+
if (code < JSON_UTF8_TWO_BYTE_FLOOR) {
|
|
253
|
+
return String.fromCharCode(code);
|
|
254
|
+
}
|
|
255
|
+
if (code < JSON_UTF8_THREE_BYTE_FLOOR) {
|
|
256
|
+
const lead: string = String.fromCharCode(JSON_UTF8_TWO_BYTE_LEAD + (code >> 6));
|
|
257
|
+
return `${lead}${String.fromCharCode(JSON_UTF8_CONTINUATION + (code & JSON_UTF8_LOW_SIX))}`;
|
|
258
|
+
}
|
|
259
|
+
const lead: string = String.fromCharCode(JSON_UTF8_THREE_BYTE_LEAD + (code >> 12));
|
|
260
|
+
const middle: string = String.fromCharCode(JSON_UTF8_CONTINUATION + ((code >> 6) & JSON_UTF8_LOW_SIX));
|
|
261
|
+
return `${lead}${middle}${String.fromCharCode(JSON_UTF8_CONTINUATION + (code & JSON_UTF8_LOW_SIX))}`;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The bytes of `text[at..end)` with the JSON escapes decoded — the body of a
|
|
266
|
+
* string literal, without its quotes.
|
|
267
|
+
*
|
|
268
|
+
* A literal with no backslash in it is answered as a substring, which is the
|
|
269
|
+
* common case and the whole reason for the first loop: a diagnostic's `file` and
|
|
270
|
+
* `code` never carry an escape, and building them out of parts would allocate an
|
|
271
|
+
* array to copy a string that was already there. An unknown escape stands for
|
|
272
|
+
* the byte it escapes, which is what `\"`, `\\` and `\/` want and is the lenient
|
|
273
|
+
* reading of anything else.
|
|
274
|
+
*/
|
|
275
|
+
const jsonUnescape = (text: string, at: i32, end: i32): string => {
|
|
276
|
+
// Both callers pass the inside of a literal `jsonEndOfString` found, so
|
|
277
|
+
// `0 <= at` and `end <= text.length` always hold. Saying so here is what
|
|
278
|
+
// proves every read below in range and drops the `substring` clamps.
|
|
279
|
+
if (at < 0 || end > toI32(text.length)) {
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
282
|
+
let i: i32 = at;
|
|
283
|
+
while (i < end && toI32(text.charCodeAt(i)) !== JSON_BACKSLASH) {
|
|
284
|
+
i += 1;
|
|
285
|
+
}
|
|
286
|
+
if (i >= end) {
|
|
287
|
+
return text.substring(at, end);
|
|
288
|
+
}
|
|
289
|
+
const parts: string[] = [];
|
|
290
|
+
let start: i32 = at;
|
|
291
|
+
while (i < end) {
|
|
292
|
+
if (toI32(text.charCodeAt(i)) !== JSON_BACKSLASH) {
|
|
293
|
+
i += 1;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
// `start === i` is an escape straight after another, and the empty run
|
|
297
|
+
// between them adds nothing to the join. The `start >= 0` half is always
|
|
298
|
+
// true — `start` is `at` or a cursor that has only moved forward — and is
|
|
299
|
+
// there because `start` is reassigned in the loop, which is where the
|
|
300
|
+
// bounds proof stops following it.
|
|
301
|
+
if (start >= 0 && start < i) {
|
|
302
|
+
parts.push(text.substring(start, i));
|
|
303
|
+
}
|
|
304
|
+
// A trailing backslash has nothing after it: `0` is no escape letter, so it
|
|
305
|
+
// falls to the default below and stands for itself.
|
|
306
|
+
const letter: i32 = i + 1 < end ? toI32(text.charCodeAt(i + 1)) : 0;
|
|
307
|
+
if (letter === 110) {
|
|
308
|
+
parts.push("\n");
|
|
309
|
+
i += 2;
|
|
310
|
+
} else if (letter === 116) {
|
|
311
|
+
parts.push("\t");
|
|
312
|
+
i += 2;
|
|
313
|
+
} else if (letter === 114) {
|
|
314
|
+
parts.push("\r");
|
|
315
|
+
i += 2;
|
|
316
|
+
} else if (letter === 98) {
|
|
317
|
+
parts.push(String.fromCharCode(8));
|
|
318
|
+
i += 2;
|
|
319
|
+
} else if (letter === 102) {
|
|
320
|
+
parts.push(String.fromCharCode(12));
|
|
321
|
+
i += 2;
|
|
322
|
+
} else if (letter === 117) {
|
|
323
|
+
const code: i32 = jsonHex4(text, i + 2, end);
|
|
324
|
+
if (code < 0) {
|
|
325
|
+
// Not four hex digits after the `u`: the escape is broken, and copying it
|
|
326
|
+
// through unchanged is the reading that loses nothing.
|
|
327
|
+
parts.push("\\u");
|
|
328
|
+
i += 2;
|
|
329
|
+
} else {
|
|
330
|
+
parts.push(jsonUtf8(code));
|
|
331
|
+
i += 6;
|
|
332
|
+
}
|
|
333
|
+
} else if (letter === 0) {
|
|
334
|
+
parts.push("\\");
|
|
335
|
+
i += 1;
|
|
336
|
+
} else {
|
|
337
|
+
parts.push(String.fromCharCode(letter));
|
|
338
|
+
i += 2;
|
|
339
|
+
}
|
|
340
|
+
start = i;
|
|
341
|
+
}
|
|
342
|
+
parts.push(text.substring(start, end));
|
|
343
|
+
return parts.join("");
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The value of `name` in `object`, or `null` when the object does not have that
|
|
348
|
+
* field.
|
|
349
|
+
*
|
|
350
|
+
* A string value comes back unquoted and unescaped; any other value comes back
|
|
351
|
+
* as the bytes it was written with, so `parseInt` reads a number and `=== "true"`
|
|
352
|
+
* reads a boolean. The scan is left to right and answers the **first** field of
|
|
353
|
+
* that name, which is what a duplicated key means to every JSON reader that does
|
|
354
|
+
* not build a map.
|
|
355
|
+
*
|
|
356
|
+
* A nested value is stepped over rather than searched, so `name` is a field of
|
|
357
|
+
* this object and not a path into it: that is the "flat" in the module header,
|
|
358
|
+
* and it is the shape the compiler's `--json` line has.
|
|
359
|
+
*/
|
|
360
|
+
export const jsonField = (object: string, name: string): string | null => {
|
|
361
|
+
const length: i32 = toI32(object.length);
|
|
362
|
+
let i: i32 = jsonSkipBlank(object, 0);
|
|
363
|
+
if (i >= length || toI32(object.charCodeAt(i)) !== JSON_OPEN_BRACE) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
i = jsonSkipBlank(object, i + 1);
|
|
367
|
+
// `jsonSkipBlank` never answers a negative index for a non-negative one, but
|
|
368
|
+
// the bounds proof does not look inside a callee, so every cursor it answers
|
|
369
|
+
// is tested for `i < 0` beside `i >= length`: the pair is what lets each read
|
|
370
|
+
// below go without a check.
|
|
371
|
+
while (i >= 0 && i < length && toI32(object.charCodeAt(i)) === JSON_QUOTE) {
|
|
372
|
+
const keyEnd: i32 = jsonEndOfString(object, i);
|
|
373
|
+
if (keyEnd < 0) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
const key: string = jsonUnescape(object, i + 1, keyEnd - 1);
|
|
377
|
+
i = jsonSkipBlank(object, keyEnd);
|
|
378
|
+
if (i < 0 || i >= length || toI32(object.charCodeAt(i)) !== JSON_COLON) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
const valueAt: i32 = jsonSkipBlank(object, i + 1);
|
|
382
|
+
const valueEnd: i32 = jsonEndOfValue(object, valueAt);
|
|
383
|
+
// `jsonEndOfValue` answers `-1` for a `valueAt` outside the object and never
|
|
384
|
+
// answers past its end, so the last three tests change no answer: they are
|
|
385
|
+
// the range the value's first byte and its `substring` are proved in.
|
|
386
|
+
if (valueEnd < 0 || valueAt < 0 || valueAt >= length || valueEnd > toI32(object.length)) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
if (key === name) {
|
|
390
|
+
if (toI32(object.charCodeAt(valueAt)) === JSON_QUOTE) {
|
|
391
|
+
return jsonUnescape(object, valueAt + 1, valueEnd - 1);
|
|
392
|
+
}
|
|
393
|
+
return object.substring(valueAt, valueEnd);
|
|
394
|
+
}
|
|
395
|
+
i = jsonSkipBlank(object, valueEnd);
|
|
396
|
+
if (i < 0 || i >= length || toI32(object.charCodeAt(i)) !== JSON_COMMA) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
i = jsonSkipBlank(object, i + 1);
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
};
|
package/std/pair.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `std/pair` — two values answered by one call.
|
|
3
|
+
*
|
|
4
|
+
* It exists for the function that has two things to say, like the lexer's
|
|
5
|
+
* `scanEscape`: where an escape ended, and whether it was malformed. Without a
|
|
6
|
+
* pair, the second answer lives in a field on some object, written by the
|
|
7
|
+
* callee and read back by the caller three lines later. With one, both come
|
|
8
|
+
* back in the return:
|
|
9
|
+
*
|
|
10
|
+
* import { Pair } from "nish/pair";
|
|
11
|
+
*
|
|
12
|
+
* const scanEscape = (at: i32): Pair<i32, boolean> => ({ first: at + 2, second: true });
|
|
13
|
+
*
|
|
14
|
+
* It is **for returning two values, not for storing them**. A struct held in an
|
|
15
|
+
* array or a class field should have named fields, and parallel arrays are the
|
|
16
|
+
* house style for two values side by side (`docs/wp15-performance.md` §1a), so
|
|
17
|
+
* a `Pair[]` is usually the slower shape and the less readable one.
|
|
18
|
+
*
|
|
19
|
+
* It is an `interface` rather than a class, so there is no constructor to call
|
|
20
|
+
* and nothing to import but the type: an object literal takes its type from the
|
|
21
|
+
* annotation, and the checker holds it to setting `first` and `second` exactly
|
|
22
|
+
* once each. There is no tuple syntax behind it and no special case in the
|
|
23
|
+
* compiler; `docs/wp23-language-surface.md` §5 is the argument for that.
|
|
24
|
+
*/
|
|
25
|
+
export interface Pair<A, B> {
|
|
26
|
+
first: A;
|
|
27
|
+
second: B;
|
|
28
|
+
}
|