@hatchingpoint/point 0.0.3

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.
@@ -0,0 +1,235 @@
1
+ import type { PointSourceSpan } from "./ast.ts";
2
+
3
+ export type PointCoreTokenType =
4
+ | "identifier"
5
+ | "number"
6
+ | "string"
7
+ | "leftBrace"
8
+ | "rightBrace"
9
+ | "leftBracket"
10
+ | "rightBracket"
11
+ | "leftParen"
12
+ | "rightParen"
13
+ | "comma"
14
+ | "colon"
15
+ | "dot"
16
+ | "equals"
17
+ | "equalsEquals"
18
+ | "bangEquals"
19
+ | "less"
20
+ | "lessEquals"
21
+ | "greater"
22
+ | "greaterEquals"
23
+ | "plus"
24
+ | "minus"
25
+ | "star"
26
+ | "slash"
27
+ | "eof";
28
+
29
+ export interface PointCoreToken {
30
+ type: PointCoreTokenType;
31
+ value: string;
32
+ span: PointSourceSpan;
33
+ }
34
+
35
+ export class PointCoreLexerError extends Error {
36
+ constructor(message: string, public readonly span: PointSourceSpan) {
37
+ super(`${message} at ${span.start.line}:${span.start.column}`);
38
+ this.name = "PointCoreLexerError";
39
+ }
40
+ }
41
+
42
+ export function lexPointCore(source: string): PointCoreToken[] {
43
+ const lexer = new CoreLexer(source);
44
+ return lexer.lex();
45
+ }
46
+
47
+ class CoreLexer {
48
+ private index = 0;
49
+ private line = 1;
50
+ private column = 1;
51
+ private readonly tokens: PointCoreToken[] = [];
52
+
53
+ constructor(private readonly source: string) {}
54
+
55
+ lex(): PointCoreToken[] {
56
+ while (!this.done()) {
57
+ const char = this.peek();
58
+ if (/\s/.test(char)) {
59
+ this.advance();
60
+ continue;
61
+ }
62
+ if (char === "/" && this.peek(1) === "/") {
63
+ while (!this.done() && this.peek() !== "\n") this.advance();
64
+ continue;
65
+ }
66
+ if (char === "{") {
67
+ this.push("leftBrace", this.advance());
68
+ continue;
69
+ }
70
+ if (char === "}") {
71
+ this.push("rightBrace", this.advance());
72
+ continue;
73
+ }
74
+ if (char === "(") {
75
+ this.push("leftParen", this.advance());
76
+ continue;
77
+ }
78
+ if (char === "[") {
79
+ this.push("leftBracket", this.advance());
80
+ continue;
81
+ }
82
+ if (char === "]") {
83
+ this.push("rightBracket", this.advance());
84
+ continue;
85
+ }
86
+ if (char === ")") {
87
+ this.push("rightParen", this.advance());
88
+ continue;
89
+ }
90
+ if (char === ",") {
91
+ this.push("comma", this.advance());
92
+ continue;
93
+ }
94
+ if (char === ":") {
95
+ this.push("colon", this.advance());
96
+ continue;
97
+ }
98
+ if (char === ".") {
99
+ this.push("dot", this.advance());
100
+ continue;
101
+ }
102
+ if (char === "=") {
103
+ if (this.peek(1) === "=") {
104
+ this.push("equalsEquals", `${this.advance()}${this.advance()}`);
105
+ continue;
106
+ }
107
+ this.push("equals", this.advance());
108
+ continue;
109
+ }
110
+ if (char === "!" && this.peek(1) === "=") {
111
+ this.push("bangEquals", `${this.advance()}${this.advance()}`);
112
+ continue;
113
+ }
114
+ if (char === "<") {
115
+ if (this.peek(1) === "=") {
116
+ this.push("lessEquals", `${this.advance()}${this.advance()}`);
117
+ continue;
118
+ }
119
+ this.push("less", this.advance());
120
+ continue;
121
+ }
122
+ if (char === ">") {
123
+ if (this.peek(1) === "=") {
124
+ this.push("greaterEquals", `${this.advance()}${this.advance()}`);
125
+ continue;
126
+ }
127
+ this.push("greater", this.advance());
128
+ continue;
129
+ }
130
+ if (char === "+") {
131
+ this.push("plus", this.advance());
132
+ continue;
133
+ }
134
+ if (char === "-") {
135
+ this.push("minus", this.advance());
136
+ continue;
137
+ }
138
+ if (char === "*") {
139
+ this.push("star", this.advance());
140
+ continue;
141
+ }
142
+ if (char === "/") {
143
+ this.push("slash", this.advance());
144
+ continue;
145
+ }
146
+ if (char === "\"") {
147
+ this.readString();
148
+ continue;
149
+ }
150
+ if (/[0-9]/.test(char)) {
151
+ this.readNumber();
152
+ continue;
153
+ }
154
+ if (/[A-Za-z_]/.test(char)) {
155
+ this.readIdentifier();
156
+ continue;
157
+ }
158
+ throw new PointCoreLexerError(`Unexpected character ${JSON.stringify(char)}`, this.spanAt());
159
+ }
160
+ this.tokens.push({ type: "eof", value: "", span: this.spanAt() });
161
+ return this.tokens;
162
+ }
163
+
164
+ private readString() {
165
+ const start = this.position();
166
+ this.advance();
167
+ let value = "";
168
+ while (!this.done() && this.peek() !== "\"") {
169
+ const next = this.advance();
170
+ if (next === "\\") {
171
+ const escaped = this.advance();
172
+ value += escaped === "n" ? "\n" : escaped;
173
+ } else {
174
+ value += next;
175
+ }
176
+ }
177
+ if (this.peek() !== "\"") throw new PointCoreLexerError("Unterminated string", { start, end: this.position() });
178
+ this.advance();
179
+ this.tokens.push({ type: "string", value, span: { start, end: this.position() } });
180
+ }
181
+
182
+ private readNumber() {
183
+ const start = this.position();
184
+ let value = "";
185
+ while (/[0-9.]/.test(this.peek())) value += this.advance();
186
+ this.tokens.push({ type: "number", value, span: { start, end: this.position() } });
187
+ }
188
+
189
+ private readIdentifier() {
190
+ const start = this.position();
191
+ let value = "";
192
+ while (/[A-Za-z0-9_]/.test(this.peek())) value += this.advance();
193
+ this.tokens.push({ type: "identifier", value, span: { start, end: this.position() } });
194
+ }
195
+
196
+ private push(type: PointCoreTokenType, value: string) {
197
+ const end = this.position();
198
+ this.tokens.push({
199
+ type,
200
+ value,
201
+ span: {
202
+ start: { line: end.line, column: end.column - value.length, offset: end.offset - value.length },
203
+ end,
204
+ },
205
+ });
206
+ }
207
+
208
+ private advance() {
209
+ const char = this.source[this.index++] ?? "";
210
+ if (char === "\n") {
211
+ this.line += 1;
212
+ this.column = 1;
213
+ } else {
214
+ this.column += 1;
215
+ }
216
+ return char;
217
+ }
218
+
219
+ private peek(offset = 0) {
220
+ return this.source[this.index + offset] ?? "";
221
+ }
222
+
223
+ private done() {
224
+ return this.index >= this.source.length;
225
+ }
226
+
227
+ private position() {
228
+ return { line: this.line, column: this.column, offset: this.index };
229
+ }
230
+
231
+ private spanAt(): PointSourceSpan {
232
+ const position = this.position();
233
+ return { start: position, end: position };
234
+ }
235
+ }
@@ -0,0 +1,362 @@
1
+ import type {
2
+ PointCoreBinaryOperator,
3
+ PointCoreDeclaration,
4
+ PointCoreExpression,
5
+ PointCoreFunctionDeclaration,
6
+ PointCoreParameter,
7
+ PointCoreProgram,
8
+ PointCoreRecordField,
9
+ PointCoreStatement,
10
+ PointCoreTypeExpression,
11
+ PointCoreValueDeclaration,
12
+ PointSourceSpan,
13
+ } from "./ast.ts";
14
+ import { lexPointCore, type PointCoreToken, type PointCoreTokenType } from "./lexer.ts";
15
+
16
+ export class PointCoreParserError extends Error {
17
+ constructor(message: string, public readonly token: PointCoreToken) {
18
+ super(`${message} at ${token.span.start.line}:${token.span.start.column}`);
19
+ this.name = "PointCoreParserError";
20
+ }
21
+ }
22
+
23
+ export function parsePointCore(source: string): PointCoreProgram {
24
+ const parser = new CoreParser(lexPointCore(source));
25
+ return parser.parseProgram();
26
+ }
27
+
28
+ class CoreParser {
29
+ private current = 0;
30
+
31
+ constructor(private readonly tokens: PointCoreToken[]) {}
32
+
33
+ parseProgram(): PointCoreProgram {
34
+ let moduleName: string | undefined;
35
+ const declarations: PointCoreDeclaration[] = [];
36
+ const start = this.peek().span.start;
37
+ while (!this.check("eof")) {
38
+ if (this.matchKeyword("module")) {
39
+ moduleName = this.consume("identifier", "Expected module name").value;
40
+ continue;
41
+ }
42
+ declarations.push(this.parseDeclaration());
43
+ }
44
+ return {
45
+ kind: "coreProgram",
46
+ module: moduleName,
47
+ declarations,
48
+ span: { start, end: this.peek().span.end },
49
+ };
50
+ }
51
+
52
+ private parseDeclaration(): PointCoreDeclaration {
53
+ if (this.matchKeyword("import")) return this.parseImport();
54
+ if (this.checkKeyword("let") || this.checkKeyword("var")) return this.parseValueDeclaration();
55
+ if (this.matchKeyword("fn")) return this.parseFunction();
56
+ if (this.matchKeyword("type")) return this.parseTypeDeclaration();
57
+ throw new PointCoreParserError("Expected import, let, var, fn, or type declaration", this.peek());
58
+ }
59
+
60
+ private parseImport(): PointCoreDeclaration {
61
+ const start = this.previous().span.start;
62
+ this.consume("leftBrace", "Expected import list");
63
+ const names: string[] = [];
64
+ do {
65
+ names.push(this.consume("identifier", "Expected imported name").value);
66
+ } while (this.match("comma"));
67
+ this.consume("rightBrace", "Expected end of import list");
68
+ this.consumeKeyword("from");
69
+ const from = this.consume("string", "Expected import source").value;
70
+ return { kind: "import", names, from, span: { start, end: this.previous().span.end } };
71
+ }
72
+
73
+ private parseValueDeclaration(): PointCoreValueDeclaration {
74
+ const keyword = this.advance();
75
+ const name = this.consume("identifier", "Expected value name");
76
+ this.consume("colon", `Expected type annotation for ${name.value}`);
77
+ const type = this.parseTypeExpression();
78
+ this.consume("equals", `Expected initializer for ${name.value}`);
79
+ const value = this.parseExpression();
80
+ return {
81
+ kind: "value",
82
+ mutable: keyword.value === "var",
83
+ name: name.value,
84
+ type,
85
+ value,
86
+ span: { start: keyword.span.start, end: value.span?.end ?? this.previous().span.end },
87
+ };
88
+ }
89
+
90
+ private parseFunction(): PointCoreFunctionDeclaration {
91
+ const start = this.previous().span.start;
92
+ const name = this.consume("identifier", "Expected function name").value;
93
+ this.consume("leftParen", "Expected function parameters");
94
+ const params = this.parseParameters();
95
+ this.consume("rightParen", "Expected end of function parameters");
96
+ this.consume("colon", "Expected function return type");
97
+ const returnType = this.parseTypeExpression();
98
+ this.consume("leftBrace", "Expected function body");
99
+ const body: PointCoreStatement[] = [];
100
+ while (!this.check("rightBrace")) body.push(this.parseStatement());
101
+ this.consume("rightBrace", "Expected end of function body");
102
+ return { kind: "function", name, params, returnType, body, span: { start, end: this.previous().span.end } };
103
+ }
104
+
105
+ private parseTypeDeclaration(): PointCoreDeclaration {
106
+ const start = this.previous().span.start;
107
+ const name = this.consume("identifier", "Expected type name").value;
108
+ this.consume("leftBrace", "Expected type body");
109
+ const fields: PointCoreParameter[] = [];
110
+ while (!this.check("rightBrace")) {
111
+ fields.push(this.parseParameter());
112
+ }
113
+ this.consume("rightBrace", "Expected end of type body");
114
+ return { kind: "type", name, fields, span: { start, end: this.previous().span.end } };
115
+ }
116
+
117
+ private parseStatement(): PointCoreStatement {
118
+ if (this.matchKeyword("return")) {
119
+ const start = this.previous().span.start;
120
+ if (this.check("rightBrace")) return { kind: "return", span: { start, end: this.previous().span.end } };
121
+ const value = this.parseExpression();
122
+ return { kind: "return", value, span: { start, end: value.span?.end ?? this.previous().span.end } };
123
+ }
124
+ if (this.matchKeyword("if")) return this.parseIfStatement();
125
+ if (this.checkKeyword("let") || this.checkKeyword("var")) return this.parseValueDeclaration();
126
+ const value = this.parseExpression();
127
+ return { kind: "expression", value, span: value.span };
128
+ }
129
+
130
+ private parseIfStatement(): PointCoreStatement {
131
+ const start = this.previous().span.start;
132
+ const condition = this.parseExpression();
133
+ const thenBody = this.parseBlockStatements("Expected if body");
134
+ let elseBody: PointCoreStatement[] = [];
135
+ if (this.matchKeyword("else")) {
136
+ elseBody = this.parseBlockStatements("Expected else body");
137
+ }
138
+ return {
139
+ kind: "if",
140
+ condition,
141
+ thenBody,
142
+ elseBody,
143
+ span: { start, end: this.previous().span.end },
144
+ };
145
+ }
146
+
147
+ private parseBlockStatements(message: string): PointCoreStatement[] {
148
+ this.consume("leftBrace", message);
149
+ const statements: PointCoreStatement[] = [];
150
+ while (!this.check("rightBrace")) statements.push(this.parseStatement());
151
+ this.consume("rightBrace", "Expected end of block");
152
+ return statements;
153
+ }
154
+
155
+ private parseParameters(): PointCoreParameter[] {
156
+ const params: PointCoreParameter[] = [];
157
+ if (this.check("rightParen")) return params;
158
+ do {
159
+ params.push(this.parseParameter());
160
+ } while (this.match("comma"));
161
+ return params;
162
+ }
163
+
164
+ private parseParameter(): PointCoreParameter {
165
+ const name = this.consume("identifier", "Expected parameter name");
166
+ this.consume("colon", `Expected type annotation for ${name.value}`);
167
+ const type = this.parseTypeExpression();
168
+ return { name: name.value, type, span: { start: name.span.start, end: type.span?.end ?? name.span.end } };
169
+ }
170
+
171
+ private parseTypeExpression(): PointCoreTypeExpression {
172
+ const token = this.consume("identifier", "Expected type name");
173
+ const args: PointCoreTypeExpression[] = [];
174
+ if (this.match("less")) {
175
+ do {
176
+ args.push(this.parseTypeExpression());
177
+ } while (this.match("comma"));
178
+ this.consume("greater", "Expected end of type arguments");
179
+ }
180
+ return { kind: "typeRef", name: token.value, args, span: { start: token.span.start, end: this.previous().span.end } };
181
+ }
182
+
183
+ private parseExpression(): PointCoreExpression {
184
+ return this.parseBinaryExpression(0);
185
+ }
186
+
187
+ private parseBinaryExpression(minPrecedence: number): PointCoreExpression {
188
+ let left = this.parsePrimaryExpression();
189
+ while (true) {
190
+ const operator = this.peekBinaryOperator();
191
+ if (!operator) break;
192
+ const precedence = precedenceFor(operator);
193
+ if (precedence < minPrecedence) break;
194
+ this.advance();
195
+ const right = this.parseBinaryExpression(precedence + 1);
196
+ left = {
197
+ kind: "binary",
198
+ operator,
199
+ left,
200
+ right,
201
+ span: { start: left.span?.start ?? this.previous().span.start, end: right.span?.end ?? this.previous().span.end },
202
+ };
203
+ }
204
+ return left;
205
+ }
206
+
207
+ private parsePrimaryExpression(): PointCoreExpression {
208
+ let expression = this.parseAtomExpression();
209
+ while (this.match("dot")) {
210
+ const name = this.consume("identifier", "Expected property name");
211
+ expression = {
212
+ kind: "property",
213
+ target: expression,
214
+ name: name.value,
215
+ span: { start: expression.span?.start ?? name.span.start, end: name.span.end },
216
+ };
217
+ }
218
+ return expression;
219
+ }
220
+
221
+ private parseAtomExpression(): PointCoreExpression {
222
+ if (this.check("string")) {
223
+ const token = this.advance();
224
+ return { kind: "literal", value: token.value, span: token.span };
225
+ }
226
+ if (this.check("number")) {
227
+ const token = this.advance();
228
+ return { kind: "literal", value: Number(token.value), span: token.span };
229
+ }
230
+ if (this.match("leftParen")) {
231
+ const start = this.previous().span.start;
232
+ const expression = this.parseExpression();
233
+ this.consume("rightParen", "Expected end of grouped expression");
234
+ return { ...expression, span: { start, end: this.previous().span.end } };
235
+ }
236
+ if (this.match("leftBracket")) return this.parseListExpression();
237
+ if (this.match("leftBrace")) return this.parseRecordExpression();
238
+ const identifier = this.consume("identifier", "Expected expression");
239
+ if (identifier.value === "true") return { kind: "literal", value: true, span: identifier.span };
240
+ if (identifier.value === "false") return { kind: "literal", value: false, span: identifier.span };
241
+ if (this.match("leftParen")) {
242
+ const args: PointCoreExpression[] = [];
243
+ if (!this.check("rightParen")) {
244
+ do {
245
+ args.push(this.parseExpression());
246
+ } while (this.match("comma"));
247
+ }
248
+ this.consume("rightParen", "Expected end of call arguments");
249
+ return {
250
+ kind: "call",
251
+ callee: identifier.value,
252
+ args,
253
+ span: { start: identifier.span.start, end: this.previous().span.end },
254
+ };
255
+ }
256
+ return { kind: "identifier", name: identifier.value, span: identifier.span };
257
+ }
258
+
259
+ private parseListExpression(): PointCoreExpression {
260
+ const start = this.previous().span.start;
261
+ const items: PointCoreExpression[] = [];
262
+ if (!this.check("rightBracket")) {
263
+ do {
264
+ items.push(this.parseExpression());
265
+ } while (this.match("comma"));
266
+ }
267
+ this.consume("rightBracket", "Expected end of list");
268
+ return { kind: "list", items, span: { start, end: this.previous().span.end } };
269
+ }
270
+
271
+ private parseRecordExpression(): PointCoreExpression {
272
+ const start = this.previous().span.start;
273
+ const fields: PointCoreRecordField[] = [];
274
+ if (!this.check("rightBrace")) {
275
+ do {
276
+ const name = this.consume("identifier", "Expected record field name");
277
+ this.consume("colon", `Expected value for record field ${name.value}`);
278
+ const value = this.parseExpression();
279
+ fields.push({
280
+ name: name.value,
281
+ value,
282
+ span: { start: name.span.start, end: value.span?.end ?? name.span.end },
283
+ });
284
+ } while (this.match("comma"));
285
+ }
286
+ this.consume("rightBrace", "Expected end of record");
287
+ return { kind: "record", fields, span: { start, end: this.previous().span.end } };
288
+ }
289
+
290
+ private peekBinaryOperator(): PointCoreBinaryOperator | null {
291
+ const token = this.peek();
292
+ if (token.type === "plus") return "+";
293
+ if (token.type === "minus") return "-";
294
+ if (token.type === "star") return "*";
295
+ if (token.type === "slash") return "/";
296
+ if (token.type === "equalsEquals") return "==";
297
+ if (token.type === "bangEquals") return "!=";
298
+ if (token.type === "less") return "<";
299
+ if (token.type === "lessEquals") return "<=";
300
+ if (token.type === "greater") return ">";
301
+ if (token.type === "greaterEquals") return ">=";
302
+ if (token.type === "identifier" && (token.value === "and" || token.value === "or")) return token.value;
303
+ return null;
304
+ }
305
+
306
+ private consumeKeyword(keyword: string) {
307
+ const token = this.consume("identifier", `Expected ${keyword}`);
308
+ if (token.value !== keyword) throw new PointCoreParserError(`Expected ${keyword}`, token);
309
+ return token;
310
+ }
311
+
312
+ private matchKeyword(keyword: string) {
313
+ if (!this.checkKeyword(keyword)) return false;
314
+ this.advance();
315
+ return true;
316
+ }
317
+
318
+ private checkKeyword(keyword: string) {
319
+ return this.check("identifier") && this.peek().value === keyword;
320
+ }
321
+
322
+ private consume(type: PointCoreTokenType, message: string) {
323
+ if (this.check(type)) return this.advance();
324
+ throw new PointCoreParserError(message, this.peek());
325
+ }
326
+
327
+ private match(type: PointCoreTokenType) {
328
+ if (!this.check(type)) return false;
329
+ this.advance();
330
+ return true;
331
+ }
332
+
333
+ private check(type: PointCoreTokenType) {
334
+ return this.peek().type === type;
335
+ }
336
+
337
+ private advance() {
338
+ if (!this.check("eof")) this.current += 1;
339
+ return this.previous();
340
+ }
341
+
342
+ private peek() {
343
+ return this.tokens[this.current] ?? this.tokens[this.tokens.length - 1]!;
344
+ }
345
+
346
+ private previous() {
347
+ return this.tokens[this.current - 1] ?? this.tokens[0]!;
348
+ }
349
+ }
350
+
351
+ function precedenceFor(operator: PointCoreBinaryOperator): number {
352
+ if (operator === "or") return 1;
353
+ if (operator === "and") return 2;
354
+ if (operator === "==" || operator === "!=") return 3;
355
+ if (operator === "<" || operator === "<=" || operator === ">" || operator === ">=") return 4;
356
+ if (operator === "+" || operator === "-") return 5;
357
+ return 6;
358
+ }
359
+
360
+ export function mergeSpans(start: PointSourceSpan, end: PointSourceSpan): PointSourceSpan {
361
+ return { start: start.start, end: end.end };
362
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./core/index.ts";