yukigo-haskell-parser 0.1.3 → 0.2.2

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.
Files changed (57) hide show
  1. package/.mocharc.json +3 -3
  2. package/CHANGELOG.md +51 -9
  3. package/README.md +5 -5
  4. package/dist/index.js +21 -13
  5. package/dist/index.js.map +1 -1
  6. package/dist/parser/grammar.cjs +397 -0
  7. package/dist/parser/grammar.cjs.map +1 -0
  8. package/dist/parser/grammar.d.cts +38 -0
  9. package/dist/parser/lexer.d.ts +2 -1
  10. package/dist/parser/lexer.js +6 -1
  11. package/dist/parser/lexer.js.map +1 -1
  12. package/dist/prelude.js +279 -279
  13. package/dist/typechecker/DeclarationCollector.d.ts +2 -0
  14. package/dist/typechecker/DeclarationCollector.js +6 -5
  15. package/dist/typechecker/DeclarationCollector.js.map +1 -1
  16. package/dist/typechecker/TypeBuilder.js +9 -7
  17. package/dist/typechecker/TypeBuilder.js.map +1 -1
  18. package/dist/typechecker/checker.d.ts +9 -4
  19. package/dist/typechecker/checker.js +66 -94
  20. package/dist/typechecker/checker.js.map +1 -1
  21. package/dist/typechecker/core.js +8 -7
  22. package/dist/typechecker/core.js.map +1 -1
  23. package/dist/typechecker/inference.d.ts +4 -1
  24. package/dist/typechecker/inference.js +39 -19
  25. package/dist/typechecker/inference.js.map +1 -1
  26. package/dist/utils/helpers.d.ts +3 -0
  27. package/dist/utils/helpers.js +5 -0
  28. package/dist/utils/helpers.js.map +1 -1
  29. package/dist/utils/types.d.ts +12 -3
  30. package/dist/utils/types.js +34 -22
  31. package/dist/utils/types.js.map +1 -1
  32. package/package.json +6 -9
  33. package/src/index.ts +223 -192
  34. package/src/parser/{grammar.ts → grammar.cjs} +191 -223
  35. package/src/parser/grammar.d.ts +3 -0
  36. package/src/parser/grammar.ne +521 -518
  37. package/src/parser/lexer.ts +357 -353
  38. package/src/prelude.ts +281 -281
  39. package/src/typechecker/DeclarationCollector.ts +160 -157
  40. package/src/typechecker/TypeBuilder.ts +153 -150
  41. package/src/typechecker/checker.ts +487 -501
  42. package/src/typechecker/core.ts +195 -192
  43. package/src/typechecker/inference.ts +1442 -1421
  44. package/src/utils/helpers.ts +34 -29
  45. package/src/utils/types.ts +75 -63
  46. package/tests/hspec.spec.ts +95 -92
  47. package/tests/lexer.spec.ts +175 -175
  48. package/tests/parser.spec.ts +838 -829
  49. package/tests/prelude.spec.ts +17 -17
  50. package/tests/typechecker.spec.ts +326 -327
  51. package/tsconfig.build.json +16 -0
  52. package/tsconfig.build.tsbuildinfo +1 -0
  53. package/tsconfig.json +29 -26
  54. package/tsconfig.tsbuildinfo +1 -1
  55. package/dist/parser/grammar.d.ts +0 -28
  56. package/dist/parser/grammar.js +0 -393
  57. package/dist/parser/grammar.js.map +0 -1
