gofront 0.0.2 → 0.0.4
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/README.md +173 -52
- package/package.json +7 -3
- package/src/codegen/expressions.js +907 -0
- package/src/codegen/source-map.js +45 -0
- package/src/codegen/statements.js +464 -0
- package/src/codegen.js +43 -996
- package/src/compiler.js +36 -22
- package/src/index.js +3 -3
- package/src/lexer.js +94 -6
- package/src/parser/expressions.js +389 -0
- package/src/parser/statements.js +355 -0
- package/src/parser/types.js +140 -0
- package/src/parser.js +72 -863
- package/src/typechecker/expressions.js +447 -0
- package/src/typechecker/statements.js +287 -0
- package/src/typechecker/types.js +208 -0
- package/src/typechecker.js +528 -709
package/src/compiler.js
CHANGED
|
@@ -113,35 +113,49 @@ export function compileFiles(files, options = {}) {
|
|
|
113
113
|
|
|
114
114
|
// local GoFront packages (./subdir)
|
|
115
115
|
const seenLocalPaths = new Set();
|
|
116
|
-
for (const
|
|
117
|
-
for (const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
116
|
+
for (const p of programs) {
|
|
117
|
+
for (const imp of p.imports) {
|
|
118
|
+
for (const { path, alias, _line } of imp.imports) {
|
|
119
|
+
if (!isLocalPath(path) || seenLocalPaths.has(path)) continue;
|
|
120
|
+
seenLocalPaths.add(path);
|
|
121
|
+
|
|
122
|
+
const depDir = resolveGwDir(path, dummyFromFile);
|
|
123
|
+
if (!depDir) {
|
|
124
|
+
console.error(
|
|
125
|
+
`gofront: warning: cannot find local package '${path}' relative to ${fromDir}`,
|
|
126
|
+
);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Recursively compile dependency
|
|
131
|
+
const dep = compileDir(depDir);
|
|
132
|
+
preambles.push(dep.js);
|
|
133
|
+
if (alias === "_") continue; // side-effect import — bundle code but don't expose namespace
|
|
134
|
+
|
|
135
|
+
// Dot import: flatten all exported symbols into global scope
|
|
136
|
+
if (alias === ".") {
|
|
137
|
+
checker.addDefinitions(dep.exportedTypes, dep.exportedSymbols);
|
|
138
|
+
// All dep symbols must be treated as bundled so codegen
|
|
139
|
+
// can emit them without a qualifier. We DON'T add a
|
|
140
|
+
// namespace, so SelectorExpr never appears for these.
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const nameUsed = alias ?? dep.pkgName;
|
|
145
|
+
bundledPackages.add(nameUsed);
|
|
146
|
+
checker.addPackageNamespace(
|
|
147
|
+
nameUsed,
|
|
148
|
+
dep.exportedSymbols,
|
|
149
|
+
dep.exportedTypes,
|
|
125
150
|
);
|
|
126
|
-
|
|
151
|
+
checker.trackImport(nameUsed, { _line }, p._filename, p._source);
|
|
127
152
|
}
|
|
128
|
-
|
|
129
|
-
// Recursively compile dependency
|
|
130
|
-
const dep = compileDir(depDir);
|
|
131
|
-
preambles.push(dep.js);
|
|
132
|
-
if (alias === "_") continue; // side-effect import — bundle code but don't expose namespace
|
|
133
|
-
const nameUsed = alias ?? dep.pkgName;
|
|
134
|
-
bundledPackages.add(nameUsed);
|
|
135
|
-
checker.addPackageNamespace(
|
|
136
|
-
nameUsed,
|
|
137
|
-
dep.exportedSymbols,
|
|
138
|
-
dep.exportedTypes,
|
|
139
|
-
);
|
|
140
153
|
}
|
|
141
154
|
}
|
|
142
155
|
|
|
143
156
|
// ── 3. Type-check ─────────────────────────────────────────────
|
|
144
157
|
const errors = checker.checkAll(programs);
|
|
158
|
+
checker.reportUnusedImports();
|
|
145
159
|
if (errors.length > 0) {
|
|
146
160
|
const msgs = errors.map((e) => e.message).join("\n");
|
|
147
161
|
throw new Error(msgs);
|
package/src/index.js
CHANGED
|
@@ -290,7 +290,7 @@ function timestamp() {
|
|
|
290
290
|
return new Date().toLocaleTimeString();
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
async function buildOnce(
|
|
293
|
+
async function buildOnce() {
|
|
294
294
|
try {
|
|
295
295
|
const startMs = performance.now();
|
|
296
296
|
const result = runCompile();
|
|
@@ -314,7 +314,7 @@ async function buildOnce(_label) {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
// Initial build
|
|
317
|
-
buildOnce(
|
|
317
|
+
buildOnce();
|
|
318
318
|
|
|
319
319
|
// Determine what to watch
|
|
320
320
|
const watchTarget = isDir ? inputPath : dirname(inputPath);
|
|
@@ -323,7 +323,7 @@ let debounce = null;
|
|
|
323
323
|
watch(watchTarget, { recursive: true }, (_event, filename) => {
|
|
324
324
|
if (filename && !filename.endsWith(".go")) return;
|
|
325
325
|
clearTimeout(debounce);
|
|
326
|
-
debounce = setTimeout(() => buildOnce(
|
|
326
|
+
debounce = setTimeout(() => buildOnce(), 80);
|
|
327
327
|
});
|
|
328
328
|
|
|
329
329
|
console.error(`[${timestamp()}] gofront: watching ${inputArg} ...`);
|
package/src/lexer.js
CHANGED
|
@@ -35,6 +35,9 @@ export const T = {
|
|
|
35
35
|
DEFER: "defer",
|
|
36
36
|
ASYNC: "async",
|
|
37
37
|
AWAIT: "await",
|
|
38
|
+
GO: "go",
|
|
39
|
+
CHAN: "chan",
|
|
40
|
+
SELECT: "select",
|
|
38
41
|
|
|
39
42
|
// Operators
|
|
40
43
|
PLUS: "+",
|
|
@@ -68,6 +71,7 @@ export const T = {
|
|
|
68
71
|
AMP: "&",
|
|
69
72
|
PIPE: "|",
|
|
70
73
|
CARET: "^",
|
|
74
|
+
AND_NOT: "&^",
|
|
71
75
|
LSHIFT: "<<",
|
|
72
76
|
RSHIFT: ">>",
|
|
73
77
|
ELLIPSIS: "...",
|
|
@@ -136,6 +140,9 @@ const KEYWORDS = new Set([
|
|
|
136
140
|
"defer",
|
|
137
141
|
"async",
|
|
138
142
|
"await",
|
|
143
|
+
"go",
|
|
144
|
+
"chan",
|
|
145
|
+
"select",
|
|
139
146
|
]);
|
|
140
147
|
|
|
141
148
|
export class Token {
|
|
@@ -217,16 +224,28 @@ export class Lexer {
|
|
|
217
224
|
while (this.pos < this.src.length && this.peek() !== "\n")
|
|
218
225
|
this.advance();
|
|
219
226
|
} else if (ch === "/" && this.peek(1) === "*") {
|
|
227
|
+
const startLine = this.line,
|
|
228
|
+
startCol = this.col;
|
|
220
229
|
this.advance();
|
|
221
230
|
this.advance();
|
|
231
|
+
let closed = false;
|
|
222
232
|
while (this.pos < this.src.length) {
|
|
223
233
|
if (this.peek() === "*" && this.peek(1) === "/") {
|
|
224
234
|
this.advance();
|
|
225
235
|
this.advance();
|
|
236
|
+
closed = true;
|
|
226
237
|
break;
|
|
227
238
|
}
|
|
228
239
|
this.advance();
|
|
229
240
|
}
|
|
241
|
+
if (!closed)
|
|
242
|
+
throw new LexError(
|
|
243
|
+
"Unterminated block comment",
|
|
244
|
+
startLine,
|
|
245
|
+
startCol,
|
|
246
|
+
this.filename,
|
|
247
|
+
this.src,
|
|
248
|
+
);
|
|
230
249
|
} else {
|
|
231
250
|
break;
|
|
232
251
|
}
|
|
@@ -343,20 +362,88 @@ export class Lexer {
|
|
|
343
362
|
readNumber() {
|
|
344
363
|
let n = "";
|
|
345
364
|
let isFloat = false;
|
|
346
|
-
|
|
347
|
-
|
|
365
|
+
|
|
366
|
+
// Check for prefix: 0b, 0o, 0x
|
|
367
|
+
const first = this.advance();
|
|
368
|
+
n += first;
|
|
369
|
+
if (first === "0") {
|
|
370
|
+
const p = this.peek();
|
|
371
|
+
if (p === "b" || p === "B") {
|
|
372
|
+
n += this.advance();
|
|
373
|
+
while (this.pos < this.src.length && /[01_]/.test(this.peek())) {
|
|
374
|
+
const ch = this.advance();
|
|
375
|
+
if (ch !== "_") n += ch;
|
|
376
|
+
}
|
|
377
|
+
return { n, isFloat: false };
|
|
378
|
+
}
|
|
379
|
+
if (p === "o" || p === "O") {
|
|
380
|
+
n += this.advance();
|
|
381
|
+
while (this.pos < this.src.length && /[0-7_]/.test(this.peek())) {
|
|
382
|
+
const ch = this.advance();
|
|
383
|
+
if (ch !== "_") n += ch;
|
|
384
|
+
}
|
|
385
|
+
return { n, isFloat: false };
|
|
386
|
+
}
|
|
387
|
+
if (p === "x" || p === "X") {
|
|
388
|
+
n += this.advance();
|
|
389
|
+
while (this.pos < this.src.length && /[0-9a-fA-F_]/.test(this.peek())) {
|
|
390
|
+
const ch = this.advance();
|
|
391
|
+
if (ch !== "_") n += ch;
|
|
392
|
+
}
|
|
393
|
+
// Hex float: 0x1.Fp10 or 0xAp-2
|
|
394
|
+
if (this.peek() === "." || this.peek() === "p" || this.peek() === "P") {
|
|
395
|
+
let frac = "";
|
|
396
|
+
if (this.peek() === ".") {
|
|
397
|
+
this.advance(); // skip the dot (don't add to n)
|
|
398
|
+
while (
|
|
399
|
+
this.pos < this.src.length &&
|
|
400
|
+
/[0-9a-fA-F_]/.test(this.peek())
|
|
401
|
+
) {
|
|
402
|
+
const ch = this.advance();
|
|
403
|
+
if (ch !== "_") frac += ch;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (this.peek() === "p" || this.peek() === "P") {
|
|
407
|
+
this.advance(); // skip p/P
|
|
408
|
+
let exp = "";
|
|
409
|
+
if (this.peek() === "+" || this.peek() === "-")
|
|
410
|
+
exp += this.advance();
|
|
411
|
+
while (this.pos < this.src.length && /[0-9]/.test(this.peek()))
|
|
412
|
+
exp += this.advance();
|
|
413
|
+
// Evaluate: JS can't parse hex floats, so convert manually
|
|
414
|
+
// n is "0x1" (integer hex part), frac is hex fractional digits
|
|
415
|
+
let mantissa = Number(n);
|
|
416
|
+
if (frac) {
|
|
417
|
+
mantissa += Number.parseInt(frac, 16) / 16 ** frac.length;
|
|
418
|
+
}
|
|
419
|
+
const value = mantissa * 2 ** Number(exp);
|
|
420
|
+
return { n: String(value), isFloat: true };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return { n, isFloat: false };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
while (this.pos < this.src.length && /[0-9_]/.test(this.peek())) {
|
|
428
|
+
const ch = this.advance();
|
|
429
|
+
if (ch !== "_") n += ch;
|
|
430
|
+
}
|
|
348
431
|
if (this.peek() === "." && /[0-9]/.test(this.peek(1))) {
|
|
349
432
|
isFloat = true;
|
|
350
433
|
n += this.advance();
|
|
351
|
-
while (this.pos < this.src.length && /[0-
|
|
352
|
-
|
|
434
|
+
while (this.pos < this.src.length && /[0-9_]/.test(this.peek())) {
|
|
435
|
+
const ch = this.advance();
|
|
436
|
+
if (ch !== "_") n += ch;
|
|
437
|
+
}
|
|
353
438
|
}
|
|
354
439
|
if (this.peek() === "e" || this.peek() === "E") {
|
|
355
440
|
isFloat = true;
|
|
356
441
|
n += this.advance();
|
|
357
442
|
if (this.peek() === "+" || this.peek() === "-") n += this.advance();
|
|
358
|
-
while (this.pos < this.src.length && /[0-
|
|
359
|
-
|
|
443
|
+
while (this.pos < this.src.length && /[0-9_]/.test(this.peek())) {
|
|
444
|
+
const ch = this.advance();
|
|
445
|
+
if (ch !== "_") n += ch;
|
|
446
|
+
}
|
|
360
447
|
}
|
|
361
448
|
return { n, isFloat };
|
|
362
449
|
}
|
|
@@ -474,6 +561,7 @@ export class Lexer {
|
|
|
474
561
|
break;
|
|
475
562
|
case "&":
|
|
476
563
|
if (this.match("&")) this.push(T.AND, "&&", l, c);
|
|
564
|
+
else if (this.match("^")) this.push(T.AND_NOT, "&^", l, c);
|
|
477
565
|
else if (this.match("=")) this.push(T.AMP_ASSIGN, "&=", l, c);
|
|
478
566
|
else this.push(T.AMP, "&", l, c);
|
|
479
567
|
break;
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// Parser expression methods — installed as a mixin on Parser.prototype.
|
|
2
|
+
|
|
3
|
+
import { T } from "../lexer.js";
|
|
4
|
+
|
|
5
|
+
// Binary operator precedence (higher = binds tighter, matching Go spec)
|
|
6
|
+
const PREC = {
|
|
7
|
+
[T.OR]: 1,
|
|
8
|
+
[T.AND]: 2,
|
|
9
|
+
[T.EQ]: 3,
|
|
10
|
+
[T.NEQ]: 3,
|
|
11
|
+
[T.LT]: 3,
|
|
12
|
+
[T.GT]: 3,
|
|
13
|
+
[T.LTE]: 3,
|
|
14
|
+
[T.GTE]: 3,
|
|
15
|
+
[T.PLUS]: 4,
|
|
16
|
+
[T.MINUS]: 4,
|
|
17
|
+
[T.PIPE]: 4,
|
|
18
|
+
[T.CARET]: 4,
|
|
19
|
+
[T.STAR]: 5,
|
|
20
|
+
[T.SLASH]: 5,
|
|
21
|
+
[T.PERCENT]: 5,
|
|
22
|
+
[T.LSHIFT]: 5,
|
|
23
|
+
[T.RSHIFT]: 5,
|
|
24
|
+
[T.AMP]: 5,
|
|
25
|
+
[T.AND_NOT]: 5,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const TYPE_KEYWORDS = new Set([
|
|
29
|
+
"int",
|
|
30
|
+
"float64",
|
|
31
|
+
"string",
|
|
32
|
+
"bool",
|
|
33
|
+
"any",
|
|
34
|
+
"byte",
|
|
35
|
+
"rune",
|
|
36
|
+
"error",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const BUILTIN_KEYWORDS = new Set([
|
|
40
|
+
"new",
|
|
41
|
+
"make",
|
|
42
|
+
"len",
|
|
43
|
+
"cap",
|
|
44
|
+
"append",
|
|
45
|
+
"delete",
|
|
46
|
+
"copy",
|
|
47
|
+
"print",
|
|
48
|
+
"println",
|
|
49
|
+
"panic",
|
|
50
|
+
"error",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
export const expressionParserMethods = {
|
|
54
|
+
parseExprList() {
|
|
55
|
+
const exprs = [this.parseExpr()];
|
|
56
|
+
while (this.match(T.COMMA)) exprs.push(this.parseExpr());
|
|
57
|
+
return exprs;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// noCompLit=true disables composite literal parsing in if/for/switch conditions
|
|
61
|
+
// to resolve the ambiguity between "if x {" and "if Type{ } {"
|
|
62
|
+
parseExpr(minPrec = 0, noCompLit = false) {
|
|
63
|
+
let left = this.parseUnary(noCompLit);
|
|
64
|
+
while (true) {
|
|
65
|
+
const prec = PREC[this.peek().type];
|
|
66
|
+
if (!prec || prec <= minPrec) break;
|
|
67
|
+
const op = this.advance().value;
|
|
68
|
+
const right = this.parseExpr(prec, noCompLit);
|
|
69
|
+
left = { kind: "BinaryExpr", left, op, right };
|
|
70
|
+
}
|
|
71
|
+
return left;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
parseUnary(noCompLit = false) {
|
|
75
|
+
const t = this.peek();
|
|
76
|
+
if (
|
|
77
|
+
t.type === T.NOT ||
|
|
78
|
+
t.type === T.MINUS ||
|
|
79
|
+
t.type === T.PLUS ||
|
|
80
|
+
t.type === T.CARET ||
|
|
81
|
+
t.type === T.AMP ||
|
|
82
|
+
t.type === T.STAR
|
|
83
|
+
) {
|
|
84
|
+
const op = this.advance().value;
|
|
85
|
+
return { kind: "UnaryExpr", op, operand: this.parseUnary(noCompLit) };
|
|
86
|
+
}
|
|
87
|
+
if (t.type === T.AWAIT) {
|
|
88
|
+
this.advance();
|
|
89
|
+
return { kind: "AwaitExpr", expr: this.parseUnary(noCompLit) };
|
|
90
|
+
}
|
|
91
|
+
return this.parsePostfix(noCompLit);
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
parsePostfix(noCompLit = false) {
|
|
95
|
+
let expr = this.parsePrimary();
|
|
96
|
+
while (true) {
|
|
97
|
+
const t = this.peek();
|
|
98
|
+
if (t.type === T.DOT) {
|
|
99
|
+
this.advance();
|
|
100
|
+
// type assertion: expr.(Type)
|
|
101
|
+
if (this.check(T.LPAREN)) {
|
|
102
|
+
this.advance();
|
|
103
|
+
// x.(type) — type switch guard expression
|
|
104
|
+
if (this.check(T.TYPE)) {
|
|
105
|
+
this.advance();
|
|
106
|
+
this.expect(T.RPAREN);
|
|
107
|
+
expr = { kind: "TypeSwitchExpr", expr };
|
|
108
|
+
} else {
|
|
109
|
+
const type = this.parseType();
|
|
110
|
+
this.expect(T.RPAREN);
|
|
111
|
+
expr = { kind: "TypeAssertExpr", expr, type };
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
const field = this.advance().value;
|
|
115
|
+
expr = { kind: "SelectorExpr", expr, field };
|
|
116
|
+
}
|
|
117
|
+
} else if (t.type === T.LBRACKET) {
|
|
118
|
+
this.advance();
|
|
119
|
+
const low = this.check(T.COLON) ? null : this.parseExpr();
|
|
120
|
+
if (this.match(T.COLON)) {
|
|
121
|
+
const high =
|
|
122
|
+
this.check(T.RBRACKET) || this.check(T.COLON)
|
|
123
|
+
? null
|
|
124
|
+
: this.parseExpr();
|
|
125
|
+
let max = null;
|
|
126
|
+
if (this.match(T.COLON)) {
|
|
127
|
+
max = this.parseExpr();
|
|
128
|
+
}
|
|
129
|
+
this.expect(T.RBRACKET);
|
|
130
|
+
expr = { kind: "SliceExpr", expr, low, high, max };
|
|
131
|
+
} else {
|
|
132
|
+
this.expect(T.RBRACKET);
|
|
133
|
+
expr = { kind: "IndexExpr", expr, index: low };
|
|
134
|
+
}
|
|
135
|
+
} else if (t.type === T.LPAREN) {
|
|
136
|
+
expr = this.parseCall(expr);
|
|
137
|
+
} else if (
|
|
138
|
+
t.type === T.LBRACE &&
|
|
139
|
+
!noCompLit &&
|
|
140
|
+
this.isCompositeLitContext(expr)
|
|
141
|
+
) {
|
|
142
|
+
expr = this.parseCompositeLit(expr);
|
|
143
|
+
} else {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return expr;
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// Composite literals are only allowed after a type name (not arbitrary exprs)
|
|
151
|
+
isCompositeLitContext(expr) {
|
|
152
|
+
return (
|
|
153
|
+
expr.kind === "Ident" ||
|
|
154
|
+
expr.kind === "SelectorExpr" ||
|
|
155
|
+
expr.kind === "TypeExpr" ||
|
|
156
|
+
expr.kind === "StructType"
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
parseCall(fn) {
|
|
161
|
+
this.expect(T.LPAREN);
|
|
162
|
+
const args = [];
|
|
163
|
+
if (!this.check(T.RPAREN)) {
|
|
164
|
+
do {
|
|
165
|
+
if (this.check(T.RPAREN)) break;
|
|
166
|
+
// map[K]V and []T as call args must be parsed as types, not expressions,
|
|
167
|
+
// when they are NOT followed by { (which would make them composite literals).
|
|
168
|
+
// Needed for make(map[K]V), make([]T, n), new(T), etc.
|
|
169
|
+
const t = this.peek();
|
|
170
|
+
const isSliceOrArrayType =
|
|
171
|
+
t.type === T.LBRACKET &&
|
|
172
|
+
(this.peek2()?.type === T.RBRACKET || // []T
|
|
173
|
+
this.peek2()?.type === T.INT); // [n]T
|
|
174
|
+
if (
|
|
175
|
+
(t.type === T.MAP || isSliceOrArrayType) &&
|
|
176
|
+
!this.looksLikeCompositeLitArg()
|
|
177
|
+
) {
|
|
178
|
+
const typeNode = this.parseType();
|
|
179
|
+
// []byte(s) / []rune(s) used as a call argument — it's a type conversion
|
|
180
|
+
if (this.check(T.LPAREN)) {
|
|
181
|
+
this.advance();
|
|
182
|
+
const convExpr = this.parseExpr();
|
|
183
|
+
this.expect(T.RPAREN);
|
|
184
|
+
args.push({
|
|
185
|
+
kind: "TypeConversion",
|
|
186
|
+
targetType: typeNode,
|
|
187
|
+
expr: convExpr,
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
args.push({ kind: "TypeExpr", type: typeNode });
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
const arg = this.parseExpr();
|
|
194
|
+
if (this.match(T.ELLIPSIS)) arg._spread = true;
|
|
195
|
+
args.push(arg);
|
|
196
|
+
}
|
|
197
|
+
} while (this.match(T.COMMA));
|
|
198
|
+
}
|
|
199
|
+
this.expect(T.RPAREN);
|
|
200
|
+
return { kind: "CallExpr", func: fn, args };
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// Returns true if the current position looks like a composite literal argument,
|
|
204
|
+
// e.g. []int{1,2,3} or map[string]int{"a":1}. We scan ahead past the type
|
|
205
|
+
// tokens to check whether a { follows, without advancing this.pos permanently.
|
|
206
|
+
looksLikeCompositeLitArg() {
|
|
207
|
+
let i = this.pos;
|
|
208
|
+
const src = this.tokens;
|
|
209
|
+
// Skip over the type tokens: [, ], map, [, K, ], V, ident, etc.
|
|
210
|
+
// Simple approach: skip until we hit something that can't be part of a type
|
|
211
|
+
let depth = 0;
|
|
212
|
+
while (i < src.length) {
|
|
213
|
+
const tt = src[i].type;
|
|
214
|
+
if (tt === T.LBRACKET) {
|
|
215
|
+
depth++;
|
|
216
|
+
i++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (tt === T.RBRACKET) {
|
|
220
|
+
depth--;
|
|
221
|
+
i++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (depth > 0) {
|
|
225
|
+
i++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (
|
|
229
|
+
tt === T.MAP ||
|
|
230
|
+
tt === T.IDENT ||
|
|
231
|
+
tt === T.STAR ||
|
|
232
|
+
tt === T.INT ||
|
|
233
|
+
tt === T.FLOAT ||
|
|
234
|
+
tt === T.STRING
|
|
235
|
+
) {
|
|
236
|
+
i++;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
// Skip whitespace tokens (shouldn't exist post-lex, but be safe)
|
|
242
|
+
while (i < src.length && src[i].type === T.SEMICOLON) i++;
|
|
243
|
+
return i < src.length && src[i].type === T.LBRACE;
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
parseCompositeLit(typeExpr) {
|
|
247
|
+
this.expect(T.LBRACE);
|
|
248
|
+
const elems = [];
|
|
249
|
+
while (!this.check(T.RBRACE) && !this.check(T.EOF)) {
|
|
250
|
+
// Implicit composite literal: {X: 1} inside []Point{{X:1}} or map[string]Point{"a": {X:1}}
|
|
251
|
+
if (this.check(T.LBRACE)) {
|
|
252
|
+
elems.push(this.parseCompositeLit(null)); // null typeExpr = inferred
|
|
253
|
+
} else {
|
|
254
|
+
const first = this.parseExpr();
|
|
255
|
+
if (this.match(T.COLON)) {
|
|
256
|
+
// Map key: value — value may also be an implicit composite lit
|
|
257
|
+
const val = this.check(T.LBRACE)
|
|
258
|
+
? this.parseCompositeLit(null)
|
|
259
|
+
: this.parseExpr();
|
|
260
|
+
elems.push({ kind: "KeyValueExpr", key: first, value: val });
|
|
261
|
+
} else {
|
|
262
|
+
elems.push(first);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
this.match(T.COMMA);
|
|
266
|
+
}
|
|
267
|
+
this.expect(T.RBRACE);
|
|
268
|
+
return { kind: "CompositeLit", typeExpr, elems };
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
parsePrimary() {
|
|
272
|
+
const line = this.peek().line;
|
|
273
|
+
const expr = this._parsePrimary();
|
|
274
|
+
if (!expr._line) expr._line = line;
|
|
275
|
+
return expr;
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
_parsePrimary() {
|
|
279
|
+
const t = this.peek();
|
|
280
|
+
|
|
281
|
+
// Literals
|
|
282
|
+
if (t.type === T.INT) {
|
|
283
|
+
this.advance();
|
|
284
|
+
return { kind: "BasicLit", litKind: "INT", value: t.value };
|
|
285
|
+
}
|
|
286
|
+
if (t.type === T.FLOAT) {
|
|
287
|
+
this.advance();
|
|
288
|
+
return { kind: "BasicLit", litKind: "FLOAT", value: t.value };
|
|
289
|
+
}
|
|
290
|
+
if (t.type === T.STRING) {
|
|
291
|
+
this.advance();
|
|
292
|
+
return { kind: "BasicLit", litKind: "STRING", value: t.value };
|
|
293
|
+
}
|
|
294
|
+
if (t.type === T.TRUE) {
|
|
295
|
+
this.advance();
|
|
296
|
+
return { kind: "BasicLit", litKind: "BOOL", value: "true" };
|
|
297
|
+
}
|
|
298
|
+
if (t.type === T.FALSE) {
|
|
299
|
+
this.advance();
|
|
300
|
+
return { kind: "BasicLit", litKind: "BOOL", value: "false" };
|
|
301
|
+
}
|
|
302
|
+
if (t.type === T.NIL) {
|
|
303
|
+
this.advance();
|
|
304
|
+
return { kind: "BasicLit", litKind: "NIL", value: "null" };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Parenthesised expression
|
|
308
|
+
if (t.type === T.LPAREN) {
|
|
309
|
+
this.advance();
|
|
310
|
+
const expr = this.parseExpr();
|
|
311
|
+
this.expect(T.RPAREN);
|
|
312
|
+
return expr;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Function literal (sync or async)
|
|
316
|
+
if (t.type === T.FUNC || t.type === T.ASYNC) {
|
|
317
|
+
const isAsync = t.type === T.ASYNC;
|
|
318
|
+
this.advance();
|
|
319
|
+
if (isAsync) this.expect(T.FUNC);
|
|
320
|
+
const params = this.parseParamList();
|
|
321
|
+
const returnType = this.parseReturnType();
|
|
322
|
+
const body = this.parseBlock();
|
|
323
|
+
return { kind: "FuncLit", params, returnType, body, async: isAsync };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// range expression (only valid inside a for statement)
|
|
327
|
+
if (t.type === T.RANGE) {
|
|
328
|
+
this.advance();
|
|
329
|
+
// Always use noCompLit here: "range slice {" — { is the for body, not a composite lit
|
|
330
|
+
return { kind: "RangeExpr", expr: this.parseExpr(0, true) };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Builtin / identifier
|
|
334
|
+
if (
|
|
335
|
+
t.type === T.IDENT ||
|
|
336
|
+
this.isTypeKeyword(t) ||
|
|
337
|
+
this.isBuiltinKeyword(t)
|
|
338
|
+
) {
|
|
339
|
+
this.advance();
|
|
340
|
+
// Type conversion or composite lit: Type(expr) or Type{...}
|
|
341
|
+
if (this.isTypeKeyword(t) && this.check(T.LPAREN)) {
|
|
342
|
+
// type conversion: int(x), string(x), etc.
|
|
343
|
+
this.advance();
|
|
344
|
+
const expr = this.parseExpr();
|
|
345
|
+
this.expect(T.RPAREN);
|
|
346
|
+
return {
|
|
347
|
+
kind: "TypeConversion",
|
|
348
|
+
targetType: { kind: "TypeName", name: t.value },
|
|
349
|
+
expr,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return { kind: "Ident", name: t.value };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Slice/array/map composite literals or type conversions:
|
|
356
|
+
// []int{1,2} → CompositeLit
|
|
357
|
+
// []byte(s), []rune(s) → TypeConversion
|
|
358
|
+
if (t.type === T.LBRACKET || t.type === T.MAP) {
|
|
359
|
+
const typeExpr = this.parseType();
|
|
360
|
+
if (this.check(T.LPAREN)) {
|
|
361
|
+
this.advance();
|
|
362
|
+
const expr = this.parseExpr();
|
|
363
|
+
this.expect(T.RPAREN);
|
|
364
|
+
return { kind: "TypeConversion", targetType: typeExpr, expr };
|
|
365
|
+
}
|
|
366
|
+
return this.parseCompositeLit(typeExpr);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Anonymous struct composite literal: struct{Name string}{Name: "Alice"}
|
|
370
|
+
if (t.type === T.STRUCT) {
|
|
371
|
+
const typeExpr = this.parseStructType();
|
|
372
|
+
return this.parseCompositeLit(typeExpr);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (t.type === T.CHAN) {
|
|
376
|
+
this.err("channels are not supported in GoFront");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.err(`Unexpected token in expression: '${t.value}'`);
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
isTypeKeyword(t) {
|
|
383
|
+
return TYPE_KEYWORDS.has(t.value);
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
isBuiltinKeyword(t) {
|
|
387
|
+
return BUILTIN_KEYWORDS.has(t.value);
|
|
388
|
+
},
|
|
389
|
+
};
|