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.
Files changed (65) hide show
  1. package/.mocharc.json +3 -3
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +10 -10
  4. package/dist/index.d.ts +13 -2
  5. package/dist/index.js +112 -32
  6. package/dist/index.js.map +1 -1
  7. package/dist/parser/grammar.js +220 -231
  8. package/dist/parser/grammar.js.map +1 -1
  9. package/dist/parser/lexer.d.ts +70 -15
  10. package/dist/parser/lexer.js +259 -50
  11. package/dist/parser/lexer.js.map +1 -1
  12. package/dist/prelude.d.ts +1 -1
  13. package/dist/prelude.js +280 -204
  14. package/dist/prelude.js.map +1 -1
  15. package/dist/typechecker/DeclarationCollector.d.ts +6 -1
  16. package/dist/typechecker/DeclarationCollector.js +39 -0
  17. package/dist/typechecker/DeclarationCollector.js.map +1 -1
  18. package/dist/typechecker/TypeBuilder.d.ts +1 -1
  19. package/dist/typechecker/TypeBuilder.js +6 -2
  20. package/dist/typechecker/TypeBuilder.js.map +1 -1
  21. package/dist/typechecker/checker.d.ts +15 -4
  22. package/dist/typechecker/checker.js +108 -25
  23. package/dist/typechecker/checker.js.map +1 -1
  24. package/dist/typechecker/core.d.ts +4 -0
  25. package/dist/typechecker/core.js +45 -19
  26. package/dist/typechecker/core.js.map +1 -1
  27. package/dist/typechecker/inference.d.ts +5 -1
  28. package/dist/typechecker/inference.js +95 -21
  29. package/dist/typechecker/inference.js.map +1 -1
  30. package/dist/utils/helpers.d.ts +1 -1
  31. package/dist/utils/helpers.js +1 -1
  32. package/dist/utils/helpers.js.map +1 -1
  33. package/dist/utils/types.d.ts +1 -1
  34. package/dist/utils/types.js +7 -0
  35. package/dist/utils/types.js.map +1 -1
  36. package/package.json +3 -3
  37. package/src/index.ts +192 -55
  38. package/src/parser/grammar.ne +519 -463
  39. package/src/parser/grammar.ts +230 -239
  40. package/src/parser/lexer.ts +353 -77
  41. package/src/prelude.ts +282 -206
  42. package/src/typechecker/DeclarationCollector.ts +157 -104
  43. package/src/typechecker/TypeBuilder.ts +150 -148
  44. package/src/typechecker/checker.ts +501 -395
  45. package/src/typechecker/core.ts +192 -162
  46. package/src/typechecker/inference.ts +1421 -1327
  47. package/src/utils/helpers.ts +29 -29
  48. package/src/utils/types.ts +63 -56
  49. package/tests/hspec.spec.ts +92 -0
  50. package/tests/lexer.spec.ts +175 -0
  51. package/tests/parser.spec.ts +829 -823
  52. package/tests/prelude.spec.ts +17 -22
  53. package/tests/typechecker.spec.ts +327 -310
  54. package/tsconfig.json +26 -17
  55. package/tsconfig.tsbuildinfo +1 -0
  56. package/dist/parser/layoutPreprocessor.d.ts +0 -1
  57. package/dist/parser/layoutPreprocessor.js +0 -22
  58. package/dist/parser/layoutPreprocessor.js.map +0 -1
  59. package/dist/parser/preprocessor.d.ts +0 -28
  60. package/dist/parser/preprocessor.js +0 -453
  61. package/dist/parser/preprocessor.js.map +0 -1
  62. package/src/parser/layoutPreprocessor.ts +0 -21
  63. package/src/parser/preprocessor.ne +0 -375
  64. package/src/parser/preprocessor.ts +0 -502
  65. package/tests/preprocessor.spec.ts +0 -69