@@ -1,353 +1,357 @@
1
- import moo, { Lexer, LexerState, Token } from "moo";
2
- import { keywords } from "../utils/types.js";
3
- declare module "moo" {
4
- interface Lexer {
5
- index: number; // Add the missing property to the type definition
6
- }
7
- }
8
-
9
- interface LayoutState {
10
- stack: number[]; // indentation stack (follows moo naming)
11
- tokenBuffer: Token[];
12
- expectingBlock: boolean;
13
- firstTokenProcessed: boolean;
14
- }
15
- type FullState = {
16
- layoutState: LayoutState;
17
- mooIndex?: number;
18
- } & LexerState;
19
-
20
- export const HaskellLexerConfig = {
21
- comment: { match: /--.*?$|{-[\s\S]*?-}/, lineBreaks: true },
22
-
23
- number:
24
- /0[xX][0-9a-fA-F]+|0[bB][01]+|0[oO][0-7]+|(?:\d*\.\d+|\d+)(?:[eE][+-]?\d+)?/,
25
- char: /'(?:\\['\\bfnrtv0]|\\u[0-9a-fA-F]{4}|[^'\\\n\r])?'/,
26
- string: { match: /"(?:\\[\s\S]|[^"\\])*"/, lineBreaks: true },
27
-
28
- lparen: "(",
29
- rparen: ")",
30
- lbracket: "{",
31
- rbracket: "}",
32
- lsquare: "[",
33
- rsquare: "]",
34
- semicolon: ";",
35
-
36
- typeArrow: "->",
37
- leftArrow: "<-",
38
- typeEquals: "::",
39
- fatArrow: "=>",
40
- op: {
41
- match:
42
- /,|>>=|>>|\\\\|\\|\.\.|\.|\+\+|\+|\-|\*\*|\*|===|!==|==|\/=|<=|>=|<|>|&&|\/|\|\||\$!|\$|\^\^|\^|#|@|~|!!|!|%|\?|:|&|`/,
43
- lineBreaks: false,
44
- },
45
- assign: "=",
46
- backslash: "\\",
47
- pipe: "|",
48
- at: "@",
49
- tilde: "~",
50
-
51
- anonymousVariable: "_",
52
-
53
- constructor: {
54
- match: /[A-Z][a-zA-Z0-9']*/,
55
- type: moo.keywords({ bool: ["True", "False"] }),
56
- },
57
- variable: {
58
- match: /[a-z_][a-zA-Z0-9_']*/,
59
- type: moo.keywords({ otherwise: "otherwise", keyword: keywords }),
60
- },
61
-
62
- NL: { match: /\r?\n/, lineBreaks: true },
63
- WS: { match: /[ \t]+/, lineBreaks: false },
64
- };
65
-
66
- export class HaskellLayoutLexer implements Lexer {
67
- public index: number;
68
- private mooLexer: Lexer;
69
- private state: LayoutState;
70
-
71
- private readonly layoutTriggers = new Set(["where", "let", "do", "of"]);
72
- private readonly noSemicolonKeywords = new Set(["in", "then", "else", "of"]);
73
- private readonly alwaysNewDeclarationKeywords = new Set([
74
- "type",
75
- "data",
76
- "class",
77
- "instance",
78
- "describe",
79
- "it",
80
- "module",
81
- "import",
82
- ]);
83
-
84
- constructor() {
85
- this.mooLexer = moo.compile(HaskellLexerConfig);
86
- this.state = {
87
- stack: [1],
88
- tokenBuffer: [],
89
- expectingBlock: false,
90
- firstTokenProcessed: false,
91
- };
92
- }
93
-
94
- [Symbol.iterator](): Iterator<Token> {
95
- return {
96
- next: () => {
97
- const value = this.next();
98
- return value
99
- ? { value, done: false }
100
- : { value: undefined, done: true };
101
- },
102
- };
103
- }
104
-
105
- reset(chunk?: string, state?: FullState): this {
106
- if (state && state.layoutState) {
107
- const fullState = state;
108
- this.state = {
109
- stack: [...fullState.layoutState.stack],
110
- tokenBuffer: [...fullState.layoutState.tokenBuffer],
111
- expectingBlock: fullState.layoutState.expectingBlock,
112
- firstTokenProcessed: fullState.layoutState.firstTokenProcessed,
113
- };
114
- this.mooLexer.reset(chunk, fullState);
115
- if (typeof fullState.mooIndex === "number") {
116
- this.mooLexer.index = fullState.mooIndex;
117
- }
118
- } else {
119
- this.state = {
120
- stack: [1],
121
- tokenBuffer: [],
122
- expectingBlock: false,
123
- firstTokenProcessed: false,
124
- };
125
- this.mooLexer.reset(chunk);
126
- }
127
- return this;
128
- }
129
-
130
- save(): FullState {
131
- return {
132
- ...this.mooLexer.save(),
133
- mooIndex: this.mooLexer.index,
134
- layoutState: {
135
- stack: [...this.state.stack],
136
- tokenBuffer: [...this.state.tokenBuffer],
137
- expectingBlock: this.state.expectingBlock,
138
- firstTokenProcessed: this.state.firstTokenProcessed,
139
- },
140
- };
141
- }
142
-
143
- pushState(state: string) {
144
- this.mooLexer.pushState(state);
145
- }
146
- popState() {
147
- return this.mooLexer.popState();
148
- }
149
- setState(state: string) {
150
- this.mooLexer.setState(state);
151
- }
152
- formatError(token: Token, message?: string) {
153
- return this.mooLexer.formatError(token, message);
154
- }
155
- has(tokenType: string) {
156
- return true; // deprecated in moo but Lexer interface requires it.
157
- }
158
-
159
- next(): Token | undefined {
160
- let token: Token | undefined;
161
-
162
- if (this.state.tokenBuffer.length > 0) {
163
- token = this.state.tokenBuffer.shift();
164
- } else {
165
- token = this.fetchAndProcessNextToken();
166
- }
167
-
168
- if (token) {
169
- this.checkLayoutTrigger(token);
170
- if (!this.state.firstTokenProcessed && token.col > 1) {
171
- this.state.stack[0] = token.col;
172
- }
173
- this.state.firstTokenProcessed = true;
174
- }
175
-
176
- return token;
177
- }
178
-
179
- private fetchAndProcessNextToken(): Token | undefined {
180
- let { token: rawToken, crossedNewline } =
181
- this.advanceSkippingWhitespace();
182
-
183
- if (!rawToken) return this.emitEOF();
184
-
185
-
186
- if (this.state.expectingBlock) return this.handleBlockStart(rawToken);
187
-
188
- if (this.isInToken(rawToken)) return this.handleInKeyword(rawToken);
189
-
190
- if (crossedNewline) return this.handleIndentationChange(rawToken);
191
-
192
- return rawToken;
193
- }
194
-
195
- private advanceSkippingWhitespace(): {
196
- token: Token | undefined;
197
- crossedNewline: boolean;
198
- } {
199
- let token = this.mooLexer.next();
200
- let crossedNewline = false;
201
-
202
- while (token && this.isSkippableToken(token)) {
203
- if (this.isNewlineToken(token)) crossedNewline = true;
204
- token = this.mooLexer.next();
205
- }
206
- return { token, crossedNewline };
207
- }
208
-
209
- private closeBlocksUntil(targetIndent: number, sourceToken: Token): Token {
210
- const ops: Token[] = [];
211
-
212
- while (
213
- this.state.stack.length > 1 &&
214
- this.state.stack[this.state.stack.length - 1] > targetIndent
215
- ) {
216
- this.state.stack.pop();
217
- ops.push(this.createVirtualToken("rbracket", "}", sourceToken));
218
- }
219
-
220
- const isAlwaysNewDeclaration =
221
- this.state.stack.length === 1 &&
222
- sourceToken.type === "keyword" &&
223
- this.alwaysNewDeclarationKeywords.has(sourceToken.value);
224
-
225
- const shouldInjectSemicolon =
226
- !sourceToken.value || !this.noSemicolonKeywords.has(sourceToken.value);
227
-
228
- if (
229
- this.state.stack.length > 1 &&
230
- this.state.stack[this.state.stack.length - 1] === targetIndent &&
231
- this.isInToken(sourceToken)
232
- ) {
233
- this.state.stack.pop();
234
- ops.push(this.createVirtualToken("rbracket", "}", sourceToken));
235
- } else if (
236
- (this.state.stack.length > 0 &&
237
- this.state.stack[this.state.stack.length - 1] === targetIndent &&
238
- shouldInjectSemicolon) ||
239
- isAlwaysNewDeclaration
240
- ) {
241
- ops.push(this.createVirtualToken("semicolon", ";", sourceToken));
242
- }
243
-
244
- this.state.tokenBuffer.unshift(...ops);
245
-
246
- return this.state.tokenBuffer.shift();
247
- }
248
-
249
- private emitEOF(): Token | undefined {
250
- if (this.state.stack.length > 1) {
251
- const dummy = {
252
- line: 0,
253
- col: 0,
254
- text: "",
255
- value: "",
256
- offset: 0,
257
- lineBreaks: 0,
258
- };
259
- const ops: Token[] = [];
260
- while (this.state.stack.length > 1) {
261
- this.state.stack.pop();
262
- ops.push(this.createVirtualToken("rbracket", "}", dummy));
263
- }
264
- this.state.tokenBuffer.push(...ops);
265
- return this.state.tokenBuffer.shift();
266
- }
267
- return undefined;
268
- }
269
-
270
- private enqueue(token: Token) {
271
- this.state.tokenBuffer.push(token);
272
- }
273
-
274
- private handleBlockStart(token: Token) {
275
- this.state.expectingBlock = false;
276
- if (token.type === "lbracket") return token;
277
-
278
- this.state.stack.push(token.col);
279
- this.enqueue(token);
280
- return this.createVirtualToken("lbracket", "{", token);
281
- }
282
- private handleInKeyword(token: Token) {
283
- const stackTop = this.state.stack[this.state.stack.length - 1];
284
- if (token.col < stackTop) {
285
- this.enqueue(token);
286
- return this.closeBlocksUntil(token.col, token);
287
- }
288
- if (this.state.stack.length > 1) {
289
- this.state.stack.pop();
290
- this.enqueue(token);
291
- return this.createVirtualToken("rbracket", "}", token);
292
- }
293
- return token;
294
- }
295
- private handleIndentationChange(token: Token) {
296
- const currentIndent = token.col;
297
- const stackTop = this.state.stack[this.state.stack.length - 1];
298
-
299
- const isAlwaysNewDeclaration =
300
- this.state.stack.length === 1 &&
301
- token.type === "keyword" &&
302
- this.alwaysNewDeclarationKeywords.has(token.value);
303
-
304
- if (currentIndent === stackTop || isAlwaysNewDeclaration) {
305
- const isContinuationKeyword =
306
- token.value && this.noSemicolonKeywords.has(token.value);
307
- if (this.state.firstTokenProcessed && !isContinuationKeyword) {
308
- this.enqueue(token);
309
- return this.createVirtualToken("semicolon", ";", token);
310
- }
311
- return token;
312
- } else if (currentIndent < stackTop) {
313
- this.enqueue(token);
314
- return this.closeBlocksUntil(currentIndent, token);
315
- }
316
- return token;
317
- }
318
-
319
- private checkLayoutTrigger(token: Token) {
320
- if (token && token.value && this.layoutTriggers.has(token.value))
321
- this.state.expectingBlock = true;
322
- }
323
- private isSkippableToken(token: Token) {
324
- return (
325
- token.type === "WS" ||
326
- this.isNewlineToken(token) ||
327
- token.type === "comment"
328
- );
329
- }
330
- private isNewlineToken(token: Token) {
331
- return token.type === "NL";
332
- }
333
- private isInToken(token: Token) {
334
- return token.type === "keyword" && token.value === "in";
335
- }
336
-
337
- private createVirtualToken(
338
- type: string,
339
- value: string,
340
- source: Token
341
- ): Token {
342
- return {
343
- type,
344
- value,
345
- text: value,
346
- line: source.line,
347
- col: source.col,
348
- offset: source.offset,
349
- lineBreaks: 0,
350
- toString: () => value,
351
- };
352
- }
353
- }
1
+ import moo, { Lexer, LexerState, Token } from "moo";
2
+ import { keywords } from "../utils/types.js";
3
+ declare module "moo" {
4
+ interface Lexer {
5
+ index: number; // Add the missing property to the type definition
6
+ }
7
+ }
8
+
9
+ interface LayoutState {
10
+ stack: number[]; // indentation stack (follows moo naming)
11
+ tokenBuffer: Token[];
12
+ expectingBlock: boolean;
13
+ firstTokenProcessed: boolean;
14
+ }
15
+ type FullState = {
16
+ layoutState: LayoutState;
17
+ mooIndex?: number;
18
+ } & LexerState;
19
+
20
+ export const HaskellLexerConfig = {
21
+ comment: { match: /--.*?$|{-[\s\S]*?-}/, lineBreaks: true },
22
+
23
+ number:
24
+ /0[xX][0-9a-fA-F]+|0[bB][01]+|0[oO][0-7]+|(?:\d*\.\d+|\d+)(?:[eE][+-]?\d+)?/,
25
+ char: /'(?:\\['\\bfnrtv0]|\\u[0-9a-fA-F]{4}|[^'\\\n\r])?'/,
26
+ string: { match: /"(?:\\[\s\S]|[^"\\])*"/, lineBreaks: true },
27
+
28
+ lparen: "(",
29
+ rparen: ")",
30
+ lbracket: "{",
31
+ rbracket: "}",
32
+ lsquare: "[",
33
+ rsquare: "]",
34
+ semicolon: ";",
35
+
36
+ typeArrow: "->",
37
+ leftArrow: "<-",
38
+ typeEquals: "::",
39
+ fatArrow: "=>",
40
+ op: {
41
+ match:
42
+ /,|>>=|>>|\\\\|\\|\.\.|\.|\+\+|\+|\-|\*\*|\*|===|!==|==|\/=|<=|>=|<|>|&&|\/|\|\||\$!|\$|\^\^|\^|#|@|~|!!|!|%|\?|:|&|`/,
43
+ lineBreaks: false,
44
+ },
45
+ assign: "=",
46
+ backslash: "\\",
47
+ pipe: "|",
48
+ at: "@",
49
+ tilde: "~",
50
+
51
+ anonymousVariable: "_",
52
+
53
+ constructor: {
54
+ match: /[A-Z][a-zA-Z0-9']*/,
55
+ type: moo.keywords({ bool: ["True", "False"] }),
56
+ },
57
+ variable: {
58
+ match: /[a-z_][a-zA-Z0-9_']*/,
59
+ type: moo.keywords({ otherwise: "otherwise", keyword: keywords }),
60
+ },
61
+
62
+ NL: { match: /\r?\n/, lineBreaks: true },
63
+ WS: { match: /[ \t]+/, lineBreaks: false },
64
+ };
65
+
66
+ export class HaskellLayoutLexer implements Lexer {
67
+ get index(): number {
68
+ return this.mooLexer.index;
69
+ }
70
+
71
+ set index(value: number) {
72
+ this.mooLexer.index = value;
73
+ }
74
+ private mooLexer: Lexer;
75
+ private state: LayoutState;
76
+
77
+ private readonly layoutTriggers = new Set(["where", "let", "do", "of"]);
78
+ private readonly noSemicolonKeywords = new Set(["in", "then", "else", "of"]);
79
+ private readonly alwaysNewDeclarationKeywords = new Set([
80
+ "type",
81
+ "data",
82
+ "class",
83
+ "instance",
84
+ "describe",
85
+ "it",
86
+ "module",
87
+ "import",
88
+ ]);
89
+
90
+ constructor() {
91
+ this.mooLexer = moo.compile(HaskellLexerConfig);
92
+ this.state = {
93
+ stack: [1],
94
+ tokenBuffer: [],
95
+ expectingBlock: false,
96
+ firstTokenProcessed: false,
97
+ };
98
+ }
99
+
100
+ [Symbol.iterator](): Iterator<Token> {
101
+ return {
102
+ next: () => {
103
+ const value = this.next();
104
+ return value
105
+ ? { value, done: false }
106
+ : { value: undefined, done: true };
107
+ },
108
+ };
109
+ }
110
+
111
+ reset(chunk?: string, state?: FullState): this {
112
+ if (state && state.layoutState) {
113
+ const fullState = state;
114
+ this.state = {
115
+ stack: [...fullState.layoutState.stack],
116
+ tokenBuffer: [...fullState.layoutState.tokenBuffer],
117
+ expectingBlock: fullState.layoutState.expectingBlock,
118
+ firstTokenProcessed: fullState.layoutState.firstTokenProcessed,
119
+ };
120
+ this.mooLexer.reset(chunk, fullState);
121
+ if (typeof fullState.mooIndex === "number") {
122
+ this.mooLexer.index = fullState.mooIndex;
123
+ }
124
+ } else {
125
+ this.state = {
126
+ stack: [1],
127
+ tokenBuffer: [],
128
+ expectingBlock: false,
129
+ firstTokenProcessed: false,
130
+ };
131
+ this.mooLexer.reset(chunk);
132
+ }
133
+ return this;
134
+ }
135
+
136
+ save(): FullState {
137
+ return {
138
+ ...this.mooLexer.save(),
139
+ mooIndex: this.mooLexer.index,
140
+ layoutState: {
141
+ stack: [...this.state.stack],
142
+ tokenBuffer: [...this.state.tokenBuffer],
143
+ expectingBlock: this.state.expectingBlock,
144
+ firstTokenProcessed: this.state.firstTokenProcessed,
145
+ },
146
+ };
147
+ }
148
+
149
+ pushState(state: string) {
150
+ this.mooLexer.pushState(state);
151
+ }
152
+ popState() {
153
+ return this.mooLexer.popState();
154
+ }
155
+ setState(state: string) {
156
+ this.mooLexer.setState(state);
157
+ }
158
+ formatError(token: Token, message?: string) {
159
+ return this.mooLexer.formatError(token, message);
160
+ }
161
+ has(tokenType: string) {
162
+ return true; // deprecated in moo but Lexer interface requires it.
163
+ }
164
+
165
+ next(): Token | undefined {
166
+ let token: Token | undefined;
167
+
168
+ if (this.state.tokenBuffer.length > 0) {
169
+ token = this.state.tokenBuffer.shift();
170
+ } else {
171
+ token = this.fetchAndProcessNextToken();
172
+ }
173
+
174
+ if (token) {
175
+ this.checkLayoutTrigger(token);
176
+ if (!this.state.firstTokenProcessed && token.col > 1) {
177
+ this.state.stack[0] = token.col;
178
+ }
179
+ this.state.firstTokenProcessed = true;
180
+ }
181
+
182
+ return token;
183
+ }
184
+
185
+ private fetchAndProcessNextToken(): Token | undefined {
186
+ let { token: rawToken, crossedNewline } = this.advanceSkippingWhitespace();
187
+
188
+ if (!rawToken) return this.emitEOF();
189
+
190
+ if (this.state.expectingBlock) return this.handleBlockStart(rawToken);
191
+
192
+ if (this.isInToken(rawToken)) return this.handleInKeyword(rawToken);
193
+
194
+ if (crossedNewline) return this.handleIndentationChange(rawToken);
195
+
196
+ return rawToken;
197
+ }
198
+
199
+ private advanceSkippingWhitespace(): {
200
+ token: Token | undefined;
201
+ crossedNewline: boolean;
202
+ } {
203
+ let token = this.mooLexer.next();
204
+ let crossedNewline = false;
205
+
206
+ while (token && this.isSkippableToken(token)) {
207
+ if (this.isNewlineToken(token)) crossedNewline = true;
208
+ token = this.mooLexer.next();
209
+ }
210
+ return { token, crossedNewline };
211
+ }
212
+
213
+ private closeBlocksUntil(targetIndent: number, sourceToken: Token): Token {
214
+ const ops: Token[] = [];
215
+
216
+ while (
217
+ this.state.stack.length > 1 &&
218
+ this.state.stack[this.state.stack.length - 1] > targetIndent
219
+ ) {
220
+ this.state.stack.pop();
221
+ ops.push(this.createVirtualToken("rbracket", "}", sourceToken));
222
+ }
223
+
224
+ const isAlwaysNewDeclaration =
225
+ this.state.stack.length === 1 &&
226
+ sourceToken.type === "keyword" &&
227
+ this.alwaysNewDeclarationKeywords.has(sourceToken.value);
228
+
229
+ const shouldInjectSemicolon =
230
+ !sourceToken.value || !this.noSemicolonKeywords.has(sourceToken.value);
231
+
232
+ if (
233
+ this.state.stack.length > 1 &&
234
+ this.state.stack[this.state.stack.length - 1] === targetIndent &&
235
+ this.isInToken(sourceToken)
236
+ ) {
237
+ this.state.stack.pop();
238
+ ops.push(this.createVirtualToken("rbracket", "}", sourceToken));
239
+ } else if (
240
+ (this.state.stack.length > 0 &&
241
+ this.state.stack[this.state.stack.length - 1] === targetIndent &&
242
+ shouldInjectSemicolon) ||
243
+ isAlwaysNewDeclaration
244
+ ) {
245
+ ops.push(this.createVirtualToken("semicolon", ";", sourceToken));
246
+ }
247
+
248
+ this.state.tokenBuffer.unshift(...ops);
249
+
250
+ return this.state.tokenBuffer.shift()!;
251
+ }
252
+
253
+ private emitEOF(): Token | undefined {
254
+ if (this.state.stack.length > 1) {
255
+ const dummy = {
256
+ line: 0,
257
+ col: 0,
258
+ text: "",
259
+ value: "",
260
+ offset: 0,
261
+ lineBreaks: 0,
262
+ };
263
+ const ops: Token[] = [];
264
+ while (this.state.stack.length > 1) {
265
+ this.state.stack.pop();
266
+ ops.push(this.createVirtualToken("rbracket", "}", dummy));
267
+ }
268
+ this.state.tokenBuffer.push(...ops);
269
+ return this.state.tokenBuffer.shift();
270
+ }
271
+ return undefined;
272
+ }
273
+
274
+ private enqueue(token: Token) {
275
+ this.state.tokenBuffer.push(token);
276
+ }
277
+
278
+ private handleBlockStart(token: Token) {
279
+ this.state.expectingBlock = false;
280
+ if (token.type === "lbracket") return token;
281
+
282
+ this.state.stack.push(token.col);
283
+ this.enqueue(token);
284
+ return this.createVirtualToken("lbracket", "{", token);
285
+ }
286
+ private handleInKeyword(token: Token) {
287
+ const stackTop = this.state.stack[this.state.stack.length - 1];
288
+ if (token.col < stackTop) {
289
+ this.enqueue(token);
290
+ return this.closeBlocksUntil(token.col, token);
291
+ }
292
+ if (this.state.stack.length > 1) {
293
+ this.state.stack.pop();
294
+ this.enqueue(token);
295
+ return this.createVirtualToken("rbracket", "}", token);
296
+ }
297
+ return token;
298
+ }
299
+ private handleIndentationChange(token: Token) {
300
+ const currentIndent = token.col;
301
+ const stackTop = this.state.stack[this.state.stack.length - 1];
302
+
303
+ const isAlwaysNewDeclaration =
304
+ this.state.stack.length === 1 &&
305
+ token.type === "keyword" &&
306
+ this.alwaysNewDeclarationKeywords.has(token.value);
307
+
308
+ if (currentIndent === stackTop || isAlwaysNewDeclaration) {
309
+ const isContinuationKeyword =
310
+ token.value && this.noSemicolonKeywords.has(token.value);
311
+ if (this.state.firstTokenProcessed && !isContinuationKeyword) {
312
+ this.enqueue(token);
313
+ return this.createVirtualToken("semicolon", ";", token);
314
+ }
315
+ return token;
316
+ } else if (currentIndent < stackTop) {
317
+ this.enqueue(token);
318
+ return this.closeBlocksUntil(currentIndent, token);
319
+ }
320
+ return token;
321
+ }
322
+
323
+ private checkLayoutTrigger(token: Token) {
324
+ if (token && token.value && this.layoutTriggers.has(token.value))
325
+ this.state.expectingBlock = true;
326
+ }
327
+ private isSkippableToken(token: Token) {
328
+ return (
329
+ token.type === "WS" ||
330
+ this.isNewlineToken(token) ||
331
+ token.type === "comment"
332
+ );
333
+ }
334
+ private isNewlineToken(token: Token) {
335
+ return token.type === "NL";
336
+ }
337
+ private isInToken(token: Token) {
338
+ return token.type === "keyword" && token.value === "in";
339
+ }
340
+
341
+ private createVirtualToken(
342
+ type: string,
343
+ value: string,
344
+ source: Token,
345
+ ): Token {
346
+ return {
347
+ type,
348
+ value,
349
+ text: value,
350
+ line: source.line,
351
+ col: source.col,
352
+ offset: source.offset,
353
+ lineBreaks: 0,
354
+ toString: () => value,
355
+ };
356
+ }
357
+ }