tla-ts 0.0.2 → 0.0.3-dev.a50fc89

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.
@@ -0,0 +1,406 @@
1
+ // A port of OCaml's Format module, which tlapm uses to lay out obligations. Only the parts tlapm
2
+ // reaches are ported: boxes, breaks, forced newlines and text.
3
+
4
+ type BoxKind = "h" | "v" | "hv" | "hov" | "b" | "fits";
5
+
6
+ type Token =
7
+ | { kind: "Text"; text: string }
8
+ | { kind: "Break"; width: number; offset: number }
9
+ | { kind: "Begin"; indent: number; box: BoxKind }
10
+ | { kind: "End" }
11
+ | { kind: "Newline" };
12
+
13
+ interface QueueElement {
14
+ size: number;
15
+ token: Token;
16
+ length: number;
17
+ }
18
+
19
+ interface ScanElement {
20
+ leftTotal: number;
21
+ element: QueueElement;
22
+ }
23
+
24
+ interface FormatElement {
25
+ box: BoxKind;
26
+ width: number;
27
+ }
28
+
29
+ interface Formatter {
30
+ queue: QueueElement[];
31
+ queueStart: number;
32
+ scanStack: ScanElement[];
33
+ formatStack: FormatElement[];
34
+ margin: number;
35
+ maxIndent: number;
36
+ spaceLeft: number;
37
+ currentIndent: number;
38
+ isNewLine: boolean;
39
+ leftTotal: number;
40
+ rightTotal: number;
41
+ depth: number;
42
+ output: string[];
43
+ }
44
+
45
+ const INFINITY = 1000000010;
46
+
47
+ const UNKNOWN = -1;
48
+
49
+ const ENCODER = new TextEncoder();
50
+
51
+ const getLength = (text: string) => {
52
+ for (let index = 0; index < text.length; index++) {
53
+ if (text.charCodeAt(index) > 0x7f) return ENCODER.encode(text).length;
54
+ }
55
+ return text.length;
56
+ };
57
+
58
+ const getSentinel = (): ScanElement => ({
59
+ leftTotal: -1,
60
+ element: { size: UNKNOWN, token: { kind: "Text", text: "" }, length: 0 },
61
+ });
62
+
63
+ const createFormatter = (): Formatter => {
64
+ const systemBox: QueueElement = {
65
+ size: UNKNOWN,
66
+ token: { kind: "Begin", indent: 0, box: "hov" },
67
+ length: 0,
68
+ };
69
+ return {
70
+ queue: [systemBox],
71
+ queueStart: 0,
72
+ scanStack: [getSentinel(), { leftTotal: 1, element: systemBox }],
73
+ formatStack: [],
74
+ margin: 78,
75
+ maxIndent: 68,
76
+ spaceLeft: 78,
77
+ currentIndent: 0,
78
+ isNewLine: true,
79
+ leftTotal: 1,
80
+ rightTotal: 1,
81
+ depth: 1,
82
+ output: [],
83
+ };
84
+ };
85
+
86
+ const enqueue = (formatter: Formatter, element: QueueElement) => {
87
+ formatter.rightTotal += element.length;
88
+ formatter.queue.push(element);
89
+ };
90
+
91
+ const breakNewLine = (formatter: Formatter, offset: number, width: number) => {
92
+ formatter.output.push("\n");
93
+ formatter.isNewLine = true;
94
+ formatter.currentIndent = Math.min(formatter.maxIndent, formatter.margin - width + offset);
95
+ formatter.spaceLeft = formatter.margin - formatter.currentIndent;
96
+ formatter.output.push(" ".repeat(formatter.currentIndent));
97
+ };
98
+
99
+ const breakSameLine = (formatter: Formatter, width: number) => {
100
+ formatter.spaceLeft -= width;
101
+ formatter.output.push(" ".repeat(width));
102
+ };
103
+
104
+ const forceBreakLine = (formatter: Formatter) => {
105
+ const top = formatter.formatStack.at(-1);
106
+ if (!top) {
107
+ formatter.output.push("\n");
108
+ return;
109
+ }
110
+ if (top.width > formatter.spaceLeft && top.box !== "fits" && top.box !== "h") {
111
+ breakNewLine(formatter, 0, top.width);
112
+ }
113
+ };
114
+
115
+ const formatToken = (formatter: Formatter, size: number, token: Token) => {
116
+ switch (token.kind) {
117
+ case "Text":
118
+ formatter.spaceLeft -= size;
119
+ formatter.output.push(token.text);
120
+ formatter.isNewLine = false;
121
+ return;
122
+ case "Begin": {
123
+ if (formatter.margin - formatter.spaceLeft > formatter.maxIndent) forceBreakLine(formatter);
124
+ const width = formatter.spaceLeft - token.indent;
125
+ const box = token.box === "v" || size > formatter.spaceLeft ? token.box : "fits";
126
+ formatter.formatStack.push({ box, width });
127
+ return;
128
+ }
129
+ case "End":
130
+ formatter.formatStack.pop();
131
+ return;
132
+ case "Newline": {
133
+ const top = formatter.formatStack.at(-1);
134
+ if (top) breakNewLine(formatter, 0, top.width);
135
+ else formatter.output.push("\n");
136
+ return;
137
+ }
138
+ case "Break": {
139
+ const top = formatter.formatStack.at(-1);
140
+ if (!top) return;
141
+ const width = top.width;
142
+ switch (top.box) {
143
+ case "hov":
144
+ if (size > formatter.spaceLeft) breakNewLine(formatter, token.offset, width);
145
+ else breakSameLine(formatter, token.width);
146
+ return;
147
+ case "b":
148
+ if (formatter.isNewLine) breakSameLine(formatter, token.width);
149
+ else if (size > formatter.spaceLeft) breakNewLine(formatter, token.offset, width);
150
+ else if (formatter.currentIndent > formatter.margin - width + token.offset) {
151
+ breakNewLine(formatter, token.offset, width);
152
+ } else breakSameLine(formatter, token.width);
153
+ return;
154
+ case "hv":
155
+ case "v":
156
+ breakNewLine(formatter, token.offset, width);
157
+ return;
158
+ case "fits":
159
+ case "h":
160
+ breakSameLine(formatter, token.width);
161
+ return;
162
+ }
163
+ }
164
+ }
165
+ };
166
+
167
+ const advanceLeft = (formatter: Formatter) => {
168
+ while (formatter.queueStart < formatter.queue.length) {
169
+ const element = formatter.queue[formatter.queueStart];
170
+ const isKnown = element.size >= 0;
171
+ if (!isKnown && formatter.rightTotal - formatter.leftTotal < formatter.spaceLeft) return;
172
+ formatter.queueStart++;
173
+ formatToken(formatter, isKnown ? element.size : INFINITY, element.token);
174
+ formatter.leftTotal += element.length;
175
+ }
176
+ formatter.queue.length = 0;
177
+ formatter.queueStart = 0;
178
+ };
179
+
180
+ const enqueueAdvance = (formatter: Formatter, element: QueueElement) => {
181
+ enqueue(formatter, element);
182
+ advanceLeft(formatter);
183
+ };
184
+
185
+ const setSize = (formatter: Formatter, isBreak: boolean) => {
186
+ const top = formatter.scanStack.at(-1);
187
+ if (!top) return;
188
+ if (top.leftTotal < formatter.leftTotal) {
189
+ formatter.scanStack = [getSentinel()];
190
+ return;
191
+ }
192
+ if (top.element.token.kind === (isBreak ? "Break" : "Begin")) {
193
+ top.element.size = formatter.rightTotal + top.element.size;
194
+ formatter.scanStack.pop();
195
+ }
196
+ };
197
+
198
+ const scanPush = (formatter: Formatter, isBreak: boolean, element: QueueElement) => {
199
+ enqueue(formatter, element);
200
+ if (isBreak) setSize(formatter, true);
201
+ formatter.scanStack.push({ leftTotal: formatter.rightTotal, element });
202
+ };
203
+
204
+ const openBox = (formatter: Formatter, box: BoxKind, indent: number) => {
205
+ formatter.depth++;
206
+ scanPush(formatter, false, {
207
+ size: -formatter.rightTotal,
208
+ token: { kind: "Begin", indent, box },
209
+ length: 0,
210
+ });
211
+ };
212
+
213
+ const closeBox = (formatter: Formatter) => {
214
+ if (formatter.depth <= 1) return;
215
+ enqueue(formatter, { size: 0, token: { kind: "End" }, length: 0 });
216
+ setSize(formatter, true);
217
+ setSize(formatter, false);
218
+ formatter.depth--;
219
+ };
220
+
221
+ const printAs = (formatter: Formatter, size: number, text: string) =>
222
+ enqueueAdvance(formatter, { size, token: { kind: "Text", text }, length: size });
223
+
224
+ const printBreak = (formatter: Formatter, width: number, offset: number) =>
225
+ scanPush(formatter, true, {
226
+ size: -formatter.rightTotal,
227
+ token: { kind: "Break", width, offset },
228
+ length: width,
229
+ });
230
+
231
+ const forceNewline = (formatter: Formatter) =>
232
+ enqueueAdvance(formatter, { size: 0, token: { kind: "Newline" }, length: 0 });
233
+
234
+ const flush = (formatter: Formatter) => {
235
+ while (formatter.depth > 1) closeBox(formatter);
236
+ formatter.rightTotal = INFINITY;
237
+ advanceLeft(formatter);
238
+ return formatter.output.join("");
239
+ };
240
+
241
+ // pp_print_newline: flush, print a newline, and start over with no open boxes.
242
+ const flushNewline = (formatter: Formatter) => {
243
+ flush(formatter);
244
+ const output = [...formatter.output, "\n"];
245
+ Object.assign(formatter, createFormatter());
246
+ formatter.output = output;
247
+ };
248
+
249
+ // Printers build a document of layout commands first and render it once, so names resolve while
250
+ // the printer still knows the context they were bound in.
251
+ type Command =
252
+ | { kind: "Open"; box: BoxKind; indent: number }
253
+ | { kind: "Close" }
254
+ | { kind: "Text"; text: string; size: number }
255
+ | { kind: "Break"; width: number; offset: number }
256
+ | { kind: "Newline" }
257
+ | { kind: "Flush" };
258
+
259
+ export type Doc = (Command | Doc)[];
260
+
261
+ type FormatArgument = string | Doc;
262
+
263
+ const BOX_KINDS: Record<string, BoxKind> = { h: "h", v: "v", hv: "hv", hov: "hov", b: "b" };
264
+
265
+ const getText = (value: string): Command => ({ kind: "Text", text: value, size: getLength(value) });
266
+
267
+ // Prints a string as is, where format would read @ and % in it as directives.
268
+ export const text = (value: string): Doc => (value === "" ? [] : [getText(value)]);
269
+
270
+ const SPACE: Command = { kind: "Break", width: 1, offset: 0 };
271
+
272
+ const CUT: Command = { kind: "Break", width: 0, offset: 0 };
273
+
274
+ const parseOpen = (directive: string): Command => {
275
+ const split = directive.search(/\d|$/);
276
+ return {
277
+ kind: "Open",
278
+ box: BOX_KINDS[directive.slice(0, split)],
279
+ indent: Number(directive.slice(split) || 0),
280
+ };
281
+ };
282
+
283
+ // Interprets the subset of OCaml format strings that tlapm's printers use, so ported printers read
284
+ // like the OCaml they come from. Each %s or %a consumes one argument.
285
+ export const format = (pattern: string, ...args: FormatArgument[]): Doc => {
286
+ const doc: Doc = [];
287
+ let argumentIndex = 0;
288
+ let pending = "";
289
+ const flushText = () => {
290
+ if (pending === "") return;
291
+ doc.push(getText(pending));
292
+ pending = "";
293
+ };
294
+ for (let index = 0; index < pattern.length; index++) {
295
+ const character = pattern[index];
296
+ if (character === "%") {
297
+ flushText();
298
+ const argument = args[argumentIndex++];
299
+ if (typeof argument !== "string") doc.push(argument);
300
+ else if (argument !== "") doc.push(getText(argument));
301
+ index++;
302
+ continue;
303
+ }
304
+ if (character !== "@") {
305
+ pending += character;
306
+ continue;
307
+ }
308
+ flushText();
309
+ switch (pattern[index + 1]) {
310
+ case "[": {
311
+ if (pattern[index + 2] !== "<") {
312
+ doc.push({ kind: "Open", box: "b", indent: 0 });
313
+ index++;
314
+ break;
315
+ }
316
+ const end = pattern.indexOf(">", index);
317
+ doc.push(parseOpen(pattern.slice(index + 3, end)));
318
+ index = end;
319
+ break;
320
+ }
321
+ case "]":
322
+ doc.push({ kind: "Close" });
323
+ index++;
324
+ break;
325
+ case " ":
326
+ doc.push(SPACE);
327
+ index++;
328
+ break;
329
+ case ",":
330
+ doc.push(CUT);
331
+ index++;
332
+ break;
333
+ case "\n":
334
+ doc.push({ kind: "Newline" });
335
+ index++;
336
+ break;
337
+ case ".":
338
+ doc.push({ kind: "Flush" });
339
+ index++;
340
+ break;
341
+ case ";": {
342
+ const end = pattern.indexOf(">", index);
343
+ const values = pattern.slice(index + 3, end).split(" ");
344
+ doc.push({ kind: "Break", width: Number(values[0]), offset: Number(values[1]) });
345
+ index = end;
346
+ break;
347
+ }
348
+ case "<": {
349
+ const end = pattern.indexOf(">", index);
350
+ doc.push({
351
+ kind: "Text",
352
+ text: String(args[argumentIndex++]),
353
+ size: Number(pattern.slice(index + 2, end)),
354
+ });
355
+ index = end + 2;
356
+ break;
357
+ }
358
+ }
359
+ }
360
+ flushText();
361
+ return doc;
362
+ };
363
+
364
+ export const formatDelimited = <Item>(
365
+ items: Item[],
366
+ formatItem: (item: Item) => Doc,
367
+ separator: Doc = [getText(","), SPACE],
368
+ ): Doc =>
369
+ items.flatMap((item, index) =>
370
+ index === 0 ? [formatItem(item)] : [separator, formatItem(item)],
371
+ );
372
+
373
+ const renderInto = (formatter: Formatter, doc: Doc) => {
374
+ for (const entry of doc) {
375
+ if (Array.isArray(entry)) {
376
+ renderInto(formatter, entry);
377
+ continue;
378
+ }
379
+ switch (entry.kind) {
380
+ case "Open":
381
+ openBox(formatter, entry.box, entry.indent);
382
+ break;
383
+ case "Close":
384
+ closeBox(formatter);
385
+ break;
386
+ case "Text":
387
+ printAs(formatter, entry.size, entry.text);
388
+ break;
389
+ case "Break":
390
+ printBreak(formatter, entry.width, entry.offset);
391
+ break;
392
+ case "Newline":
393
+ forceNewline(formatter);
394
+ break;
395
+ case "Flush":
396
+ flushNewline(formatter);
397
+ break;
398
+ }
399
+ }
400
+ };
401
+
402
+ export const renderDoc = (doc: Doc) => {
403
+ const formatter = createFormatter();
404
+ renderInto(formatter, doc);
405
+ return flush(formatter);
406
+ };