yukigo-haskell-parser 0.1.2 → 0.2.1
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 +41 -7
- package/README.md +5 -10
- package/dist/index.js +21 -13
- package/dist/index.js.map +1 -1
- package/dist/parser/grammar.cjs +397 -0
- package/dist/parser/grammar.cjs.map +1 -0
- package/dist/parser/grammar.d.cts +38 -0
- package/dist/parser/lexer.d.ts +2 -1
- package/dist/parser/lexer.js +6 -1
- package/dist/parser/lexer.js.map +1 -1
- package/dist/prelude.js +279 -279
- package/dist/typechecker/DeclarationCollector.d.ts +2 -0
- package/dist/typechecker/DeclarationCollector.js +6 -5
- package/dist/typechecker/DeclarationCollector.js.map +1 -1
- package/dist/typechecker/TypeBuilder.js +9 -7
- package/dist/typechecker/TypeBuilder.js.map +1 -1
- package/dist/typechecker/checker.d.ts +9 -4
- package/dist/typechecker/checker.js +66 -94
- package/dist/typechecker/checker.js.map +1 -1
- package/dist/typechecker/core.js +8 -7
- package/dist/typechecker/core.js.map +1 -1
- package/dist/typechecker/inference.d.ts +4 -1
- package/dist/typechecker/inference.js +39 -19
- package/dist/typechecker/inference.js.map +1 -1
- package/dist/utils/helpers.d.ts +3 -0
- package/dist/utils/helpers.js +5 -0
- package/dist/utils/helpers.js.map +1 -1
- package/dist/utils/types.d.ts +12 -3
- package/dist/utils/types.js +34 -22
- package/dist/utils/types.js.map +1 -1
- package/package.json +9 -10
- package/src/index.ts +223 -192
- package/src/parser/{grammar.ts → grammar.cjs} +191 -223
- package/src/parser/grammar.d.ts +3 -0
- package/src/parser/grammar.ne +521 -518
- package/src/parser/lexer.ts +357 -353
- package/src/prelude.ts +281 -281
- package/src/typechecker/DeclarationCollector.ts +160 -157
- package/src/typechecker/TypeBuilder.ts +153 -150
- package/src/typechecker/checker.ts +487 -501
- package/src/typechecker/core.ts +195 -192
- package/src/typechecker/inference.ts +1442 -1421
- package/src/utils/helpers.ts +34 -29
- package/src/utils/types.ts +75 -63
- package/tests/hspec.spec.ts +95 -92
- package/tests/lexer.spec.ts +175 -175
- package/tests/parser.spec.ts +838 -829
- package/tests/prelude.spec.ts +17 -17
- package/tests/typechecker.spec.ts +326 -327
- package/tsconfig.build.json +16 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +29 -26
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/parser/grammar.d.ts +0 -28
- package/dist/parser/grammar.js +0 -393
- package/dist/parser/grammar.js.map +0 -1
package/tests/lexer.spec.ts
CHANGED
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
import { assert, expect } from "chai";
|
|
2
|
-
import { HaskellLayoutLexer } from "../src/parser/lexer.js";
|
|
3
|
-
|
|
4
|
-
describe("HaskellLayoutLexer", () => {
|
|
5
|
-
let lexer: HaskellLayoutLexer;
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
lexer = new HaskellLayoutLexer();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
// Helper: Returns array of objects { type, value } for better debugging
|
|
12
|
-
const getTokens = (input: string) => {
|
|
13
|
-
lexer.reset(input);
|
|
14
|
-
const tokens = [];
|
|
15
|
-
for (const token of lexer) {
|
|
16
|
-
tokens.push({ type: token.type, value: token.value });
|
|
17
|
-
}
|
|
18
|
-
return tokens;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const getTypes = (input: string) => getTokens(input).map((t) => t.type);
|
|
22
|
-
|
|
23
|
-
it("should ignore basic whitespace", () => {
|
|
24
|
-
const input = "x = 5";
|
|
25
|
-
const types = getTypes(input);
|
|
26
|
-
expect(types).to.not.include("WS");
|
|
27
|
-
expect(types).to.not.include("NL");
|
|
28
|
-
expect(types).to.include("assign");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("Layout Rule: 'do' blocks", () => {
|
|
32
|
-
it("should inject braces for indented 'do' block", () => {
|
|
33
|
-
const input = `
|
|
34
|
-
do
|
|
35
|
-
x
|
|
36
|
-
y
|
|
37
|
-
`;
|
|
38
|
-
const tokens = getTokens(input);
|
|
39
|
-
// We check values for keywords since type is now generic 'keyword' or 'variable'
|
|
40
|
-
expect(tokens[0].value).to.equal("do");
|
|
41
|
-
expect(tokens[1].type).to.equal("lbracket"); // { Injected
|
|
42
|
-
expect(tokens[2].value).to.equal("x");
|
|
43
|
-
expect(tokens[3].type).to.equal("semicolon"); // ; Injected
|
|
44
|
-
expect(tokens[4].value).to.equal("y");
|
|
45
|
-
expect(tokens[5].type).to.equal("rbracket"); // } Injected
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("should handle explicit braces (no layout injection)", () => {
|
|
49
|
-
const input = "do { x; y }";
|
|
50
|
-
const types = getTypes(input);
|
|
51
|
-
expect(types).to.include("lbracket");
|
|
52
|
-
expect(types).to.include("semicolon");
|
|
53
|
-
expect(types).to.include("rbracket");
|
|
54
|
-
// Ensure we didn't double up
|
|
55
|
-
expect(types.filter((t) => t === "lbracket").length).to.equal(1);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("Layout Rule: 'let' blocks", () => {
|
|
60
|
-
it("should inject braces and handle 'in' dedent", () => {
|
|
61
|
-
const input = `
|
|
62
|
-
let
|
|
63
|
-
x = 1
|
|
64
|
-
in x
|
|
65
|
-
`;
|
|
66
|
-
const tokens = getTokens(input);
|
|
67
|
-
|
|
68
|
-
const letIndex = tokens.findIndex((t) => t.value === "let");
|
|
69
|
-
expect(tokens[letIndex + 1].type).to.equal("lbracket");
|
|
70
|
-
|
|
71
|
-
const inIndex = tokens.findIndex((t) => t.value === "in");
|
|
72
|
-
expect(tokens[inIndex - 1].type).to.equal("rbracket");
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("Layout Rule: 'where' clauses", () => {
|
|
77
|
-
it("should handle nested blocks correctly", () => {
|
|
78
|
-
const input = `
|
|
79
|
-
f = do
|
|
80
|
-
let x = 1
|
|
81
|
-
return x
|
|
82
|
-
`;
|
|
83
|
-
const types = getTypes(input);
|
|
84
|
-
|
|
85
|
-
const lbrackets = types.filter((t) => t === "lbracket").length;
|
|
86
|
-
const rbrackets = types.filter((t) => t === "rbracket").length;
|
|
87
|
-
const semicolons = types.filter((t) => t === "semicolon").length;
|
|
88
|
-
|
|
89
|
-
expect(lbrackets).to.equal(2);
|
|
90
|
-
expect(rbrackets).to.equal(2);
|
|
91
|
-
expect(semicolons).to.equal(1);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe("Layout Rule: Separators", () => {
|
|
96
|
-
it("should inject semicolons for aligned declarations", () => {
|
|
97
|
-
// NOTE: We removed the leading newline here.
|
|
98
|
-
// If we keep the newline, the lexer will insert a semicolon before 'x' too
|
|
99
|
-
// (because x is at col 1, which matches stack 1).
|
|
100
|
-
const input = "x = 1\ny = 2";
|
|
101
|
-
|
|
102
|
-
const tokens = getTokens(input);
|
|
103
|
-
|
|
104
|
-
// Expected: x, =, 1, SEMICOLON, y, =, 2
|
|
105
|
-
const semiToken = tokens.find((t) => t.type === "semicolon");
|
|
106
|
-
expect(semiToken).to.not.be.undefined;
|
|
107
|
-
|
|
108
|
-
const semiIndex = tokens.findIndex((t) => t.type === "semicolon");
|
|
109
|
-
// Should be after '1' (index 2) and before 'y' (index 4)
|
|
110
|
-
expect(semiIndex).to.equal(3);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe("Nearley Integration Requirements", () => {
|
|
115
|
-
it("should save and restore state correctly (Backtracking)", () => {
|
|
116
|
-
const input = `
|
|
117
|
-
do
|
|
118
|
-
x
|
|
119
|
-
y
|
|
120
|
-
`;
|
|
121
|
-
lexer.reset(input);
|
|
122
|
-
|
|
123
|
-
lexer.next();
|
|
124
|
-
lexer.next();
|
|
125
|
-
|
|
126
|
-
const state = lexer.save();
|
|
127
|
-
|
|
128
|
-
const t1 = lexer.next();
|
|
129
|
-
const t2 = lexer.next();
|
|
130
|
-
|
|
131
|
-
lexer.reset(input, state);
|
|
132
|
-
|
|
133
|
-
const t1_restored = lexer.next();
|
|
134
|
-
const t2_restored = lexer.next();
|
|
135
|
-
|
|
136
|
-
expect(t1_restored?.value).to.equal(t1?.value);
|
|
137
|
-
expect(t2_restored?.type).to.equal(t2?.type);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
describe("Layout Rule: Separators", () => {
|
|
141
|
-
it("should not inject semicolons or blocks for if-then-else", () => {
|
|
142
|
-
const input = "f x = if x < 4 then 10 else 20";
|
|
143
|
-
const input2 = "f x = \n if x < 4\nthen 10\nelse 20";
|
|
144
|
-
const inlinetokes = getTokens(input);
|
|
145
|
-
const multiTokens = getTokens(input2);
|
|
146
|
-
expect(multiTokens.length).to.eq(inlinetokes.length);
|
|
147
|
-
multiTokens.forEach((tok, i) => {
|
|
148
|
-
const inlineTok = inlinetokes[i];
|
|
149
|
-
expect(tok.type).to.eq(inlineTok.type);
|
|
150
|
-
expect(tok.value).to.eq(inlineTok.value);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
describe("Layout Rule: 'where' clauses (Multi-line)", () => {
|
|
155
|
-
it("should handle multi-line where clause with alignment", () => {
|
|
156
|
-
const input = `
|
|
157
|
-
areaOfCircle radius = pi * radius_squared
|
|
158
|
-
where pi = 3.14159
|
|
159
|
-
radius_squared = radius * radius`;
|
|
160
|
-
const tokens = getTokens(input);
|
|
161
|
-
|
|
162
|
-
const whereIndex = tokens.findIndex((t) => t.value === "where");
|
|
163
|
-
expect(whereIndex).to.not.equal(-1);
|
|
164
|
-
expect(tokens[whereIndex + 1].type).to.equal("lbracket");
|
|
165
|
-
|
|
166
|
-
const semiIndex = tokens.findIndex(
|
|
167
|
-
(t, i) => i > whereIndex && t.type === "semicolon"
|
|
168
|
-
);
|
|
169
|
-
expect(semiIndex).to.not.equal(-1);
|
|
170
|
-
|
|
171
|
-
const lastToken = tokens[tokens.length - 1];
|
|
172
|
-
expect(lastToken.type).to.equal("rbracket");
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
});
|
|
1
|
+
import { assert, expect } from "chai";
|
|
2
|
+
import { HaskellLayoutLexer } from "../src/parser/lexer.js";
|
|
3
|
+
|
|
4
|
+
describe("HaskellLayoutLexer", () => {
|
|
5
|
+
let lexer: HaskellLayoutLexer;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
lexer = new HaskellLayoutLexer();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Helper: Returns array of objects { type, value } for better debugging
|
|
12
|
+
const getTokens = (input: string) => {
|
|
13
|
+
lexer.reset(input);
|
|
14
|
+
const tokens = [];
|
|
15
|
+
for (const token of lexer) {
|
|
16
|
+
tokens.push({ type: token.type, value: token.value });
|
|
17
|
+
}
|
|
18
|
+
return tokens;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getTypes = (input: string) => getTokens(input).map((t) => t.type);
|
|
22
|
+
|
|
23
|
+
it("should ignore basic whitespace", () => {
|
|
24
|
+
const input = "x = 5";
|
|
25
|
+
const types = getTypes(input);
|
|
26
|
+
expect(types).to.not.include("WS");
|
|
27
|
+
expect(types).to.not.include("NL");
|
|
28
|
+
expect(types).to.include("assign");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("Layout Rule: 'do' blocks", () => {
|
|
32
|
+
it("should inject braces for indented 'do' block", () => {
|
|
33
|
+
const input = `
|
|
34
|
+
do
|
|
35
|
+
x
|
|
36
|
+
y
|
|
37
|
+
`;
|
|
38
|
+
const tokens = getTokens(input);
|
|
39
|
+
// We check values for keywords since type is now generic 'keyword' or 'variable'
|
|
40
|
+
expect(tokens[0].value).to.equal("do");
|
|
41
|
+
expect(tokens[1].type).to.equal("lbracket"); // { Injected
|
|
42
|
+
expect(tokens[2].value).to.equal("x");
|
|
43
|
+
expect(tokens[3].type).to.equal("semicolon"); // ; Injected
|
|
44
|
+
expect(tokens[4].value).to.equal("y");
|
|
45
|
+
expect(tokens[5].type).to.equal("rbracket"); // } Injected
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should handle explicit braces (no layout injection)", () => {
|
|
49
|
+
const input = "do { x; y }";
|
|
50
|
+
const types = getTypes(input);
|
|
51
|
+
expect(types).to.include("lbracket");
|
|
52
|
+
expect(types).to.include("semicolon");
|
|
53
|
+
expect(types).to.include("rbracket");
|
|
54
|
+
// Ensure we didn't double up
|
|
55
|
+
expect(types.filter((t) => t === "lbracket").length).to.equal(1);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("Layout Rule: 'let' blocks", () => {
|
|
60
|
+
it("should inject braces and handle 'in' dedent", () => {
|
|
61
|
+
const input = `
|
|
62
|
+
let
|
|
63
|
+
x = 1
|
|
64
|
+
in x
|
|
65
|
+
`;
|
|
66
|
+
const tokens = getTokens(input);
|
|
67
|
+
|
|
68
|
+
const letIndex = tokens.findIndex((t) => t.value === "let");
|
|
69
|
+
expect(tokens[letIndex + 1].type).to.equal("lbracket");
|
|
70
|
+
|
|
71
|
+
const inIndex = tokens.findIndex((t) => t.value === "in");
|
|
72
|
+
expect(tokens[inIndex - 1].type).to.equal("rbracket");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("Layout Rule: 'where' clauses", () => {
|
|
77
|
+
it("should handle nested blocks correctly", () => {
|
|
78
|
+
const input = `
|
|
79
|
+
f = do
|
|
80
|
+
let x = 1
|
|
81
|
+
return x
|
|
82
|
+
`;
|
|
83
|
+
const types = getTypes(input);
|
|
84
|
+
|
|
85
|
+
const lbrackets = types.filter((t) => t === "lbracket").length;
|
|
86
|
+
const rbrackets = types.filter((t) => t === "rbracket").length;
|
|
87
|
+
const semicolons = types.filter((t) => t === "semicolon").length;
|
|
88
|
+
|
|
89
|
+
expect(lbrackets).to.equal(2);
|
|
90
|
+
expect(rbrackets).to.equal(2);
|
|
91
|
+
expect(semicolons).to.equal(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("Layout Rule: Separators", () => {
|
|
96
|
+
it("should inject semicolons for aligned declarations", () => {
|
|
97
|
+
// NOTE: We removed the leading newline here.
|
|
98
|
+
// If we keep the newline, the lexer will insert a semicolon before 'x' too
|
|
99
|
+
// (because x is at col 1, which matches stack 1).
|
|
100
|
+
const input = "x = 1\ny = 2";
|
|
101
|
+
|
|
102
|
+
const tokens = getTokens(input);
|
|
103
|
+
|
|
104
|
+
// Expected: x, =, 1, SEMICOLON, y, =, 2
|
|
105
|
+
const semiToken = tokens.find((t) => t.type === "semicolon");
|
|
106
|
+
expect(semiToken).to.not.be.undefined;
|
|
107
|
+
|
|
108
|
+
const semiIndex = tokens.findIndex((t) => t.type === "semicolon");
|
|
109
|
+
// Should be after '1' (index 2) and before 'y' (index 4)
|
|
110
|
+
expect(semiIndex).to.equal(3);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("Nearley Integration Requirements", () => {
|
|
115
|
+
it("should save and restore state correctly (Backtracking)", () => {
|
|
116
|
+
const input = `
|
|
117
|
+
do
|
|
118
|
+
x
|
|
119
|
+
y
|
|
120
|
+
`;
|
|
121
|
+
lexer.reset(input);
|
|
122
|
+
|
|
123
|
+
lexer.next();
|
|
124
|
+
lexer.next();
|
|
125
|
+
|
|
126
|
+
const state = lexer.save();
|
|
127
|
+
|
|
128
|
+
const t1 = lexer.next();
|
|
129
|
+
const t2 = lexer.next();
|
|
130
|
+
|
|
131
|
+
lexer.reset(input, state);
|
|
132
|
+
|
|
133
|
+
const t1_restored = lexer.next();
|
|
134
|
+
const t2_restored = lexer.next();
|
|
135
|
+
|
|
136
|
+
expect(t1_restored?.value).to.equal(t1?.value);
|
|
137
|
+
expect(t2_restored?.type).to.equal(t2?.type);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe("Layout Rule: Separators", () => {
|
|
141
|
+
it("should not inject semicolons or blocks for if-then-else", () => {
|
|
142
|
+
const input = "f x = if x < 4 then 10 else 20";
|
|
143
|
+
const input2 = "f x = \n if x < 4\nthen 10\nelse 20";
|
|
144
|
+
const inlinetokes = getTokens(input);
|
|
145
|
+
const multiTokens = getTokens(input2);
|
|
146
|
+
expect(multiTokens.length).to.eq(inlinetokes.length);
|
|
147
|
+
multiTokens.forEach((tok, i) => {
|
|
148
|
+
const inlineTok = inlinetokes[i];
|
|
149
|
+
expect(tok.type).to.eq(inlineTok.type);
|
|
150
|
+
expect(tok.value).to.eq(inlineTok.value);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe("Layout Rule: 'where' clauses (Multi-line)", () => {
|
|
155
|
+
it("should handle multi-line where clause with alignment", () => {
|
|
156
|
+
const input = `
|
|
157
|
+
areaOfCircle radius = pi * radius_squared
|
|
158
|
+
where pi = 3.14159
|
|
159
|
+
radius_squared = radius * radius`;
|
|
160
|
+
const tokens = getTokens(input);
|
|
161
|
+
|
|
162
|
+
const whereIndex = tokens.findIndex((t) => t.value === "where");
|
|
163
|
+
expect(whereIndex).to.not.equal(-1);
|
|
164
|
+
expect(tokens[whereIndex + 1].type).to.equal("lbracket");
|
|
165
|
+
|
|
166
|
+
const semiIndex = tokens.findIndex(
|
|
167
|
+
(t, i) => i > whereIndex && t.type === "semicolon"
|
|
168
|
+
);
|
|
169
|
+
expect(semiIndex).to.not.equal(-1);
|
|
170
|
+
|
|
171
|
+
const lastToken = tokens[tokens.length - 1];
|
|
172
|
+
expect(lastToken.type).to.equal("rbracket");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|