Skip to content

Commit 40ec421

Browse files
authored
Merge pull request #1 from KevinBatdorf/add-tests-for-compile-from-source
2 parents 96dcfcd + bea3742 commit 40ec421

File tree

7 files changed

+129
-33
lines changed

7 files changed

+129
-33
lines changed

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,18 @@ run(bytes);
8989

9090
## Types
9191

92-
All major types are exported:
92+
All error classes and major types are exported:
9393

9494
```ts
95-
import type { AST, Token, CompilerErrorType, ParseErrorType, TokenErrorType, RunFunction } from "pinky-compiler";
95+
import type {
96+
CompilerError,
97+
ParseError,
98+
TokenError,
99+
type AST,
100+
type Token,
101+
type CompileFromSourceResult,
102+
type RunFunction
103+
} from "pinky-compiler";
96104
```
97105

98106
Additionally, you can import the syntax types listed [here](https://github.com/KevinBatdorf/pinky-compiler/blob/main/src/index.ts).

src/compiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import {
4545
import type { AST } from "./parser";
4646
import type { Expression, Statement } from "./syntax";
4747

48-
class CompilerError extends Error {
48+
export class CompilerError extends Error {
4949
line: number;
5050
column: number;
5151
tokenLength: number;

src/from-source.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, test } from "vitest";
2+
import { CompilerError, compileFromSource, ParseError, TokenError } from ".";
3+
4+
describe("compileFromSource (discriminated union)", () => {
5+
test("returns valid bytes and no error for correct source", () => {
6+
const source = 'println "hello world"';
7+
const result = compileFromSource(source);
8+
9+
expect(result.errorType).toBeNull();
10+
expect(result.error).toBeNull();
11+
expect(result.bytes).toBeInstanceOf(Uint8Array);
12+
expect(result.strings).toBeInstanceOf(Uint8Array);
13+
});
14+
15+
test("returns TokenError for invalid token", () => {
16+
const source = 'println "unterminated string';
17+
const result = compileFromSource(source);
18+
19+
expect(result.errorType).toBe("TokenError");
20+
expect(result.error).toBeInstanceOf(TokenError);
21+
expect(result.bytes).toBeNull();
22+
expect(result.strings).toBeNull();
23+
});
24+
25+
test("returns ParseError for invalid syntax", () => {
26+
const source = "print )";
27+
const result = compileFromSource(source);
28+
29+
expect(result.errorType).toBe("ParseError");
30+
expect(result.error).toBeInstanceOf(ParseError);
31+
expect(result.bytes).toBeNull();
32+
expect(result.strings).toBeNull();
33+
});
34+
35+
test("returns CompilerError for semantic error", () => {
36+
const source = "print x";
37+
const result = compileFromSource(source);
38+
39+
expect(result.errorType).toBe("CompilerError");
40+
expect(result.error).toBeInstanceOf(CompilerError);
41+
expect(result.bytes).toBeNull();
42+
expect(result.strings).toBeNull();
43+
});
44+
});

src/index.ts

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,73 @@
1-
import { compile } from "./compiler";
1+
import { type CompilerError, compile } from "./compiler";
22
import { loadWasm } from "./compiler/exports";
3-
import { tokenize } from "./lexer";
4-
import { parse } from "./parser";
3+
import { type TokenError, tokenize } from "./lexer";
4+
import { type ParseError, parse } from "./parser";
55

6-
export type { CompilerErrorType } from "./compiler";
7-
export { compile } from "./compiler";
8-
export type { RunFunction } from "./compiler/exports";
9-
export { loadWasm } from "./compiler/exports";
10-
export type { TokenErrorType } from "./lexer";
11-
export { tokenize } from "./lexer";
12-
export type { AST, ParseErrorType } from "./parser";
13-
export { parse } from "./parser";
6+
export { CompilerError, compile } from "./compiler";
7+
export { loadWasm, type RunFunction } from "./compiler/exports";
8+
export { TokenError, tokenize } from "./lexer";
9+
export { type AST, ParseError, parse } from "./parser";
1410
export * from "./syntax";
1511
export type { Token } from "./tokens";
1612

13+
export type CompileFromSourceResult =
14+
| { errorType: null; error: null; bytes: Uint8Array; strings: Uint8Array }
15+
| { errorType: "TokenError"; error: TokenError; bytes: null; strings: null }
16+
| { errorType: "ParseError"; error: ParseError; bytes: null; strings: null }
17+
| {
18+
errorType: "CompilerError";
19+
error: CompilerError;
20+
bytes: null;
21+
strings: null;
22+
};
23+
1724
/**
1825
* Compiles Pinky source code from a string.
1926
* This function tokenizes the source code, parses the tokens into an AST,
2027
* and then compiles the AST into WebAssembly binary.
2128
*
2229
* ```
2330
* const source = "x := 42 print(x)";
24-
* const wasmBytes = compileFromSource(source);
31+
* const { bytes } = compileFromSource(source);
2532
* ```
2633
*/
27-
export const compileFromSource = (source: string) => {
28-
const { tokens } = tokenize(source);
29-
const { ast } = parse(tokens);
30-
return compile(ast);
34+
export const compileFromSource = (source: string): CompileFromSourceResult => {
35+
const { tokens, error: tokenError } = tokenize(source);
36+
if (tokenError) {
37+
return {
38+
bytes: null,
39+
strings: null,
40+
errorType: "TokenError",
41+
error: tokenError,
42+
};
43+
}
44+
45+
const { ast, error: parseError } = parse(tokens);
46+
if (parseError) {
47+
return {
48+
bytes: null,
49+
strings: null,
50+
errorType: "ParseError",
51+
error: parseError,
52+
};
53+
}
54+
55+
const { bytes, meta, error: compilerError } = compile(ast);
56+
if (compilerError) {
57+
return {
58+
bytes: null,
59+
strings: null,
60+
errorType: "CompilerError",
61+
error: compilerError,
62+
};
63+
}
64+
65+
return {
66+
bytes,
67+
strings: meta?.strings ?? null,
68+
errorType: null,
69+
error: compilerError,
70+
};
3171
};
3272

3373
/**

src/lexer.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
lookahead,
1313
match,
1414
peek,
15+
TokenError,
1516
} from "./lexer";
1617

1718
test("isAlpha", () => {
@@ -420,11 +421,7 @@ test("should error on invalid input", () => {
420421
{ type: "RPAREN", value: ")", start: 10, end: 11, column: 11, line: 1 },
421422
{ type: "EOF", value: "", start: 11, end: 11, column: 12, line: 1 },
422423
]);
423-
expect(error).toEqual({
424-
line: 1,
425-
column: 12,
426-
message: "Unexpected character ':'",
427-
});
424+
expect(error).toEqual(new TokenError("Unexpected character ':'", 1, 12));
428425
});
429426

430427
test("should tokenize >= and <=", () => {

src/lexer.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { keywords, type Token, tokenTypes } from "./tokens";
22

3-
export type TokenErrorType = null | {
3+
export class TokenError extends Error {
44
line: number;
55
column: number;
6-
message: string;
7-
};
6+
constructor(message: string, line: number, column: number) {
7+
super(message);
8+
this.name = "TokenError";
9+
this.line = line;
10+
this.column = column;
11+
}
12+
}
13+
export type TokenErrorType = null | TokenError;
814

915
export const isAlpha = (char: string): boolean =>
1016
(char >= "a" && char <= "z") || (char >= "A" && char <= "Z") || char === "_";
@@ -125,11 +131,12 @@ export const tokenize = (
125131
const errorReturn = (
126132
message: string,
127133
details?: { line?: number; column?: number },
128-
): TokenErrorType => ({
129-
message,
130-
line: details?.line || pos.line,
131-
column: details?.column || pos.column,
132-
});
134+
): TokenErrorType =>
135+
new TokenError(
136+
message,
137+
details?.line || pos.line,
138+
details?.column || pos.column,
139+
);
133140

134141
while (!isEndOfFile(peek(src, pos.current))) {
135142
const ch = peek(src, pos.current);

src/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { symbolForTokenType, type Token, type TokenType } from "./tokens";
1212

1313
type ParserState = { tokens: Token[]; current: number };
1414
export type AST = Program;
15-
class ParseError extends Error {
15+
export class ParseError extends Error {
1616
body: Program;
1717
line: number;
1818
column: number;

0 commit comments

Comments
 (0)