@mark1russell7/coeval 0.15.0 → 0.17.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 +9 -4
- package/build/index.d.ts +828 -249
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3138 -386
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +193 -21
- package/src/conform.ts +49 -5
- package/src/deref.ts +97 -14
- package/src/evaluate.ts +311 -35
- package/src/facts.ts +113 -78
- package/src/formula.ts +55 -10
- package/src/index.ts +33 -1
- package/src/meaning.ts +472 -0
- package/src/merge.ts +46 -14
- package/src/reads.ts +9 -2
- package/src/reference.ts +90 -14
- package/src/replay.ts +964 -18
- package/src/schedule.ts +14 -4
- package/src/static.ts +2 -2
- package/src/stdlib.ts +81 -6
- package/src/syntax.ts +357 -52
- package/src/types.ts +443 -16
package/src/syntax.ts
CHANGED
|
@@ -37,6 +37,23 @@ export interface SyntaxLiterals {
|
|
|
37
37
|
readonly strings?: false | { readonly quote?: '"' | "'"; readonly escape?: "backslash" | "double" };
|
|
38
38
|
/** The words that are literals, with their values. The default is `true`, `false` and `null`. */
|
|
39
39
|
readonly words?: Readonly<Record<string, unknown>>;
|
|
40
|
+
/**
|
|
41
|
+
* The kinds of the data envelopes of a text, a number and a truth value, with dots between the keys (K159, D65). A
|
|
42
|
+
* literal in a place that takes a document gets its envelope. Such a place is the whole text, the document after a
|
|
43
|
+
* header name, or an argument that its form names in `wrap`. Thus `list(1)` gives `list(number(1))`, and `print`
|
|
44
|
+
* writes the envelope as the bare literal there. Without this field, a literal stays bare in each place.
|
|
45
|
+
*/
|
|
46
|
+
readonly envelopes?: SyntaxEnvelopes;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The kinds of the data envelopes of the literals (K159, D65). A literal without a kind stays bare, for example `null`. */
|
|
50
|
+
export interface SyntaxEnvelopes {
|
|
51
|
+
/** The kind of a finite number, for example `number`. */
|
|
52
|
+
readonly number?: string;
|
|
53
|
+
/** The kind of a text, for example `text`. */
|
|
54
|
+
readonly text?: string;
|
|
55
|
+
/** The kind of a truth value, for example `bool`. */
|
|
56
|
+
readonly bool?: string;
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
/** A reference syntax that a pattern gives, for example the address `A1` of a cell. */
|
|
@@ -63,16 +80,40 @@ export interface SyntaxRefs {
|
|
|
63
80
|
|
|
64
81
|
/** A call form: a name with arguments in parentheses that makes a construct, for example `if(test, then, else)`. */
|
|
65
82
|
export interface SyntaxForm {
|
|
66
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* The name in the text: a plain name, or plain names with dots between them, for example `math.product` (K161). A
|
|
85
|
+
* name with dots needs the field steps of `refs.fields`, and its first key is not a word operator.
|
|
86
|
+
*/
|
|
67
87
|
readonly name: string;
|
|
68
88
|
/** The type of the document, with dots between the keys. */
|
|
69
89
|
readonly kind: string;
|
|
70
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* The fields of the value, in the order of the arguments. Without them, the value is the list of the arguments. A
|
|
92
|
+
* form `named` permits only these keys, and without them it permits each key.
|
|
93
|
+
*/
|
|
71
94
|
readonly fields?: readonly string[];
|
|
72
|
-
/** The smallest number of arguments. The default is the number of the fields, or `0`. */
|
|
95
|
+
/** The smallest number of arguments. The default is the number of the fields, or `0`. Only `list` and `fields` read it. */
|
|
73
96
|
readonly min?: number;
|
|
97
|
+
/**
|
|
98
|
+
* The way that the arguments make the value (D63). The default is `fields` with fields, or `list`.
|
|
99
|
+
*
|
|
100
|
+
* - `list`: the value is the list of the arguments, for example `list(1, 2)`.
|
|
101
|
+
* - `fields`: the arguments go into the fields, in order, for example `if(test, then, else)`.
|
|
102
|
+
* - `named`: each argument is a key and a document, and the value is a record, for example `panel(width: 300)`.
|
|
103
|
+
* - `literal`: the one argument is a literal, and the value is its data, for example `text("big")`.
|
|
104
|
+
*/
|
|
105
|
+
readonly args?: SyntaxFormArgs;
|
|
106
|
+
/**
|
|
107
|
+
* The arguments that take a document (K159, D65): `true` for each argument, or the names of some fields. A literal
|
|
108
|
+
* in such an argument gets its data envelope of `literals.envelopes`. Without that field, `wrap` changes nothing. A
|
|
109
|
+
* form `literal` takes no `wrap`, and a form `list` takes only `true`.
|
|
110
|
+
*/
|
|
111
|
+
readonly wrap?: boolean | readonly string[];
|
|
74
112
|
}
|
|
75
113
|
|
|
114
|
+
/** The way that the arguments of a call form make the value of its document (D63). */
|
|
115
|
+
export type SyntaxFormArgs = "list" | "fields" | "named" | "literal";
|
|
116
|
+
|
|
76
117
|
/** The call syntax of a syntax profile. */
|
|
77
118
|
export interface SyntaxCalls {
|
|
78
119
|
/** The operations that a call `name(args)` can name. `true` permits each name. The default is `true`. */
|
|
@@ -83,6 +124,16 @@ export interface SyntaxCalls {
|
|
|
83
124
|
readonly separator?: string;
|
|
84
125
|
}
|
|
85
126
|
|
|
127
|
+
/** The text of the header of a document (K157, D62). */
|
|
128
|
+
export interface SyntaxHeader {
|
|
129
|
+
/**
|
|
130
|
+
* The symbol between a name and its document, for example `=` in `total = 2 + 3`. The text gives the document the
|
|
131
|
+
* header `name`. Without the symbol, a document with a name has no text. The symbol is a group of symbols, and no
|
|
132
|
+
* infix or postfix operator is the symbol or the start of it.
|
|
133
|
+
*/
|
|
134
|
+
readonly bind?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
86
137
|
/**
|
|
87
138
|
* The text syntax of a language (D41): an operator table, literal rules, reference syntax and call syntax. `parse`
|
|
88
139
|
* reads a text into a document, and `print` writes a document as text. The profile is data, thus a site, an editor
|
|
@@ -97,6 +148,8 @@ export interface SyntaxProfile {
|
|
|
97
148
|
readonly refs?: SyntaxRefs;
|
|
98
149
|
/** The call syntax. */
|
|
99
150
|
readonly calls?: SyntaxCalls;
|
|
151
|
+
/** The text of the header of a document. */
|
|
152
|
+
readonly header?: SyntaxHeader;
|
|
100
153
|
/** The deepest nesting that `parse` accepts. The default is `256`. */
|
|
101
154
|
readonly maxDepth?: number;
|
|
102
155
|
}
|
|
@@ -117,7 +170,8 @@ export type ParseResult = { readonly ok: true; readonly doc: Json } | { readonly
|
|
|
117
170
|
/**
|
|
118
171
|
* The standard syntax of the formula kit. It gives the site and the playground a text form. The operators are
|
|
119
172
|
* `or`, `and`, `not`, the comparisons, `+ - * / %`, a minus sign and `^`, from the loosest to the tightest. A call
|
|
120
|
-
* `name(args)` applies the operation `ops.name`. `if(test, then, else)` and `list(items)` make the constructs.
|
|
173
|
+
* `name(args)` applies the operation `ops.name`. `if(test, then, else)` and `list(items)` make the constructs. The
|
|
174
|
+
* text `total = 2 + 3` gives the document the header name `total` (D62).
|
|
121
175
|
*/
|
|
122
176
|
export const STD_SYNTAX: SyntaxProfile = /* @__PURE__ */ Object.freeze({
|
|
123
177
|
operators: [
|
|
@@ -145,6 +199,7 @@ export const STD_SYNTAX: SyntaxProfile = /* @__PURE__ */ Object.freeze({
|
|
|
145
199
|
{ name: "list", kind: "list" },
|
|
146
200
|
],
|
|
147
201
|
},
|
|
202
|
+
header: { bind: "=" },
|
|
148
203
|
} satisfies SyntaxProfile);
|
|
149
204
|
|
|
150
205
|
// === The compiled profile ===
|
|
@@ -162,8 +217,17 @@ interface Op {
|
|
|
162
217
|
interface Form {
|
|
163
218
|
readonly name: string;
|
|
164
219
|
readonly type: Json;
|
|
220
|
+
readonly args: SyntaxFormArgs;
|
|
165
221
|
readonly fields: readonly string[] | undefined;
|
|
166
222
|
readonly min: number;
|
|
223
|
+
/** The arguments that take a document: `true` for each, or the names of the fields. */
|
|
224
|
+
readonly wrap: boolean | ReadonlySet<string>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The type of a data envelope, and its key for a comparison with the path of a type. */
|
|
228
|
+
interface EnvelopeKind {
|
|
229
|
+
readonly type: Json;
|
|
230
|
+
readonly key: string;
|
|
167
231
|
}
|
|
168
232
|
|
|
169
233
|
interface Pattern {
|
|
@@ -184,6 +248,7 @@ interface Compiled {
|
|
|
184
248
|
readonly quote: string | undefined;
|
|
185
249
|
readonly doubled: boolean;
|
|
186
250
|
readonly words: ReadonlyMap<string, unknown>;
|
|
251
|
+
readonly envelopes: { readonly number?: EnvelopeKind; readonly text?: EnvelopeKind; readonly bool?: EnvelopeKind } | undefined;
|
|
187
252
|
readonly bare: string;
|
|
188
253
|
readonly roots: ReadonlySet<string>;
|
|
189
254
|
readonly fields: boolean;
|
|
@@ -192,6 +257,7 @@ interface Compiled {
|
|
|
192
257
|
readonly forms: ReadonlyMap<string, Form>;
|
|
193
258
|
readonly formsByKind: ReadonlyMap<string, readonly Form[]>;
|
|
194
259
|
readonly separator: string;
|
|
260
|
+
readonly bind: string | undefined;
|
|
195
261
|
readonly reserved: ReadonlySet<string>;
|
|
196
262
|
readonly opWords: ReadonlySet<string>;
|
|
197
263
|
readonly maxDepth: number;
|
|
@@ -204,6 +270,7 @@ const NUMBER_AT = /(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
|
|
|
204
270
|
const SYMBOL = /^[^\sA-Za-z0-9_"'`()$.]+$/;
|
|
205
271
|
const DEFAULT_WORDS: Readonly<Record<string, unknown>> = { true: true, false: false, null: null };
|
|
206
272
|
const DEFAULT_ROOTS: readonly string[] = ["self", "world", "event", "caps"];
|
|
273
|
+
const FORM_ARGS: readonly SyntaxFormArgs[] = ["list", "fields", "named", "literal"];
|
|
207
274
|
|
|
208
275
|
const compiledProfiles = new WeakMap<SyntaxProfile, Compiled>();
|
|
209
276
|
|
|
@@ -249,6 +316,40 @@ function compileOps(profile: SyntaxProfile, separator: string): Pick<Compiled, "
|
|
|
249
316
|
return { prefix, binary, prefixSymbols: longestFirst(prefix), binarySymbols: longestFirst(binary), byKind, opWords };
|
|
250
317
|
}
|
|
251
318
|
|
|
319
|
+
/** This function examines the bind symbol of a header. A text with the symbol after a name was not valid before. */
|
|
320
|
+
function compileBind(bind: string | undefined, separator: string, binary: ReadonlyMap<string, Op>): string | undefined {
|
|
321
|
+
if (bind === undefined) return undefined;
|
|
322
|
+
if (!SYMBOL.test(bind) || bind.includes(separator)) throw new TypeError(`The bind symbol "${bind}" is not a group of symbols.`);
|
|
323
|
+
for (const symbol of binary.keys()) {
|
|
324
|
+
if (bind.startsWith(symbol)) throw new TypeError(`The bind symbol "${bind}" starts with the operator "${symbol}".`);
|
|
325
|
+
}
|
|
326
|
+
return bind;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** This function examines the field `wrap` of a call form (K159). A name that is not a field of the form throws. */
|
|
330
|
+
function compileWrap(given: SyntaxForm, args: SyntaxFormArgs): boolean | ReadonlySet<string> {
|
|
331
|
+
const wrap = given.wrap;
|
|
332
|
+
if (wrap === undefined || wrap === false) return false;
|
|
333
|
+
if (args === "literal") throw new TypeError(`The call form "${given.name}" with the arguments literal takes no wrap.`);
|
|
334
|
+
if (wrap === true) return true;
|
|
335
|
+
if (args === "list") throw new TypeError(`The call form "${given.name}" with the arguments list takes only the wrap true.`);
|
|
336
|
+
for (const name of wrap) {
|
|
337
|
+
if (given.fields !== undefined && !given.fields.includes(name)) throw new TypeError(`The call form "${given.name}" has no field ${name} for its wrap.`);
|
|
338
|
+
}
|
|
339
|
+
return new Set(wrap);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** This function gives the types of the data envelopes of a profile (K159), or `undefined` without them. */
|
|
343
|
+
function compileEnvelopes(given: SyntaxEnvelopes | undefined): Compiled["envelopes"] {
|
|
344
|
+
if (given === undefined) return undefined;
|
|
345
|
+
const out: { number?: EnvelopeKind; text?: EnvelopeKind; bool?: EnvelopeKind } = {};
|
|
346
|
+
for (const key of ["number", "text", "bool"] as const) {
|
|
347
|
+
const kind = given[key];
|
|
348
|
+
if (kind !== undefined) out[key] = typeOfKind(kind);
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
|
|
252
353
|
function compileProfile(profile: SyntaxProfile): Compiled {
|
|
253
354
|
const cached = compiledProfiles.get(profile);
|
|
254
355
|
if (cached !== undefined) return cached;
|
|
@@ -263,9 +364,16 @@ function compileProfile(profile: SyntaxProfile): Compiled {
|
|
|
263
364
|
const forms = new Map<string, Form>();
|
|
264
365
|
const formsByKind = new Map<string, Form[]>();
|
|
265
366
|
for (const given of calls.forms ?? []) {
|
|
266
|
-
|
|
367
|
+
const keys = given.name.split(".");
|
|
368
|
+
if (!keys.every((key) => NAME.test(key))) throw new TypeError(`The call form "${given.name}" is not a plain name or a path of plain names.`);
|
|
369
|
+
if (keys.length > 1 && refs.fields === false) throw new TypeError(`The call form "${given.name}" has dots, but the syntax has no field steps.`);
|
|
370
|
+
if (keys.length > 1 && ops.opWords.has(keys[0] as string)) throw new TypeError(`The call form "${given.name}" starts with the word operator "${keys[0] as string}".`);
|
|
267
371
|
const { type, key } = typeOfKind(given.kind);
|
|
268
|
-
const
|
|
372
|
+
const args = given.args ?? (given.fields === undefined ? "list" : "fields");
|
|
373
|
+
if (!FORM_ARGS.includes(args)) throw new TypeError(`The call form "${given.name}" has the unknown arguments ${String(args)}.`);
|
|
374
|
+
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.`);
|
|
375
|
+
if (given.min !== undefined && (args === "named" || args === "literal")) throw new TypeError(`The call form "${given.name}" with the arguments ${args} takes no min.`);
|
|
376
|
+
const form: Form = { name: given.name, type, args, fields: given.fields, min: given.min ?? given.fields?.length ?? 0, wrap: compileWrap(given, args) };
|
|
269
377
|
forms.set(form.name, form);
|
|
270
378
|
addTo(formsByKind, key, form);
|
|
271
379
|
}
|
|
@@ -276,6 +384,7 @@ function compileProfile(profile: SyntaxProfile): Compiled {
|
|
|
276
384
|
quote: strings === undefined ? undefined : (strings.quote ?? '"'),
|
|
277
385
|
doubled: strings !== undefined && strings.escape === "double",
|
|
278
386
|
words,
|
|
387
|
+
envelopes: compileEnvelopes(literals.envelopes),
|
|
279
388
|
bare: refs.bare ?? "names",
|
|
280
389
|
roots,
|
|
281
390
|
fields: refs.fields ?? true,
|
|
@@ -287,6 +396,7 @@ function compileProfile(profile: SyntaxProfile): Compiled {
|
|
|
287
396
|
forms,
|
|
288
397
|
formsByKind,
|
|
289
398
|
separator,
|
|
399
|
+
bind: compileBind(profile.header?.bind, separator, ops.binary),
|
|
290
400
|
reserved: new Set([...words.keys(), ...ops.opWords, ...roots]),
|
|
291
401
|
maxDepth: profile.maxDepth ?? 256,
|
|
292
402
|
};
|
|
@@ -309,6 +419,34 @@ function opDoc(op: Op | Form, value: Json): Json {
|
|
|
309
419
|
return { type: op.type, value };
|
|
310
420
|
}
|
|
311
421
|
|
|
422
|
+
/** This function gives the kind of the data envelope of a literal (K159), or `undefined` when the literal has none. */
|
|
423
|
+
function envelopeOf(syn: Compiled, value: unknown): EnvelopeKind | undefined {
|
|
424
|
+
const kinds = syn.envelopes;
|
|
425
|
+
if (kinds === undefined || !isScalar(value)) return undefined;
|
|
426
|
+
return typeof value === "string" ? kinds.text : typeof value === "number" ? kinds.number : typeof value === "boolean" ? kinds.bool : undefined;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** This function gives a literal in a place that takes a document its data envelope (K159). Each other document stays. */
|
|
430
|
+
function envelop(syn: Compiled, doc: Json): Json {
|
|
431
|
+
const kind = isEnvelope(doc) ? undefined : envelopeOf(syn, doc);
|
|
432
|
+
return kind === undefined ? doc : { type: kind.type, value: doc };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** This function tells if an argument of a form takes a document (K159). */
|
|
436
|
+
function wraps(form: Form, key: Key): boolean {
|
|
437
|
+
return form.wrap === true || (form.wrap !== false && form.wrap.has(String(key)));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** This function tells if a value is a literal that JSON writes exactly: a text, a finite number, a truth value or `null`. */
|
|
441
|
+
function isScalar(value: unknown): value is string | number | boolean | null {
|
|
442
|
+
return value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function setOwn(target: Record<string, Json>, key: string, value: Json): void {
|
|
446
|
+
if (key === "__proto__") Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
447
|
+
else target[key] = value;
|
|
448
|
+
}
|
|
449
|
+
|
|
312
450
|
const ESCAPES: Readonly<Record<string, string>> = { '"': '"', "'": "'", "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: "\t" };
|
|
313
451
|
|
|
314
452
|
class Parser {
|
|
@@ -323,10 +461,11 @@ class Parser {
|
|
|
323
461
|
parse(): Json {
|
|
324
462
|
this.skip();
|
|
325
463
|
if (this.pos >= this.text.length) this.stop("The text is empty.", 0, 0);
|
|
326
|
-
const doc = this.
|
|
464
|
+
const doc = this.bound();
|
|
327
465
|
this.skip();
|
|
328
466
|
if (this.pos < this.text.length) this.stop("The text continues after the expression.", this.pos, this.tokenLength());
|
|
329
|
-
|
|
467
|
+
// The whole text is a document, thus a literal gets its data envelope (K159).
|
|
468
|
+
return envelop(this.syn, doc);
|
|
330
469
|
}
|
|
331
470
|
|
|
332
471
|
position(): number {
|
|
@@ -350,6 +489,53 @@ class Parser {
|
|
|
350
489
|
return (this.at(NAME_AT) ?? this.at(DIGITS_AT) ?? "x").length;
|
|
351
490
|
}
|
|
352
491
|
|
|
492
|
+
/**
|
|
493
|
+
* This method reads an expression that can have a header name: `name = expr` (D62). The bind symbol of the profile
|
|
494
|
+
* follows the name. A longer operator that starts with the symbol wins, for example `==` over `=`.
|
|
495
|
+
*/
|
|
496
|
+
private bound(): Json {
|
|
497
|
+
const name = this.bindingName();
|
|
498
|
+
if (name === undefined) return this.expr(0);
|
|
499
|
+
this.skip();
|
|
500
|
+
const start = this.pos;
|
|
501
|
+
const doc = this.expr(0);
|
|
502
|
+
if (isEnvelope(doc)) {
|
|
503
|
+
if (doc.name !== undefined) this.stop(`The document already has the name ${doc.name}.`, start, this.pos - start);
|
|
504
|
+
return { ...doc, name };
|
|
505
|
+
}
|
|
506
|
+
const kind = envelopeOf(this.syn, doc);
|
|
507
|
+
return kind === undefined ? { type: "lit", value: doc, name } : { type: kind.type, value: doc, name };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** This method reads a name and the bind symbol after it. Without them, it gives `undefined` and keeps the position. */
|
|
511
|
+
private bindingName(): string | undefined {
|
|
512
|
+
const bind = this.syn.bind;
|
|
513
|
+
if (bind === undefined) return undefined;
|
|
514
|
+
this.skip();
|
|
515
|
+
const back = this.pos;
|
|
516
|
+
let name: string | undefined;
|
|
517
|
+
if (this.text[this.pos] === "`") {
|
|
518
|
+
try {
|
|
519
|
+
name = this.quoted();
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (!(error instanceof Stop)) throw error;
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
name = this.at(NAME_AT);
|
|
525
|
+
if (name !== undefined) this.pos += name.length;
|
|
526
|
+
}
|
|
527
|
+
if (name !== undefined) {
|
|
528
|
+
this.skip();
|
|
529
|
+
const longer = this.syn.binarySymbols.some((symbol) => symbol.length > bind.length && symbol.startsWith(bind) && this.text.startsWith(symbol, this.pos));
|
|
530
|
+
if (this.text.startsWith(bind, this.pos) && !longer) {
|
|
531
|
+
this.pos += bind.length;
|
|
532
|
+
return name;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
this.pos = back;
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
|
|
353
539
|
/** This method reads an expression by precedence climbing. It stops at an operator below `min`. */
|
|
354
540
|
private expr(min: number): Json {
|
|
355
541
|
const start = this.pos;
|
|
@@ -486,7 +672,7 @@ class Parser {
|
|
|
486
672
|
private group(): Json {
|
|
487
673
|
const open = this.pos;
|
|
488
674
|
this.pos += 1;
|
|
489
|
-
const inner = this.
|
|
675
|
+
const inner = this.bound();
|
|
490
676
|
this.skip();
|
|
491
677
|
if (this.text[this.pos] !== ")") {
|
|
492
678
|
if (this.pos >= this.text.length) this.stop("A closing parenthesis is missing.", open, 1);
|
|
@@ -527,7 +713,7 @@ class Parser {
|
|
|
527
713
|
if (prefix !== undefined && prefix.word) return opDoc(prefix, [this.expr(prefix.prec + 1)]);
|
|
528
714
|
if (this.syn.binary.get(first)?.word === true) this.stop(`A term is missing before ${first}.`, start, first.length);
|
|
529
715
|
}
|
|
530
|
-
const path:
|
|
716
|
+
const path: Json[] = [first];
|
|
531
717
|
let allPlain = plain;
|
|
532
718
|
for (;;) {
|
|
533
719
|
const back = this.pos;
|
|
@@ -538,6 +724,12 @@ class Parser {
|
|
|
538
724
|
}
|
|
539
725
|
this.pos += 1;
|
|
540
726
|
this.skip();
|
|
727
|
+
// A step in parentheses is a document, for example `self.parent.(nearest(type: "panel"))` (SYNTAX.STEP).
|
|
728
|
+
if (this.text[this.pos] === "(") {
|
|
729
|
+
path.push(this.group());
|
|
730
|
+
allPlain = false;
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
541
733
|
if (this.text[this.pos] === "`") {
|
|
542
734
|
path.push(this.quoted());
|
|
543
735
|
allPlain = false;
|
|
@@ -553,7 +745,7 @@ class Parser {
|
|
|
553
745
|
const back = this.pos;
|
|
554
746
|
this.skip();
|
|
555
747
|
if (this.text[this.pos] === "(") {
|
|
556
|
-
if (!allPlain) this.stop("
|
|
748
|
+
if (!allPlain) this.stop("Each key of the name of a call is a plain name.", start, back - start);
|
|
557
749
|
return this.call(path.join("."), start, back - start);
|
|
558
750
|
}
|
|
559
751
|
this.pos = back;
|
|
@@ -562,39 +754,78 @@ class Parser {
|
|
|
562
754
|
return { type: "ref", value: full };
|
|
563
755
|
}
|
|
564
756
|
|
|
565
|
-
|
|
757
|
+
/** This method reads the arguments of a call in parentheses. `item` reads one argument. */
|
|
758
|
+
private argumentsOf<T>(item: () => T): T[] {
|
|
566
759
|
const open = this.pos;
|
|
567
|
-
const args:
|
|
760
|
+
const args: T[] = [];
|
|
568
761
|
this.pos += 1;
|
|
569
762
|
this.skip();
|
|
570
|
-
if (this.text[this.pos] === ")")
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
this.pos += 1;
|
|
581
|
-
break;
|
|
582
|
-
}
|
|
583
|
-
if (this.pos >= this.text.length) this.stop("A closing parenthesis is missing.", open, 1);
|
|
584
|
-
this.stop(`A "${this.syn.separator}" or a closing parenthesis is missing.`, this.pos, this.tokenLength());
|
|
763
|
+
if (this.text[this.pos] === ")") {
|
|
764
|
+
this.pos += 1;
|
|
765
|
+
return args;
|
|
766
|
+
}
|
|
767
|
+
for (;;) {
|
|
768
|
+
args.push(item());
|
|
769
|
+
this.skip();
|
|
770
|
+
if (this.text.startsWith(this.syn.separator, this.pos)) {
|
|
771
|
+
this.pos += this.syn.separator.length;
|
|
772
|
+
continue;
|
|
585
773
|
}
|
|
774
|
+
if (this.text[this.pos] === ")") {
|
|
775
|
+
this.pos += 1;
|
|
776
|
+
return args;
|
|
777
|
+
}
|
|
778
|
+
if (this.pos >= this.text.length) this.stop("A closing parenthesis is missing.", open, 1);
|
|
779
|
+
this.stop(`A "${this.syn.separator}" or a closing parenthesis is missing.`, this.pos, this.tokenLength());
|
|
586
780
|
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** This method reads the arguments `key: expr` of a form `named` into a record (SYNTAX.RECORD). */
|
|
784
|
+
private namedArguments(form: Form): Json {
|
|
785
|
+
const value: Record<string, Json> = {};
|
|
786
|
+
this.argumentsOf(() => {
|
|
787
|
+
this.skip();
|
|
788
|
+
const at = this.pos;
|
|
789
|
+
let key: string | undefined;
|
|
790
|
+
if (this.text[at] === "`") key = this.quoted();
|
|
791
|
+
else {
|
|
792
|
+
key = this.at(NAME_AT);
|
|
793
|
+
if (key !== undefined) this.pos += key.length;
|
|
794
|
+
}
|
|
795
|
+
const end = this.pos;
|
|
796
|
+
this.skip();
|
|
797
|
+
if (key === undefined || this.text[this.pos] !== ":") {
|
|
798
|
+
this.stop(`Each argument of the form ${form.name} is a key, a ":" and a document.`, at, Math.max(end - at, this.tokenLength()));
|
|
799
|
+
}
|
|
800
|
+
if (form.fields !== undefined && !form.fields.includes(key)) this.stop(`The form ${form.name} has no field ${key}.`, at, end - at);
|
|
801
|
+
if (Object.hasOwn(value, key)) this.stop(`The form ${form.name} has the field ${key} two times.`, at, end - at);
|
|
802
|
+
this.pos += 1;
|
|
803
|
+
const arg = this.bound();
|
|
804
|
+
setOwn(value, key, wraps(form, key) ? envelop(this.syn, arg) : arg);
|
|
805
|
+
});
|
|
806
|
+
return opDoc(form, value);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private call(name: string, start: number, nameLength: number): Json {
|
|
587
810
|
const form = this.syn.forms.get(name);
|
|
811
|
+
if (form?.args === "named") return this.namedArguments(form);
|
|
812
|
+
const args = this.argumentsOf(() => this.bound());
|
|
813
|
+
if (form?.args === "literal") {
|
|
814
|
+
const [only] = args;
|
|
815
|
+
if (args.length !== 1 || !isScalar(only)) this.stop(`The form ${name} takes one literal that JSON writes.`, start, this.pos - start);
|
|
816
|
+
return opDoc(form, only);
|
|
817
|
+
}
|
|
588
818
|
if (form !== undefined) {
|
|
589
819
|
const most = form.fields?.length ?? Number.POSITIVE_INFINITY;
|
|
590
820
|
if (args.length < form.min || args.length > most) {
|
|
591
821
|
const range = most === form.min ? String(most) : most === Number.POSITIVE_INFINITY ? `${String(form.min)} or more` : `${String(form.min)} to ${String(most)}`;
|
|
592
822
|
this.stop(`The form ${name} takes ${range} arguments.`, start, this.pos - start);
|
|
593
823
|
}
|
|
594
|
-
if (form.fields === undefined) return opDoc(form, args);
|
|
824
|
+
if (form.fields === undefined) return opDoc(form, form.wrap === true ? args.map((arg) => envelop(this.syn, arg)) : args);
|
|
595
825
|
const value: Record<string, Json> = {};
|
|
596
826
|
args.forEach((arg, index) => {
|
|
597
|
-
|
|
827
|
+
const key = form.fields?.[index] as string;
|
|
828
|
+
value[key] = wraps(form, key) ? envelop(this.syn, arg) : arg;
|
|
598
829
|
});
|
|
599
830
|
return opDoc(form, value);
|
|
600
831
|
}
|
|
@@ -625,13 +856,18 @@ export function parse(profile: SyntaxProfile, text: string): ParseResult {
|
|
|
625
856
|
|
|
626
857
|
// === Print ===
|
|
627
858
|
|
|
628
|
-
/**
|
|
629
|
-
|
|
859
|
+
/**
|
|
860
|
+
* The text of a part, from its document and its path in the value of the parent. `wrapped` is `true` when the place
|
|
861
|
+
* of the part takes a document (K159).
|
|
862
|
+
*/
|
|
863
|
+
type SubText = (child: unknown, path: readonly Key[], wrapped?: boolean) => Result<string>;
|
|
630
864
|
|
|
631
865
|
type Shape =
|
|
632
866
|
| { readonly tag: "text"; readonly text: string }
|
|
633
867
|
| { readonly tag: "op"; readonly op: Op; readonly terms: readonly unknown[] }
|
|
634
|
-
| { readonly tag: "call"; readonly name: string; readonly args: readonly (readonly [Key, unknown])[] }
|
|
868
|
+
| { readonly tag: "call"; readonly name: string; readonly args: readonly (readonly [Key, unknown, boolean?])[]; readonly named?: boolean }
|
|
869
|
+
| { readonly tag: "path"; readonly segments: readonly (string | readonly [Key, unknown])[] }
|
|
870
|
+
| { readonly tag: "bind"; readonly name: string; readonly doc: unknown }
|
|
635
871
|
| { readonly tag: "none"; readonly reason: string };
|
|
636
872
|
|
|
637
873
|
function none(reason: string): Shape {
|
|
@@ -672,8 +908,10 @@ function plainSegment(syn: Compiled, name: string): boolean {
|
|
|
672
908
|
return NAME.test(name) && !syn.reserved.has(name) && !syn.patterns.some((p) => p.whole.test(name));
|
|
673
909
|
}
|
|
674
910
|
|
|
675
|
-
function
|
|
911
|
+
/** This function gives the text of a later step of a path, or the step and its index when the step is a document. */
|
|
912
|
+
function laterSegment(step: unknown, index: number): string | readonly [Key, unknown] | undefined {
|
|
676
913
|
if (typeof step === "string") return NAME.test(step) ? step : quoteName(step);
|
|
914
|
+
if (isEnvelope(step)) return [index, step];
|
|
677
915
|
return typeof step === "number" && Number.isSafeInteger(step) && step >= 0 ? String(step) : undefined;
|
|
678
916
|
}
|
|
679
917
|
|
|
@@ -686,16 +924,18 @@ function refShape(syn: Compiled, value: unknown): Shape {
|
|
|
686
924
|
const [root, ...rest] = value as readonly unknown[];
|
|
687
925
|
const many = rest.length > 1 || (root !== syn.bare && rest.length > 0);
|
|
688
926
|
if (many && !syn.fields) return none("This syntax has no field steps.");
|
|
689
|
-
const later = rest.slice(1).map(laterSegment);
|
|
690
927
|
let first: string | undefined;
|
|
691
|
-
let tail =
|
|
692
|
-
if (root === syn.bare && typeof rest[0] === "string")
|
|
693
|
-
|
|
928
|
+
let tail: (string | readonly [Key, unknown] | undefined)[] = [];
|
|
929
|
+
if (root === syn.bare && typeof rest[0] === "string") {
|
|
930
|
+
first = plainSegment(syn, rest[0]) ? rest[0] : quoteName(rest[0]);
|
|
931
|
+
tail = rest.slice(1).map((step, index) => laterSegment(step, index + 2));
|
|
932
|
+
} else if (typeof root === "string" && syn.roots.has(root) && !syn.patterns.some((p) => p.whole.test(root))) {
|
|
694
933
|
first = root;
|
|
695
|
-
tail = rest.map(laterSegment);
|
|
934
|
+
tail = rest.map((step, index) => laterSegment(step, index + 1));
|
|
696
935
|
}
|
|
697
936
|
if (first === undefined || tail.includes(undefined)) return none("This syntax has no text for the path of the reference.");
|
|
698
|
-
|
|
937
|
+
const segments = [first, ...(tail as (string | readonly [Key, unknown])[])];
|
|
938
|
+
return segments.every((segment) => typeof segment === "string") ? { tag: "text", text: segments.join(".") } : { tag: "path", segments };
|
|
699
939
|
}
|
|
700
940
|
|
|
701
941
|
function callName(syn: Compiled, keys: readonly Key[]): string | undefined {
|
|
@@ -705,17 +945,48 @@ function callName(syn: Compiled, keys: readonly Key[]): string | undefined {
|
|
|
705
945
|
return syn.ops === true || syn.ops.has(name) ? name : undefined;
|
|
706
946
|
}
|
|
707
947
|
|
|
708
|
-
|
|
948
|
+
const WRAPPED = "This syntax reads a literal in this place as its data envelope, thus a bare literal has no text here.";
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* This function gives the text of a document in a place that takes a document (K159). A data envelope of a literal
|
|
952
|
+
* is the bare literal. A literal without its envelope has no text, because `parse` gives it the envelope. The
|
|
953
|
+
* function gives `undefined` for each other document.
|
|
954
|
+
*/
|
|
955
|
+
function wrappedShape(syn: Compiled, doc: unknown): Shape | undefined {
|
|
956
|
+
if (syn.envelopes === undefined) return undefined;
|
|
957
|
+
if (!isEnvelope(doc)) return envelopeOf(syn, doc) === undefined ? undefined : none(WRAPPED);
|
|
958
|
+
if (doc.name !== undefined || doc.own !== undefined || !Object.hasOwn(doc, "value")) return undefined;
|
|
959
|
+
const path = typePath(doc.type);
|
|
960
|
+
if (path === undefined) return undefined;
|
|
961
|
+
const key = JSON.stringify(path);
|
|
962
|
+
if (key === JSON.stringify(["lit"])) return envelopeOf(syn, doc.value) === undefined ? undefined : none(WRAPPED);
|
|
963
|
+
if (envelopeOf(syn, doc.value)?.key !== key) return undefined;
|
|
964
|
+
const literal = literalShape(syn, doc.value);
|
|
965
|
+
return literal.tag === "text" ? literal : undefined;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function formShape(syn: Compiled, form: Form, value: unknown): Shape | undefined {
|
|
969
|
+
if (form.args === "literal") {
|
|
970
|
+
if (!isScalar(value)) return undefined;
|
|
971
|
+
const literal = literalShape(syn, value);
|
|
972
|
+
return literal.tag === "text" ? { tag: "text", text: `${form.name}(${literal.text})` } : undefined;
|
|
973
|
+
}
|
|
974
|
+
if (form.args === "named") {
|
|
975
|
+
if (!isRecord(value)) return undefined;
|
|
976
|
+
const keys = Object.keys(value);
|
|
977
|
+
if (form.fields !== undefined && !keys.every((key) => form.fields?.includes(key) === true)) return undefined;
|
|
978
|
+
return { tag: "call", name: form.name, args: keys.map((key) => [key, value[key], wraps(form, key)] as const), named: true };
|
|
979
|
+
}
|
|
709
980
|
if (form.fields === undefined) {
|
|
710
981
|
if (!Array.isArray(value) || value.length < form.min) return undefined;
|
|
711
|
-
return { tag: "call", name: form.name, args: value.map((item, index) => [index, item] as const) };
|
|
982
|
+
return { tag: "call", name: form.name, args: value.map((item, index) => [index, item, wraps(form, index)] as const) };
|
|
712
983
|
}
|
|
713
984
|
if (!isRecord(value)) return undefined;
|
|
714
985
|
// The fields that the value holds must be the first fields of the form, because the arguments go in order.
|
|
715
986
|
const count = Object.keys(value).length;
|
|
716
987
|
const present = form.fields.slice(0, count);
|
|
717
988
|
if (count < form.min || present.length < count || !present.every((key) => Object.hasOwn(value, key))) return undefined;
|
|
718
|
-
return { tag: "call", name: form.name, args: present.map((key) => [key, value[key]] as const) };
|
|
989
|
+
return { tag: "call", name: form.name, args: present.map((key) => [key, value[key], wraps(form, key)] as const) };
|
|
719
990
|
}
|
|
720
991
|
|
|
721
992
|
function opShape(syn: Compiled, key: string, terms: readonly unknown[]): Shape | undefined {
|
|
@@ -729,7 +1000,12 @@ function opShape(syn: Compiled, key: string, terms: readonly unknown[]): Shape |
|
|
|
729
1000
|
/** This function gives the shape of a document: a literal or a reference, an operator, a call, or no text. */
|
|
730
1001
|
function shapeOf(syn: Compiled, doc: unknown): Shape {
|
|
731
1002
|
if (!isEnvelope(doc)) return Array.isArray(doc) || isRecord(doc) ? none("This syntax has no text for a list or a record literal.") : literalShape(syn, doc);
|
|
732
|
-
if (doc.
|
|
1003
|
+
if (doc.own !== undefined) return none("This syntax has no text for the own entries of a document.");
|
|
1004
|
+
if (doc.name !== undefined) {
|
|
1005
|
+
if (syn.bind === undefined) return none("This syntax has no text for the name of a document.");
|
|
1006
|
+
const { name, ...rest } = doc;
|
|
1007
|
+
return { tag: "bind", name, doc: rest };
|
|
1008
|
+
}
|
|
733
1009
|
const path = typePath(doc.type);
|
|
734
1010
|
if (path === undefined) return none("This syntax has no text for a computed type.");
|
|
735
1011
|
const value: unknown = doc.value;
|
|
@@ -745,7 +1021,7 @@ function shapeOf(syn: Compiled, doc: unknown): Shape {
|
|
|
745
1021
|
const op = Array.isArray(value) ? opShape(syn, key, value) : undefined;
|
|
746
1022
|
if (op !== undefined) return op;
|
|
747
1023
|
for (const form of syn.formsByKind.get(key) ?? []) {
|
|
748
|
-
const shape = formShape(form, value);
|
|
1024
|
+
const shape = formShape(syn, form, value);
|
|
749
1025
|
if (shape !== undefined) return shape;
|
|
750
1026
|
}
|
|
751
1027
|
const called = path[0] === "ops" && Array.isArray(value) ? callName(syn, path.slice(1)) : undefined;
|
|
@@ -755,7 +1031,8 @@ function shapeOf(syn: Compiled, doc: unknown): Shape {
|
|
|
755
1031
|
|
|
756
1032
|
function levelOf(syn: Compiled, doc: unknown): number {
|
|
757
1033
|
const shape = shapeOf(syn, doc);
|
|
758
|
-
|
|
1034
|
+
// A document with a name binds more loosely than each operator, thus it needs parentheses as a term.
|
|
1035
|
+
return shape.tag === "op" ? shape.op.prec : shape.tag === "bind" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
759
1036
|
}
|
|
760
1037
|
|
|
761
1038
|
/** This function tells if a term needs parentheses in its position, so that `parse` reads the same tree. */
|
|
@@ -789,20 +1066,46 @@ function printNode(syn: Compiled, doc: unknown, sub: SubText): Result<string> {
|
|
|
789
1066
|
if (shape.tag === "text") return ok(shape.text);
|
|
790
1067
|
if (shape.tag === "none") return fail("#VALUE!", shape.reason);
|
|
791
1068
|
if (shape.tag === "op") return opText(syn, shape.op, shape.terms, sub);
|
|
1069
|
+
if (shape.tag === "bind") {
|
|
1070
|
+
// The document after a header name takes a document, thus a data envelope of a literal is the bare literal (K159).
|
|
1071
|
+
const head = wrappedShape(syn, shape.doc);
|
|
1072
|
+
const inner = head === undefined ? printNode(syn, shape.doc, sub) : head.tag === "text" ? ok(head.text) : fail("#VALUE!", WRAPPED);
|
|
1073
|
+
return inner.ok ? ok(`${NAME.test(shape.name) ? shape.name : quoteName(shape.name)} ${syn.bind as string} ${inner.value}`) : inner;
|
|
1074
|
+
}
|
|
1075
|
+
if (shape.tag === "path") {
|
|
1076
|
+
const texts: string[] = [];
|
|
1077
|
+
for (const segment of shape.segments) {
|
|
1078
|
+
if (typeof segment === "string") {
|
|
1079
|
+
texts.push(segment);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
const result = sub(segment[1], [segment[0]]);
|
|
1083
|
+
if (!result.ok) return result;
|
|
1084
|
+
texts.push(`(${result.value})`);
|
|
1085
|
+
}
|
|
1086
|
+
return ok(texts.join("."));
|
|
1087
|
+
}
|
|
792
1088
|
const texts: string[] = [];
|
|
793
|
-
for (const [key, arg] of shape.args) {
|
|
794
|
-
const result = sub(arg, [key]);
|
|
1089
|
+
for (const [key, arg, wrapped] of shape.args) {
|
|
1090
|
+
const result = sub(arg, [key], wrapped);
|
|
795
1091
|
if (!result.ok) return result;
|
|
796
|
-
texts.push(result.value);
|
|
1092
|
+
texts.push(shape.named === true ? `${NAME.test(String(key)) ? String(key) : quoteName(String(key))}: ${result.value}` : result.value);
|
|
797
1093
|
}
|
|
798
1094
|
return ok(`${shape.name}(${texts.join(`${syn.separator} `)})`);
|
|
799
1095
|
}
|
|
800
1096
|
|
|
801
1097
|
function printTree(syn: Compiled, doc: unknown, place: readonly Key[]): Result<string> {
|
|
802
|
-
const result = printNode(syn, doc, (child, path) => printTree(syn, child, [...place, ...path]));
|
|
1098
|
+
const result = printNode(syn, doc, (child, path, wrapped) => (wrapped === true ? printPlace : printTree)(syn, child, [...place, ...path]));
|
|
803
1099
|
return result.ok || result.error.place !== undefined ? result : fail(result.error.code, result.error.reason, { place });
|
|
804
1100
|
}
|
|
805
1101
|
|
|
1102
|
+
/** This function writes a document in a place that takes a document, for example the whole text (K159). */
|
|
1103
|
+
function printPlace(syn: Compiled, doc: unknown, place: readonly Key[]): Result<string> {
|
|
1104
|
+
const shape = wrappedShape(syn, doc);
|
|
1105
|
+
if (shape === undefined) return printTree(syn, doc, place);
|
|
1106
|
+
return shape.tag === "text" ? ok(shape.text) : fail("#VALUE!", WRAPPED, { place });
|
|
1107
|
+
}
|
|
1108
|
+
|
|
806
1109
|
/**
|
|
807
1110
|
* This function writes a document as text with the rules of a profile (SYNTAX.PRINT). It writes the fewest
|
|
808
1111
|
* parentheses that keep the tree. `parse` reads the text back to the same document (SYNTAX.ROUNDTRIP).
|
|
@@ -815,7 +1118,7 @@ function printTree(syn: Compiled, doc: unknown, place: readonly Key[]): Result<s
|
|
|
815
1118
|
export function print(profile: SyntaxProfile, doc: unknown): Result<string> {
|
|
816
1119
|
const syn = compileProfile(profile);
|
|
817
1120
|
try {
|
|
818
|
-
return
|
|
1121
|
+
return printPlace(syn, doc, []);
|
|
819
1122
|
} catch (error) {
|
|
820
1123
|
if (isStackOverflow(error)) return fail("#DEPTH!", "The document is too deep for the stack of the host.", { cause: error });
|
|
821
1124
|
throw error;
|
|
@@ -834,7 +1137,9 @@ export function syntaxFormat(profile: SyntaxProfile): MachinePart {
|
|
|
834
1137
|
const syn = compileProfile(profile);
|
|
835
1138
|
const code: EvalFn = ({ doc, parts, place }) => {
|
|
836
1139
|
const byPath = new Map<string, Part>(parts.map((part) => [JSON.stringify(part.path), part]));
|
|
837
|
-
return printNode(syn, doc, (child, path) => {
|
|
1140
|
+
return printNode(syn, doc, (child, path, wrapped) => {
|
|
1141
|
+
const own = wrapped === true ? wrappedShape(syn, child) : undefined;
|
|
1142
|
+
if (own !== undefined) return own.tag === "text" ? ok(own.text) : fail("#VALUE!", WRAPPED, { place: [...place, ...path] });
|
|
838
1143
|
const part = byPath.get(JSON.stringify(path));
|
|
839
1144
|
if (part === undefined) return printTree(syn, child, [...place, ...path]);
|
|
840
1145
|
const result = part.evaluate();
|