@mark1russell7/coeval 0.22.0 → 0.24.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/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, docEquals, 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
- /** The result of `parse`: a document, or the problems of the text. */
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
- constructor(readonly issue: SyntaxIssue) {}
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
- private pos = 0;
454
- private depth = 0;
455
-
456
- constructor(
457
- private readonly syn: Compiled,
458
- private readonly text: string,
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
- private stop(message: string, offset: number, length: number): never {
532
+ protected stop(message: string, offset: number, length: number): never {
476
533
  throw new Stop({ offset, length, message });
477
534
  }
478
535
 
479
- private skip(): void {
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
- private tokenLength(): number {
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
- private bound(): Json {
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.group();
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
- private escape(): string {
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
- private group(): Json {
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.stop("A closing parenthesis is missing.", this.pos, this.tokenLength());
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 reads the arguments of a call in parentheses. `item` reads one argument. */
758
- private argumentsOf<T>(item: () => T): T[] {
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("A closing parenthesis is missing.", open, 1);
779
- this.stop(`A "${this.syn.separator}" or a closing parenthesis is missing.`, this.pos, this.tokenLength());
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
- /** This method reads the arguments `key: expr` of a form `named` into a record (SYNTAX.RECORD). */
784
- private namedArguments(form: Form): Json {
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
- this.argumentsOf(() => {
929
+ const entry = (): void => {
787
930
  this.skip();
788
931
  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
- }
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 form ${form.name} is a key, a ":" and a document.`, at, Math.max(end - at, this.tokenLength()));
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.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);
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
- return opDoc(form, value);
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.namedArguments(form);
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. The issue gives the offset, the length and a message (SYNTAX.SPAN).
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 parser = new Parser(compileProfile(profile), text);
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: [error.issue] };
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
- return shape.tag === "op" ? shape.op.prec : shape.tag === "bind" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
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(`${NAME.test(shape.name) ? shape.name : quoteName(shape.name)} ${syn.bind as string} ${inner.value}`) : inner;
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 ? `${NAME.test(String(key)) ? String(key) : quoteName(String(key))}: ${result.value}` : result.value);
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
  }
@@ -1149,3 +1531,210 @@ export function syntaxFormat(profile: SyntaxProfile): MachinePart {
1149
1531
  };
1150
1532
  return { world: { defaults: { format: "syntax.format" } }, evaluators: { "syntax.format": code } };
1151
1533
  }
1534
+
1535
+ // === The check of a profile ===
1536
+
1537
+ /**
1538
+ * The name of a finding of `checkSyntax` (D140). Each name holds a rule of the parser or the printer.
1539
+ *
1540
+ * - `invalid`: `parse` throws a `TypeError` for the profile. The reason is its message.
1541
+ * - `operator`: the document of one operator does not come back. A negative precedence stops an infix operator,
1542
+ * because an expression starts at the precedence `0`. The kinds `ref`, `lit` and `ext.number` have their own text.
1543
+ * - `grouping`: an operator with a second operator in a term does not come back. The parser reads a term from the
1544
+ * precedence plus `1`, and the printer compares precedences, thus a gap under `1` loses the tree. A postfix operator
1545
+ * at the precedence of a `right` infix operator loses it too.
1546
+ * - `longest-match`: the printer writes two postfix operators without a space, and a longer symbol reads their text.
1547
+ * - `word`: a word of `literals.words` does not read as its value. A word operator reads first, and a word that is
1548
+ * not a plain name has no token.
1549
+ * - `root`: a root does not come back. A word operator reads first, and a literal word reads a root without steps.
1550
+ * - `pattern`: a pattern of references reads a text that has another meaning, because the parser tries patterns first.
1551
+ * - `form`: the document of a call form does not come back. A word operator reads first, the last form of a name
1552
+ * hides the others, and a `min` above the fields permits no text.
1553
+ * - `call`: a name of `calls.ops` does not come back. A form of the name wins, a word operator reads first, and a name
1554
+ * with dots needs the field steps.
1555
+ * - `separator`: the separator does not end an argument, and a neutral separator keeps the same document. An operator
1556
+ * reads first, the parser skips a space before it, or a name or a bracket at its start changes the term.
1557
+ * - `construct`: the document of a construct form does not come back, for example two forms have one kind.
1558
+ */
1559
+ export type SyntaxFindingCode = "invalid" | "operator" | "grouping" | "longest-match" | "word" | "root" | "pattern" | "form" | "call" | "separator" | "construct";
1560
+
1561
+ /** One fault of a syntax profile: its name, its place in the profile and the reason (D140). */
1562
+ export interface SyntaxFinding {
1563
+ /** The name of the fault. */
1564
+ readonly code: SyntaxFindingCode;
1565
+ /** The keys from the root of the profile to the entry, for example `["operators", 3]`. A default list has keys too. */
1566
+ readonly place: readonly Key[];
1567
+ /** The rule that the entry breaks, and the text that shows it. */
1568
+ readonly reason: string;
1569
+ }
1570
+
1571
+ /** The state of one check: the profile, its compiled form, a leaf document and the two functions that record a fault. */
1572
+ interface ProfileCheck {
1573
+ readonly profile: SyntaxProfile;
1574
+ readonly syn: Compiled;
1575
+ readonly leaf: Json;
1576
+ /** This function adds a finding. A pattern that reads the start of `text` takes the code and the place. */
1577
+ readonly report: (code: SyntaxFindingCode, place: readonly Key[], text: string, reason: string) => void;
1578
+ /** This function reports a document that breaks the law, and it tells if the law holds. `start` stands for a refused text. */
1579
+ readonly keeps: (code: SyntaxFindingCode, place: readonly Key[], rule: string, doc: Json, start?: string) => boolean;
1580
+ }
1581
+
1582
+ /** This function tells how a document breaks `parse(print(doc))`. It gives `undefined` when the document comes back. */
1583
+ function lawBreak(profile: SyntaxProfile, doc: Json): { readonly text: string | undefined; readonly why: string } | undefined {
1584
+ const printed = print(profile, doc);
1585
+ if (!printed.ok) return { text: undefined, why: printed.error.reason };
1586
+ const text = printed.value;
1587
+ const back = parse(profile, text);
1588
+ if (back.ok) return docEquals(back.doc, doc) ? undefined : { text, why: `The text ${text} reads as ${JSON.stringify(back.doc)}.` };
1589
+ return { text, why: `The text ${text} gives the issue: ${back.issues[0]?.message ?? ""}` };
1590
+ }
1591
+
1592
+ /** This function gives the index of the first pattern that `parse` uses at the start of a text, or `-1`. */
1593
+ function patternAt(syn: Compiled, text: string): number {
1594
+ return syn.patterns.findIndex(({ sticky }) => {
1595
+ sticky.lastIndex = 0;
1596
+ const match = sticky.exec(text)?.[0];
1597
+ return match !== undefined && match !== "" && !/[A-Za-z0-9_$(]/.test(text[match.length] ?? "");
1598
+ });
1599
+ }
1600
+
1601
+ /** This function gives each small document of an operator with `term` in one term. A `flat` operator also has three terms. */
1602
+ function operatorDocs(op: Op, term: Json, leaf: Json): readonly Json[] {
1603
+ if (op.fixity !== "infix") return [opDoc(op, [term])];
1604
+ const pairs = [[term, leaf], [leaf, term]];
1605
+ return (op.assoc === "flat" ? [...pairs, [term, leaf, leaf], [leaf, term, leaf]] : pairs).map((terms) => opDoc(op, terms));
1606
+ }
1607
+
1608
+ /** This function gives the longer symbol that reads two postfix operators without a space, or `undefined`. */
1609
+ function gluedSymbol(syn: Compiled, outer: Op, inner: Op): string | undefined {
1610
+ if (outer.word || inner.word || outer.fixity !== "postfix" || inner.fixity !== "postfix") return undefined;
1611
+ const glued = inner.symbol + outer.symbol;
1612
+ const read = syn.binarySymbols.find((symbol) => glued.startsWith(symbol));
1613
+ return read === inner.symbol ? undefined : read;
1614
+ }
1615
+
1616
+ function checkOperators({ profile, syn, leaf, keeps }: ProfileCheck): void {
1617
+ const ops = (profile.operators ?? []).map((given) => (given.fixity === "prefix" ? syn.prefix : syn.binary).get(given.symbol) as Op);
1618
+ const sound = ops.filter((op, index) => operatorDocs(op, leaf, leaf).every((doc) => keeps("operator", ["operators", index], `The operator ${op.symbol} does not come back.`, doc)));
1619
+ for (const outer of sound) {
1620
+ const place = ["operators", ops.indexOf(outer)];
1621
+ for (const inner of sound) {
1622
+ const glued = gluedSymbol(syn, outer, inner);
1623
+ const code = glued === undefined ? "grouping" : "longest-match";
1624
+ const rule = glued === undefined ? `The operator ${outer.symbol} with ${inner.symbol} in a term loses the tree.` : `The symbol ${glued} reads the postfix operators ${inner.symbol} and ${outer.symbol}.`;
1625
+ for (const term of operatorDocs(inner, leaf, leaf)) for (const doc of operatorDocs(outer, term, leaf)) keeps(code, place, rule, doc);
1626
+ }
1627
+ }
1628
+ }
1629
+
1630
+ function checkLiterals({ profile, syn, report }: ProfileCheck): void {
1631
+ for (const [word, value] of syn.words) {
1632
+ const back = parse(profile, word);
1633
+ if (!back.ok || !docEquals(back.doc, envelop(syn, literalDoc(value)))) report("word", ["literals", "words", word], word, `The word ${word} does not read as its value.`);
1634
+ }
1635
+ for (const value of [0, -1, "a"]) {
1636
+ const shape = literalShape(syn, value);
1637
+ if (shape.tag === "text" && patternAt(syn, shape.text) >= 0) report("pattern", [], shape.text, `The literal ${shape.text} reads as a reference.`);
1638
+ }
1639
+ }
1640
+
1641
+ function checkRoots({ profile, syn, keeps }: ProfileCheck): void {
1642
+ (profile.refs?.roots ?? DEFAULT_ROOTS).forEach((root, index) => {
1643
+ for (const path of syn.fields ? [[root], [root, "x"]] : [[root]]) {
1644
+ keeps("root", ["refs", "roots", index], `The root ${root} does not come back.`, { type: "ref", value: path }, path.join("."));
1645
+ }
1646
+ });
1647
+ }
1648
+
1649
+ /** This function gives a value that a call form writes: the fewest arguments, and at least one. */
1650
+ function formValue(given: SyntaxForm, leaf: Json): Json {
1651
+ const args = given.args ?? (given.fields === undefined ? "list" : "fields");
1652
+ const least = Math.max(given.min ?? given.fields?.length ?? 0, 1);
1653
+ if (args === "literal") return 1;
1654
+ if (args === "list") return Array.from({ length: least }, () => leaf);
1655
+ const keys = (given.fields ?? ["x"]).slice(0, args === "named" ? 1 : least);
1656
+ return Object.fromEntries(keys.map((key) => [key, leaf]));
1657
+ }
1658
+
1659
+ function checkCalls({ profile, syn, leaf, keeps }: ProfileCheck): void {
1660
+ const calls = profile.calls ?? {};
1661
+ (calls.forms ?? []).forEach((form, index) => {
1662
+ keeps("form", ["calls", "forms", index], `The form ${form.name} does not come back.`, { type: typeOfKind(form.kind).type, value: formValue(form, leaf) });
1663
+ });
1664
+ // The dotted text of a call name needs the field steps. A profile with `ops: true` and no field steps has no dotted call.
1665
+ if (syn.ops === true && syn.fields) keeps("call", ["calls", "ops"], "A call name with a dot does not come back.", { type: ["ops", "m", "p"], value: [leaf] });
1666
+ if (typeof calls.ops === "boolean" || calls.ops === undefined) return;
1667
+ calls.ops.forEach((name, index) => {
1668
+ keeps("call", ["calls", "ops", index], `The call ${name} does not come back.`, { type: ["ops", ...name.split(".")], value: [leaf] });
1669
+ });
1670
+ }
1671
+
1672
+ /** This function writes two terms with the separator. It blames the separator only when a neutral separator keeps the document. */
1673
+ function checkSeparator({ profile, syn, leaf, report }: ProfileCheck): void {
1674
+ if (syn.ops !== true && syn.ops.size === 0 && syn.forms.size === 0 && syn.constructKinds.size === 0) return;
1675
+ const open: SyntaxProfile = { ...profile, calls: { ...profile.calls, ops: true } };
1676
+ const neutral: SyntaxProfile = { ...profile, calls: { ...open.calls, separator: "\u0001" } };
1677
+ const postfix = [...syn.binary.values()].filter((op) => op.fixity === "postfix").map((op) => opDoc(op, [leaf]));
1678
+ const terms: Json[] = [leaf, { type: "ref", value: [syn.bare, "b c"] }, "a", 1, ...postfix];
1679
+ const docs = terms.map((term): Json => ({ type: ["ops", "probe__"], value: [term, leaf] }));
1680
+ const entries = { x: leaf, y: leaf };
1681
+ const { record, let: bind } = syn.constructs;
1682
+ if (record !== undefined) docs.push({ type: record, value: entries });
1683
+ if (bind !== undefined) docs.push({ type: bind, value: { bind: entries, body: leaf } });
1684
+ for (const doc of docs) {
1685
+ const broken = lawBreak(neutral, doc) === undefined ? lawBreak(open, doc) : undefined;
1686
+ if (broken !== undefined) report("separator", ["calls", "separator"], "", `The separator "${syn.separator}" does not end an argument. ${broken.why}`);
1687
+ }
1688
+ }
1689
+
1690
+ function checkConstructs({ syn, leaf, keeps }: ProfileCheck): void {
1691
+ const entries = { x: leaf };
1692
+ const values: Readonly<Record<keyof SyntaxConstructs, Json>> = { record: entries, let: { bind: entries, body: leaf }, fn: { params: ["x"], body: leaf }, call: { fn: leaf, args: [leaf] } };
1693
+ for (const name of CONSTRUCTS) {
1694
+ const type = syn.constructs[name];
1695
+ if (type !== undefined) keeps("construct", ["constructs", name], `The form ${name} does not come back.`, { type, value: values[name] });
1696
+ }
1697
+ }
1698
+
1699
+ /**
1700
+ * This function finds the faults of a syntax profile before a host uses it (SYNTAX.PROFILE-CHECK, D140). A fault is a
1701
+ * document of an entry that breaks `parse(print(doc))`, or a text of an entry that reads another way. The function
1702
+ * writes and reads one small document for each entry, for each pair of operators and for each root. Thus an empty list
1703
+ * tells that these small documents come back. It does not prove the law for each document. `parse` and `print` do not
1704
+ * call this function, thus it adds no cost to them.
1705
+ *
1706
+ * @param profile - The syntax profile.
1707
+ * @returns At most one finding for each code and place, in the order of the operators, the literals, the references, the calls and the constructs.
1708
+ * @throws An error of a `read` or a `write` function of a pattern.
1709
+ */
1710
+ export function checkSyntax(profile: SyntaxProfile): readonly SyntaxFinding[] {
1711
+ // A small `maxDepth` is a limit that the host chose, not a fault, thus each probe has the default limit.
1712
+ const open: SyntaxProfile = { ...profile, maxDepth: 256 };
1713
+ let syn: Compiled;
1714
+ try {
1715
+ syn = compileProfile(open);
1716
+ } catch (error) {
1717
+ if (error instanceof TypeError) return [{ code: "invalid", place: [], reason: error.message }];
1718
+ throw error;
1719
+ }
1720
+ const found = new Map<string, SyntaxFinding>();
1721
+ const report: ProfileCheck["report"] = (code, place, text, reason) => {
1722
+ const index = patternAt(syn, text);
1723
+ const finding: SyntaxFinding = index < 0 ? { code, place, reason } : { code: "pattern", place: ["refs", "patterns", index], reason: `The pattern reads the start of the text ${text}. ${reason}` };
1724
+ const key = JSON.stringify([finding.code, finding.place]);
1725
+ if (!found.has(key)) found.set(key, finding);
1726
+ };
1727
+ const keeps: ProfileCheck["keeps"] = (code, place, rule, doc, start = "") => {
1728
+ const broken = lawBreak(open, doc);
1729
+ if (broken !== undefined) report(code, place, broken.text ?? start, `${rule} ${broken.why}`);
1730
+ return broken === undefined;
1731
+ };
1732
+ const check: ProfileCheck = { profile: open, syn, leaf: { type: "ref", value: [syn.bare, "a"] }, report, keeps };
1733
+ checkOperators(check);
1734
+ checkLiterals(check);
1735
+ checkRoots(check);
1736
+ checkCalls(check);
1737
+ checkSeparator(check);
1738
+ checkConstructs(check);
1739
+ return [...found.values()];
1740
+ }