yukigo-haskell-parser 0.1.0 → 0.1.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.
- package/.mocharc.json +3 -3
- package/CHANGELOG.md +6 -0
- package/README.md +10 -10
- package/dist/index.d.ts +13 -2
- package/dist/index.js +112 -32
- package/dist/index.js.map +1 -1
- package/dist/parser/grammar.js +220 -231
- package/dist/parser/grammar.js.map +1 -1
- package/dist/parser/lexer.d.ts +70 -15
- package/dist/parser/lexer.js +259 -50
- package/dist/parser/lexer.js.map +1 -1
- package/dist/prelude.d.ts +1 -1
- package/dist/prelude.js +280 -204
- package/dist/prelude.js.map +1 -1
- package/dist/typechecker/DeclarationCollector.d.ts +6 -1
- package/dist/typechecker/DeclarationCollector.js +39 -0
- package/dist/typechecker/DeclarationCollector.js.map +1 -1
- package/dist/typechecker/TypeBuilder.d.ts +1 -1
- package/dist/typechecker/TypeBuilder.js +6 -2
- package/dist/typechecker/TypeBuilder.js.map +1 -1
- package/dist/typechecker/checker.d.ts +15 -4
- package/dist/typechecker/checker.js +108 -25
- package/dist/typechecker/checker.js.map +1 -1
- package/dist/typechecker/core.d.ts +4 -0
- package/dist/typechecker/core.js +45 -19
- package/dist/typechecker/core.js.map +1 -1
- package/dist/typechecker/inference.d.ts +5 -1
- package/dist/typechecker/inference.js +95 -21
- package/dist/typechecker/inference.js.map +1 -1
- package/dist/utils/helpers.d.ts +1 -1
- package/dist/utils/helpers.js +1 -1
- package/dist/utils/helpers.js.map +1 -1
- package/dist/utils/types.d.ts +1 -1
- package/dist/utils/types.js +7 -0
- package/dist/utils/types.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +192 -55
- package/src/parser/grammar.ne +519 -463
- package/src/parser/grammar.ts +230 -239
- package/src/parser/lexer.ts +353 -77
- package/src/prelude.ts +282 -206
- package/src/typechecker/DeclarationCollector.ts +157 -104
- package/src/typechecker/TypeBuilder.ts +150 -148
- package/src/typechecker/checker.ts +501 -395
- package/src/typechecker/core.ts +192 -162
- package/src/typechecker/inference.ts +1421 -1327
- package/src/utils/helpers.ts +29 -29
- package/src/utils/types.ts +63 -56
- package/tests/hspec.spec.ts +92 -0
- package/tests/lexer.spec.ts +175 -0
- package/tests/parser.spec.ts +829 -823
- package/tests/prelude.spec.ts +17 -22
- package/tests/typechecker.spec.ts +327 -310
- package/tsconfig.json +26 -17
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/parser/layoutPreprocessor.d.ts +0 -1
- package/dist/parser/layoutPreprocessor.js +0 -22
- package/dist/parser/layoutPreprocessor.js.map +0 -1
- package/dist/parser/preprocessor.d.ts +0 -28
- package/dist/parser/preprocessor.js +0 -453
- package/dist/parser/preprocessor.js.map +0 -1
- package/src/parser/layoutPreprocessor.ts +0 -21
- package/src/parser/preprocessor.ne +0 -375
- package/src/parser/preprocessor.ts +0 -502
- package/tests/preprocessor.spec.ts +0 -69
package/src/parser/lexer.ts
CHANGED
|
@@ -1,77 +1,353 @@
|
|
|
1
|
-
import moo, { Lexer } from "moo";
|
|
2
|
-
import { keywords } from "../utils/types.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
}
|