@mark1russell7/coeval 0.21.0 → 0.23.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 +23 -5
- package/build/index.d.ts +420 -116
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5669 -3655
- package/build/index.js.map +1 -1
- package/package.json +4 -4
- package/src/codec-laws.ts +152 -0
- package/src/codec.ts +2 -2
- package/src/conform.ts +662 -70
- package/src/deref.ts +17 -3
- package/src/durable.ts +21 -9
- package/src/errors.ts +17 -1
- package/src/evaluate.ts +55 -21
- package/src/formula.ts +82 -22
- package/src/index.ts +16 -0
- package/src/instances.ts +61 -6
- package/src/meaning.ts +43 -10
- package/src/parts.ts +7 -4
- package/src/providers.ts +4 -1
- package/src/reference.ts +134 -9
- package/src/schedule.ts +286 -45
- package/src/static.ts +116 -23
- package/src/stdlib.ts +229 -6
- package/src/syntax.ts +430 -48
- package/src/types.ts +2 -1
- package/src/world.ts +106 -1
package/src/syntax.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Json, type Key, isEnvelope, isRecord, typePath } from "./doc.ts";
|
|
1
|
+
import { type Json, type JsonRecord, type Key, isEnvelope, isRecord, typePath } from "./doc.ts";
|
|
2
2
|
import { type Result, describe, fail, isStackOverflow, ok } from "./errors.ts";
|
|
3
3
|
import type { EvalFn, Part } from "./evaluate.ts";
|
|
4
4
|
import type { MachinePart } from "./merge.ts";
|
|
@@ -134,6 +134,22 @@ export interface SyntaxHeader {
|
|
|
134
134
|
readonly bind?: string;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* The text forms of the constructs of the formula kit (K145, D127). Each field is the kind of one form, with dots
|
|
139
|
+
* between the keys. A construct without a field has no text. Each text of a form was an issue before, and no form
|
|
140
|
+
* reserves a name.
|
|
141
|
+
*/
|
|
142
|
+
export interface SyntaxConstructs {
|
|
143
|
+
/** The kind of the form `{a: 1, b: x}`. Its value is a record of documents. */
|
|
144
|
+
readonly record?: string;
|
|
145
|
+
/** The kind of the form `let {a: 1} in a + 1`. Its value has the fields `bind` and `body`. */
|
|
146
|
+
readonly let?: string;
|
|
147
|
+
/** The kind of the form `(x, y) => x + y`. Its value has the fields `params` and `body`. */
|
|
148
|
+
readonly fn?: string;
|
|
149
|
+
/** The kind of the form `(f)(1, 2)`: a call of a value. Its value has the fields `fn` and `args`. */
|
|
150
|
+
readonly call?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
137
153
|
/**
|
|
138
154
|
* The text syntax of a language (D41): an operator table, literal rules, reference syntax and call syntax. `parse`
|
|
139
155
|
* reads a text into a document, and `print` writes a document as text. The profile is data, thus a site, an editor
|
|
@@ -150,6 +166,8 @@ export interface SyntaxProfile {
|
|
|
150
166
|
readonly calls?: SyntaxCalls;
|
|
151
167
|
/** The text of the header of a document. */
|
|
152
168
|
readonly header?: SyntaxHeader;
|
|
169
|
+
/** The text forms of the constructs. Without a field, the construct has no text. */
|
|
170
|
+
readonly constructs?: SyntaxConstructs;
|
|
153
171
|
/** The deepest nesting that `parse` accepts. The default is `256`. */
|
|
154
172
|
readonly maxDepth?: number;
|
|
155
173
|
}
|
|
@@ -164,14 +182,21 @@ export interface SyntaxIssue {
|
|
|
164
182
|
readonly message: string;
|
|
165
183
|
}
|
|
166
184
|
|
|
167
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* The most issues that `parse` gives for one text (D138). A person reads a list of this size, and a text with more
|
|
187
|
+
* issues needs another kind of help.
|
|
188
|
+
*/
|
|
189
|
+
export const MAX_SYNTAX_ISSUES = 20;
|
|
190
|
+
|
|
191
|
+
/** The result of `parse`: a document, or the problems of the text in the order of their offsets (D138). */
|
|
168
192
|
export type ParseResult = { readonly ok: true; readonly doc: Json } | { readonly ok: false; readonly issues: readonly SyntaxIssue[] };
|
|
169
193
|
|
|
170
194
|
/**
|
|
171
195
|
* The standard syntax of the formula kit. It gives the site and the playground a text form. The operators are
|
|
172
196
|
* `or`, `and`, `not`, the comparisons, `+ - * / %`, a minus sign and `^`, from the loosest to the tightest. A call
|
|
173
197
|
* `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).
|
|
198
|
+
* text `total = 2 + 3` gives the document the header name `total` (D62). The forms `{a: 1}`, `let {a: 1} in a`,
|
|
199
|
+
* `(x) => x` and `(f)(1)` make `record`, `let`, `fn` and `call` (D127).
|
|
175
200
|
*/
|
|
176
201
|
export const STD_SYNTAX: SyntaxProfile = /* @__PURE__ */ Object.freeze({
|
|
177
202
|
operators: [
|
|
@@ -200,6 +225,7 @@ export const STD_SYNTAX: SyntaxProfile = /* @__PURE__ */ Object.freeze({
|
|
|
200
225
|
],
|
|
201
226
|
},
|
|
202
227
|
header: { bind: "=" },
|
|
228
|
+
constructs: { record: "record", let: "let", fn: "fn", call: "call" },
|
|
203
229
|
} satisfies SyntaxProfile);
|
|
204
230
|
|
|
205
231
|
// === The compiled profile ===
|
|
@@ -258,6 +284,10 @@ interface Compiled {
|
|
|
258
284
|
readonly formsByKind: ReadonlyMap<string, readonly Form[]>;
|
|
259
285
|
readonly separator: string;
|
|
260
286
|
readonly bind: string | undefined;
|
|
287
|
+
/** The type of the document of each construct that has a text form. */
|
|
288
|
+
readonly constructs: { readonly [Name in keyof SyntaxConstructs]?: Json };
|
|
289
|
+
/** The construct of each kind, by the path of the kind as a JSON text. */
|
|
290
|
+
readonly constructKinds: ReadonlyMap<string, keyof SyntaxConstructs>;
|
|
261
291
|
readonly reserved: ReadonlySet<string>;
|
|
262
292
|
readonly opWords: ReadonlySet<string>;
|
|
263
293
|
readonly maxDepth: number;
|
|
@@ -271,6 +301,7 @@ const SYMBOL = /^[^\sA-Za-z0-9_"'`()$.]+$/;
|
|
|
271
301
|
const DEFAULT_WORDS: Readonly<Record<string, unknown>> = { true: true, false: false, null: null };
|
|
272
302
|
const DEFAULT_ROOTS: readonly string[] = ["self", "world", "event", "caps"];
|
|
273
303
|
const FORM_ARGS: readonly SyntaxFormArgs[] = ["list", "fields", "named", "literal"];
|
|
304
|
+
const CONSTRUCTS: readonly (keyof SyntaxConstructs)[] = ["record", "let", "fn", "call"];
|
|
274
305
|
|
|
275
306
|
const compiledProfiles = new WeakMap<SyntaxProfile, Compiled>();
|
|
276
307
|
|
|
@@ -326,6 +357,25 @@ function compileBind(bind: string | undefined, separator: string, binary: Readon
|
|
|
326
357
|
return bind;
|
|
327
358
|
}
|
|
328
359
|
|
|
360
|
+
/** This function gives the types of the construct forms (K145). An operator that reads the same text throws. */
|
|
361
|
+
function compileConstructs(given: SyntaxConstructs | undefined, ops: Pick<Compiled, "prefixSymbols" | "binarySymbols" | "opWords">): Pick<Compiled, "constructs" | "constructKinds"> {
|
|
362
|
+
const constructs: { -readonly [Name in keyof SyntaxConstructs]?: Json } = {};
|
|
363
|
+
const constructKinds = new Map<string, keyof SyntaxConstructs>();
|
|
364
|
+
for (const name of CONSTRUCTS) {
|
|
365
|
+
const kind = given?.[name];
|
|
366
|
+
if (kind === undefined) continue;
|
|
367
|
+
const { type, key } = typeOfKind(kind);
|
|
368
|
+
constructs[name] = type;
|
|
369
|
+
constructKinds.set(key, name);
|
|
370
|
+
}
|
|
371
|
+
const braces = constructs.record !== undefined || constructs.let !== undefined;
|
|
372
|
+
for (const symbol of [...ops.prefixSymbols, ...ops.binarySymbols]) {
|
|
373
|
+
if ((braces && /^[{}]/.test(symbol)) || (constructs.fn !== undefined && symbol.startsWith("=>"))) throw new TypeError(`The operator "${symbol}" reads the text of a construct form.`);
|
|
374
|
+
}
|
|
375
|
+
if (constructs.let !== undefined && ops.opWords.has("let")) throw new TypeError('The operator "let" reads the text of a construct form.');
|
|
376
|
+
return { constructs, constructKinds };
|
|
377
|
+
}
|
|
378
|
+
|
|
329
379
|
/** This function examines the field `wrap` of a call form (K159). A name that is not a field of the form throws. */
|
|
330
380
|
function compileWrap(given: SyntaxForm, args: SyntaxFormArgs): boolean | ReadonlySet<string> {
|
|
331
381
|
const wrap = given.wrap;
|
|
@@ -397,6 +447,7 @@ function compileProfile(profile: SyntaxProfile): Compiled {
|
|
|
397
447
|
formsByKind,
|
|
398
448
|
separator,
|
|
399
449
|
bind: compileBind(profile.header?.bind, separator, ops.binary),
|
|
450
|
+
...compileConstructs(profile.constructs, ops),
|
|
400
451
|
reserved: new Set([...words.keys(), ...ops.opWords, ...roots]),
|
|
401
452
|
maxDepth: profile.maxDepth ?? 256,
|
|
402
453
|
};
|
|
@@ -408,7 +459,11 @@ function compileProfile(profile: SyntaxProfile): Compiled {
|
|
|
408
459
|
|
|
409
460
|
/** The stop of a parse. The parser throws it, and `parse` gives its issue. */
|
|
410
461
|
class Stop {
|
|
411
|
-
|
|
462
|
+
readonly issue: SyntaxIssue;
|
|
463
|
+
|
|
464
|
+
constructor(issue: SyntaxIssue) {
|
|
465
|
+
this.issue = issue;
|
|
466
|
+
}
|
|
412
467
|
}
|
|
413
468
|
|
|
414
469
|
function literalDoc(value: unknown): Json {
|
|
@@ -450,13 +505,15 @@ function setOwn(target: Record<string, Json>, key: string, value: Json): void {
|
|
|
450
505
|
const ESCAPES: Readonly<Record<string, string>> = { '"': '"', "'": "'", "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: "\t" };
|
|
451
506
|
|
|
452
507
|
class Parser {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
508
|
+
protected readonly syn: Compiled;
|
|
509
|
+
protected readonly text: string;
|
|
510
|
+
protected pos = 0;
|
|
511
|
+
protected depth = 0;
|
|
512
|
+
|
|
513
|
+
constructor(syn: Compiled, text: string) {
|
|
514
|
+
this.syn = syn;
|
|
515
|
+
this.text = text;
|
|
516
|
+
}
|
|
460
517
|
|
|
461
518
|
parse(): Json {
|
|
462
519
|
this.skip();
|
|
@@ -472,11 +529,11 @@ class Parser {
|
|
|
472
529
|
return this.pos;
|
|
473
530
|
}
|
|
474
531
|
|
|
475
|
-
|
|
532
|
+
protected stop(message: string, offset: number, length: number): never {
|
|
476
533
|
throw new Stop({ offset, length, message });
|
|
477
534
|
}
|
|
478
535
|
|
|
479
|
-
|
|
536
|
+
protected skip(): void {
|
|
480
537
|
while (this.pos < this.text.length && /\s/.test(this.text[this.pos] as string)) this.pos += 1;
|
|
481
538
|
}
|
|
482
539
|
|
|
@@ -485,7 +542,7 @@ class Parser {
|
|
|
485
542
|
return pattern.exec(this.text)?.[0];
|
|
486
543
|
}
|
|
487
544
|
|
|
488
|
-
|
|
545
|
+
protected tokenLength(): number {
|
|
489
546
|
return (this.at(NAME_AT) ?? this.at(DIGITS_AT) ?? "x").length;
|
|
490
547
|
}
|
|
491
548
|
|
|
@@ -493,7 +550,7 @@ class Parser {
|
|
|
493
550
|
* This method reads an expression that can have a header name: `name = expr` (D62). The bind symbol of the profile
|
|
494
551
|
* follows the name. A longer operator that starts with the symbol wins, for example `==` over `=`.
|
|
495
552
|
*/
|
|
496
|
-
|
|
553
|
+
protected bound(): Json {
|
|
497
554
|
const name = this.bindingName();
|
|
498
555
|
if (name === undefined) return this.expr(0);
|
|
499
556
|
this.skip();
|
|
@@ -605,7 +662,8 @@ class Parser {
|
|
|
605
662
|
if (/[0-9]/.test(c)) return this.number(false);
|
|
606
663
|
if (c === "-" && this.syn.signed && /[0-9]/.test(this.text[start + 1] ?? "")) return this.number(true);
|
|
607
664
|
if (c === this.syn.quote) return this.string();
|
|
608
|
-
if (c === "(") return this.
|
|
665
|
+
if (c === "(") return this.parenthesized();
|
|
666
|
+
if (c === "{" && this.syn.constructs.record !== undefined) return { type: this.syn.constructs.record, value: this.entries(undefined) };
|
|
609
667
|
if (c === "`" || /[A-Za-z_]/.test(c)) return this.named();
|
|
610
668
|
for (const symbol of this.syn.prefixSymbols) {
|
|
611
669
|
if (this.text.startsWith(symbol, start)) {
|
|
@@ -654,7 +712,7 @@ class Parser {
|
|
|
654
712
|
}
|
|
655
713
|
}
|
|
656
714
|
|
|
657
|
-
|
|
715
|
+
protected escape(): string {
|
|
658
716
|
const start = this.pos;
|
|
659
717
|
const c = this.text[start + 1] ?? "";
|
|
660
718
|
if (c === "u") {
|
|
@@ -669,14 +727,72 @@ class Parser {
|
|
|
669
727
|
return out;
|
|
670
728
|
}
|
|
671
729
|
|
|
672
|
-
|
|
730
|
+
/** This method reads a function `(x) => x`, or a group with the calls of a value after it (K145). */
|
|
731
|
+
private parenthesized(): Json {
|
|
732
|
+
const { fn, call } = this.syn.constructs;
|
|
733
|
+
const params = fn === undefined ? undefined : this.arrowParams();
|
|
734
|
+
if (fn !== undefined && params !== undefined) {
|
|
735
|
+
this.skip();
|
|
736
|
+
return { type: fn, value: { params, body: this.bound() } };
|
|
737
|
+
}
|
|
738
|
+
let out = this.group();
|
|
739
|
+
if (call === undefined) return out;
|
|
740
|
+
for (;;) {
|
|
741
|
+
const back = this.pos;
|
|
742
|
+
this.skip();
|
|
743
|
+
if (this.text[this.pos] !== "(") {
|
|
744
|
+
this.pos = back;
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
747
|
+
out = { type: call, value: { fn: out, args: this.argumentsOf(() => this.bound()) } };
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** This method reads `(a, b) =>` and gives the names. Without that text, it gives `undefined` and keeps the position. */
|
|
752
|
+
private arrowParams(): string[] | undefined {
|
|
753
|
+
const back = this.pos;
|
|
754
|
+
const params: string[] = [];
|
|
755
|
+
this.pos += 1;
|
|
756
|
+
for (let first = true; ; first = false) {
|
|
757
|
+
this.skip();
|
|
758
|
+
if (first && this.text[this.pos] === ")") break;
|
|
759
|
+
const name = this.key();
|
|
760
|
+
this.skip();
|
|
761
|
+
if (name !== undefined) params.push(name);
|
|
762
|
+
if (name !== undefined && this.text[this.pos] === ")") break;
|
|
763
|
+
if (name === undefined || !this.text.startsWith(this.syn.separator, this.pos)) {
|
|
764
|
+
this.pos = back;
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
this.pos += this.syn.separator.length;
|
|
768
|
+
}
|
|
769
|
+
this.pos += 1;
|
|
770
|
+
this.skip();
|
|
771
|
+
if (!this.text.startsWith("=>", this.pos)) {
|
|
772
|
+
this.pos = back;
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
this.pos += 2;
|
|
776
|
+
return params;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** This method reads the bindings between braces, the word `in` and the body of a `let` (K145). */
|
|
780
|
+
private letTerm(type: Json): Json {
|
|
781
|
+
const bind = this.entries(undefined);
|
|
782
|
+
this.skip();
|
|
783
|
+
if (this.at(NAME_AT) !== "in") this.stop('The word "in" is missing after the bindings of let.', this.pos, Math.min(this.tokenLength(), this.text.length - this.pos));
|
|
784
|
+
this.pos += 2;
|
|
785
|
+
return { type, value: { bind, body: this.bound() } };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
protected group(): Json {
|
|
673
789
|
const open = this.pos;
|
|
674
790
|
this.pos += 1;
|
|
675
791
|
const inner = this.bound();
|
|
676
792
|
this.skip();
|
|
677
793
|
if (this.text[this.pos] !== ")") {
|
|
678
794
|
if (this.pos >= this.text.length) this.stop("A closing parenthesis is missing.", open, 1);
|
|
679
|
-
this.
|
|
795
|
+
this.noEnd(")", false);
|
|
680
796
|
}
|
|
681
797
|
this.pos += 1;
|
|
682
798
|
return inner;
|
|
@@ -702,6 +818,14 @@ class Parser {
|
|
|
702
818
|
}
|
|
703
819
|
}
|
|
704
820
|
|
|
821
|
+
/** This method reads a plain name or a name in backticks. Without a name, it gives `undefined`. */
|
|
822
|
+
private key(): string | undefined {
|
|
823
|
+
if (this.text[this.pos] === "`") return this.quoted();
|
|
824
|
+
const name = this.at(NAME_AT);
|
|
825
|
+
if (name !== undefined) this.pos += name.length;
|
|
826
|
+
return name;
|
|
827
|
+
}
|
|
828
|
+
|
|
705
829
|
/** This method reads a name: a word operator, a call, a literal word or a reference. */
|
|
706
830
|
private named(): Json {
|
|
707
831
|
const start = this.pos;
|
|
@@ -712,6 +836,14 @@ class Parser {
|
|
|
712
836
|
const prefix = this.syn.prefix.get(first);
|
|
713
837
|
if (prefix !== undefined && prefix.word) return opDoc(prefix, [this.expr(prefix.prec + 1)]);
|
|
714
838
|
if (this.syn.binary.get(first)?.word === true) this.stop(`A term is missing before ${first}.`, start, first.length);
|
|
839
|
+
// The word `let` starts a form only before a brace, thus `let` stays a good name (D127).
|
|
840
|
+
const construct = first === "let" ? this.syn.constructs.let : undefined;
|
|
841
|
+
if (construct !== undefined) {
|
|
842
|
+
const back = this.pos;
|
|
843
|
+
this.skip();
|
|
844
|
+
if (this.text[this.pos] === "{") return this.letTerm(construct);
|
|
845
|
+
this.pos = back;
|
|
846
|
+
}
|
|
715
847
|
}
|
|
716
848
|
const path: Json[] = [first];
|
|
717
849
|
let allPlain = plain;
|
|
@@ -754,13 +886,20 @@ class Parser {
|
|
|
754
886
|
return { type: "ref", value: full };
|
|
755
887
|
}
|
|
756
888
|
|
|
757
|
-
/** This method
|
|
758
|
-
|
|
889
|
+
/** This method stops at a token where a group or a list must end. A list can also continue with its separator. */
|
|
890
|
+
protected noEnd(close: string, list: boolean): never {
|
|
891
|
+
const closer = close === ")" ? "parenthesis" : "brace";
|
|
892
|
+
const before = list ? `"${this.syn.separator}" or a ` : "";
|
|
893
|
+
return this.stop(`A ${before}closing ${closer} is missing.`, this.pos, this.tokenLength());
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** This method reads the arguments of a call in parentheses. `item` reads one argument. `close` ends the arguments. */
|
|
897
|
+
protected argumentsOf<T>(item: () => T, close = ")"): T[] {
|
|
759
898
|
const open = this.pos;
|
|
760
899
|
const args: T[] = [];
|
|
761
900
|
this.pos += 1;
|
|
762
901
|
this.skip();
|
|
763
|
-
if (this.text[this.pos] ===
|
|
902
|
+
if (this.text[this.pos] === close) {
|
|
764
903
|
this.pos += 1;
|
|
765
904
|
return args;
|
|
766
905
|
}
|
|
@@ -771,44 +910,44 @@ class Parser {
|
|
|
771
910
|
this.pos += this.syn.separator.length;
|
|
772
911
|
continue;
|
|
773
912
|
}
|
|
774
|
-
if (this.text[this.pos] ===
|
|
913
|
+
if (this.text[this.pos] === close) {
|
|
775
914
|
this.pos += 1;
|
|
776
915
|
return args;
|
|
777
916
|
}
|
|
778
|
-
if (this.pos >= this.text.length) this.stop(
|
|
779
|
-
this.
|
|
917
|
+
if (this.pos >= this.text.length) this.stop(`A closing ${close === ")" ? "parenthesis" : "brace"} is missing.`, open, 1);
|
|
918
|
+
this.noEnd(close, true);
|
|
780
919
|
}
|
|
781
920
|
}
|
|
782
921
|
|
|
783
|
-
/**
|
|
784
|
-
|
|
922
|
+
/**
|
|
923
|
+
* This method reads the entries `key: expr` into a record: the arguments of a form `named` (SYNTAX.RECORD), or the
|
|
924
|
+
* entries between braces without a form (SYNTAX.RECORD-TEXT).
|
|
925
|
+
*/
|
|
926
|
+
private entries(form: Form | undefined): Record<string, Json> {
|
|
927
|
+
const owner = form === undefined ? "record" : `form ${form.name}`;
|
|
785
928
|
const value: Record<string, Json> = {};
|
|
786
|
-
|
|
929
|
+
const entry = (): void => {
|
|
787
930
|
this.skip();
|
|
788
931
|
const at = this.pos;
|
|
789
|
-
|
|
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
|
-
}
|
|
932
|
+
const key = this.key();
|
|
795
933
|
const end = this.pos;
|
|
796
934
|
this.skip();
|
|
797
935
|
if (key === undefined || this.text[this.pos] !== ":") {
|
|
798
|
-
this.stop(`Each argument of the
|
|
936
|
+
this.stop(`Each ${form === undefined ? "entry" : "argument"} of the ${owner} is a key, a ":" and a document.`, at, Math.max(end - at, this.tokenLength()));
|
|
799
937
|
}
|
|
800
|
-
if (form
|
|
801
|
-
if (Object.hasOwn(value, key)) this.stop(`The
|
|
938
|
+
if (form?.fields !== undefined && !form.fields.includes(key)) this.stop(`The form ${form.name} has no field ${key}.`, at, end - at);
|
|
939
|
+
if (Object.hasOwn(value, key)) this.stop(`The ${owner} has the field ${key} two times.`, at, end - at);
|
|
802
940
|
this.pos += 1;
|
|
803
941
|
const arg = this.bound();
|
|
804
|
-
setOwn(value, key, wraps(form, key) ? envelop(this.syn, arg) : arg);
|
|
805
|
-
}
|
|
806
|
-
|
|
942
|
+
setOwn(value, key, form !== undefined && wraps(form, key) ? envelop(this.syn, arg) : arg);
|
|
943
|
+
};
|
|
944
|
+
this.argumentsOf(entry, form === undefined ? "}" : ")");
|
|
945
|
+
return value;
|
|
807
946
|
}
|
|
808
947
|
|
|
809
948
|
private call(name: string, start: number, nameLength: number): Json {
|
|
810
949
|
const form = this.syn.forms.get(name);
|
|
811
|
-
if (form?.args === "named") return this.
|
|
950
|
+
if (form?.args === "named") return opDoc(form, this.entries(form));
|
|
812
951
|
const args = this.argumentsOf(() => this.bound());
|
|
813
952
|
if (form?.args === "literal") {
|
|
814
953
|
const [only] = args;
|
|
@@ -834,9 +973,177 @@ class Parser {
|
|
|
834
973
|
}
|
|
835
974
|
}
|
|
836
975
|
|
|
976
|
+
// === Recovery after the first issue (D138) ===
|
|
977
|
+
|
|
978
|
+
/** The end of a recovery. The class `Recovery` throws it, and its method `run` catches it. */
|
|
979
|
+
class End {}
|
|
980
|
+
|
|
981
|
+
function hasBraces(syn: Compiled): boolean {
|
|
982
|
+
return syn.constructs.record !== undefined || syn.constructs.let !== undefined;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** This function gives the closer of an opening bracket of a profile, or `undefined` for each other character. */
|
|
986
|
+
function closerOf(syn: Compiled, c: string | undefined): string | undefined {
|
|
987
|
+
if (c === "(") return ")";
|
|
988
|
+
return c === "{" && hasBraces(syn) ? "}" : undefined;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function isCloser(syn: Compiled, c: string | undefined): boolean {
|
|
992
|
+
return c === ")" || (c === "}" && hasBraces(syn));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** This function gives the position after a quoted text at `at`, or after one other character. An open quote gives `Infinity`. */
|
|
996
|
+
function pastToken(syn: Compiled, text: string, at: number): number {
|
|
997
|
+
const c = text[at];
|
|
998
|
+
if (c !== syn.quote && c !== "`") return at + 1;
|
|
999
|
+
for (let end = at + 1; end < text.length; end += 1) {
|
|
1000
|
+
if (text[end] === "\\" && c === syn.quote && !syn.doubled) end += 1;
|
|
1001
|
+
else if (text[end] === c) return end + 1;
|
|
1002
|
+
}
|
|
1003
|
+
return Number.POSITIVE_INFINITY;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/** This function tells if each quote of a text closes, and each bracket closes in the order of the brackets. */
|
|
1007
|
+
function balanced(syn: Compiled, text: string): boolean {
|
|
1008
|
+
const open: (string | undefined)[] = [];
|
|
1009
|
+
let at = 0;
|
|
1010
|
+
while (at < text.length) {
|
|
1011
|
+
const c = text[at];
|
|
1012
|
+
const closer = closerOf(syn, c);
|
|
1013
|
+
if (closer !== undefined) open.push(closer);
|
|
1014
|
+
else if (isCloser(syn, c) && open.pop() !== c) return false;
|
|
1015
|
+
at = pastToken(syn, text, at);
|
|
1016
|
+
}
|
|
1017
|
+
return at === text.length && open.length === 0;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* This parser reads a text again after its first issue and collects the later issues (SYNTAX.ISSUES-SYNC). A fault in
|
|
1022
|
+
* a group or in a list moves the position to a synchronising token of that bracket. The first read has no part of
|
|
1023
|
+
* this class, thus a valid text does not pay for it.
|
|
1024
|
+
*/
|
|
1025
|
+
class Recovery extends Parser {
|
|
1026
|
+
private readonly issues: SyntaxIssue[];
|
|
1027
|
+
/** The offset of the token after the last synchronising token. An issue at this token can be a result of the fault. */
|
|
1028
|
+
private quiet = -1;
|
|
1029
|
+
|
|
1030
|
+
constructor(syn: Compiled, text: string, issues: SyntaxIssue[]) {
|
|
1031
|
+
super(syn, text);
|
|
1032
|
+
this.issues = issues;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** This method reads the text and gives the issues. An error of a host in the later text ends the read. */
|
|
1036
|
+
run(): readonly SyntaxIssue[] {
|
|
1037
|
+
try {
|
|
1038
|
+
this.parse();
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
if (error instanceof Stop) this.note(error.issue);
|
|
1041
|
+
}
|
|
1042
|
+
return this.issues;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** An item at the offset of the item before tells that no step moved forward, thus the read ends (SYNTAX.ISSUES-BOUND). */
|
|
1046
|
+
protected override argumentsOf<T>(item: () => T, close = ")"): T[] {
|
|
1047
|
+
let last = -1;
|
|
1048
|
+
return super.argumentsOf(() => {
|
|
1049
|
+
if (this.pos === last) throw new End();
|
|
1050
|
+
last = this.pos;
|
|
1051
|
+
return this.attempt(item, close, true);
|
|
1052
|
+
}, close);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
protected override group(): Json {
|
|
1056
|
+
this.pos += 1;
|
|
1057
|
+
const inner = this.attempt(() => this.bound(), ")", false);
|
|
1058
|
+
this.pos += 1;
|
|
1059
|
+
return inner;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/** A bad escape stays inside its text, thus the read continues after the escape. */
|
|
1063
|
+
protected override escape(): string {
|
|
1064
|
+
try {
|
|
1065
|
+
return super.escape();
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
this.note((error as Stop).issue);
|
|
1068
|
+
this.pos += 2;
|
|
1069
|
+
return "";
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* This method reads one item of a bracket. A fault moves to the next synchronising token, and the item is then `null`.
|
|
1075
|
+
* A colon in a list tells that the name of a form has a fault, thus the method skips the other items of the list. A
|
|
1076
|
+
* separator in a group tells that a function has a fault, thus the fault also ends the item around the group.
|
|
1077
|
+
*/
|
|
1078
|
+
private attempt<T>(item: () => T, close: string, list: boolean): T {
|
|
1079
|
+
const depth = this.depth;
|
|
1080
|
+
try {
|
|
1081
|
+
const made = item();
|
|
1082
|
+
this.skip();
|
|
1083
|
+
if (this.text[this.pos] !== close && !(list && this.atSeparator())) this.noEnd(close, list);
|
|
1084
|
+
return made;
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
if (!(error instanceof Stop)) throw error;
|
|
1087
|
+
this.depth = depth;
|
|
1088
|
+
this.note(error.issue);
|
|
1089
|
+
const tuple = !list && this.atSeparator();
|
|
1090
|
+
this.sync(close, list && this.text[this.pos] !== ":");
|
|
1091
|
+
if (tuple) {
|
|
1092
|
+
this.pos += 1;
|
|
1093
|
+
throw error;
|
|
1094
|
+
}
|
|
1095
|
+
return null as T;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private atSeparator(): boolean {
|
|
1100
|
+
return this.text.startsWith(this.syn.separator, this.pos);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* This method keeps a later issue (SYNTAX.ISSUES-CASCADE). The offsets rise, because an issue that starts at or before
|
|
1105
|
+
* the last issue can come from the removed text. The count of the arguments of a form is an example. An issue at the
|
|
1106
|
+
* first token after a synchronising token can come from the same fault, for example a comma inside a number.
|
|
1107
|
+
*/
|
|
1108
|
+
private note(issue: SyntaxIssue): void {
|
|
1109
|
+
const last = this.issues[this.issues.length - 1] as SyntaxIssue;
|
|
1110
|
+
if (issue.offset > last.offset && issue.offset !== this.quiet && this.issues.length < MAX_SYNTAX_ISSUES) this.issues.push(issue);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/** This method moves to the closer `close` of the bracket, or to a separator of a list, and skips each inner bracket. */
|
|
1114
|
+
private sync(close: string, list: boolean): void {
|
|
1115
|
+
if (this.issues.length >= MAX_SYNTAX_ISSUES) throw new End();
|
|
1116
|
+
let depth = 0;
|
|
1117
|
+
while (this.pos < this.text.length) {
|
|
1118
|
+
const at = this.pos;
|
|
1119
|
+
const c = this.text[at];
|
|
1120
|
+
if (depth === 0 && (c === close || (list && this.atSeparator()))) {
|
|
1121
|
+
this.pos += c === close ? 1 : this.syn.separator.length;
|
|
1122
|
+
this.skip();
|
|
1123
|
+
this.quiet = this.pos;
|
|
1124
|
+
this.pos = at;
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (closerOf(this.syn, c) !== undefined) depth += 1;
|
|
1128
|
+
else if (isCloser(this.syn, c)) depth -= 1;
|
|
1129
|
+
this.pos = pastToken(this.syn, this.text, at);
|
|
1130
|
+
}
|
|
1131
|
+
throw new End();
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* This function gives each issue of a bad text, with the issue of the first read as the first issue (SYNTAX.ISSUES-FIRST).
|
|
1137
|
+
* Open quotes or brackets make each synchronising token unsure, thus such a text keeps only the first issue.
|
|
1138
|
+
*/
|
|
1139
|
+
function issuesOf(syn: Compiled, text: string, first: SyntaxIssue): readonly SyntaxIssue[] {
|
|
1140
|
+
return balanced(syn, text) ? new Recovery(syn, text, [first]).run() : [first];
|
|
1141
|
+
}
|
|
1142
|
+
|
|
837
1143
|
/**
|
|
838
1144
|
* This function reads a text into a document with the rules of a profile (SYNTAX.PARSE). It does not throw for a
|
|
839
|
-
* bad text.
|
|
1145
|
+
* bad text. Each issue gives the offset, the length and a message (SYNTAX.SPAN). The field `issues` gives at most
|
|
1146
|
+
* `MAX_SYNTAX_ISSUES` issues (SYNTAX.ISSUES-ALL). A host that reads the first issue reads the first fault of the text.
|
|
840
1147
|
*
|
|
841
1148
|
* @param profile - The syntax profile.
|
|
842
1149
|
* @param text - The text.
|
|
@@ -844,11 +1151,12 @@ class Parser {
|
|
|
844
1151
|
* @throws {TypeError} The profile is not valid, for example an operator has two entries of the same place.
|
|
845
1152
|
*/
|
|
846
1153
|
export function parse(profile: SyntaxProfile, text: string): ParseResult {
|
|
847
|
-
const
|
|
1154
|
+
const syn = compileProfile(profile);
|
|
1155
|
+
const parser = new Parser(syn, text);
|
|
848
1156
|
try {
|
|
849
1157
|
return { ok: true, doc: parser.parse() };
|
|
850
1158
|
} catch (error) {
|
|
851
|
-
if (error instanceof Stop) return { ok: false, issues:
|
|
1159
|
+
if (error instanceof Stop) return { ok: false, issues: issuesOf(syn, text, error.issue) };
|
|
852
1160
|
if (isStackOverflow(error)) return { ok: false, issues: [{ offset: parser.position(), length: 0, message: "The text is too deep for the stack of the host." }] };
|
|
853
1161
|
throw error;
|
|
854
1162
|
}
|
|
@@ -868,8 +1176,16 @@ type Shape =
|
|
|
868
1176
|
| { readonly tag: "call"; readonly name: string; readonly args: readonly (readonly [Key, unknown, boolean?])[]; readonly named?: boolean }
|
|
869
1177
|
| { readonly tag: "path"; readonly segments: readonly (string | readonly [Key, unknown])[] }
|
|
870
1178
|
| { readonly tag: "bind"; readonly name: string; readonly doc: unknown }
|
|
1179
|
+
| ConstructShape
|
|
871
1180
|
| { readonly tag: "none"; readonly reason: string };
|
|
872
1181
|
|
|
1182
|
+
/** The shape of a construct form (K145). The tag `apply` is a call of a value. */
|
|
1183
|
+
type ConstructShape =
|
|
1184
|
+
| { readonly tag: "record"; readonly entries: JsonRecord }
|
|
1185
|
+
| { readonly tag: "let"; readonly bind: JsonRecord; readonly body: unknown }
|
|
1186
|
+
| { readonly tag: "fn"; readonly params: readonly string[]; readonly body: unknown }
|
|
1187
|
+
| { readonly tag: "apply"; readonly target: unknown; readonly args: readonly unknown[] };
|
|
1188
|
+
|
|
873
1189
|
function none(reason: string): Shape {
|
|
874
1190
|
return { tag: "none", reason };
|
|
875
1191
|
}
|
|
@@ -989,6 +1305,24 @@ function formShape(syn: Compiled, form: Form, value: unknown): Shape | undefined
|
|
|
989
1305
|
return { tag: "call", name: form.name, args: present.map((key) => [key, value[key], wraps(form, key)] as const) };
|
|
990
1306
|
}
|
|
991
1307
|
|
|
1308
|
+
/** This function tells if a record has exactly these two keys. */
|
|
1309
|
+
function hasKeys(value: JsonRecord, first: string, second: string): boolean {
|
|
1310
|
+
return Object.keys(value).length === 2 && Object.hasOwn(value, first) && Object.hasOwn(value, second);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/** This function gives the shape of a construct, or `undefined` when its form does not write the value. */
|
|
1314
|
+
function constructShape(construct: keyof SyntaxConstructs, value: unknown): ConstructShape | undefined {
|
|
1315
|
+
if (!isRecord(value)) return undefined;
|
|
1316
|
+
if (construct === "record") return { tag: "record", entries: value };
|
|
1317
|
+
const { bind, params, args } = value;
|
|
1318
|
+
if (construct === "let") return hasKeys(value, "bind", "body") && isRecord(bind) ? { tag: "let", bind, body: value["body"] } : undefined;
|
|
1319
|
+
if (construct === "fn") {
|
|
1320
|
+
const names = Array.isArray(params) && params.every((param) => typeof param === "string") ? (params as readonly string[]) : undefined;
|
|
1321
|
+
return hasKeys(value, "params", "body") && names !== undefined ? { tag: "fn", params: names, body: value["body"] } : undefined;
|
|
1322
|
+
}
|
|
1323
|
+
return hasKeys(value, "fn", "args") && Array.isArray(args) ? { tag: "apply", target: value["fn"], args } : undefined;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
992
1326
|
function opShape(syn: Compiled, key: string, terms: readonly unknown[]): Shape | undefined {
|
|
993
1327
|
for (const op of syn.byKind.get(key) ?? []) {
|
|
994
1328
|
const fits = op.fixity === "infix" ? terms.length === 2 || (op.assoc === "flat" && terms.length > 2) : terms.length === 1;
|
|
@@ -1024,6 +1358,9 @@ function shapeOf(syn: Compiled, doc: unknown): Shape {
|
|
|
1024
1358
|
const shape = formShape(syn, form, value);
|
|
1025
1359
|
if (shape !== undefined) return shape;
|
|
1026
1360
|
}
|
|
1361
|
+
const construct = syn.constructKinds.get(key);
|
|
1362
|
+
const written = construct === undefined ? undefined : constructShape(construct, value);
|
|
1363
|
+
if (written !== undefined) return written;
|
|
1027
1364
|
const called = path[0] === "ops" && Array.isArray(value) ? callName(syn, path.slice(1)) : undefined;
|
|
1028
1365
|
if (called !== undefined) return { tag: "call", name: called, args: (value as readonly unknown[]).map((item, index) => [index, item] as const) };
|
|
1029
1366
|
return none(`This syntax has no text for the kind ${name}.`);
|
|
@@ -1031,8 +1368,10 @@ function shapeOf(syn: Compiled, doc: unknown): Shape {
|
|
|
1031
1368
|
|
|
1032
1369
|
function levelOf(syn: Compiled, doc: unknown): number {
|
|
1033
1370
|
const shape = shapeOf(syn, doc);
|
|
1034
|
-
// A document with a name binds more loosely than each operator, thus it needs parentheses as a term.
|
|
1035
|
-
|
|
1371
|
+
// A document with a name binds more loosely than each operator, thus it needs parentheses as a term. The body of a
|
|
1372
|
+
// let or a function goes as far as it can, thus these forms need parentheses too (D127).
|
|
1373
|
+
if (shape.tag === "bind" || shape.tag === "let" || shape.tag === "fn") return Number.NEGATIVE_INFINITY;
|
|
1374
|
+
return shape.tag === "op" ? shape.op.prec : Number.POSITIVE_INFINITY;
|
|
1036
1375
|
}
|
|
1037
1376
|
|
|
1038
1377
|
/** This function tells if a term needs parentheses in its position, so that `parse` reads the same tree. */
|
|
@@ -1060,17 +1399,60 @@ function opText(syn: Compiled, op: Op, terms: readonly unknown[], sub: SubText):
|
|
|
1060
1399
|
return ok(texts.join(` ${op.symbol} `));
|
|
1061
1400
|
}
|
|
1062
1401
|
|
|
1402
|
+
function keyText(key: string): string {
|
|
1403
|
+
return NAME.test(key) ? key : quoteName(key);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/** This function gives the text of each part. `path` is the place of the parts in the value of their document. */
|
|
1407
|
+
function partTexts(parts: readonly (readonly [Key, unknown])[], path: readonly Key[], sub: SubText): Result<readonly string[]> {
|
|
1408
|
+
const texts: string[] = [];
|
|
1409
|
+
for (const [key, part] of parts) {
|
|
1410
|
+
const result = sub(part, [...path, key]);
|
|
1411
|
+
if (!result.ok) return result;
|
|
1412
|
+
texts.push(result.value);
|
|
1413
|
+
}
|
|
1414
|
+
return ok(texts);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/** This function writes a record between braces. `path` is the place of the record in the value of its document. */
|
|
1418
|
+
function bracesText(syn: Compiled, record: JsonRecord, path: readonly Key[], sub: SubText): Result<string> {
|
|
1419
|
+
const keys = Object.keys(record);
|
|
1420
|
+
const texts = partTexts(keys.map((key) => [key, record[key]] as const), path, sub);
|
|
1421
|
+
if (!texts.ok) return texts;
|
|
1422
|
+
return ok(`{${texts.value.map((text, index) => `${keyText(keys[index] as string)}: ${text}`).join(`${syn.separator} `)}}`);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/** This function writes a construct form (K145). */
|
|
1426
|
+
function constructText(syn: Compiled, shape: ConstructShape, sub: SubText): Result<string> {
|
|
1427
|
+
if (shape.tag === "record") return bracesText(syn, shape.entries, [], sub);
|
|
1428
|
+
if (shape.tag === "apply") {
|
|
1429
|
+
const target = sub(shape.target, ["fn"]);
|
|
1430
|
+
if (!target.ok) return target;
|
|
1431
|
+
const args = partTexts(shape.args.map((arg, index) => [index, arg] as const), ["args"], sub);
|
|
1432
|
+
if (!args.ok) return args;
|
|
1433
|
+
// A call of a value follows a group, or another call of a value.
|
|
1434
|
+
const head = shapeOf(syn, shape.target).tag === "apply" ? target.value : `(${target.value})`;
|
|
1435
|
+
return ok(`${head}(${args.value.join(`${syn.separator} `)})`);
|
|
1436
|
+
}
|
|
1437
|
+
const head = shape.tag === "fn" ? ok(`(${shape.params.map(keyText).join(`${syn.separator} `)}) =>`) : bracesText(syn, shape.bind, ["bind"], sub);
|
|
1438
|
+
if (!head.ok) return head;
|
|
1439
|
+
const body = sub(shape.body, ["body"]);
|
|
1440
|
+
if (!body.ok) return body;
|
|
1441
|
+
return ok(shape.tag === "fn" ? `${head.value} ${body.value}` : `let ${head.value} in ${body.value}`);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1063
1444
|
/** This function writes one document. `sub` gives the text of each part. */
|
|
1064
1445
|
function printNode(syn: Compiled, doc: unknown, sub: SubText): Result<string> {
|
|
1065
1446
|
const shape = shapeOf(syn, doc);
|
|
1066
1447
|
if (shape.tag === "text") return ok(shape.text);
|
|
1067
1448
|
if (shape.tag === "none") return fail("#VALUE!", shape.reason);
|
|
1068
1449
|
if (shape.tag === "op") return opText(syn, shape.op, shape.terms, sub);
|
|
1450
|
+
if (shape.tag === "record" || shape.tag === "let" || shape.tag === "fn" || shape.tag === "apply") return constructText(syn, shape, sub);
|
|
1069
1451
|
if (shape.tag === "bind") {
|
|
1070
1452
|
// The document after a header name takes a document, thus a data envelope of a literal is the bare literal (K159).
|
|
1071
1453
|
const head = wrappedShape(syn, shape.doc);
|
|
1072
1454
|
const inner = head === undefined ? printNode(syn, shape.doc, sub) : head.tag === "text" ? ok(head.text) : fail("#VALUE!", WRAPPED);
|
|
1073
|
-
return inner.ok ? ok(`${
|
|
1455
|
+
return inner.ok ? ok(`${keyText(shape.name)} ${syn.bind as string} ${inner.value}`) : inner;
|
|
1074
1456
|
}
|
|
1075
1457
|
if (shape.tag === "path") {
|
|
1076
1458
|
const texts: string[] = [];
|
|
@@ -1089,7 +1471,7 @@ function printNode(syn: Compiled, doc: unknown, sub: SubText): Result<string> {
|
|
|
1089
1471
|
for (const [key, arg, wrapped] of shape.args) {
|
|
1090
1472
|
const result = sub(arg, [key], wrapped);
|
|
1091
1473
|
if (!result.ok) return result;
|
|
1092
|
-
texts.push(shape.named === true ? `${
|
|
1474
|
+
texts.push(shape.named === true ? `${keyText(String(key))}: ${result.value}` : result.value);
|
|
1093
1475
|
}
|
|
1094
1476
|
return ok(`${shape.name}(${texts.join(`${syn.separator} `)})`);
|
|
1095
1477
|
}
|