@mark1russell7/coeval 0.16.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/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
- /** The name in the text. */
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
- /** The fields of the value, in the order of the arguments. Without them, the value is the list of the arguments. */
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
- if (!NAME.test(given.name)) throw new TypeError(`The call form "${given.name}" is not a plain name.`);
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 form: Form = { name: given.name, type, fields: given.fields, min: given.min ?? given.fields?.length ?? 0 };
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.expr(0);
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
- return doc;
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.expr(0);
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: Key[] = [first];
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("The name of a call is a plain name.", start, back - start);
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
- private call(name: string, start: number, nameLength: number): Json {
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: Json[] = [];
760
+ const args: T[] = [];
568
761
  this.pos += 1;
569
762
  this.skip();
570
- if (this.text[this.pos] === ")") this.pos += 1;
571
- else {
572
- for (;;) {
573
- args.push(this.expr(0));
574
- this.skip();
575
- if (this.text.startsWith(this.syn.separator, this.pos)) {
576
- this.pos += this.syn.separator.length;
577
- continue;
578
- }
579
- if (this.text[this.pos] === ")") {
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
- value[form.fields?.[index] as string] = arg;
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
- /** The text of a part, from its document and its path in the value of the parent. */
629
- type SubText = (child: unknown, path: readonly Key[]) => Result<string>;
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 laterSegment(step: unknown): string | undefined {
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 = later;
692
- if (root === syn.bare && typeof rest[0] === "string") first = plainSegment(syn, rest[0]) ? rest[0] : quoteName(rest[0]);
693
- else if (typeof root === "string" && syn.roots.has(root) && !syn.patterns.some((p) => p.whole.test(root))) {
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
- return { tag: "text", text: [first, ...tail].join(".") };
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
- function formShape(form: Form, value: unknown): Shape | undefined {
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.name !== undefined || doc.own !== undefined) return none("This syntax has no text for the header of a document.");
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
- return shape.tag === "op" ? shape.op.prec : Number.POSITIVE_INFINITY;
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 printTree(syn, doc, []);
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();