@@ -1,29 +1,29 @@
1
- import { inspect } from "util";
2
- import { AST } from "yukigo-ast";
3
- import { Function } from "yukigo-ast";
4
-
5
- export function groupFunctionDeclarations(ast: AST): AST {
6
- const groups: Record<string, Function[]> = {};
7
- const others: AST = [];
8
-
9
- for (const node of ast) {
10
- if (node instanceof Function) {
11
- const name = node.identifier.value;
12
- if (!groups[name]) {
13
- groups[name] = [];
14
- }
15
- groups[name].push(node);
16
- } else {
17
- others.push(node);
18
- }
19
- }
20
-
21
- // Merge equations for each function name
22
- const functionGroups: Function[] = Object.values(groups).map((functions) => {
23
- const identifier = functions[0].identifier;
24
- const allEquations = functions.flatMap((f) => f.equations);
25
- return new Function(identifier, allEquations, identifier.loc);
26
- });
27
-
28
- return [...others, ...functionGroups];
29
- }
1
+ import { inspect } from "util";
2
+ import { AST } from "yukigo-ast";
3
+ import { Function } from "yukigo-ast";
4
+
5
+ export function groupFunctionDeclarations(ast: AST): AST {
6
+ const groups: Record<string, Function[]> = {};
7
+ const others: AST = [];
8
+
9
+ for (const node of ast) {
10
+ if (node instanceof Function) {
11
+ const name = node.identifier.value;
12
+ if (!groups[name]) {
13
+ groups[name] = [];
14
+ }
15
+ groups[name].push(node);
16
+ } else {
17
+ others.push(node);
18
+ }
19
+ }
20
+
21
+ // Merge equations for each function name
22
+ const functionGroups: Function[] = Object.values(groups).map((functions) => {
23
+ const identifier = functions[0].identifier;
24
+ const allEquations = functions.flatMap((f) => f.equations);
25
+ return new Function(identifier, allEquations, identifier.loc);
26
+ });
27
+
28
+ return [...others, ...functionGroups];
29
+ }
@@ -1,56 +1,63 @@
1
- import { YukigoPrimitive } from "yukigo-ast";
2
-
3
- export const keywords = [
4
- "type",
5
- "where",
6
- "in",
7
- "if",
8
- "else",
9
- "then",
10
- "data",
11
- "case",
12
- "class",
13
- "do",
14
- "default",
15
- "deriving",
16
- "import",
17
- "infix",
18
- "infixl",
19
- "infixr",
20
- "instance",
21
- "let",
22
- "module",
23
- "newtype",
24
- "of",
25
- "qualified",
26
- "hiding",
27
- "foreign",
28
- ];
29
-
30
- export const typeMappings: { [key: string]: YukigoPrimitive } = {
31
- Float: "YuNumber",
32
- Double: "YuNumber",
33
- Int: "YuNumber",
34
- Integer: "YuNumber",
35
- String: "YuString",
36
- Char: "YuChar",
37
- Boolean: "YuBoolean",
38
- Bool: "YuBoolean",
39
- };
40
-
41
- export const typeClasses: Map<string, string[]> = new Map([
42
- ["Bounded", ["YuChar", "YuNumber"]],
43
- ["Enum", ["YuChar", "YuNumber"]],
44
- ["Ord", ["YuNumber", "YuString", "YuChar"]],
45
- ["Eq", ["YuNumber", "YuString", "YuChar", "YuBoolean"]],
46
- ["Floating", ["YuNumber"]],
47
- ["Fractional", ["YuNumber"]],
48
- ["Integral", ["YuNumber"]],
49
- ["Num", ["YuNumber"]],
50
- ["Random", ["YuBoolean", "YuChar", "YuNumber"]],
51
- ["Read", ["YuList", "YuChar", "YuNumber"]],
52
- ["Real", ["YuNumber"]],
53
- ["RealFloat", ["YuNumber"]],
54
- ["RealFrac", ["YuNumber"]],
55
- ["Show", ["YuNumber", "YuString", "YuChar", "YuList"]],
56
- ]);
1
+ import { YukigoPrimitive } from "yukigo-ast";
2
+
3
+ export const keywords = [
4
+ "type",
5
+ "where",
6
+ "in",
7
+ "if",
8
+ "else",
9
+ "then",
10
+ "data",
11
+ "case",
12
+ "error",
13
+ "class",
14
+ "do",
15
+ "default",
16
+ "deriving",
17
+ "import",
18
+ "infix",
19
+ "infixl",
20
+ "infixr",
21
+ "instance",
22
+ "let",
23
+ "module",
24
+ "newtype",
25
+ "of",
26
+ "qualified",
27
+ "hiding",
28
+ "foreign",
29
+ "describe",
30
+ "it",
31
+ "shouldBe",
32
+ "shouldNotBe",
33
+ "shouldSatisfy",
34
+ "shouldThrow"
35
+ ];
36
+
37
+ export const typeMappings: { [key: string]: YukigoPrimitive } = {
38
+ Float: "YuNumber",
39
+ Double: "YuNumber",
40
+ Int: "YuNumber",
41
+ Integer: "YuNumber",
42
+ String: "YuString",
43
+ Char: "YuChar",
44
+ Boolean: "YuBoolean",
45
+ Bool: "YuBoolean",
46
+ };
47
+
48
+ export const typeClasses: Map<string, string[]> = new Map([
49
+ ["Bounded", ["YuChar", "YuNumber"]],
50
+ ["Enum", ["YuChar", "YuNumber"]],
51
+ ["Ord", ["YuNumber", "YuString", "YuChar"]],
52
+ ["Eq", ["YuNumber", "YuString", "YuChar", "YuBoolean"]],
53
+ ["Floating", ["YuNumber"]],
54
+ ["Fractional", ["YuNumber"]],
55
+ ["Integral", ["YuNumber"]],
56
+ ["Num", ["YuNumber"]],
57
+ ["Random", ["YuBoolean", "YuChar", "YuNumber"]],
58
+ ["Read", ["YuList", "YuChar", "YuNumber"]],
59
+ ["Real", ["YuNumber"]],
60
+ ["RealFloat", ["YuNumber"]],
61
+ ["RealFrac", ["YuNumber"]],
62
+ ["Show", ["YuNumber", "YuString", "YuChar", "YuList"]],
63
+ ]);
@@ -0,0 +1,92 @@
1
+ import { expect } from "chai";
2
+ import { YukigoHaskellParser } from "../src/index.js";
3
+ import {
4
+ TestGroup,
5
+ Test,
6
+ Assert,
7
+ Equality,
8
+ Truth,
9
+ Failure,
10
+ Raise,
11
+ NumberPrimitive,
12
+ SymbolPrimitive,
13
+ Application,
14
+ StringPrimitive,
15
+ BooleanPrimitive
16
+ } from "yukigo-ast";
17
+
18
+ describe("HSpec Parsing", () => {
19
+ const parser = new YukigoHaskellParser("", { typecheck: true, includePrims: false });
20
+
21
+ it("should parse describe and it blocks", () => {
22
+ const code = `
23
+ describe "Math" do
24
+ it "adds 1 and 1" do
25
+ (1 + 1) shouldBe 2
26
+ `;
27
+ const ast = parser.parse(code);
28
+ expect(ast).to.have.lengthOf(1);
29
+ expect(ast[0]).to.be.instanceOf(TestGroup);
30
+
31
+ const group = ast[0] as TestGroup;
32
+ expect(group.name).to.be.instanceOf(StringPrimitive);
33
+ expect((group.name as StringPrimitive).value).to.equal("Math");
34
+
35
+ expect(group.group.statements).to.have.lengthOf(1);
36
+ expect(group.group.statements[0]).to.be.instanceOf(Test);
37
+
38
+ const test = group.group.statements[0] as Test;
39
+ expect(test.name).to.be.instanceOf(StringPrimitive);
40
+ expect((test.name as StringPrimitive).value).to.equal("adds 1 and 1");
41
+
42
+ expect(test.body.statements).to.have.lengthOf(1);
43
+ expect(test.body.statements[0]).to.be.instanceOf(Assert);
44
+
45
+ const assert = test.body.statements[0] as Assert;
46
+ expect(assert.negated).to.be.instanceOf(BooleanPrimitive);
47
+ expect((assert.negated as BooleanPrimitive).value).to.be.false;
48
+ expect(assert.body).to.be.instanceOf(Equality);
49
+
50
+ const equality = assert.body as Equality;
51
+ expect(equality.expected).to.be.instanceOf(NumberPrimitive);
52
+ expect((equality.expected as NumberPrimitive).value).to.equal(2);
53
+ });
54
+
55
+ it("should parse shouldNotBe", () => {
56
+ const code = `it "negation" do 1 shouldNotBe 2`;
57
+ const ast = parser.parse(code);
58
+ const test = ast[0] as Test;
59
+ const assert = test.body.statements[0] as Assert;
60
+ expect((assert.negated as BooleanPrimitive).value).to.be.true;
61
+ expect(assert.body).to.be.instanceOf(Equality);
62
+ });
63
+
64
+ it("should parse shouldSatisfy", () => {
65
+ const code = `it "satisfy" do 5 shouldSatisfy odd`;
66
+ const ast = parser.parse(code);
67
+ const test = ast[0] as Test;
68
+ const assert = test.body.statements[0] as Assert;
69
+ expect(assert.body).to.be.instanceOf(Truth);
70
+
71
+ const truth = assert.body as Truth;
72
+ expect(truth.body).to.be.instanceOf(Application);
73
+ const app = truth.body as Application;
74
+ expect(app.functionExpr).to.be.instanceOf(SymbolPrimitive);
75
+ expect((app.functionExpr as SymbolPrimitive).value).to.equal("odd");
76
+ expect(app.parameter).to.be.instanceOf(NumberPrimitive);
77
+ expect((app.parameter as NumberPrimitive).value).to.equal(5);
78
+ });
79
+
80
+ it("should parse shouldThrow", () => {
81
+ const code = `it "throws" do error "fail" shouldThrow anyException`;
82
+ const ast = parser.parse(code);
83
+ const test = ast[0] as Test;
84
+ const assert = test.body.statements[0] as Assert;
85
+ expect(assert.body).to.be.instanceOf(Failure);
86
+
87
+ const failure = assert.body as Failure;
88
+ expect(failure.func).to.be.instanceOf(Raise);
89
+ expect(failure.message).to.be.instanceOf(SymbolPrimitive);
90
+ expect((failure.message as SymbolPrimitive).value).to.equal("anyException");
91
+ });
92
+ });
@@ -0,0 +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
+ });