functionalscript 0.35.1 → 0.36.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/fs/base64/module.f.js +6 -5
- package/fs/base64/proof.f.d.ts +1 -0
- package/fs/base64/proof.f.js +10 -1
- package/fs/cas/mcp/module.f.d.ts +8 -4
- package/fs/cas/mcp/module.f.js +33 -21
- package/fs/cas/mcp/proof.f.d.ts +0 -7
- package/fs/cas/mcp/proof.f.js +47 -139
- package/fs/cas/module.f.js +3 -10
- package/fs/ci/config/module.f.d.ts +7 -7
- package/fs/ci/config/module.f.js +8 -8
- package/fs/crypto/sha2/module.f.js +20 -19
- package/fs/crypto/sha2/proof.f.d.ts +1 -0
- package/fs/crypto/sha2/proof.f.js +18 -0
- package/fs/djs/parser/proof.f.js +12 -0
- package/fs/effects/module.f.d.ts +15 -0
- package/fs/effects/module.f.js +14 -0
- package/fs/effects/node/module.f.js +5 -15
- package/fs/effects/node/module.js +3 -1
- package/fs/effects/node/proof.f.d.ts +3 -0
- package/fs/effects/node/proof.f.js +19 -1
- package/fs/effects/node/virtual/module.f.js +1 -2
- package/fs/effects/proof.f.d.ts +5 -0
- package/fs/effects/proof.f.js +22 -1
- package/fs/fjs/proof.f.d.ts +1 -0
- package/fs/fjs/proof.f.js +6 -0
- package/fs/fsc/module.f.d.ts +1 -0
- package/fs/fsc/module.f.js +5 -1
- package/fs/js/tokenizer/module.f.d.ts +1 -1
- package/fs/js/tokenizer/module.f.js +7 -1
- package/fs/js/tokenizer/proof.f.js +59 -0
- package/fs/json/parser/proof.f.js +18 -0
- package/fs/path/module.f.d.ts +6 -0
- package/fs/path/module.f.js +6 -0
- package/fs/path/proof.f.d.ts +1 -0
- package/fs/path/proof.f.js +40 -2
- package/fs/text/utf8/module.f.d.ts +6 -0
- package/fs/text/utf8/module.f.js +15 -2
- package/fs/text/utf8/proof.f.js +53 -0
- package/fs/types/result/module.f.d.ts +4 -0
- package/fs/types/result/module.f.js +4 -0
- package/fs/types/result/proof.f.d.ts +1 -0
- package/fs/types/result/proof.f.js +12 -2
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { mask } from "../../types/bigint/module.f.js";
|
|
2
|
-
import { vec, length, empty, msb } from "../../types/bit_vec/module.f.js";
|
|
2
|
+
import { vec, length, empty, msb, chunkList, uint } from "../../types/bit_vec/module.f.js";
|
|
3
3
|
import { fold } from "../../types/list/module.f.js";
|
|
4
|
-
const { concat,
|
|
4
|
+
const { concat, front } = msb;
|
|
5
|
+
// `chunkList(msb)` depends on neither `chunkLength` nor `v`/`state` — shared
|
|
6
|
+
// across every `base(...)` config (32-bit and 64-bit SHA-2 variants).
|
|
7
|
+
const chunkListMsb = chunkList(msb);
|
|
5
8
|
const lastOne = vec(1n)(1n);
|
|
6
9
|
const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }) => {
|
|
7
10
|
const bitLength = 1n << logBitLen;
|
|
@@ -109,6 +112,20 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }) => {
|
|
|
109
112
|
]);
|
|
110
113
|
};
|
|
111
114
|
const chunkLength = bitLength << 4n; // * 16
|
|
115
|
+
// `chunkListMsb(chunkLength)` depends on `chunkLength` but not `v`/`state`
|
|
116
|
+
// — computed once per `base(...)` config, not once per `append` call.
|
|
117
|
+
const chunkListChunkLength = chunkListMsb(chunkLength);
|
|
118
|
+
// Folds one block (or the final, shorter-than-`chunkLength` leftover) into
|
|
119
|
+
// `State`. `chunkList` yields chunks of exactly `chunkLength` bits except
|
|
120
|
+
// possibly the last one, which is why `remainder` only ever holds that
|
|
121
|
+
// last chunk (`empty` otherwise) — same shape as `State` itself, so no
|
|
122
|
+
// separate accumulator type is needed.
|
|
123
|
+
const appendChunk = (chunk) => (state) => length(chunk) === chunkLength
|
|
124
|
+
? { hash: compress(state.hash)(uint(chunk)), len: state.len + chunkLength, remainder: empty }
|
|
125
|
+
: { ...state, remainder: chunk };
|
|
126
|
+
// `fold(appendChunk)` depends on neither `v` nor `state` — only the
|
|
127
|
+
// starting accumulator passed to it per `append` call does.
|
|
128
|
+
const foldChunks = fold(appendChunk);
|
|
112
129
|
const fromV8 = (a) => a.reduce((p, v) => (p << bitLength) | v);
|
|
113
130
|
// See https://www.rfc-editor.org/rfc/rfc6234#section-4
|
|
114
131
|
const lastChunkLength = chunkLength - 1n - (bitLength << 1n);
|
|
@@ -117,23 +134,7 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }) => {
|
|
|
117
134
|
chunkLength,
|
|
118
135
|
compress,
|
|
119
136
|
fromV8,
|
|
120
|
-
append: (v) => (state) => {
|
|
121
|
-
let { remainder, hash, len } = state;
|
|
122
|
-
remainder = concat(remainder)(v);
|
|
123
|
-
let remainderLen = length(remainder);
|
|
124
|
-
while (remainderLen >= chunkLength) {
|
|
125
|
-
const [u, nr] = popFront(chunkLength)(remainder);
|
|
126
|
-
hash = compress(hash)(u);
|
|
127
|
-
remainder = nr;
|
|
128
|
-
remainderLen -= chunkLength;
|
|
129
|
-
len += chunkLength;
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
hash,
|
|
133
|
-
len,
|
|
134
|
-
remainder
|
|
135
|
-
};
|
|
136
|
-
},
|
|
137
|
+
append: (v) => (state) => foldChunks({ ...state, remainder: empty })(chunkListChunkLength(concat(state.remainder)(v))),
|
|
137
138
|
end: (hashLength) => {
|
|
138
139
|
const offset = (bitLength << 3n) - hashLength;
|
|
139
140
|
const result = vec(hashLength);
|
|
@@ -149,4 +149,22 @@ export const proof = {
|
|
|
149
149
|
}
|
|
150
150
|
},
|
|
151
151
|
},
|
|
152
|
+
// Regression guard (sha2-append-quadratic): `append` used to be
|
|
153
|
+
// quadratic in the input size, not linear — a single call on a
|
|
154
|
+
// 100,000-byte `Vec` used to cost real seconds and scale worse than
|
|
155
|
+
// O(n²); now near-linear (~40-60ms on Node, well under `bun test`'s 5s
|
|
156
|
+
// per-test limit on Bun). No timing assertion (duration varies by
|
|
157
|
+
// engine/machine); relies on the test runner's own per-test timing to
|
|
158
|
+
// catch a regression, same convention as `fs/base64/proof.f.ts`
|
|
159
|
+
// `encodeLargeVecIsSlow`.
|
|
160
|
+
appendLargeVecIsFast: () => {
|
|
161
|
+
const big = repeat(100000n)(vec(8n)(0xffn));
|
|
162
|
+
let state = sha256.init;
|
|
163
|
+
state = sha256.append(big)(state);
|
|
164
|
+
const h = sha256.end(state);
|
|
165
|
+
const x = 0xbe87f6dbe42cdf682276fbecab3636fbfcaa008cf454d635dd77872b50d940aan;
|
|
166
|
+
if (uint(h) !== x) {
|
|
167
|
+
throw h;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
152
170
|
};
|
package/fs/djs/parser/proof.f.js
CHANGED
|
@@ -251,6 +251,18 @@ export const proof = {
|
|
|
251
251
|
throw obj[1].message;
|
|
252
252
|
}
|
|
253
253
|
},
|
|
254
|
+
// A literal control character inside a string is not valid JSON
|
|
255
|
+
// syntax (RFC 8259 §7), and DJS string literals are JSON strings.
|
|
256
|
+
() => {
|
|
257
|
+
const tokenList = tokenizeString('export default "\t"');
|
|
258
|
+
const obj = parseFromTokens(tokenList);
|
|
259
|
+
if (obj[0] !== 'error') {
|
|
260
|
+
throw obj;
|
|
261
|
+
}
|
|
262
|
+
if (obj[1].message !== 'unexpected token') {
|
|
263
|
+
throw obj[1].message;
|
|
264
|
+
}
|
|
265
|
+
},
|
|
254
266
|
() => {
|
|
255
267
|
const tokenList = tokenizeString('export default [,]');
|
|
256
268
|
const obj = parseFromTokens(tokenList);
|
package/fs/effects/module.f.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core effect type constructors and combinators.
|
|
3
3
|
*
|
|
4
|
+
* Effect helpers are **step adapters**: functions that return a continuation
|
|
5
|
+
* `(t: T) => Effect<Q, R>` meant to be passed into `.step`, never wrappers
|
|
6
|
+
* that take the effect itself as an argument. `Effect` has no pipeline
|
|
7
|
+
* operator to lean on and must not be extended with new methods, so
|
|
8
|
+
* `.step(adapterA).step(adapterB)` is how helpers compose — flat,
|
|
9
|
+
* left-to-right, in evaluation order. See {@link okStep} for an example.
|
|
10
|
+
*
|
|
4
11
|
* @module
|
|
5
12
|
*/
|
|
6
13
|
import { type List } from '../types/list/module.f.ts';
|
|
14
|
+
import type { Result } from '../types/result/module.f.ts';
|
|
7
15
|
export type Operation = readonly [string, (..._: readonly never[]) => unknown];
|
|
8
16
|
export type Effect<O extends Operation, T> = {
|
|
9
17
|
value: Value<O, T>;
|
|
@@ -43,6 +51,13 @@ export declare const foldStep: <O extends Operation, T, S>(f: (item: T) => (stat
|
|
|
43
51
|
* results. The `void` accumulator sibling of `foldStep`.
|
|
44
52
|
*/
|
|
45
53
|
export declare const forEachStep: <O extends Operation, T>(f: (item: T) => Effect<O, void>) => (items: List<T>) => Effect<O, void>;
|
|
54
|
+
/**
|
|
55
|
+
* A step adapter for the `error` short-circuit: `error` → pass it through
|
|
56
|
+
* unchanged as `pure`, `ok` → continue with `f`. Collapses the hand-written
|
|
57
|
+
* `r[0] === 'error' ? pure(r) : f(r[1])` check that recurs at every site
|
|
58
|
+
* chaining `Effect<O, Result<T, E>>` steps.
|
|
59
|
+
*/
|
|
60
|
+
export declare const okStep: <T, E, O extends Operation, R>(f: (value: T) => Effect<O, Result<R, E>>) => (r: Result<T, E>) => Effect<O, Result<R, E>>;
|
|
46
61
|
/**
|
|
47
62
|
* The decoded form of an effect's next step: either a final `result`, or a
|
|
48
63
|
* `command` to perform with its `payload` and the `continuation` to resume
|
package/fs/effects/module.f.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core effect type constructors and combinators.
|
|
3
3
|
*
|
|
4
|
+
* Effect helpers are **step adapters**: functions that return a continuation
|
|
5
|
+
* `(t: T) => Effect<Q, R>` meant to be passed into `.step`, never wrappers
|
|
6
|
+
* that take the effect itself as an argument. `Effect` has no pipeline
|
|
7
|
+
* operator to lean on and must not be extended with new methods, so
|
|
8
|
+
* `.step(adapterA).step(adapterB)` is how helpers compose — flat,
|
|
9
|
+
* left-to-right, in evaluation order. See {@link okStep} for an example.
|
|
10
|
+
*
|
|
4
11
|
* @module
|
|
5
12
|
*/
|
|
6
13
|
import { fold } from "../types/list/module.f.js";
|
|
@@ -39,6 +46,13 @@ export const foldStep = (f) => (init) => (items) => fold(item => acc => acc.step
|
|
|
39
46
|
* results. The `void` accumulator sibling of `foldStep`.
|
|
40
47
|
*/
|
|
41
48
|
export const forEachStep = (f) => (items) => foldStep((item) => () => f(item))(undefined)(items);
|
|
49
|
+
/**
|
|
50
|
+
* A step adapter for the `error` short-circuit: `error` → pass it through
|
|
51
|
+
* unchanged as `pure`, `ok` → continue with `f`. Collapses the hand-written
|
|
52
|
+
* `r[0] === 'error' ? pure(r) : f(r[1])` check that recurs at every site
|
|
53
|
+
* chaining `Effect<O, Result<T, E>>` steps.
|
|
54
|
+
*/
|
|
55
|
+
export const okStep = (f) => (r) => r[0] === 'error' ? pure(r) : f(r[1]);
|
|
42
56
|
/**
|
|
43
57
|
* Decodes an effect's next step: a pure result, or a command to perform.
|
|
44
58
|
*
|
|
@@ -13,8 +13,8 @@ import { toCodePointList } from "../../text/utf8/module.f.js";
|
|
|
13
13
|
import { codePointListToString } from "../../text/utf16/module.f.js";
|
|
14
14
|
import { reverse } from "../../types/list/module.f.js";
|
|
15
15
|
import { length } from "../../types/bit_vec/module.f.js";
|
|
16
|
-
import { ok, error as resultError } from "../../types/result/module.f.js";
|
|
17
|
-
import { do_, pure } from "../module.f.js";
|
|
16
|
+
import { ok, error as resultError, mapOk } from "../../types/result/module.f.js";
|
|
17
|
+
import { do_, okStep, pure } from "../module.f.js";
|
|
18
18
|
/**
|
|
19
19
|
* True if `e` is a "file or directory does not exist" (`ENOENT`) error.
|
|
20
20
|
*
|
|
@@ -43,7 +43,7 @@ export const readFile = do_('readFile');
|
|
|
43
43
|
* on errors (e.g. convert them into domain-specific errors) or `unwrap` at the
|
|
44
44
|
* call site.
|
|
45
45
|
*/
|
|
46
|
-
export const readUtf8File = (path) => readFile(path).step(r => pure(
|
|
46
|
+
export const readUtf8File = (path) => readFile(path).step(r => pure(mapOk(utf8ToString)(r)));
|
|
47
47
|
export const readdir = do_('readdir');
|
|
48
48
|
export const writeFile = do_('writeFile');
|
|
49
49
|
/** Writes a string to `path` as UTF-8 bytes. */
|
|
@@ -70,22 +70,12 @@ const writeLoop = (path) => {
|
|
|
70
70
|
return pure(resultError('invalid buffer size'));
|
|
71
71
|
}
|
|
72
72
|
return writeBytes(path, offset, v)
|
|
73
|
-
.step((
|
|
74
|
-
if (r[0] === 'error') {
|
|
75
|
-
return pure(r);
|
|
76
|
-
}
|
|
77
|
-
return f(offset + Number(lenV >> 3n), tail);
|
|
78
|
-
});
|
|
73
|
+
.step(okStep(() => f(offset + Number(lenV >> 3n), tail)));
|
|
79
74
|
});
|
|
80
75
|
return f;
|
|
81
76
|
};
|
|
82
77
|
export const writeFromStream = (path, e) => createExclusive(path)
|
|
83
|
-
.step((
|
|
84
|
-
if (r === 'error') {
|
|
85
|
-
return pure(resultError(v));
|
|
86
|
-
}
|
|
87
|
-
return writeLoop(path)(0, e);
|
|
88
|
-
});
|
|
78
|
+
.step(okStep(() => writeLoop(path)(0, e)));
|
|
89
79
|
export const stat = do_('stat');
|
|
90
80
|
export const createServer = do_('createServer');
|
|
91
81
|
export const listen = do_('listen');
|
|
@@ -146,6 +146,8 @@ const readStdinByte = async () => {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
};
|
|
149
|
+
const randomMax = Number(1n << 32n);
|
|
150
|
+
const { randomInt } = crypto;
|
|
149
151
|
const runNodeEffect = asyncRun({
|
|
150
152
|
...memoryOperationMap(),
|
|
151
153
|
all: async (...effects) => await Promise.all(effects.map(runNodeEffect)),
|
|
@@ -191,7 +193,7 @@ const runNodeEffect = asyncRun({
|
|
|
191
193
|
await fh.close();
|
|
192
194
|
}
|
|
193
195
|
}),
|
|
194
|
-
randomInt: async () =>
|
|
196
|
+
randomInt: async () => randomInt(randomMax),
|
|
195
197
|
access: path => asyncTryCatch(() => access(path)),
|
|
196
198
|
createExclusive: path => asyncTryCatch(async () => {
|
|
197
199
|
const fh = await open(path, 'wx');
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { empty, isVec, uint, vec8 } from "../../types/bit_vec/module.f.js";
|
|
2
2
|
import { utf8, utf8ToString } from "../../text/module.f.js";
|
|
3
3
|
import { decode, pure } from "../module.f.js";
|
|
4
|
-
import { both, fetch, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, writeFile, writeUtf8File, rename, readBytes, randomInt } from "./module.f.js";
|
|
4
|
+
import { both, fetch, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, writeFile, writeUtf8File, rename, readBytes, randomInt, writeFromStream } from "./module.f.js";
|
|
5
5
|
import { create as memCreate, read as memRead, write as memWrite } from "../memory/module.f.js";
|
|
6
|
+
import { empty as listEmpty } from "../list/module.f.js";
|
|
6
7
|
import { emptyState, virtual } from "./virtual/module.f.js";
|
|
7
8
|
export const proof = {
|
|
8
9
|
map: () => {
|
|
@@ -553,4 +554,21 @@ export const proof = {
|
|
|
553
554
|
}
|
|
554
555
|
},
|
|
555
556
|
},
|
|
557
|
+
writeFromStream: {
|
|
558
|
+
createExclusiveFails: () => {
|
|
559
|
+
// The destination already exists, so `createExclusive` fails (EEXIST) and
|
|
560
|
+
// the error propagates without ever touching `writeBytes`.
|
|
561
|
+
const [state, [t, result]] = virtual({
|
|
562
|
+
...emptyState,
|
|
563
|
+
root: { hello: [vec8(0x2an)] },
|
|
564
|
+
})(writeFromStream('hello', listEmpty()));
|
|
565
|
+
if (t !== 'error') {
|
|
566
|
+
throw result;
|
|
567
|
+
}
|
|
568
|
+
const file = state.root.hello;
|
|
569
|
+
if (!Array.isArray(file) || uint(file[0]) !== 0x2an) {
|
|
570
|
+
throw file;
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
},
|
|
556
574
|
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import { todo } from "../../../asserts/module.f.js";
|
|
7
|
-
import { join, parse } from "../../../path/module.f.js";
|
|
7
|
+
import { isProperPrefix, join, parse } from "../../../path/module.f.js";
|
|
8
8
|
import { utf8ToString } from "../../../text/module.f.js";
|
|
9
9
|
import { empty, length, maxLengthBytes, msb, vec } from "../../../types/bit_vec/module.f.js";
|
|
10
10
|
import { error, ok } from "../../../types/result/module.f.js";
|
|
@@ -218,7 +218,6 @@ const insertEntityAt = (dir, path, entity) => {
|
|
|
218
218
|
}
|
|
219
219
|
return [{ ...dir, [first]: newSub }, result];
|
|
220
220
|
};
|
|
221
|
-
const isProperPrefix = (prefix, path) => prefix.length < path.length && prefix.every((seg, i) => seg === path[i]);
|
|
222
221
|
const rename = (src, dst) => (state) => {
|
|
223
222
|
const srcParsed = parse(src);
|
|
224
223
|
const dstParsed = parse(dst);
|
package/fs/effects/proof.f.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare const proof: {
|
|
|
2
2
|
lazy: {
|
|
3
3
|
value: () => void;
|
|
4
4
|
deferred: () => void;
|
|
5
|
+
step: () => void;
|
|
5
6
|
};
|
|
6
7
|
foldStep: {
|
|
7
8
|
empty: () => void;
|
|
@@ -12,6 +13,10 @@ export declare const proof: {
|
|
|
12
13
|
empty: () => void;
|
|
13
14
|
runs: () => void;
|
|
14
15
|
};
|
|
16
|
+
okStep: {
|
|
17
|
+
ok: () => void;
|
|
18
|
+
error: () => void;
|
|
19
|
+
};
|
|
15
20
|
decode: () => void;
|
|
16
21
|
match: {
|
|
17
22
|
done: () => void;
|
package/fs/effects/proof.f.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { decode, do_, foldStep, forEachStep, lazy, match, pure } from "./module.f.js";
|
|
1
|
+
import { decode, do_, foldStep, forEachStep, lazy, match, okStep, pure } from "./module.f.js";
|
|
2
|
+
import { error, ok } from "../types/result/module.f.js";
|
|
2
3
|
const assertPure = (e, expected) => {
|
|
3
4
|
const d = decode(e);
|
|
4
5
|
if (!d.done) {
|
|
@@ -26,6 +27,10 @@ export const proof = {
|
|
|
26
27
|
throw 'decode must force the thunk';
|
|
27
28
|
}
|
|
28
29
|
},
|
|
30
|
+
step: () => {
|
|
31
|
+
const e = lazy(() => 5).step(v => pure(v * 2));
|
|
32
|
+
assertPure(e, 10);
|
|
33
|
+
},
|
|
29
34
|
},
|
|
30
35
|
foldStep: {
|
|
31
36
|
empty: () => {
|
|
@@ -51,6 +56,22 @@ export const proof = {
|
|
|
51
56
|
assertPure(e, undefined);
|
|
52
57
|
},
|
|
53
58
|
},
|
|
59
|
+
okStep: {
|
|
60
|
+
ok: () => {
|
|
61
|
+
const e = pure(ok(5)).step(okStep((v) => pure(ok(v * 2))));
|
|
62
|
+
const d = decode(e);
|
|
63
|
+
if (!d.done || d.result[0] !== 'ok' || d.result[1] !== 10) {
|
|
64
|
+
throw e.value;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
error: () => {
|
|
68
|
+
const e = pure(error('oops')).step(okStep(v => pure(ok(v * 2))));
|
|
69
|
+
const d = decode(e);
|
|
70
|
+
if (!d.done || d.result[0] !== 'error' || d.result[1] !== 'oops') {
|
|
71
|
+
throw e.value;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
},
|
|
54
75
|
decode: () => {
|
|
55
76
|
const d = decode(do_('add')(2, 3));
|
|
56
77
|
if (d.done) {
|
package/fs/fjs/proof.f.d.ts
CHANGED
package/fs/fjs/proof.f.js
CHANGED
|
@@ -22,6 +22,12 @@ export const proof = {
|
|
|
22
22
|
// `run` strips the command and file name, so `main` sees two arguments
|
|
23
23
|
assertEq(code, 2);
|
|
24
24
|
},
|
|
25
|
+
mcp: () => {
|
|
26
|
+
// stdin is empty in the virtual environment, so the server sees EOF
|
|
27
|
+
// immediately and shuts down cleanly, exercising the `mcp` handler.
|
|
28
|
+
const [, code] = run({})(['mcp']);
|
|
29
|
+
assertEq(code, 0);
|
|
30
|
+
},
|
|
25
31
|
throw: {
|
|
26
32
|
runImportError: () => {
|
|
27
33
|
run({})(['run', 'missing.f.ts']);
|
package/fs/fsc/module.f.d.ts
CHANGED
package/fs/fsc/module.f.js
CHANGED
|
@@ -9,6 +9,7 @@ import { reduce as listReduce, toArray, map } from "../types/list/module.f.js";
|
|
|
9
9
|
import { range as asciiRange } from "../text/ascii/module.f.js";
|
|
10
10
|
import { fn } from "../types/function/module.f.js";
|
|
11
11
|
import { one } from "../types/range/module.f.js";
|
|
12
|
+
import { assertEq } from "../asserts/module.f.js";
|
|
12
13
|
const fromCharCode = String.fromCharCode;
|
|
13
14
|
const unexpectedSymbol = codePoint => [[`unexpected symbol ${codePoint}`], unexpectedSymbol];
|
|
14
15
|
const def = () => unexpectedSymbol;
|
|
@@ -65,5 +66,8 @@ export const proof = {
|
|
|
65
66
|
b(undefined);
|
|
66
67
|
union(a)(b);
|
|
67
68
|
}
|
|
68
|
-
}
|
|
69
|
+
},
|
|
70
|
+
// `def` is the range-map's default handler; the public API never calls it directly
|
|
71
|
+
// (`init` covers every code point), so exercise it here where it's in scope.
|
|
72
|
+
defHandler: () => assertEq(def(undefined), unexpectedSymbol)
|
|
69
73
|
};
|
|
@@ -92,7 +92,7 @@ export type JsTokenWithMetadata = {
|
|
|
92
92
|
readonly token: JsToken;
|
|
93
93
|
readonly metadata: TokenMetadata;
|
|
94
94
|
};
|
|
95
|
-
type ErrorMessage = '" are missing' | 'unescaped character' | 'invalid hex value' | 'unexpected character' | 'invalid number' | 'invalid token' | '*\/ expected' | 'unterminated string literal' | 'eof';
|
|
95
|
+
type ErrorMessage = '" are missing' | 'unescaped character' | 'invalid hex value' | 'unexpected character' | 'invalid number' | 'invalid token' | '*\/ expected' | 'unterminated string literal' | 'unescaped control character in string' | 'eof';
|
|
96
96
|
export declare const isKeywordToken: (token: JsToken) => boolean;
|
|
97
97
|
export declare const tokenize: (input: List<number>) => (path: string) => List<JsTokenWithMetadata>;
|
|
98
98
|
export declare const proof: {
|
|
@@ -372,10 +372,16 @@ const invalidNumberStateOp = create(() => () => [empty, { kind: 'invalidNumber'
|
|
|
372
372
|
return [{ first: { kind: 'error', message: 'invalid number' }, tail: next[0] }, next[1]];
|
|
373
373
|
})
|
|
374
374
|
]);
|
|
375
|
+
const rangeSetStringControl = [
|
|
376
|
+
[0x00, 0x09],
|
|
377
|
+
[0x0b, 0x0c],
|
|
378
|
+
[0x0e, 0x1f],
|
|
379
|
+
];
|
|
375
380
|
const parseStringStateOp = create((state) => input => [empty, { kind: 'string', value: appendChar(state.value)(input) }])([
|
|
376
381
|
rangeFunc(one(quotationMark))(state => () => [[{ kind: 'string', value: state.value }], { kind: 'initial' }]),
|
|
377
382
|
rangeFunc(one(reverseSolidus))(state => () => [empty, { kind: 'escapeChar', value: state.value }]),
|
|
378
|
-
rangeSetFunc(rangeSetNewLine)(() => () => [[{ kind: 'error', message: 'unterminated string literal' }], { kind: 'nl' }])
|
|
383
|
+
rangeSetFunc(rangeSetNewLine)(() => () => [[{ kind: 'error', message: 'unterminated string literal' }], { kind: 'nl' }]),
|
|
384
|
+
rangeSetFunc(rangeSetStringControl)(state => () => [[{ kind: 'error', message: 'unescaped control character in string' }], { kind: 'string', value: state.value }])
|
|
379
385
|
]);
|
|
380
386
|
const parseEscapeDefault = state => input => {
|
|
381
387
|
const next = tokenizeOp(input, { kind: 'string', value: state.value });
|
|
@@ -141,6 +141,65 @@ export const proof = {
|
|
|
141
141
|
throw result;
|
|
142
142
|
}
|
|
143
143
|
},
|
|
144
|
+
() => {
|
|
145
|
+
// literal TAB inside a string is rejected; the escaped form is not
|
|
146
|
+
const result = stringify(tokenizeString('"\t"'));
|
|
147
|
+
if (result !== '[{"kind":"error","message":"unescaped control character in string"},{"kind":"string","value":""},{"kind":"eof"}]') {
|
|
148
|
+
throw result;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
() => {
|
|
152
|
+
const result = stringify(tokenizeString('"\\t"'));
|
|
153
|
+
if (result !== '[{"kind":"string","value":"\\t"},{"kind":"eof"}]') {
|
|
154
|
+
throw result;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
() => {
|
|
158
|
+
// literal VT (no JSON short escape; only \u000b is valid)
|
|
159
|
+
const result = stringify(tokenizeString('"\v"'));
|
|
160
|
+
if (result !== '[{"kind":"error","message":"unescaped control character in string"},{"kind":"string","value":""},{"kind":"eof"}]') {
|
|
161
|
+
throw result;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
() => {
|
|
165
|
+
const result = stringify(tokenizeString('"\\u000b"'));
|
|
166
|
+
if (result !== '[{"kind":"string","value":"\\u000b"},{"kind":"eof"}]') {
|
|
167
|
+
throw result;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
() => {
|
|
171
|
+
// literal FF is rejected; the escaped form is not
|
|
172
|
+
const result = stringify(tokenizeString('"\f"'));
|
|
173
|
+
if (result !== '[{"kind":"error","message":"unescaped control character in string"},{"kind":"string","value":""},{"kind":"eof"}]') {
|
|
174
|
+
throw result;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
() => {
|
|
178
|
+
const result = stringify(tokenizeString('"\\f"'));
|
|
179
|
+
if (result !== '[{"kind":"string","value":"\\f"},{"kind":"eof"}]') {
|
|
180
|
+
throw result;
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
() => {
|
|
184
|
+
// literal NUL (no JSON short escape; only \u0000 is valid)
|
|
185
|
+
const result = stringify(tokenizeString('"\0"'));
|
|
186
|
+
if (result !== '[{"kind":"error","message":"unescaped control character in string"},{"kind":"string","value":""},{"kind":"eof"}]') {
|
|
187
|
+
throw result;
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
() => {
|
|
191
|
+
const result = stringify(tokenizeString('"\\u0000"'));
|
|
192
|
+
if (result !== '[{"kind":"string","value":"\\u0000"},{"kind":"eof"}]') {
|
|
193
|
+
throw result;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
() => {
|
|
197
|
+
// control character surrounded by ordinary text still recovers the rest of the string
|
|
198
|
+
const result = stringify(tokenizeString('"a\tb"'));
|
|
199
|
+
if (result !== '[{"kind":"error","message":"unescaped control character in string"},{"kind":"string","value":"ab"},{"kind":"eof"}]') {
|
|
200
|
+
throw result;
|
|
201
|
+
}
|
|
202
|
+
},
|
|
144
203
|
() => {
|
|
145
204
|
const result = stringify(tokenizeString('"\\u1234"'));
|
|
146
205
|
if (result !== '[{"kind":"string","value":"ሴ"},{"kind":"eof"}]') {
|
|
@@ -155,6 +155,24 @@ export const proof = {
|
|
|
155
155
|
throw result;
|
|
156
156
|
}
|
|
157
157
|
},
|
|
158
|
+
// A literal control character inside a string is not valid JSON (RFC
|
|
159
|
+
// 8259 §7) even though the shared tokenizer would otherwise accept it.
|
|
160
|
+
() => {
|
|
161
|
+
const tokenList = tokenizeString('"\t"');
|
|
162
|
+
const obj = parse(tokenList);
|
|
163
|
+
const result = stringify(obj);
|
|
164
|
+
if (result !== '["error","unexpected token"]') {
|
|
165
|
+
throw result;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
() => {
|
|
169
|
+
const tokenList = tokenizeString('{"a":"\t"}');
|
|
170
|
+
const obj = parse(tokenList);
|
|
171
|
+
const result = stringify(obj);
|
|
172
|
+
if (result !== '["error","unexpected token"]') {
|
|
173
|
+
throw result;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
158
176
|
() => {
|
|
159
177
|
const tokenList = tokenizeString('[,]');
|
|
160
178
|
const obj = parse(tokenList);
|
package/fs/path/module.f.d.ts
CHANGED
|
@@ -38,3 +38,9 @@ export declare const join: (...list: readonly string[]) => string;
|
|
|
38
38
|
* E.g. `relativize('/repo', '/repo/fs/a.ts')` → `'./fs/a.ts'`.
|
|
39
39
|
*/
|
|
40
40
|
export declare const relativize: (base: string, path: string) => string;
|
|
41
|
+
/**
|
|
42
|
+
* Returns `true` when `prefix` is a strict ancestor of `path` in segment space:
|
|
43
|
+
* every segment of `prefix` matches the corresponding segment of `path`, and
|
|
44
|
+
* `path` has at least one additional segment.
|
|
45
|
+
*/
|
|
46
|
+
export declare const isProperPrefix: (prefix: readonly string[], path: readonly string[]) => boolean;
|
package/fs/path/module.f.js
CHANGED
|
@@ -63,3 +63,9 @@ export const join = (...list) => list.join('/');
|
|
|
63
63
|
* E.g. `relativize('/repo', '/repo/fs/a.ts')` → `'./fs/a.ts'`.
|
|
64
64
|
*/
|
|
65
65
|
export const relativize = (base, path) => base !== '' && path.startsWith(base) ? `.${path.slice(base.length)}` : path;
|
|
66
|
+
/**
|
|
67
|
+
* Returns `true` when `prefix` is a strict ancestor of `path` in segment space:
|
|
68
|
+
* every segment of `prefix` matches the corresponding segment of `path`, and
|
|
69
|
+
* `path` has at least one additional segment.
|
|
70
|
+
*/
|
|
71
|
+
export const isProperPrefix = (prefix, path) => prefix.length < path.length && prefix.every((seg, i) => seg === path[i]);
|
package/fs/path/proof.f.d.ts
CHANGED
package/fs/path/proof.f.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { concat, join, normalize, relativize, toPosix } from "./module.f.js";
|
|
1
|
+
import { concat, isProperPrefix, join, normalize, relativize, toPosix } from "./module.f.js";
|
|
2
2
|
const normalizeTest = [
|
|
3
3
|
() => {
|
|
4
4
|
const norm = normalize("dir/file.json");
|
|
@@ -129,4 +129,42 @@ const toPosixTest = [
|
|
|
129
129
|
}
|
|
130
130
|
},
|
|
131
131
|
];
|
|
132
|
-
|
|
132
|
+
const isProperPrefixTest = [
|
|
133
|
+
() => {
|
|
134
|
+
const r = isProperPrefix(['a', 'b'], ['a', 'b', 'c']);
|
|
135
|
+
if (r !== true) {
|
|
136
|
+
throw r;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
() => {
|
|
140
|
+
const r = isProperPrefix(['a', 'b'], ['a', 'b']);
|
|
141
|
+
if (r !== false) {
|
|
142
|
+
throw r;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
() => {
|
|
146
|
+
const r = isProperPrefix(['a', 'x'], ['a', 'b', 'c']);
|
|
147
|
+
if (r !== false) {
|
|
148
|
+
throw r;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
() => {
|
|
152
|
+
const r = isProperPrefix(['a', 'b', 'c'], ['a', 'b']);
|
|
153
|
+
if (r !== false) {
|
|
154
|
+
throw r;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
() => {
|
|
158
|
+
const r = isProperPrefix([], ['a']);
|
|
159
|
+
if (r !== true) {
|
|
160
|
+
throw r;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
() => {
|
|
164
|
+
const r = isProperPrefix([], []);
|
|
165
|
+
if (r !== false) {
|
|
166
|
+
throw r;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
export const proof = { normalizeTest, concatTest, joinTest, relativizeTest, toPosixTest, isProperPrefixTest };
|
|
@@ -46,6 +46,12 @@ export declare const utf8StateToError: (state: Utf8NonEmptyState) => I32;
|
|
|
46
46
|
/**
|
|
47
47
|
* Decodes a byte into a Unicode code point, using a given UTF-8 state.
|
|
48
48
|
*
|
|
49
|
+
* Rejects overlong 3-/4-byte encodings (Unicode Table 3-7): a lead `E0` must
|
|
50
|
+
* be followed by a continuation `>= 0xA0`, and a lead `F0` by a continuation
|
|
51
|
+
* `>= 0x90`. It does not itself reject surrogates (`ED A0..BF`) or code
|
|
52
|
+
* points above `U+10FFFF` (`F4 90..BF`); {@link fromVec}'s
|
|
53
|
+
* `isValidCodePoint` pass filters those out of the raw code-point stream.
|
|
54
|
+
*
|
|
49
55
|
* @param state - The current UTF-8 decoding state.
|
|
50
56
|
* @param byte - A single byte to decode.
|
|
51
57
|
* @returns A tuple containing:
|
package/fs/text/utf8/module.f.js
CHANGED
|
@@ -135,6 +135,12 @@ export const utf8StateToError = (state) => {
|
|
|
135
135
|
/**
|
|
136
136
|
* Decodes a byte into a Unicode code point, using a given UTF-8 state.
|
|
137
137
|
*
|
|
138
|
+
* Rejects overlong 3-/4-byte encodings (Unicode Table 3-7): a lead `E0` must
|
|
139
|
+
* be followed by a continuation `>= 0xA0`, and a lead `F0` by a continuation
|
|
140
|
+
* `>= 0x90`. It does not itself reject surrogates (`ED A0..BF`) or code
|
|
141
|
+
* points above `U+10FFFF` (`F4 90..BF`); {@link fromVec}'s
|
|
142
|
+
* `isValidCodePoint` pass filters those out of the raw code-point stream.
|
|
143
|
+
*
|
|
138
144
|
* @param state - The current UTF-8 decoding state.
|
|
139
145
|
* @param byte - A single byte to decode.
|
|
140
146
|
* @returns A tuple containing:
|
|
@@ -159,8 +165,15 @@ export const utf8ByteToCodePointOp = (byte, state) => {
|
|
|
159
165
|
if (s0 < lead3Tag) {
|
|
160
166
|
return [[((s0 & lead2Mask) << 6) + contPayload(byte)], null];
|
|
161
167
|
}
|
|
162
|
-
if (s0 < 0b1111_1000)
|
|
163
|
-
|
|
168
|
+
if (s0 < 0b1111_1000) {
|
|
169
|
+
// Reject overlong 3-/4-byte encodings: after lead `E0` the
|
|
170
|
+
// first continuation must be >= 0xA0, after lead `F0` it
|
|
171
|
+
// must be >= 0x90 (Unicode Table 3-7).
|
|
172
|
+
const overlong = s0 === lead3Tag && byte < 0b1010_0000 ||
|
|
173
|
+
s0 === lead4Tag && byte < 0b1001_0000;
|
|
174
|
+
if (!overlong)
|
|
175
|
+
return [[], [s0, byte]];
|
|
176
|
+
}
|
|
164
177
|
break;
|
|
165
178
|
}
|
|
166
179
|
case 2: {
|