@mark1russell7/coeval 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/build/index.d.ts +201 -7
- package/build/index.d.ts.map +1 -1
- package/build/index.js +720 -110
- package/build/index.js.map +1 -1
- package/package.json +3 -3
- package/src/agree.ts +105 -2
- package/src/evaluate.ts +49 -5
- package/src/formula.ts +62 -6
- package/src/index.ts +7 -3
- package/src/merge.ts +130 -10
- package/src/reference.ts +13 -2
- package/src/schedule.ts +11 -2
- package/src/static.ts +53 -4
- package/src/stdlib.ts +44 -18
- package/src/syntax.ts +114 -19
- package/src/types.ts +65 -0
- package/src/typing.ts +147 -12
package/src/syntax.ts
CHANGED
|
@@ -109,6 +109,18 @@ export interface SyntaxForm {
|
|
|
109
109
|
* form `literal` takes no `wrap`, and a form `list` takes only `true`.
|
|
110
110
|
*/
|
|
111
111
|
readonly wrap?: boolean | readonly string[];
|
|
112
|
+
/**
|
|
113
|
+
* The fields that take data with documents in it (L1c, D177), for example the clauses of `handle`. Such an
|
|
114
|
+
* argument is a list between brackets, a record between braces, or a document. A literal in it stays its data. A
|
|
115
|
+
* record of data has no key `type`. A name of `data` is a field of the form, and it is not in `wrap`.
|
|
116
|
+
*/
|
|
117
|
+
readonly data?: readonly string[];
|
|
118
|
+
/**
|
|
119
|
+
* The name of a domain with a codec (K143, D141). A form `literal` of the kind `ext.domain` needs it. The text
|
|
120
|
+
* `date("2026-01-02")` then gives the value `{ domain: "date", data: "2026-01-02" }`. The syntax does not read the
|
|
121
|
+
* data: the codec of the machine reads it at evaluation. Each other form takes no `domain`.
|
|
122
|
+
*/
|
|
123
|
+
readonly domain?: string;
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
/** The way that the arguments of a call form make the value of its document (D63). */
|
|
@@ -248,6 +260,10 @@ interface Form {
|
|
|
248
260
|
readonly min: number;
|
|
249
261
|
/** The arguments that take a document: `true` for each, or the names of the fields. */
|
|
250
262
|
readonly wrap: boolean | ReadonlySet<string>;
|
|
263
|
+
/** The domain of a form literal of `ext.domain` (D141). */
|
|
264
|
+
readonly domain: string | undefined;
|
|
265
|
+
/** The fields that take data with documents in it (D177). */
|
|
266
|
+
readonly data: ReadonlySet<string>;
|
|
251
267
|
}
|
|
252
268
|
|
|
253
269
|
/** The type of a data envelope, and its key for a comparison with the path of a type. */
|
|
@@ -291,6 +307,8 @@ interface Compiled {
|
|
|
291
307
|
readonly reserved: ReadonlySet<string>;
|
|
292
308
|
readonly opWords: ReadonlySet<string>;
|
|
293
309
|
readonly maxDepth: number;
|
|
310
|
+
/** `true` when a form has a field of data (D177). Such a field reads brackets and braces. */
|
|
311
|
+
readonly brackets: boolean;
|
|
294
312
|
}
|
|
295
313
|
|
|
296
314
|
const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -376,6 +394,14 @@ function compileConstructs(given: SyntaxConstructs | undefined, ops: Pick<Compil
|
|
|
376
394
|
return { constructs, constructKinds };
|
|
377
395
|
}
|
|
378
396
|
|
|
397
|
+
/** This function examines the field `domain` of a call form (D141). A form `literal` of `ext.domain` needs a name, and each other form refuses it. */
|
|
398
|
+
function compileDomain(given: SyntaxForm, args: SyntaxFormArgs): string | undefined {
|
|
399
|
+
const writes = args === "literal" && given.kind === "ext.domain";
|
|
400
|
+
if (writes && (typeof given.domain !== "string" || given.domain === "")) throw new TypeError(`The call form "${given.name}" of ext.domain needs the name of its domain.`);
|
|
401
|
+
if (!writes && given.domain !== undefined) throw new TypeError(`The call form "${given.name}" takes no domain: only a form literal of ext.domain has one.`);
|
|
402
|
+
return given.domain;
|
|
403
|
+
}
|
|
404
|
+
|
|
379
405
|
/** This function examines the field `wrap` of a call form (K159). A name that is not a field of the form throws. */
|
|
380
406
|
function compileWrap(given: SyntaxForm, args: SyntaxFormArgs): boolean | ReadonlySet<string> {
|
|
381
407
|
const wrap = given.wrap;
|
|
@@ -390,6 +416,17 @@ function compileWrap(given: SyntaxForm, args: SyntaxFormArgs): boolean | Readonl
|
|
|
390
416
|
}
|
|
391
417
|
|
|
392
418
|
/** This function gives the types of the data envelopes of a profile (K159), or `undefined` without them. */
|
|
419
|
+
/** This function examines the field `data` of a call form (D177). Each name is a field of the form, and no name is in `wrap`. */
|
|
420
|
+
function compileData(given: SyntaxForm, wrap: boolean | ReadonlySet<string>): ReadonlySet<string> {
|
|
421
|
+
const data = given.data ?? [];
|
|
422
|
+
if (data.length > 0 && given.fields === undefined) throw new TypeError(`The call form "${given.name}" takes data only with fields.`);
|
|
423
|
+
for (const name of data) {
|
|
424
|
+
if (given.fields?.includes(name) !== true) throw new TypeError(`The call form "${given.name}" has no field ${name} for its data.`);
|
|
425
|
+
if (wrap === true || (wrap !== false && wrap.has(name))) throw new TypeError(`The call form "${given.name}" has the field ${name} in wrap and in data.`);
|
|
426
|
+
}
|
|
427
|
+
return new Set(data);
|
|
428
|
+
}
|
|
429
|
+
|
|
393
430
|
function compileEnvelopes(given: SyntaxEnvelopes | undefined): Compiled["envelopes"] {
|
|
394
431
|
if (given === undefined) return undefined;
|
|
395
432
|
const out: { number?: EnvelopeKind; text?: EnvelopeKind; bool?: EnvelopeKind } = {};
|
|
@@ -423,10 +460,16 @@ function compileProfile(profile: SyntaxProfile): Compiled {
|
|
|
423
460
|
if (!FORM_ARGS.includes(args)) throw new TypeError(`The call form "${given.name}" has the unknown arguments ${String(args)}.`);
|
|
424
461
|
if ((args === "fields") !== (given.fields !== undefined) && args !== "named") throw new TypeError(`The call form "${given.name}" with the arguments ${args} ${args === "fields" ? "needs" : "takes no"} fields.`);
|
|
425
462
|
if (given.min !== undefined && (args === "named" || args === "literal")) throw new TypeError(`The call form "${given.name}" with the arguments ${args} takes no min.`);
|
|
426
|
-
const
|
|
463
|
+
const wrap = compileWrap(given, args);
|
|
464
|
+
const form: Form = { name: given.name, type, args, fields: given.fields, min: given.min ?? given.fields?.length ?? 0, wrap, domain: compileDomain(given, args), data: compileData(given, wrap) };
|
|
427
465
|
forms.set(form.name, form);
|
|
428
466
|
addTo(formsByKind, key, form);
|
|
429
467
|
}
|
|
468
|
+
// A field of data reads brackets and braces, thus no operator starts with one of them (D177).
|
|
469
|
+
const brackets = [...forms.values()].some((form) => form.data.size > 0);
|
|
470
|
+
for (const symbol of brackets ? [...ops.prefixSymbols, ...ops.binarySymbols] : []) {
|
|
471
|
+
if (/^[[\]{}]/.test(symbol)) throw new TypeError(`The operator "${symbol}" reads the text of a field of data.`);
|
|
472
|
+
}
|
|
430
473
|
const compiled: Compiled = {
|
|
431
474
|
...ops,
|
|
432
475
|
numbers: literals.numbers ?? true,
|
|
@@ -450,6 +493,7 @@ function compileProfile(profile: SyntaxProfile): Compiled {
|
|
|
450
493
|
...compileConstructs(profile.constructs, ops),
|
|
451
494
|
reserved: new Set([...words.keys(), ...ops.opWords, ...roots]),
|
|
452
495
|
maxDepth: profile.maxDepth ?? 256,
|
|
496
|
+
brackets,
|
|
453
497
|
};
|
|
454
498
|
compiledProfiles.set(profile, compiled);
|
|
455
499
|
return compiled;
|
|
@@ -888,7 +932,7 @@ class Parser {
|
|
|
888
932
|
|
|
889
933
|
/** This method stops at a token where a group or a list must end. A list can also continue with its separator. */
|
|
890
934
|
protected noEnd(close: string, list: boolean): never {
|
|
891
|
-
const closer = close
|
|
935
|
+
const closer = closerName(close);
|
|
892
936
|
const before = list ? `"${this.syn.separator}" or a ` : "";
|
|
893
937
|
return this.stop(`A ${before}closing ${closer} is missing.`, this.pos, this.tokenLength());
|
|
894
938
|
}
|
|
@@ -914,17 +958,33 @@ class Parser {
|
|
|
914
958
|
this.pos += 1;
|
|
915
959
|
return args;
|
|
916
960
|
}
|
|
917
|
-
if (this.pos >= this.text.length) this.stop(`A closing ${close
|
|
961
|
+
if (this.pos >= this.text.length) this.stop(`A closing ${closerName(close)} is missing.`, open, 1);
|
|
918
962
|
this.noEnd(close, true);
|
|
919
963
|
}
|
|
920
964
|
}
|
|
921
965
|
|
|
922
966
|
/**
|
|
923
|
-
* This method reads the
|
|
924
|
-
*
|
|
967
|
+
* This method reads the argument of a field of data (D177): a list between brackets, a record between braces, or a
|
|
968
|
+
* document. A literal in it stays its data.
|
|
925
969
|
*/
|
|
926
|
-
private
|
|
927
|
-
|
|
970
|
+
private data(): Json {
|
|
971
|
+
this.skip();
|
|
972
|
+
const start = this.pos;
|
|
973
|
+
const c = this.text[start];
|
|
974
|
+
if (c !== "[" && c !== "{") return this.bound();
|
|
975
|
+
this.depth += 1;
|
|
976
|
+
if (this.depth > this.syn.maxDepth) this.stop(`The text is deeper than ${String(this.syn.maxDepth)} levels.`, start, 0);
|
|
977
|
+
const value = c === "[" ? this.argumentsOf(() => this.data(), "]") : this.entries(undefined, true);
|
|
978
|
+
this.depth -= 1;
|
|
979
|
+
return value;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* This method reads the entries `key: expr` into a record. The record is the arguments of a form `named`
|
|
984
|
+
* (SYNTAX.RECORD), or the entries between braces (SYNTAX.RECORD-TEXT). It is also a record of data (D177).
|
|
985
|
+
*/
|
|
986
|
+
private entries(form: Form | undefined, data = false): Record<string, Json> {
|
|
987
|
+
const owner = data ? "record of data" : form === undefined ? "record" : `form ${form.name}`;
|
|
928
988
|
const value: Record<string, Json> = {};
|
|
929
989
|
const entry = (): void => {
|
|
930
990
|
this.skip();
|
|
@@ -936,9 +996,10 @@ class Parser {
|
|
|
936
996
|
this.stop(`Each ${form === undefined ? "entry" : "argument"} of the ${owner} is a key, a ":" and a document.`, at, Math.max(end - at, this.tokenLength()));
|
|
937
997
|
}
|
|
938
998
|
if (form?.fields !== undefined && !form.fields.includes(key)) this.stop(`The form ${form.name} has no field ${key}.`, at, end - at);
|
|
999
|
+
if (data && key === "type") this.stop("A record of data has no key type, because the key type makes a document.", at, end - at);
|
|
939
1000
|
if (Object.hasOwn(value, key)) this.stop(`The ${owner} has the field ${key} two times.`, at, end - at);
|
|
940
1001
|
this.pos += 1;
|
|
941
|
-
const arg = this.bound();
|
|
1002
|
+
const arg = data || form?.data.has(key) === true ? this.data() : this.bound();
|
|
942
1003
|
setOwn(value, key, form !== undefined && wraps(form, key) ? envelop(this.syn, arg) : arg);
|
|
943
1004
|
};
|
|
944
1005
|
this.argumentsOf(entry, form === undefined ? "}" : ")");
|
|
@@ -948,11 +1009,16 @@ class Parser {
|
|
|
948
1009
|
private call(name: string, start: number, nameLength: number): Json {
|
|
949
1010
|
const form = this.syn.forms.get(name);
|
|
950
1011
|
if (form?.args === "named") return opDoc(form, this.entries(form));
|
|
951
|
-
|
|
1012
|
+
let index = 0;
|
|
1013
|
+
const args = this.argumentsOf(() => {
|
|
1014
|
+
const key = form?.fields?.[index];
|
|
1015
|
+
index += 1;
|
|
1016
|
+
return key !== undefined && form?.data.has(key) === true ? this.data() : this.bound();
|
|
1017
|
+
});
|
|
952
1018
|
if (form?.args === "literal") {
|
|
953
1019
|
const [only] = args;
|
|
954
1020
|
if (args.length !== 1 || !isScalar(only)) this.stop(`The form ${name} takes one literal that JSON writes.`, start, this.pos - start);
|
|
955
|
-
return opDoc(form, only);
|
|
1021
|
+
return opDoc(form, form.domain === undefined ? only : { domain: form.domain, data: only });
|
|
956
1022
|
}
|
|
957
1023
|
if (form !== undefined) {
|
|
958
1024
|
const most = form.fields?.length ?? Number.POSITIVE_INFINITY;
|
|
@@ -979,17 +1045,23 @@ class Parser {
|
|
|
979
1045
|
class End {}
|
|
980
1046
|
|
|
981
1047
|
function hasBraces(syn: Compiled): boolean {
|
|
982
|
-
return syn.constructs.record !== undefined || syn.constructs.let !== undefined;
|
|
1048
|
+
return syn.constructs.record !== undefined || syn.constructs.let !== undefined || syn.brackets;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** This function gives the name of a closer in a message. */
|
|
1052
|
+
function closerName(close: string): string {
|
|
1053
|
+
return close === ")" ? "parenthesis" : close === "]" ? "bracket" : "brace";
|
|
983
1054
|
}
|
|
984
1055
|
|
|
985
1056
|
/** This function gives the closer of an opening bracket of a profile, or `undefined` for each other character. */
|
|
986
1057
|
function closerOf(syn: Compiled, c: string | undefined): string | undefined {
|
|
987
1058
|
if (c === "(") return ")";
|
|
1059
|
+
if (c === "[" && syn.brackets) return "]";
|
|
988
1060
|
return c === "{" && hasBraces(syn) ? "}" : undefined;
|
|
989
1061
|
}
|
|
990
1062
|
|
|
991
1063
|
function isCloser(syn: Compiled, c: string | undefined): boolean {
|
|
992
|
-
return c === ")" || (c === "}" && hasBraces(syn));
|
|
1064
|
+
return c === ")" || (c === "]" && syn.brackets) || (c === "}" && hasBraces(syn));
|
|
993
1065
|
}
|
|
994
1066
|
|
|
995
1067
|
/** This function gives the position after a quoted text at `at`, or after one other character. An open quote gives `Infinity`. */
|
|
@@ -1173,7 +1245,7 @@ type SubText = (child: unknown, path: readonly Key[], wrapped?: boolean) => Resu
|
|
|
1173
1245
|
type Shape =
|
|
1174
1246
|
| { readonly tag: "text"; readonly text: string }
|
|
1175
1247
|
| { readonly tag: "op"; readonly op: Op; readonly terms: readonly unknown[] }
|
|
1176
|
-
| { readonly tag: "call"; readonly name: string; readonly args: readonly (readonly [Key, unknown, boolean?])[]; readonly named?: boolean }
|
|
1248
|
+
| { readonly tag: "call"; readonly name: string; readonly args: readonly (readonly [Key, unknown, boolean?, boolean?])[]; readonly named?: boolean }
|
|
1177
1249
|
| { readonly tag: "path"; readonly segments: readonly (string | readonly [Key, unknown])[] }
|
|
1178
1250
|
| { readonly tag: "bind"; readonly name: string; readonly doc: unknown }
|
|
1179
1251
|
| ConstructShape
|
|
@@ -1256,6 +1328,8 @@ function refShape(syn: Compiled, value: unknown): Shape {
|
|
|
1256
1328
|
|
|
1257
1329
|
function callName(syn: Compiled, keys: readonly Key[]): string | undefined {
|
|
1258
1330
|
if (keys.length === 0 || !keys.every((key) => typeof key === "string" && NAME.test(key))) return undefined;
|
|
1331
|
+
// `parse` reads the dots of a call name as field steps, thus a profile without them has no such text (D142).
|
|
1332
|
+
if (keys.length > 1 && !syn.fields) return undefined;
|
|
1259
1333
|
const name = keys.join(".");
|
|
1260
1334
|
if (syn.forms.has(name) || syn.opWords.has(keys[0] as string)) return undefined;
|
|
1261
1335
|
return syn.ops === true || syn.ops.has(name) ? name : undefined;
|
|
@@ -1281,17 +1355,24 @@ function wrappedShape(syn: Compiled, doc: unknown): Shape | undefined {
|
|
|
1281
1355
|
return literal.tag === "text" ? literal : undefined;
|
|
1282
1356
|
}
|
|
1283
1357
|
|
|
1358
|
+
/** This function tells if a value of `ext.domain` is a record with this domain and its data, and no other key. */
|
|
1359
|
+
function holdsDomain(value: unknown, domain: string): boolean {
|
|
1360
|
+
return isRecord(value) && value["domain"] === domain && Object.hasOwn(value, "data") && Object.keys(value).length === 2;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1284
1363
|
function formShape(syn: Compiled, form: Form, value: unknown): Shape | undefined {
|
|
1285
1364
|
if (form.args === "literal") {
|
|
1286
|
-
if (!
|
|
1287
|
-
const
|
|
1365
|
+
if (form.domain !== undefined && !holdsDomain(value, form.domain)) return undefined;
|
|
1366
|
+
const data = form.domain === undefined ? value : (value as Readonly<Record<string, unknown>>)["data"];
|
|
1367
|
+
if (!isScalar(data)) return undefined;
|
|
1368
|
+
const literal = literalShape(syn, data);
|
|
1288
1369
|
return literal.tag === "text" ? { tag: "text", text: `${form.name}(${literal.text})` } : undefined;
|
|
1289
1370
|
}
|
|
1290
1371
|
if (form.args === "named") {
|
|
1291
1372
|
if (!isRecord(value)) return undefined;
|
|
1292
1373
|
const keys = Object.keys(value);
|
|
1293
1374
|
if (form.fields !== undefined && !keys.every((key) => form.fields?.includes(key) === true)) return undefined;
|
|
1294
|
-
return { tag: "call", name: form.name, args: keys.map((key) => [key, value[key], wraps(form, key)] as const), named: true };
|
|
1375
|
+
return { tag: "call", name: form.name, args: keys.map((key) => [key, value[key], wraps(form, key), form.data.has(key)] as const), named: true };
|
|
1295
1376
|
}
|
|
1296
1377
|
if (form.fields === undefined) {
|
|
1297
1378
|
if (!Array.isArray(value) || value.length < form.min) return undefined;
|
|
@@ -1302,7 +1383,7 @@ function formShape(syn: Compiled, form: Form, value: unknown): Shape | undefined
|
|
|
1302
1383
|
const count = Object.keys(value).length;
|
|
1303
1384
|
const present = form.fields.slice(0, count);
|
|
1304
1385
|
if (count < form.min || present.length < count || !present.every((key) => Object.hasOwn(value, key))) return undefined;
|
|
1305
|
-
return { tag: "call", name: form.name, args: present.map((key) => [key, value[key], wraps(form, key)] as const) };
|
|
1386
|
+
return { tag: "call", name: form.name, args: present.map((key) => [key, value[key], wraps(form, key), form.data.has(key)] as const) };
|
|
1306
1387
|
}
|
|
1307
1388
|
|
|
1308
1389
|
/** This function tells if a record has exactly these two keys. */
|
|
@@ -1422,6 +1503,20 @@ function bracesText(syn: Compiled, record: JsonRecord, path: readonly Key[], sub
|
|
|
1422
1503
|
return ok(`{${texts.value.map((text, index) => `${keyText(keys[index] as string)}: ${text}`).join(`${syn.separator} `)}}`);
|
|
1423
1504
|
}
|
|
1424
1505
|
|
|
1506
|
+
/**
|
|
1507
|
+
* This function writes the argument of a field of data (D177): a list between brackets, or a record between braces.
|
|
1508
|
+
* The function `sub` writes a literal and a document. A record that is an envelope is a document.
|
|
1509
|
+
*/
|
|
1510
|
+
function dataText(syn: Compiled, value: unknown, path: readonly Key[], sub: SubText): Result<string> {
|
|
1511
|
+
const each: SubText = (child, at) => dataText(syn, child, at, sub);
|
|
1512
|
+
if (Array.isArray(value)) {
|
|
1513
|
+
const texts = partTexts(value.map((item, index) => [index, item] as const), path, each);
|
|
1514
|
+
return texts.ok ? ok(`[${texts.value.join(`${syn.separator} `)}]`) : texts;
|
|
1515
|
+
}
|
|
1516
|
+
// A record with the key type that is not an envelope goes to sub, which gives no text for it. The parser refuses its text.
|
|
1517
|
+
return isRecord(value) && !isEnvelope(value) && !Object.hasOwn(value, "type") ? bracesText(syn, value, path, each) : sub(value, path);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1425
1520
|
/** This function writes a construct form (K145). */
|
|
1426
1521
|
function constructText(syn: Compiled, shape: ConstructShape, sub: SubText): Result<string> {
|
|
1427
1522
|
if (shape.tag === "record") return bracesText(syn, shape.entries, [], sub);
|
|
@@ -1468,8 +1563,8 @@ function printNode(syn: Compiled, doc: unknown, sub: SubText): Result<string> {
|
|
|
1468
1563
|
return ok(texts.join("."));
|
|
1469
1564
|
}
|
|
1470
1565
|
const texts: string[] = [];
|
|
1471
|
-
for (const [key, arg, wrapped] of shape.args) {
|
|
1472
|
-
const result = sub(arg, [key], wrapped);
|
|
1566
|
+
for (const [key, arg, wrapped, data] of shape.args) {
|
|
1567
|
+
const result = data === true ? dataText(syn, arg, [key], sub) : sub(arg, [key], wrapped);
|
|
1473
1568
|
if (!result.ok) return result;
|
|
1474
1569
|
texts.push(shape.named === true ? `${keyText(String(key))}: ${result.value}` : result.value);
|
|
1475
1570
|
}
|
package/src/types.ts
CHANGED
|
@@ -722,6 +722,71 @@ function narrowAt(type: unknown, path: readonly string[], at: number, narrowed:
|
|
|
722
722
|
return { record: fields, ...(optional.length === 0 ? {} : { optional }), ...(form.closed ? { closed: true } : {}) };
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
+
/**
|
|
726
|
+
* What the result of a test proves of the value at one path (STATIC.OCCURRENCE, D147). The path starts at a root, or at
|
|
727
|
+
* `names` and a name, and each later key is a field of a record. The fact `is` tells that the value is a member of the
|
|
728
|
+
* type, and `not` tells that it is not. The facts `present` and `absent` tell if the path reaches a value.
|
|
729
|
+
*/
|
|
730
|
+
export type Proof =
|
|
731
|
+
| { readonly path: readonly string[]; readonly fact: "is" | "not"; readonly type: ValueType }
|
|
732
|
+
| { readonly path: readonly string[]; readonly fact: "present" | "absent" };
|
|
733
|
+
|
|
734
|
+
/** This function gives the type of a value that is a member of both types. The result is the narrower known type. */
|
|
735
|
+
function meetOf(type: ValueType, proved: ValueType, domains: Readonly<Record<string, Domain>> | undefined): ValueType {
|
|
736
|
+
if (isUnknown(proved, domains)) return type;
|
|
737
|
+
const form = formOf(type, domains);
|
|
738
|
+
if (form.t === "one") return { one: form.values.filter((value) => member(value, proved, domains)) };
|
|
739
|
+
return !isUnknown(type, domains) && isPrecise(type, domains) && fits(type, proved, domains) ? type : proved;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** This function gives the type of a value of the first type that is not a member of the second type. */
|
|
743
|
+
function withoutOf(type: ValueType, removed: ValueType, domains: Readonly<Record<string, Domain>> | undefined): ValueType {
|
|
744
|
+
if (isUnknown(removed, domains)) return type;
|
|
745
|
+
const form = formOf(type, domains);
|
|
746
|
+
if (form.t === "one") return { one: form.values.filter((value) => !member(value, removed, domains)) };
|
|
747
|
+
if (form.t === "union") {
|
|
748
|
+
const members = form.members.map((each) => withoutOf(each, removed, domains)).filter((each) => each !== "never");
|
|
749
|
+
return members.length === 0 ? "never" : members.length === 1 ? (members[0] as ValueType) : { union: members };
|
|
750
|
+
}
|
|
751
|
+
// Only a precise type can leave: a gradual type fits each type, but its values do not.
|
|
752
|
+
return isPrecise(type, domains) && fits(type, removed, domains) ? "never" : type;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* This function gives a type with one proof at a path (STATIC.OCCURRENCE). A path that leaves the records of the type
|
|
757
|
+
* gives the type back. A fact about a field also tells if the field exists. A value of a type without `undefined`
|
|
758
|
+
* exists. An optional field without a remaining type does not.
|
|
759
|
+
*
|
|
760
|
+
* @param type - The type of the value at the first `at` keys of the path.
|
|
761
|
+
* @param proof - The proof.
|
|
762
|
+
* @param at - The number of keys of the path that the type already holds.
|
|
763
|
+
* @param domains - The domains of the machine.
|
|
764
|
+
* @returns The narrowed type.
|
|
765
|
+
*/
|
|
766
|
+
export function narrowType(type: ValueType, proof: Proof, at: number, domains: Readonly<Record<string, Domain>> | undefined): ValueType {
|
|
767
|
+
if (at === proof.path.length) {
|
|
768
|
+
if (proof.fact === "is") return meetOf(type, proof.type, domains);
|
|
769
|
+
return proof.fact === "not" ? withoutOf(type, proof.type, domains) : type;
|
|
770
|
+
}
|
|
771
|
+
const form = formOf(type, domains);
|
|
772
|
+
const key = proof.path[at] as string;
|
|
773
|
+
if (form.t !== "record" || !Object.hasOwn(form.fields, key)) return type;
|
|
774
|
+
const before = form.fields[key] as ValueType;
|
|
775
|
+
const optional = new Set(form.optional);
|
|
776
|
+
let after: ValueType | undefined = narrowType(before, proof, at + 1, domains);
|
|
777
|
+
if (at + 1 === proof.path.length && optional.has(key)) {
|
|
778
|
+
if (proof.fact === "present" || (proof.fact === "is" && !member(undefined, proof.type, domains))) optional.delete(key);
|
|
779
|
+
else if (proof.fact === "absent" || after === "never") after = undefined;
|
|
780
|
+
}
|
|
781
|
+
const fields: Record<string, ValueType> = {};
|
|
782
|
+
for (const name of Object.keys(form.fields)) if (name !== key) setOwn(fields, name, form.fields[name] as ValueType);
|
|
783
|
+
// The absent case removes the field of a closed record. An open record keeps it as an optional field of the type `never`.
|
|
784
|
+
if (after !== undefined) setOwn(fields, key, after);
|
|
785
|
+
else if (form.closed) optional.delete(key);
|
|
786
|
+
else setOwn(fields, key, "never");
|
|
787
|
+
return { record: fields, ...(optional.size === 0 ? {} : { optional: [...optional] }), ...(form.closed ? { closed: true } : {}) };
|
|
788
|
+
}
|
|
789
|
+
|
|
725
790
|
/**
|
|
726
791
|
* This function is the split rule of `fits` (TYPE.FITS-FINITE, D84, TYPE.FITS-PATH, D89). A record type fits when each
|
|
727
792
|
* record of its split fits. A split takes the first field in the order of `splitsOf`. Each member of the field gives a
|
package/src/typing.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Envelope, type Key, isRecord, typeName } from "./doc.ts";
|
|
2
2
|
import type { Machine } from "./evaluate.ts";
|
|
3
3
|
import type { DocVisit, Issue, WorldIssue } from "./static.ts";
|
|
4
|
-
import { type ValueType, domainOf, fits, isUnknown, typeIssues, typeText } from "./types.ts";
|
|
4
|
+
import { type Domain, type Proof, type ValueType, domainOf, fits, isPrecise, isUnknown, narrowType, typeIssues, typeText } from "./types.ts";
|
|
5
5
|
import { type Kind, type Shape, eachKind, kindAt } from "./world.ts";
|
|
6
6
|
|
|
7
7
|
/*
|
|
@@ -58,6 +58,27 @@ export interface TypeInput {
|
|
|
58
58
|
* @returns The type.
|
|
59
59
|
*/
|
|
60
60
|
contract(type: unknown, what: string): ValueType;
|
|
61
|
+
/**
|
|
62
|
+
* This function gives the path that a part reads, from the rule `reads` of the part (STATIC.OCCURRENCE).
|
|
63
|
+
*
|
|
64
|
+
* @param path - The path of the part in the value.
|
|
65
|
+
* @returns The path of the read, or `undefined` for each other part.
|
|
66
|
+
*/
|
|
67
|
+
readOf(path: readonly Key[]): readonly string[] | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* This function gives what the result of a part proves. Only a rule with `usesProofs` gets this, for a part that the
|
|
70
|
+
* check typed before.
|
|
71
|
+
*
|
|
72
|
+
* @param path - The path of the part in the value.
|
|
73
|
+
* @returns The proofs of the part.
|
|
74
|
+
*/
|
|
75
|
+
provenBy(path: readonly Key[]): Proven;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** What a test proves of its paths: `yes` when the test is true, and `no` when it is false (STATIC.OCCURRENCE). */
|
|
79
|
+
export interface Proven {
|
|
80
|
+
readonly yes: readonly Proof[];
|
|
81
|
+
readonly no: readonly Proof[];
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
/** What a type rule tells for one part: the type that the part must have, and the types of the names of the part. */
|
|
@@ -73,6 +94,10 @@ export interface PartType {
|
|
|
73
94
|
* Each other root keeps its type. The parts of an evaluator without a rule get `unknown` for each of these roots.
|
|
74
95
|
*/
|
|
75
96
|
readonly roots?: Omit<RootTypes, "names">;
|
|
97
|
+
/** The proofs that hold in the part (STATIC.OCCURRENCE). They narrow the names and the roots of the part. */
|
|
98
|
+
readonly narrow?: readonly Proof[];
|
|
99
|
+
/** `true` when the proofs of `narrow` also hold in each later part. */
|
|
100
|
+
readonly keep?: boolean;
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
/**
|
|
@@ -82,7 +107,7 @@ export interface PartType {
|
|
|
82
107
|
*
|
|
83
108
|
* The host keeps the types true: each root of the context holds a member of its type. A host thing, or a machine
|
|
84
109
|
* with `adopt`, keeps each step true too. The context binds each name of `names`, thus the option `names` of `check`
|
|
85
|
-
* lists it too.
|
|
110
|
+
* lists it too. A narrowing also needs one value for two reads of one path (STATIC.OCCURRENCE).
|
|
86
111
|
*/
|
|
87
112
|
export interface RootTypes {
|
|
88
113
|
/** The type of each name that the host binds. A name of the host without a type here has the type `unknown`. */
|
|
@@ -116,6 +141,8 @@ export interface TypeProblem {
|
|
|
116
141
|
* (TYPE.RULE).
|
|
117
142
|
*/
|
|
118
143
|
export interface TypeRule {
|
|
144
|
+
/** `true` when the document reads a value from outside the document. The check then has no proof of its type (PLAN.GUARD-BOUNDARY). */
|
|
145
|
+
readonly read?: true;
|
|
119
146
|
/**
|
|
120
147
|
* This function tells the type that a part must have, and the types of the names for the part. The check asks it
|
|
121
148
|
* before it types the part. The parts that give the names of a rule `keysAt` come first.
|
|
@@ -142,6 +169,24 @@ export interface TypeRule {
|
|
|
142
169
|
* @returns The value types that the rule reads, and the problems that the rule finds in its declarations.
|
|
143
170
|
*/
|
|
144
171
|
readonly declared?: () => readonly Declared[];
|
|
172
|
+
/** The field of the value that the check types before each other part. A branch then reads the proofs of its test. */
|
|
173
|
+
readonly first?: string;
|
|
174
|
+
/** `true` when the rule reads `provenBy`. The check asks the rule `proves` only of a part of such a rule. */
|
|
175
|
+
readonly usesProofs?: boolean;
|
|
176
|
+
/**
|
|
177
|
+
* This function gives the path that the document reads. A path with a computed step is not such a path.
|
|
178
|
+
*
|
|
179
|
+
* @param input - The document.
|
|
180
|
+
* @returns The path, or `undefined`.
|
|
181
|
+
*/
|
|
182
|
+
readonly reads?: (input: TypeInput) => readonly string[] | undefined;
|
|
183
|
+
/**
|
|
184
|
+
* This function gives what the result of the document proves (STATIC.OCCURRENCE).
|
|
185
|
+
*
|
|
186
|
+
* @param input - The document and its parts.
|
|
187
|
+
* @returns The proofs of a true result and of a false result.
|
|
188
|
+
*/
|
|
189
|
+
readonly proves?: (input: TypeInput) => Proven;
|
|
145
190
|
}
|
|
146
191
|
|
|
147
192
|
/**
|
|
@@ -174,6 +219,33 @@ export interface Typed {
|
|
|
174
219
|
readonly type: ValueType;
|
|
175
220
|
/** The type that the position of the document needs, when it needs one. */
|
|
176
221
|
readonly expected?: ValueType;
|
|
222
|
+
/** `true` when the rule of the document tells a read of a value that comes from outside the document. */
|
|
223
|
+
readonly read?: true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** A place where a promise of the types holds only with a test at run time (PLAN.GUARD). */
|
|
227
|
+
export interface Boundary {
|
|
228
|
+
/** The keys from the root document to the document. */
|
|
229
|
+
readonly place: readonly Key[];
|
|
230
|
+
/** The type that the value of the place must have. */
|
|
231
|
+
readonly type: ValueType;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* This function gives the boundaries of a check. A part with an imprecise type at a position that needs a type is a
|
|
236
|
+
* boundary. A read of a known type is a boundary too, because the check trusts the root types (PLAN.GUARD-BOUNDARY).
|
|
237
|
+
*
|
|
238
|
+
* @param typing - The places of a check of types.
|
|
239
|
+
* @param domains - The domains of the machine.
|
|
240
|
+
* @returns The boundaries, in the order of the document.
|
|
241
|
+
*/
|
|
242
|
+
export function boundariesOf(typing: { readonly places: readonly Typed[] }, domains?: Readonly<Record<string, Domain>>): readonly Boundary[] {
|
|
243
|
+
const found: Boundary[] = [];
|
|
244
|
+
for (const item of typing.places) {
|
|
245
|
+
if (item.expected !== undefined && item.expected !== "unknown" && !isPrecise(item.type, domains)) found.push({ place: item.place, type: item.expected });
|
|
246
|
+
else if (item.read === true && item.type !== "unknown") found.push({ place: item.place, type: item.type });
|
|
247
|
+
}
|
|
248
|
+
return found;
|
|
177
249
|
}
|
|
178
250
|
|
|
179
251
|
/** One document that the walk of a check visited, with what the pass of types needs. The package does not export it. */
|
|
@@ -191,6 +263,9 @@ export interface TypeNode {
|
|
|
191
263
|
marked: boolean;
|
|
192
264
|
}
|
|
193
265
|
|
|
266
|
+
/** The result of a test that proves nothing. */
|
|
267
|
+
const NO_PROOFS: Proven = { yes: [], no: [] };
|
|
268
|
+
|
|
194
269
|
/** The result of the pass of types. */
|
|
195
270
|
export interface TypePass {
|
|
196
271
|
readonly type: ValueType;
|
|
@@ -226,8 +301,11 @@ interface Frame {
|
|
|
226
301
|
next: number;
|
|
227
302
|
readonly typed: Map<string, ValueType>;
|
|
228
303
|
readonly needs: readonly Need[];
|
|
229
|
-
|
|
230
|
-
|
|
304
|
+
/** The types of the names for the parts. A part with `keep` changes them for the later parts. */
|
|
305
|
+
names: ReadonlyMap<string, ValueType>;
|
|
306
|
+
roots: Roots;
|
|
307
|
+
/** The proofs of the typed parts, for a rule with `usesProofs`. */
|
|
308
|
+
proven: Map<string, Proven> | undefined;
|
|
231
309
|
}
|
|
232
310
|
|
|
233
311
|
function pathKey(path: readonly Key[]): string {
|
|
@@ -238,12 +316,16 @@ function pathKey(path: readonly Key[]): string {
|
|
|
238
316
|
export function ruleOf(machine: Machine, name: string | undefined): TypeRule | undefined {
|
|
239
317
|
if (name === undefined || (machine.runners !== undefined && Object.hasOwn(machine.runners, name))) return undefined;
|
|
240
318
|
const evaluator = Object.hasOwn(machine.evaluators, name) ? machine.evaluators[name] : undefined;
|
|
241
|
-
const rule = typeof evaluator === "object" && evaluator !== null ? (evaluator as { readonly type?: unknown }).type : undefined;
|
|
319
|
+
const rule = (typeof evaluator === "object" && evaluator !== null) || typeof evaluator === "function" ? (evaluator as { readonly type?: unknown }).type : undefined;
|
|
242
320
|
return typeof rule === "object" && rule !== null && typeof (rule as TypeRule).synth === "function" ? (rule as TypeRule) : undefined;
|
|
243
321
|
}
|
|
244
322
|
|
|
245
|
-
/** This function gives the order of the parts of a document
|
|
246
|
-
function orderOf(node: TypeNode, nodes: readonly TypeNode[]): readonly number[] {
|
|
323
|
+
/** This function gives the order of the parts of a document. The part `first` of the rule comes first, or else the parts that give the names of a rule `keysAt`. */
|
|
324
|
+
function orderOf(node: TypeNode, nodes: readonly TypeNode[], first: string | undefined): readonly number[] {
|
|
325
|
+
if (first !== undefined) {
|
|
326
|
+
const isFirst = (index: number): boolean => String((nodes[index] as TypeNode).visit.slot[0]) === first;
|
|
327
|
+
return [...node.children.filter(isFirst), ...node.children.filter((index) => !isFirst(index))];
|
|
328
|
+
}
|
|
247
329
|
const sources = (node.visit.kind?.scope ?? []).flatMap((rule) => ("keysAt" in rule.names ? [rule.names.keysAt] : []));
|
|
248
330
|
if (sources.length === 0 || node.children.length < 2) return node.children;
|
|
249
331
|
const gives = (index: number): boolean => {
|
|
@@ -269,7 +351,7 @@ function message(error: unknown): string {
|
|
|
269
351
|
* @param rootTypes - The types of the roots and of the names of the host.
|
|
270
352
|
* @returns The type of the root, the type issues in the order of the document, and the type of each document.
|
|
271
353
|
*/
|
|
272
|
-
export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: string, expect: ValueType | undefined, rootTypes: RootTypes = {}): TypePass {
|
|
354
|
+
export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: string, expect: ValueType | undefined, rootTypes: RootTypes = {}, hasEffect: (node: TypeNode) => boolean = () => false): TypePass {
|
|
273
355
|
if (nodes.length === 0) return { type: "unknown", issues: [], places: [] };
|
|
274
356
|
const domains = machine.domains;
|
|
275
357
|
const contracts = verb === "eval";
|
|
@@ -277,6 +359,28 @@ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: str
|
|
|
277
359
|
const types = new Array<ValueType>(nodes.length);
|
|
278
360
|
const needed = new Array<ValueType | undefined>(nodes.length);
|
|
279
361
|
const seen = new Set<unknown>();
|
|
362
|
+
const inputs = new Array<TypeInput>(nodes.length);
|
|
363
|
+
let changes: boolean | undefined;
|
|
364
|
+
|
|
365
|
+
// A document with an effect can change a root between two reads, thus only its names narrow (STATIC.OCCURRENCE-EFFECT).
|
|
366
|
+
const rootsChange = (): boolean => (changes ??= nodes.some(hasEffect));
|
|
367
|
+
|
|
368
|
+
const narrowed = (names: ReadonlyMap<string, ValueType>, roots: Roots, proofs: readonly Proof[]): { readonly names: ReadonlyMap<string, ValueType>; readonly roots: Roots } => {
|
|
369
|
+
let nextNames = names;
|
|
370
|
+
let nextRoots = roots;
|
|
371
|
+
for (const proof of proofs) {
|
|
372
|
+
const head = proof.path[0];
|
|
373
|
+
if (head === "names") {
|
|
374
|
+
const name = proof.path[1];
|
|
375
|
+
if (name === undefined) continue;
|
|
376
|
+
if (nextNames === names) nextNames = new Map(names);
|
|
377
|
+
(nextNames as Map<string, ValueType>).set(name, narrowType(nextNames.get(name) ?? "unknown", proof, 2, domains));
|
|
378
|
+
} else if ((head === "self" || head === "world" || head === "event") && !rootsChange()) {
|
|
379
|
+
nextRoots = { ...nextRoots, [head]: narrowType(nextRoots[head], proof, 1, domains) };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { names: nextNames, roots: nextRoots };
|
|
383
|
+
};
|
|
280
384
|
|
|
281
385
|
const issueAt = (index: number, problem: TypeProblem): void => {
|
|
282
386
|
const visit = (nodes[index] as TypeNode).visit;
|
|
@@ -316,8 +420,18 @@ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: str
|
|
|
316
420
|
nameType: (name) => names.get(name),
|
|
317
421
|
rootType: (root) => (root === "caps" ? CAPS_TYPE : root === "self" || root === "world" || root === "event" ? roots[root] : "unknown"),
|
|
318
422
|
contract: (type, what) => contract(index, type, what),
|
|
423
|
+
readOf: (path) => {
|
|
424
|
+
const key = pathKey(path);
|
|
425
|
+
const part = node.children.find((child) => pathKey((nodes[child] as TypeNode).visit.slot) === key);
|
|
426
|
+
const read = part === undefined ? undefined : ruleOf(machine, (nodes[part] as TypeNode).evaluator)?.reads;
|
|
427
|
+
return read === undefined || (nodes[part as number] as TypeNode).marked ? undefined : read(inputs[part as number] as TypeInput);
|
|
428
|
+
},
|
|
429
|
+
provenBy: (path) => frame.proven?.get(pathKey(path)) ?? NO_PROOFS,
|
|
319
430
|
};
|
|
320
|
-
|
|
431
|
+
inputs[index] = input;
|
|
432
|
+
const rule = node.marked ? undefined : ruleOf(machine, node.evaluator);
|
|
433
|
+
const frame: Frame = { index, rule, input, order: orderOf(node, nodes, rule?.first), next: 0, typed, needs, names, roots, proven: undefined };
|
|
434
|
+
return frame;
|
|
321
435
|
};
|
|
322
436
|
|
|
323
437
|
const ruleFailed = (frame: Frame, error: unknown): void => {
|
|
@@ -362,6 +476,16 @@ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: str
|
|
|
362
476
|
if (contracts && node.expect !== undefined) need(contract(child, node.expect, `expected type of the ${what}`), what);
|
|
363
477
|
if (part.expect !== undefined) need(part.expect, part.what ?? what);
|
|
364
478
|
let names = frame.names;
|
|
479
|
+
let kept = frame.roots;
|
|
480
|
+
if (part.narrow !== undefined && part.narrow.length > 0) {
|
|
481
|
+
const after = narrowed(names, kept, part.narrow);
|
|
482
|
+
names = after.names;
|
|
483
|
+
kept = after.roots;
|
|
484
|
+
if (part.keep === true) {
|
|
485
|
+
frame.names = names;
|
|
486
|
+
frame.roots = kept;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
365
489
|
if (node.bound.length > 0) {
|
|
366
490
|
// Each name that the scope rules bind hides the name around it. A name without a type from the rule gets `unknown`.
|
|
367
491
|
const copy = new Map(names);
|
|
@@ -369,7 +493,7 @@ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: str
|
|
|
369
493
|
names = copy;
|
|
370
494
|
}
|
|
371
495
|
// No contract tells how an evaluator changes the roots, thus only a rule keeps their types for its parts (TYPE.ROOTS).
|
|
372
|
-
let roots = frame.rule === undefined ? UNKNOWN_ROOTS :
|
|
496
|
+
let roots = frame.rule === undefined ? UNKNOWN_ROOTS : kept;
|
|
373
497
|
const changes = part.roots;
|
|
374
498
|
if (typeof changes === "object" && changes !== null) {
|
|
375
499
|
const copy: Record<(typeof CHANGING)[number], ValueType> = { ...roots };
|
|
@@ -403,12 +527,23 @@ export function typePass(machine: Machine, nodes: readonly TypeNode[], verb: str
|
|
|
403
527
|
}
|
|
404
528
|
types[frame.index] = type;
|
|
405
529
|
needed[frame.index] = frame.needs.length === 0 ? undefined : (frame.needs[frame.needs.length - 1] as Need).type;
|
|
406
|
-
stack[stack.length - 1]
|
|
530
|
+
const parent = stack[stack.length - 1];
|
|
531
|
+
if (parent === undefined) continue;
|
|
532
|
+
const key = pathKey(node.visit.slot);
|
|
533
|
+
parent.typed.set(key, type);
|
|
534
|
+
if (parent.rule?.usesProofs === true && frame.rule?.proves !== undefined) {
|
|
535
|
+
try {
|
|
536
|
+
(parent.proven ??= new Map()).set(key, frame.rule.proves(frame.input));
|
|
537
|
+
} catch (error) {
|
|
538
|
+
ruleFailed(frame, error);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
407
541
|
}
|
|
408
542
|
|
|
409
543
|
const places = nodes.map((node, index) => {
|
|
410
544
|
const want = needed[index];
|
|
411
|
-
const
|
|
545
|
+
const read = !node.marked && ruleOf(machine, node.evaluator)?.read === true;
|
|
546
|
+
const entry = { type: types[index] ?? "unknown", ...(want === undefined ? {} : { expected: want }), ...(read ? { read } : {}) } as Typed;
|
|
412
547
|
// The keys of a place are made only when a reader asks for them, as a visit does.
|
|
413
548
|
Object.defineProperty(entry, "place", { enumerable: true, get: () => node.visit.place });
|
|
414
549
|
Object.defineProperty(entry, "at", { enumerable: true, get: () => node.visit.at });
|