@hatchingpoint/point 0.0.3 → 0.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatchingpoint/point",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Point language compiler and CLI.",
package/src/core/ast.ts CHANGED
@@ -78,6 +78,13 @@ export interface PointCoreRecordField {
78
78
  export type PointCoreStatement =
79
79
  | { kind: "return"; value?: PointCoreExpression; span?: PointSourceSpan }
80
80
  | PointCoreValueDeclaration
81
+ | {
82
+ kind: "assignment";
83
+ name: string;
84
+ operator: "=" | "+=";
85
+ value: PointCoreExpression;
86
+ span?: PointSourceSpan;
87
+ }
81
88
  | {
82
89
  kind: "if";
83
90
  condition: PointCoreExpression;
package/src/core/check.ts CHANGED
@@ -24,6 +24,8 @@ export interface PointCoreDiagnostic {
24
24
  }
25
25
 
26
26
  type DiagnosticMetadata = Partial<Pick<PointCoreDiagnostic, "expected" | "actual" | "repair" | "relatedRefs">>;
27
+ type ScopeEntry = { type: PointCoreTypeExpression; mutable: boolean };
28
+ type Scope = Map<string, ScopeEntry>;
27
29
 
28
30
  const PRIMITIVE_TYPES = new Set(["Text", "Int", "Float", "Bool", "Void", "List"]);
29
31
 
@@ -36,7 +38,7 @@ class CoreChecker {
36
38
  private readonly diagnostics: PointCoreDiagnostic[] = [];
37
39
  private readonly types = new Set(PRIMITIVE_TYPES);
38
40
  private readonly typeDeclarations = new Map<string, PointCoreTypeDeclaration>();
39
- private readonly globals = new Map<string, PointCoreTypeExpression>();
41
+ private readonly globals: Scope = new Map();
40
42
  private readonly functions = new Map<string, PointCoreFunctionDeclaration>();
41
43
 
42
44
  constructor(private readonly program: PointCoreProgram) {}
@@ -70,7 +72,7 @@ class CoreChecker {
70
72
  if (this.globals.has(declaration.name)) {
71
73
  this.push("duplicate-value", `Duplicate value ${declaration.name}`, `value.${declaration.name}`, declaration.span);
72
74
  }
73
- this.globals.set(declaration.name, declaration.type);
75
+ this.globals.set(declaration.name, { type: declaration.type, mutable: declaration.mutable });
74
76
  }
75
77
 
76
78
  private checkDeclaration(declaration: PointCoreDeclaration) {
@@ -92,7 +94,7 @@ class CoreChecker {
92
94
  const locals = new Map(this.globals);
93
95
  for (const param of declaration.params) {
94
96
  this.checkType(param.type, `fn.${declaration.name}.${param.name}.type`);
95
- locals.set(param.name, param.type);
97
+ locals.set(param.name, { type: param.type, mutable: false });
96
98
  }
97
99
  for (const statement of declaration.body) {
98
100
  this.checkStatement(statement, declaration, locals);
@@ -102,7 +104,7 @@ class CoreChecker {
102
104
  private checkStatement(
103
105
  statement: PointCoreStatement,
104
106
  fn: PointCoreFunctionDeclaration,
105
- locals: Map<string, PointCoreTypeExpression>,
107
+ locals: Scope,
106
108
  ) {
107
109
  if (statement.kind === "return") {
108
110
  if (!statement.value) {
@@ -117,7 +119,11 @@ class CoreChecker {
117
119
  if (statement.kind === "value") {
118
120
  this.checkType(statement.type, `fn.${fn.name}.${statement.name}.type`);
119
121
  this.checkExpressionAssignable(statement.value, statement.type, `fn.${fn.name}.${statement.name}.value`, locals);
120
- locals.set(statement.name, statement.type);
122
+ locals.set(statement.name, { type: statement.type, mutable: statement.mutable });
123
+ return;
124
+ }
125
+ if (statement.kind === "assignment") {
126
+ this.checkAssignment(statement, fn, locals);
121
127
  return;
122
128
  }
123
129
  if (statement.kind === "if") {
@@ -131,11 +137,42 @@ class CoreChecker {
131
137
  this.typeOfExpression(statement.value, locals, `fn.${fn.name}.expression`);
132
138
  }
133
139
 
140
+ private checkAssignment(
141
+ statement: Extract<PointCoreStatement, { kind: "assignment" }>,
142
+ fn: PointCoreFunctionDeclaration,
143
+ locals: Scope,
144
+ ) {
145
+ const target = locals.get(statement.name);
146
+ const path = `fn.${fn.name}.${statement.name}.assignment`;
147
+ if (!target) {
148
+ this.push("unknown-identifier", `Unknown identifier ${statement.name}`, path, statement.span, {
149
+ actual: statement.name,
150
+ repair: `Declare var ${statement.name}: <Type> before assigning to it.`,
151
+ });
152
+ this.typeOfExpression(statement.value, locals, `${path}.value`);
153
+ return;
154
+ }
155
+ if (!target.mutable) {
156
+ this.push("immutable-assignment", `Cannot assign to immutable value ${statement.name}`, path, statement.span, {
157
+ actual: statement.name,
158
+ repair: `Declare ${statement.name} with var if it needs to change.`,
159
+ });
160
+ }
161
+ if (statement.operator === "+=" && !isNumeric(String(target.type.name))) {
162
+ this.push("operator-type-mismatch", "+= requires a numeric target", path, statement.span, {
163
+ expected: "Int or Float target",
164
+ actual: formatType(target.type),
165
+ repair: "Use += only with Int or Float values.",
166
+ });
167
+ }
168
+ this.checkExpressionAssignable(statement.value, target.type, `${path}.value`, locals);
169
+ }
170
+
134
171
  private checkExpressionAssignable(
135
172
  expression: PointCoreExpression,
136
173
  expected: PointCoreTypeExpression,
137
174
  path: string,
138
- scope: Map<string, PointCoreTypeExpression>,
175
+ scope: Scope,
139
176
  ) {
140
177
  if (expression.kind === "list") {
141
178
  this.checkListAssignable(expression, expected, path, scope);
@@ -159,7 +196,7 @@ class CoreChecker {
159
196
  expression: Extract<PointCoreExpression, { kind: "list" }>,
160
197
  expected: PointCoreTypeExpression,
161
198
  path: string,
162
- scope: Map<string, PointCoreTypeExpression>,
199
+ scope: Scope,
163
200
  ) {
164
201
  if (expected.name !== "List" || expected.args.length !== 1) {
165
202
  this.push("type-mismatch", `Expected ${formatType(expected)}, got List`, path, expression.span, {
@@ -178,7 +215,7 @@ class CoreChecker {
178
215
  expression: Extract<PointCoreExpression, { kind: "record" }>,
179
216
  expected: PointCoreTypeExpression,
180
217
  path: string,
181
- scope: Map<string, PointCoreTypeExpression>,
218
+ scope: Scope,
182
219
  ) {
183
220
  const declaration = this.typeDeclarations.get(String(expected.name));
184
221
  if (!declaration) {
@@ -216,7 +253,7 @@ class CoreChecker {
216
253
 
217
254
  private typeOfExpression(
218
255
  expression: PointCoreExpression,
219
- scope: Map<string, PointCoreTypeExpression>,
256
+ scope: Scope,
220
257
  path: string,
221
258
  ): PointCoreTypeExpression | null {
222
259
  if (expression.kind === "literal") {
@@ -236,15 +273,15 @@ class CoreChecker {
236
273
  return null;
237
274
  }
238
275
  if (expression.kind === "identifier") {
239
- const type = scope.get(expression.name);
240
- if (!type) {
276
+ const entry = scope.get(expression.name);
277
+ if (!entry) {
241
278
  this.push("unknown-identifier", `Unknown identifier ${expression.name}`, path, expression.span, {
242
279
  actual: expression.name,
243
280
  repair: `Declare ${expression.name}, pass it as a parameter, or replace it with an in-scope symbol.`,
244
281
  });
245
282
  return null;
246
283
  }
247
- return type;
284
+ return entry.type;
248
285
  }
249
286
  if (expression.kind === "binary") {
250
287
  return this.typeOfBinaryExpression(expression, scope, path);
@@ -278,7 +315,7 @@ class CoreChecker {
278
315
 
279
316
  private typeOfListExpression(
280
317
  expression: Extract<PointCoreExpression, { kind: "list" }>,
281
- scope: Map<string, PointCoreTypeExpression>,
318
+ scope: Scope,
282
319
  path: string,
283
320
  ): PointCoreTypeExpression | null {
284
321
  if (expression.items.length === 0) {
@@ -298,7 +335,7 @@ class CoreChecker {
298
335
 
299
336
  private typeOfPropertyExpression(
300
337
  expression: Extract<PointCoreExpression, { kind: "property" }>,
301
- scope: Map<string, PointCoreTypeExpression>,
338
+ scope: Scope,
302
339
  path: string,
303
340
  ): PointCoreTypeExpression | null {
304
341
  const targetType = this.typeOfExpression(expression.target, scope, `${path}.target`);
@@ -326,7 +363,7 @@ class CoreChecker {
326
363
 
327
364
  private typeOfBinaryExpression(
328
365
  expression: Extract<PointCoreExpression, { kind: "binary" }>,
329
- scope: Map<string, PointCoreTypeExpression>,
366
+ scope: Scope,
330
367
  path: string,
331
368
  ): PointCoreTypeExpression | null {
332
369
  const left = this.typeOfExpression(expression.left, scope, `${path}.left`);
package/src/core/cli.ts CHANGED
@@ -3,7 +3,7 @@ import { checkPointCore } from "./check.ts";
3
3
  import { createPointCoreIndex, createPointCoreRepairPlan, explainPointCoreRef } from "./context.ts";
4
4
  import { emitPointCoreTypeScript } from "./emit-typescript.ts";
5
5
  import { formatPointCore } from "./format.ts";
6
- import { parsePointCore } from "./parser.ts";
6
+ import { isSemanticPointSyntax, parsePointCore } from "./parser.ts";
7
7
 
8
8
  const DEFAULT_INPUT = "examples/math.point";
9
9
  const DEFAULT_OUTPUT = "generated/math.ast.json";
@@ -24,12 +24,20 @@ export async function main() {
24
24
  const diagnostics = checkPointCore(program);
25
25
 
26
26
  if (command === "fmt") {
27
+ if (isSemanticPointSyntax(source)) {
28
+ console.log(`Point core fmt preserved semantic source: ${input}`);
29
+ return;
30
+ }
27
31
  await Bun.write(inputPath, formatPointCore(program));
28
32
  console.log(`Point core fmt wrote ${input}`);
29
33
  return;
30
34
  }
31
35
 
32
36
  if (command === "fmt-check") {
37
+ if (isSemanticPointSyntax(source)) {
38
+ console.log(`Point core fmt check passed: ${input}`);
39
+ return;
40
+ }
33
41
  const formatted = formatPointCore(program);
34
42
  if (source !== formatted) {
35
43
  console.error(`Point core fmt check failed: ${input}`);
@@ -111,7 +119,9 @@ async function runProjectCommand(command: string) {
111
119
  if (command === "fmt-all") {
112
120
  await Promise.all(
113
121
  results.map((result) =>
114
- Bun.write(resolve(process.cwd(), result.input), formatPointCore(result.program)),
122
+ isSemanticPointSyntax(result.source)
123
+ ? Promise.resolve()
124
+ : Bun.write(resolve(process.cwd(), result.input), formatPointCore(result.program)),
115
125
  ),
116
126
  );
117
127
  console.log(`Point core fmt wrote ${results.length} files`);
@@ -119,7 +129,9 @@ async function runProjectCommand(command: string) {
119
129
  }
120
130
 
121
131
  if (command === "fmt-check-all") {
122
- const unformatted = results.filter((result) => result.source !== formatPointCore(result.program));
132
+ const unformatted = results.filter(
133
+ (result) => !isSemanticPointSyntax(result.source) && result.source !== formatPointCore(result.program),
134
+ );
123
135
  if (unformatted.length > 0) {
124
136
  console.error(JSON.stringify({ ok: false, unformatted: unformatted.map((result) => result.input) }, null, 2));
125
137
  process.exit(1);
@@ -57,6 +57,7 @@ function emitStatement(statement: PointCoreStatement): string[] {
57
57
  return [statement.value ? `return ${emitExpression(statement.value)};` : "return;"];
58
58
  }
59
59
  if (statement.kind === "value") return [emitValue(statement, false)];
60
+ if (statement.kind === "assignment") return [`${statement.name} ${statement.operator} ${emitExpression(statement.value)};`];
60
61
  if (statement.kind === "if") {
61
62
  const lines = [
62
63
  `if (${emitCondition(statement.condition)}) {`,
@@ -51,6 +51,7 @@ function formatStatementLines(statement: PointCoreStatement): string[] {
51
51
  return [statement.value ? `return ${formatExpression(statement.value)}` : "return"];
52
52
  }
53
53
  if (statement.kind === "value") return [formatValue(statement)];
54
+ if (statement.kind === "assignment") return [`${statement.name} ${statement.operator} ${formatExpression(statement.value)}`];
54
55
  if (statement.kind === "if") return formatIf(statement);
55
56
  return [formatExpression(statement.value)];
56
57
  }
package/src/core/lexer.ts CHANGED
@@ -14,6 +14,7 @@ export type PointCoreTokenType =
14
14
  | "colon"
15
15
  | "dot"
16
16
  | "equals"
17
+ | "plusEquals"
17
18
  | "equalsEquals"
18
19
  | "bangEquals"
19
20
  | "less"
@@ -128,6 +129,10 @@ class CoreLexer {
128
129
  continue;
129
130
  }
130
131
  if (char === "+") {
132
+ if (this.peek(1) === "=") {
133
+ this.push("plusEquals", `${this.advance()}${this.advance()}`);
134
+ continue;
135
+ }
131
136
  this.push("plus", this.advance());
132
137
  continue;
133
138
  }
@@ -21,10 +21,241 @@ export class PointCoreParserError extends Error {
21
21
  }
22
22
 
23
23
  export function parsePointCore(source: string): PointCoreProgram {
24
- const parser = new CoreParser(lexPointCore(source));
24
+ const parser = new CoreParser(lexPointCore(lowerSemanticPointSyntax(source)));
25
25
  return parser.parseProgram();
26
26
  }
27
27
 
28
+ export function isSemanticPointSyntax(source: string): boolean {
29
+ return source
30
+ .split(/\r?\n/)
31
+ .some((line) => /^(record|rule|label)\s+/.test(line.trim()));
32
+ }
33
+
34
+ function lowerSemanticPointSyntax(source: string): string {
35
+ if (!isSemanticPointSyntax(source)) return source;
36
+ const lines = source.split(/\r?\n/);
37
+ const output: string[] = [];
38
+ const records = new Map<string, Map<string, string>>();
39
+ let index = 0;
40
+
41
+ while (index < lines.length) {
42
+ const line = lines[index] ?? "";
43
+ const trimmed = line.trim();
44
+ if (!trimmed) {
45
+ index += 1;
46
+ continue;
47
+ }
48
+ if (trimmed.startsWith("module ")) {
49
+ output.push(trimmed);
50
+ index += 1;
51
+ continue;
52
+ }
53
+ if (trimmed.startsWith("record ")) {
54
+ const lowered = lowerRecord(lines, index, records);
55
+ output.push(...lowered.lines);
56
+ index = lowered.next;
57
+ continue;
58
+ }
59
+ if (trimmed.startsWith("rule ")) {
60
+ const lowered = lowerRule(lines, index, records);
61
+ output.push(...lowered.lines);
62
+ index = lowered.next;
63
+ continue;
64
+ }
65
+ if (trimmed.startsWith("label ")) {
66
+ const lowered = lowerLabel(lines, index, records);
67
+ output.push(...lowered.lines);
68
+ index = lowered.next;
69
+ continue;
70
+ }
71
+ output.push(line);
72
+ index += 1;
73
+ }
74
+
75
+ return `${output.join("\n")}\n`;
76
+ }
77
+
78
+ function lowerRecord(
79
+ lines: string[],
80
+ start: number,
81
+ records: Map<string, Map<string, string>>,
82
+ ): { lines: string[]; next: number } {
83
+ const name = (lines[start] ?? "").trim().slice("record ".length).trim();
84
+ const fields = new Map<string, string>();
85
+ const output = [`type ${name} {`];
86
+ let index = start + 1;
87
+ for (; index < lines.length; index += 1) {
88
+ const trimmed = (lines[index] ?? "").trim();
89
+ if (!trimmed) continue;
90
+ if (isSemanticTopLevel(trimmed)) break;
91
+ const colon = trimmed.indexOf(":");
92
+ if (colon === -1) throw new Error(`Expected field type in record ${name}: ${trimmed}`);
93
+ const label = trimmed.slice(0, colon).trim();
94
+ const fieldName = toIdentifier(label);
95
+ fields.set(label, fieldName);
96
+ output.push(` ${fieldName}: ${trimmed.slice(colon + 1).trim()}`);
97
+ }
98
+ output.push("}", "");
99
+ records.set(name, fields);
100
+ return { lines: output, next: index };
101
+ }
102
+
103
+ function lowerRule(
104
+ lines: string[],
105
+ start: number,
106
+ records: Map<string, Map<string, string>>,
107
+ ): { lines: string[]; next: number } {
108
+ const label = (lines[start] ?? "").trim().slice("rule ".length).trim();
109
+ const body = collectSemanticBody(lines, start + 1);
110
+ const params: string[] = [];
111
+ const paramTypes = new Map<string, string>();
112
+ let outputName = "result";
113
+ let outputType = "Void";
114
+ const statements: string[] = [];
115
+
116
+ for (const line of body.lines) {
117
+ if (line.startsWith("input ")) {
118
+ const param = parseTypedBinding(line.slice("input ".length));
119
+ params.push(`${param.name}: ${param.type}`);
120
+ paramTypes.set(param.name, param.type);
121
+ continue;
122
+ }
123
+ if (line.startsWith("output ")) {
124
+ const output = parseOutputBinding(line.slice("output ".length));
125
+ outputName = output.name;
126
+ outputType = output.type;
127
+ continue;
128
+ }
129
+ const startsAt = line.match(/^([A-Za-z_][A-Za-z0-9_]*) starts at (.+)$/);
130
+ if (startsAt) {
131
+ statements.push(`var ${startsAt[1]}: ${outputType} = ${lowerExpression(startsAt[2] ?? "", paramTypes, records)}`);
132
+ continue;
133
+ }
134
+ const addWhen = line.match(/^add (.+) when (.+)$/);
135
+ if (addWhen) {
136
+ statements.push(`if ${lowerExpression(addWhen[2] ?? "", paramTypes, records)} {`);
137
+ statements.push(` ${outputName} += ${lowerExpression(addWhen[1] ?? "", paramTypes, records)}`);
138
+ statements.push("}");
139
+ continue;
140
+ }
141
+ if (line.startsWith("return ")) {
142
+ statements.push(`return ${lowerExpression(line.slice("return ".length), paramTypes, records)}`);
143
+ continue;
144
+ }
145
+ throw new Error(`Unknown rule statement: ${line}`);
146
+ }
147
+
148
+ const functionName = `${toIdentifier(label)}${toPascalCase(outputName)}`;
149
+ return {
150
+ lines: [`fn ${functionName}(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
151
+ next: body.next,
152
+ };
153
+ }
154
+
155
+ function lowerLabel(
156
+ lines: string[],
157
+ start: number,
158
+ records: Map<string, Map<string, string>>,
159
+ ): { lines: string[]; next: number } {
160
+ const label = (lines[start] ?? "").trim().slice("label ".length).trim();
161
+ const body = collectSemanticBody(lines, start + 1);
162
+ const params: string[] = [];
163
+ const paramTypes = new Map<string, string>();
164
+ let outputType = "Text";
165
+ const statements: string[] = [];
166
+
167
+ for (const line of body.lines) {
168
+ if (line.startsWith("input ")) {
169
+ const param = parseTypedBinding(line.slice("input ".length));
170
+ params.push(`${param.name}: ${param.type}`);
171
+ paramTypes.set(param.name, param.type);
172
+ continue;
173
+ }
174
+ if (line.startsWith("output ")) {
175
+ outputType = parseOutputBinding(line.slice("output ".length)).type;
176
+ continue;
177
+ }
178
+ const whenReturn = line.match(/^when (.+) return (.+)$/);
179
+ if (whenReturn) {
180
+ statements.push(`if ${lowerExpression(whenReturn[1] ?? "", paramTypes, records)} {`);
181
+ statements.push(` return ${lowerExpression(whenReturn[2] ?? "", paramTypes, records)}`);
182
+ statements.push("}");
183
+ continue;
184
+ }
185
+ if (line.startsWith("otherwise return ")) {
186
+ statements.push(`return ${lowerExpression(line.slice("otherwise return ".length), paramTypes, records)}`);
187
+ continue;
188
+ }
189
+ throw new Error(`Unknown label statement: ${line}`);
190
+ }
191
+
192
+ return {
193
+ lines: [`fn ${toIdentifier(label)}Label(${params.join(", ")}): ${outputType} {`, ...indentRaw(statements), "}", ""],
194
+ next: body.next,
195
+ };
196
+ }
197
+
198
+ function collectSemanticBody(lines: string[], start: number): { lines: string[]; next: number } {
199
+ const body: string[] = [];
200
+ let index = start;
201
+ for (; index < lines.length; index += 1) {
202
+ const trimmed = (lines[index] ?? "").trim();
203
+ if (!trimmed) continue;
204
+ if (isSemanticTopLevel(trimmed)) break;
205
+ body.push(trimmed);
206
+ }
207
+ return { lines: body, next: index };
208
+ }
209
+
210
+ function isSemanticTopLevel(line: string): boolean {
211
+ return /^(module|record|rule|label|type|fn|let|var|import)\s+/.test(line);
212
+ }
213
+
214
+ function parseTypedBinding(source: string): { name: string; type: string } {
215
+ const colon = source.indexOf(":");
216
+ if (colon === -1) throw new Error(`Expected typed binding: ${source}`);
217
+ return { name: source.slice(0, colon).trim(), type: source.slice(colon + 1).trim() };
218
+ }
219
+
220
+ function parseOutputBinding(source: string): { name: string; type: string } {
221
+ const colon = source.indexOf(":");
222
+ if (colon !== -1) return parseTypedBinding(source);
223
+ return { name: "result", type: source.trim() };
224
+ }
225
+
226
+ function lowerExpression(
227
+ source: string,
228
+ paramTypes: Map<string, string>,
229
+ records: Map<string, Map<string, string>>,
230
+ ): string {
231
+ let expression = source.trim();
232
+ for (const [param, type] of paramTypes) {
233
+ const fields = records.get(type);
234
+ if (!fields) continue;
235
+ const labels = [...fields.keys()].sort((a, b) => b.length - a.length);
236
+ for (const label of labels) {
237
+ const field = fields.get(label);
238
+ if (!field) continue;
239
+ expression = expression.replaceAll(`${param}.${label}`, `${param}.${field}`);
240
+ }
241
+ }
242
+ return expression;
243
+ }
244
+
245
+ function toIdentifier(label: string): string {
246
+ const words = label.match(/[A-Za-z0-9]+/g) ?? [];
247
+ return words.map((word, index) => (index === 0 ? word.toLowerCase() : toPascalCase(word))).join("");
248
+ }
249
+
250
+ function toPascalCase(label: string): string {
251
+ const words = label.match(/[A-Za-z0-9]+/g) ?? [];
252
+ return words.map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join("");
253
+ }
254
+
255
+ function indentRaw(lines: string[]): string[] {
256
+ return lines.map((line) => ` ${line}`);
257
+ }
258
+
28
259
  class CoreParser {
29
260
  private current = 0;
30
261
 
@@ -123,10 +354,25 @@ class CoreParser {
123
354
  }
124
355
  if (this.matchKeyword("if")) return this.parseIfStatement();
125
356
  if (this.checkKeyword("let") || this.checkKeyword("var")) return this.parseValueDeclaration();
357
+ if (this.check("identifier") && (this.checkNext("equals") || this.checkNext("plusEquals"))) return this.parseAssignment();
126
358
  const value = this.parseExpression();
127
359
  return { kind: "expression", value, span: value.span };
128
360
  }
129
361
 
362
+ private parseAssignment(): PointCoreStatement {
363
+ const name = this.consume("identifier", "Expected assignment target");
364
+ const operator = this.match("plusEquals") ? "+=" : "=";
365
+ if (operator === "=") this.consume("equals", "Expected assignment operator");
366
+ const value = this.parseExpression();
367
+ return {
368
+ kind: "assignment",
369
+ name: name.value,
370
+ operator,
371
+ value,
372
+ span: { start: name.span.start, end: value.span?.end ?? this.previous().span.end },
373
+ };
374
+ }
375
+
130
376
  private parseIfStatement(): PointCoreStatement {
131
377
  const start = this.previous().span.start;
132
378
  const condition = this.parseExpression();
@@ -334,13 +580,17 @@ class CoreParser {
334
580
  return this.peek().type === type;
335
581
  }
336
582
 
583
+ private checkNext(type: PointCoreTokenType) {
584
+ return this.peek(1).type === type;
585
+ }
586
+
337
587
  private advance() {
338
588
  if (!this.check("eof")) this.current += 1;
339
589
  return this.previous();
340
590
  }
341
591
 
342
- private peek() {
343
- return this.tokens[this.current] ?? this.tokens[this.tokens.length - 1]!;
592
+ private peek(offset = 0) {
593
+ return this.tokens[this.current + offset] ?? this.tokens[this.tokens.length - 1]!;
344
594
  }
345
595
 
346
596
  private previous() {