gofront 1.0.0 → 1.0.1
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 +1 -1
- package/package.json +1 -1
- package/src/cli-core.js +65 -0
- package/src/codegen/expressions.js +3 -0
- package/src/codegen/statements.js +3 -0
- package/src/codegen/stdlib/builder.js +3 -0
- package/src/codegen/stdlib/bytes.js +3 -0
- package/src/codegen/stdlib/errors.js +3 -0
- package/src/codegen/stdlib/fmt.js +3 -0
- package/src/codegen/stdlib/gom.js +3 -0
- package/src/codegen/stdlib/html.js +3 -0
- package/src/codegen/stdlib/index.js +3 -0
- package/src/codegen/stdlib/io.js +3 -0
- package/src/codegen/stdlib/maps.js +3 -0
- package/src/codegen/stdlib/math.js +3 -0
- package/src/codegen/stdlib/os.js +3 -0
- package/src/codegen/stdlib/path.js +3 -0
- package/src/codegen/stdlib/rand.js +3 -0
- package/src/codegen/stdlib/regexp.js +3 -0
- package/src/codegen/stdlib/slices.js +3 -0
- package/src/codegen/stdlib/sort.js +3 -0
- package/src/codegen/stdlib/strconv.js +3 -0
- package/src/codegen/stdlib/strings.js +3 -0
- package/src/codegen/stdlib/time.js +3 -0
- package/src/codegen/stdlib/unicode.js +3 -0
- package/src/codegen/stdlib/utf8.js +3 -0
- package/src/codegen/templ.js +3 -0
- package/src/compiler.js +63 -4
- package/src/index.js +32 -126
- package/src/parser/declarations.js +347 -0
- package/src/parser/index.js +3 -346
- package/src/typechecker/assignability.js +3 -0
- package/src/typechecker/expressions.js +9 -0
- package/src/typechecker/index.js +2 -1
- package/src/typechecker/resolve.js +9 -2
- package/src/typechecker/statements.js +3 -0
- package/src/typechecker/termination.js +3 -0
- package/src/typechecker/types.js +1 -0
package/src/index.js
CHANGED
|
@@ -13,27 +13,15 @@
|
|
|
13
13
|
// gofront <file.go> --tokens — dump tokens of first file (debug)
|
|
14
14
|
// gofront init [dir] — scaffold a new GoFront project
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
existsSync,
|
|
18
|
-
mkdirSync,
|
|
19
|
-
readFileSync,
|
|
20
|
-
statSync,
|
|
21
|
-
watch,
|
|
22
|
-
writeFileSync,
|
|
23
|
-
} from "node:fs";
|
|
24
16
|
import { createRequire } from "node:module";
|
|
25
17
|
|
|
26
18
|
const _require = createRequire(import.meta.url);
|
|
27
19
|
const { version } = _require("../package.json");
|
|
28
20
|
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
21
|
+
import { statSync, watch, writeFileSync } from "node:fs";
|
|
22
|
+
import { dirname, resolve } from "node:path";
|
|
23
|
+
import { handleInit, maybeMinify, runCompile } from "./cli-core.js";
|
|
32
24
|
import { createDevServer, liveReloadClient } from "./dev-server.js";
|
|
33
|
-
import { Lexer } from "./lexer.js";
|
|
34
|
-
import { minify } from "./minifier.js";
|
|
35
|
-
import { Parser } from "./parser/index.js";
|
|
36
|
-
import { TypeChecker } from "./typechecker/index.js";
|
|
37
25
|
|
|
38
26
|
// ── Parse CLI args ───────────────────────────────────────────
|
|
39
27
|
|
|
@@ -73,40 +61,13 @@ Usage:
|
|
|
73
61
|
if (args[0] === "init") {
|
|
74
62
|
const targetArg = args[1] ?? ".";
|
|
75
63
|
const targetDir = resolve(targetArg);
|
|
76
|
-
|
|
77
|
-
if (targetArg !== ".") {
|
|
78
|
-
try {
|
|
79
|
-
mkdirSync(targetDir, { recursive: true });
|
|
80
|
-
} catch (e) {
|
|
81
|
-
console.error(`gofront: cannot create '${targetArg}': ${e.message}`);
|
|
82
|
-
process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const mainPath = join(targetDir, "main.go");
|
|
87
|
-
if (existsSync(mainPath)) {
|
|
88
|
-
console.error(`gofront: ${mainPath} already exists — nothing written`);
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const pkgName =
|
|
93
|
-
targetArg === "." ? basename(resolve(".")) : basename(targetDir);
|
|
94
|
-
// package names must be valid identifiers
|
|
95
|
-
const safePkg = pkgName.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^[0-9]/, "_");
|
|
96
|
-
|
|
97
|
-
const template = `package ${safePkg}
|
|
98
|
-
|
|
99
|
-
func main() {
|
|
100
|
-
\tconsole.log("Hello from GoFront!")
|
|
101
|
-
}
|
|
102
|
-
`;
|
|
64
|
+
let mainPath;
|
|
103
65
|
try {
|
|
104
|
-
|
|
66
|
+
({ mainPath } = handleInit(targetDir));
|
|
105
67
|
} catch (e) {
|
|
106
|
-
console.error(`gofront:
|
|
68
|
+
console.error(`gofront: ${e.message}`);
|
|
107
69
|
process.exit(1);
|
|
108
70
|
}
|
|
109
|
-
|
|
110
71
|
console.error(`gofront: created ${mainPath}`);
|
|
111
72
|
console.error(
|
|
112
73
|
`gofront: run gofront ${targetArg === "." ? "main.go" : `${targetArg}/main.go`} to compile`,
|
|
@@ -139,95 +100,32 @@ try {
|
|
|
139
100
|
process.exit(1);
|
|
140
101
|
}
|
|
141
102
|
|
|
142
|
-
// ── Compile function (used for initial build and re-builds) ──
|
|
143
|
-
|
|
144
|
-
function runCompile() {
|
|
145
|
-
if (isDir) {
|
|
146
|
-
const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
|
|
147
|
-
return compileDir(inputPath, { sourceMap, outputDir });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Single-file mode
|
|
151
|
-
let source;
|
|
152
|
-
try {
|
|
153
|
-
source = readFileSync(inputPath, "utf8");
|
|
154
|
-
} catch (e) {
|
|
155
|
-
throw new Error(`cannot read '${inputArg}': ${e.message}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const tokens = new Lexer(source, basename(inputPath)).tokenize();
|
|
159
|
-
|
|
160
|
-
if (dumpTokens) {
|
|
161
|
-
for (const tok of tokens) console.log(tok.toString());
|
|
162
|
-
process.exit(0);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const ast = new Parser(tokens, basename(inputPath), source).parse();
|
|
166
|
-
ast._source = source;
|
|
167
|
-
|
|
168
|
-
if (dumpAst) {
|
|
169
|
-
console.log(JSON.stringify(ast, null, 2));
|
|
170
|
-
process.exit(0);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const checker = new TypeChecker();
|
|
174
|
-
const jsImports = new Map();
|
|
175
|
-
const bundledPackages = new Set();
|
|
176
|
-
const preambles = [];
|
|
177
|
-
|
|
178
|
-
resolveImports(
|
|
179
|
-
[ast],
|
|
180
|
-
inputPath,
|
|
181
|
-
checker,
|
|
182
|
-
jsImports,
|
|
183
|
-
bundledPackages,
|
|
184
|
-
preambles,
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const errors = checker.check(ast);
|
|
188
|
-
checker.reportUnusedImports();
|
|
189
|
-
if (errors.length > 0) {
|
|
190
|
-
throw new Error(errors.map((e) => e.message).join("\n"));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const cg = new CodeGen(checker, jsImports, bundledPackages);
|
|
194
|
-
let js = cg.generate(ast);
|
|
195
|
-
|
|
196
|
-
if (sourceMap) {
|
|
197
|
-
const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
|
|
198
|
-
const srcPath = relative(outputDir, inputPath);
|
|
199
|
-
const map = cg.getSourceMap(srcPath, [source]);
|
|
200
|
-
const b64 = Buffer.from(map).toString("base64");
|
|
201
|
-
js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const output = preambles.length > 0 ? `${preambles.join("\n")}\n${js}` : js;
|
|
205
|
-
return { js: output };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ── Minify helper ────────────────────────────────────────────
|
|
209
|
-
|
|
210
|
-
function maybeMinify(js) {
|
|
211
|
-
if (!minifyOutput) return js;
|
|
212
|
-
if (sourceMap) {
|
|
213
|
-
console.error("gofront: --source-map and --minify cannot be used together");
|
|
214
|
-
process.exit(1);
|
|
215
|
-
}
|
|
216
|
-
return minify(js, { mangle: mangleOutput });
|
|
217
|
-
}
|
|
218
|
-
|
|
219
103
|
// ── Single-shot mode ─────────────────────────────────────────
|
|
220
104
|
|
|
221
105
|
if (!watchMode) {
|
|
222
106
|
let result;
|
|
223
107
|
const startMs = performance.now();
|
|
224
108
|
try {
|
|
225
|
-
result = runCompile(
|
|
109
|
+
result = runCompile(inputPath, isDir, {
|
|
110
|
+
sourceMap,
|
|
111
|
+
outputFile,
|
|
112
|
+
dumpTokens,
|
|
113
|
+
dumpAst,
|
|
114
|
+
});
|
|
226
115
|
} catch (e) {
|
|
227
116
|
console.error(`gofront: ${e.message}`);
|
|
228
117
|
process.exit(1);
|
|
229
118
|
}
|
|
230
119
|
|
|
120
|
+
if (result.tokens) {
|
|
121
|
+
for (const tok of result.tokens) console.log(tok.toString());
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
if (result.ast) {
|
|
125
|
+
console.log(JSON.stringify(result.ast, null, 2));
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
231
129
|
if (checkOnly) {
|
|
232
130
|
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
233
131
|
console.error(`gofront: ${inputArg} — OK (${elapsedMs}ms)`);
|
|
@@ -236,7 +134,11 @@ if (!watchMode) {
|
|
|
236
134
|
|
|
237
135
|
let js;
|
|
238
136
|
try {
|
|
239
|
-
js = maybeMinify(result.js
|
|
137
|
+
js = maybeMinify(result.js, {
|
|
138
|
+
minify: minifyOutput,
|
|
139
|
+
mangle: mangleOutput,
|
|
140
|
+
sourceMap,
|
|
141
|
+
});
|
|
240
142
|
} catch (e) {
|
|
241
143
|
console.error(`gofront: minify failed: ${e.message}`);
|
|
242
144
|
process.exit(1);
|
|
@@ -278,8 +180,12 @@ if (serveMode) {
|
|
|
278
180
|
function buildOnce(changedFile = null) {
|
|
279
181
|
try {
|
|
280
182
|
const startMs = performance.now();
|
|
281
|
-
const result = runCompile();
|
|
282
|
-
let js = maybeMinify(result.js
|
|
183
|
+
const result = runCompile(inputPath, isDir, { sourceMap, outputFile });
|
|
184
|
+
let js = maybeMinify(result.js, {
|
|
185
|
+
minify: minifyOutput,
|
|
186
|
+
mangle: mangleOutput,
|
|
187
|
+
sourceMap,
|
|
188
|
+
});
|
|
283
189
|
if (serveMode) {
|
|
284
190
|
// Insert live reload client before the source map comment so it stays last
|
|
285
191
|
const smIdx = js.lastIndexOf("\n//# sourceMappingURL=");
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { T } from "../tokens.js";
|
|
2
|
+
|
|
3
|
+
export const declarationParseMethods = {
|
|
4
|
+
parseTopDecl() {
|
|
5
|
+
const t = this.peek();
|
|
6
|
+
switch (t.type) {
|
|
7
|
+
case T.ASYNC:
|
|
8
|
+
return this.parseAsyncFuncOrMethod();
|
|
9
|
+
case T.FUNC:
|
|
10
|
+
return this.parseFuncOrMethod();
|
|
11
|
+
case T.TYPE:
|
|
12
|
+
return this.parseTypeDecl();
|
|
13
|
+
case T.VAR:
|
|
14
|
+
return this.parseVarDecl();
|
|
15
|
+
case T.CONST:
|
|
16
|
+
return this.parseConstDecl();
|
|
17
|
+
default:
|
|
18
|
+
this.err("Expected top-level declaration");
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
parseFuncOrMethod() {
|
|
23
|
+
const _line = this.peek().line;
|
|
24
|
+
this.expect(T.FUNC);
|
|
25
|
+
// method: func (recv Type) Name(...)
|
|
26
|
+
if (this.check(T.LPAREN)) {
|
|
27
|
+
this.expect(T.LPAREN);
|
|
28
|
+
const recvName =
|
|
29
|
+
this.check(T.IDENT) && !this.isReceiverTerminator(this.peek2())
|
|
30
|
+
? this.advance().value
|
|
31
|
+
: "_";
|
|
32
|
+
this.match(T.STAR); // pointer receiver: (c *Counter) — strip *, treat same as value receiver
|
|
33
|
+
const recvType = this.parseTypeName();
|
|
34
|
+
this.expect(T.RPAREN);
|
|
35
|
+
const name = this.expect(T.IDENT).value;
|
|
36
|
+
const { params, returnType } = this.parseSignature();
|
|
37
|
+
const body = this.parseBlock();
|
|
38
|
+
this.semi();
|
|
39
|
+
return {
|
|
40
|
+
kind: "MethodDecl",
|
|
41
|
+
recvName,
|
|
42
|
+
recvType,
|
|
43
|
+
name,
|
|
44
|
+
params,
|
|
45
|
+
returnType,
|
|
46
|
+
body,
|
|
47
|
+
_line,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// plain function
|
|
51
|
+
const name = this.expect(T.IDENT).value;
|
|
52
|
+
// generic function: func Name[T any, U Stringer](...) ...
|
|
53
|
+
let typeParams = null;
|
|
54
|
+
if (this.check(T.LBRACKET) && this._looksLikeTypeParamList()) {
|
|
55
|
+
typeParams = this.parseTypeParamList();
|
|
56
|
+
}
|
|
57
|
+
const { params, returnType } = this.parseSignature();
|
|
58
|
+
const body = this.parseBlock();
|
|
59
|
+
this.semi();
|
|
60
|
+
return {
|
|
61
|
+
kind: "FuncDecl",
|
|
62
|
+
name,
|
|
63
|
+
typeParams,
|
|
64
|
+
params,
|
|
65
|
+
returnType,
|
|
66
|
+
body,
|
|
67
|
+
_line,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
parseAsyncFuncOrMethod() {
|
|
72
|
+
this.expect(T.ASYNC);
|
|
73
|
+
const decl = this.parseFuncOrMethod();
|
|
74
|
+
decl.async = true;
|
|
75
|
+
return decl;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// Returns true if tok is a receiver-list terminator (i.e. not a type name)
|
|
79
|
+
isReceiverTerminator(tok) {
|
|
80
|
+
return (
|
|
81
|
+
tok &&
|
|
82
|
+
(tok.type === T.RPAREN ||
|
|
83
|
+
tok.type === T.COMMA ||
|
|
84
|
+
tok.type === T.SEMICOLON ||
|
|
85
|
+
tok.type === T.EOF)
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// Lookahead: does the [ ... ] after a func name or type name look like a type param list?
|
|
90
|
+
// Type param lists contain IDENT followed by a constraint (another IDENT or keyword),
|
|
91
|
+
// not expressions like integers or operators.
|
|
92
|
+
_looksLikeTypeParamList() {
|
|
93
|
+
const i = this.pos + 1; // skip [
|
|
94
|
+
const src = this.tokens;
|
|
95
|
+
// First token must be an IDENT (the type param name)
|
|
96
|
+
if (!src[i] || src[i].type !== T.IDENT) return false;
|
|
97
|
+
// Second token should be a constraint (IDENT like 'any', 'comparable', another type name)
|
|
98
|
+
// or TILDE for union constraints
|
|
99
|
+
const second = src[i + 1];
|
|
100
|
+
if (!second) return false;
|
|
101
|
+
if (second.type === T.IDENT) return true; // T any, T Stringer
|
|
102
|
+
if (second.type === T.TILDE) return true; // T ~int
|
|
103
|
+
if (second.type === T.INTERFACE) return true; // T interface{...}
|
|
104
|
+
return false;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
parseSignature() {
|
|
108
|
+
const params = this.parseParamList();
|
|
109
|
+
const returnType = this.parseReturnType();
|
|
110
|
+
return { params, returnType };
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// Parses (a, b int, c string) style param list
|
|
114
|
+
parseParamList() {
|
|
115
|
+
this.expect(T.LPAREN);
|
|
116
|
+
const params = [];
|
|
117
|
+
if (!this.check(T.RPAREN)) {
|
|
118
|
+
do {
|
|
119
|
+
if (this.check(T.RPAREN)) break;
|
|
120
|
+
// Collect names, then type
|
|
121
|
+
const names = [];
|
|
122
|
+
if (this.check(T.IDENT) && this._isNamedParam()) {
|
|
123
|
+
// named: a, b int or ns ...int
|
|
124
|
+
names.push(this.advance().value);
|
|
125
|
+
while (
|
|
126
|
+
this.match(T.COMMA) &&
|
|
127
|
+
this.check(T.IDENT) &&
|
|
128
|
+
this._isNamedParam()
|
|
129
|
+
) {
|
|
130
|
+
names.push(this.advance().value);
|
|
131
|
+
}
|
|
132
|
+
// variadic: name ...T
|
|
133
|
+
const variadic = this.match(T.ELLIPSIS);
|
|
134
|
+
const type = this.parseType();
|
|
135
|
+
for (const n of names) params.push({ name: n, type, variadic });
|
|
136
|
+
} else {
|
|
137
|
+
// unnamed: just a type (possibly variadic: ...T)
|
|
138
|
+
const variadic = this.match(T.ELLIPSIS);
|
|
139
|
+
const type = this.parseType();
|
|
140
|
+
params.push({ name: "_", type, variadic });
|
|
141
|
+
}
|
|
142
|
+
} while (this.match(T.COMMA));
|
|
143
|
+
}
|
|
144
|
+
this.expect(T.RPAREN);
|
|
145
|
+
return params;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// Lookahead: determine if current IDENT starts a named param (name type)
|
|
149
|
+
// vs an unnamed param (just a type). Scans IDENT (COMMA IDENT)* and checks
|
|
150
|
+
// if a type-start token follows — if so, the IDENTs are names; if ) or ,
|
|
151
|
+
// follows, the IDENTs are themselves types.
|
|
152
|
+
_isNamedParam() {
|
|
153
|
+
let pos = this.pos + 1; // skip current IDENT
|
|
154
|
+
while (
|
|
155
|
+
this.tokens[pos]?.type === T.COMMA &&
|
|
156
|
+
this.tokens[pos + 1]?.type === T.IDENT
|
|
157
|
+
) {
|
|
158
|
+
pos += 2;
|
|
159
|
+
}
|
|
160
|
+
const next = this.tokens[pos];
|
|
161
|
+
if (!next) return false;
|
|
162
|
+
return this.looksLikeType(next) || next.type === T.ELLIPSIS;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
looksLikeType(tok) {
|
|
166
|
+
return (
|
|
167
|
+
tok.type === T.LBRACKET ||
|
|
168
|
+
tok.type === T.STAR ||
|
|
169
|
+
tok.type === T.MAP ||
|
|
170
|
+
tok.type === T.FUNC ||
|
|
171
|
+
tok.type === T.INTERFACE ||
|
|
172
|
+
tok.type === T.STRUCT ||
|
|
173
|
+
(tok.type === T.IDENT && tok.value !== "_")
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
parseReturnType(inTypeExpr = false) {
|
|
178
|
+
// No return type
|
|
179
|
+
if (
|
|
180
|
+
this.check(T.LBRACE) ||
|
|
181
|
+
this.check(T.RBRACE) ||
|
|
182
|
+
this.check(T.SEMICOLON) ||
|
|
183
|
+
this.check(T.EOF)
|
|
184
|
+
)
|
|
185
|
+
return null;
|
|
186
|
+
// Inside a type expression (e.g. func type in a tuple), `,` and `)` also end the type
|
|
187
|
+
if (inTypeExpr && (this.check(T.COMMA) || this.check(T.RPAREN)))
|
|
188
|
+
return null;
|
|
189
|
+
// Multiple return values: (int, string) or named (result int, ok bool)
|
|
190
|
+
if (this.check(T.LPAREN)) {
|
|
191
|
+
this.advance();
|
|
192
|
+
const types = [];
|
|
193
|
+
const names = []; // non-null if named returns
|
|
194
|
+
let isNamed = false;
|
|
195
|
+
do {
|
|
196
|
+
// Named returns: only treat IDENT as a name if peek2 is an actual type token
|
|
197
|
+
if (this.check(T.IDENT) && this.looksLikeType(this.peek2())) {
|
|
198
|
+
names.push(this.advance().value);
|
|
199
|
+
isNamed = true;
|
|
200
|
+
} else {
|
|
201
|
+
names.push(null);
|
|
202
|
+
}
|
|
203
|
+
types.push(this.parseType());
|
|
204
|
+
} while (this.match(T.COMMA));
|
|
205
|
+
this.expect(T.RPAREN);
|
|
206
|
+
const typeNode =
|
|
207
|
+
types.length === 1 ? types[0] : { kind: "TupleType", types };
|
|
208
|
+
// Attach named return info so typechecker can inject them into scope
|
|
209
|
+
if (isNamed)
|
|
210
|
+
typeNode._namedReturns = names.map((n, i) => ({
|
|
211
|
+
name: n,
|
|
212
|
+
type: types[i],
|
|
213
|
+
}));
|
|
214
|
+
return typeNode;
|
|
215
|
+
}
|
|
216
|
+
return this.parseType();
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
parseTypeDecl() {
|
|
220
|
+
this.expect(T.TYPE);
|
|
221
|
+
if (this.match(T.LPAREN)) {
|
|
222
|
+
const decls = [];
|
|
223
|
+
while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
|
|
224
|
+
const name = this.expect(T.IDENT).value;
|
|
225
|
+
const isAlias = this.match(T.ASSIGN);
|
|
226
|
+
const type = this.parseType();
|
|
227
|
+
decls.push({ kind: "TypeDecl", name, type, isAlias });
|
|
228
|
+
this.semi();
|
|
229
|
+
}
|
|
230
|
+
this.expect(T.RPAREN);
|
|
231
|
+
this.semi();
|
|
232
|
+
return decls;
|
|
233
|
+
}
|
|
234
|
+
const name = this.expect(T.IDENT).value;
|
|
235
|
+
// generic type: type Name[T any] struct { ... }
|
|
236
|
+
let typeParams = null;
|
|
237
|
+
if (this.check(T.LBRACKET) && this._looksLikeTypeParamList()) {
|
|
238
|
+
typeParams = this.parseTypeParamList();
|
|
239
|
+
}
|
|
240
|
+
const isAlias = this.match(T.ASSIGN);
|
|
241
|
+
const type = this.parseType();
|
|
242
|
+
this.semi();
|
|
243
|
+
return { kind: "TypeDecl", name, typeParams, type, isAlias };
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
parseVarDecl() {
|
|
247
|
+
this.expect(T.VAR);
|
|
248
|
+
const decls = [];
|
|
249
|
+
if (this.match(T.LPAREN)) {
|
|
250
|
+
while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
|
|
251
|
+
decls.push(this.parseVarSpec());
|
|
252
|
+
this.semi();
|
|
253
|
+
}
|
|
254
|
+
this.expect(T.RPAREN);
|
|
255
|
+
} else {
|
|
256
|
+
decls.push(this.parseVarSpec());
|
|
257
|
+
}
|
|
258
|
+
this.semi();
|
|
259
|
+
return { kind: "VarDecl", decls };
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
parseVarSpec() {
|
|
263
|
+
const names = [this.expect(T.IDENT).value];
|
|
264
|
+
while (this.match(T.COMMA)) names.push(this.expect(T.IDENT).value);
|
|
265
|
+
let type = null;
|
|
266
|
+
let value = null;
|
|
267
|
+
if (
|
|
268
|
+
!this.check(T.ASSIGN) &&
|
|
269
|
+
!this.check(T.SEMICOLON) &&
|
|
270
|
+
!this.check(T.RPAREN)
|
|
271
|
+
) {
|
|
272
|
+
type = this.parseType();
|
|
273
|
+
}
|
|
274
|
+
if (this.match(T.ASSIGN)) value = this.parseExprList();
|
|
275
|
+
return { names, type, value };
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
parseConstDecl() {
|
|
279
|
+
this.expect(T.CONST);
|
|
280
|
+
const decls = [];
|
|
281
|
+
if (this.match(T.LPAREN)) {
|
|
282
|
+
let iotaVal = 0;
|
|
283
|
+
let prevRawExprs = null; // raw AST (with iota Idents) from the previous spec
|
|
284
|
+
let prevType = null;
|
|
285
|
+
while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
|
|
286
|
+
const names = [this.expect(T.IDENT).value];
|
|
287
|
+
while (this.match(T.COMMA)) names.push(this.expect(T.IDENT).value);
|
|
288
|
+
let type = null;
|
|
289
|
+
let rawExprs; // before iota substitution
|
|
290
|
+
if (this.check(T.ASSIGN)) {
|
|
291
|
+
this.advance();
|
|
292
|
+
rawExprs = this.parseExprList();
|
|
293
|
+
prevRawExprs = rawExprs;
|
|
294
|
+
prevType = null;
|
|
295
|
+
} else if (!this.check(T.SEMICOLON) && !this.check(T.RPAREN)) {
|
|
296
|
+
// optional type annotation before =
|
|
297
|
+
type = this.parseType();
|
|
298
|
+
prevType = type;
|
|
299
|
+
this.expect(T.ASSIGN);
|
|
300
|
+
rawExprs = this.parseExprList();
|
|
301
|
+
prevRawExprs = rawExprs;
|
|
302
|
+
} else {
|
|
303
|
+
// Omitted expression — repeat previous expression with updated iota
|
|
304
|
+
rawExprs = prevRawExprs ?? [
|
|
305
|
+
{ kind: "BasicLit", litKind: "INT", value: String(iotaVal) },
|
|
306
|
+
];
|
|
307
|
+
type = prevType ?? null;
|
|
308
|
+
}
|
|
309
|
+
// Substitute iota deeply in a cloned expression AST
|
|
310
|
+
const value = rawExprs.map((v) => this._substituteIota(v, iotaVal));
|
|
311
|
+
decls.push({ names, type, value });
|
|
312
|
+
iotaVal++;
|
|
313
|
+
this.semi();
|
|
314
|
+
}
|
|
315
|
+
this.expect(T.RPAREN);
|
|
316
|
+
} else {
|
|
317
|
+
const names = [this.expect(T.IDENT).value];
|
|
318
|
+
while (this.match(T.COMMA)) names.push(this.expect(T.IDENT).value);
|
|
319
|
+
let type = null;
|
|
320
|
+
if (!this.check(T.ASSIGN)) type = this.parseType();
|
|
321
|
+
this.expect(T.ASSIGN);
|
|
322
|
+
const value = this.parseExprList();
|
|
323
|
+
decls.push({ names, type, value });
|
|
324
|
+
}
|
|
325
|
+
this.semi();
|
|
326
|
+
return { kind: "ConstDecl", decls };
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
// Deep-clone an expression AST, replacing `iota` identifiers with the given integer.
|
|
330
|
+
_substituteIota(node, val) {
|
|
331
|
+
if (!node || typeof node !== "object") return node;
|
|
332
|
+
if (node.kind === "Ident" && node.name === "iota") {
|
|
333
|
+
return { kind: "BasicLit", litKind: "INT", value: String(val) };
|
|
334
|
+
}
|
|
335
|
+
// Shallow-clone the node, then recursively substitute in all child properties
|
|
336
|
+
const clone = { ...node };
|
|
337
|
+
for (const key of Object.keys(clone)) {
|
|
338
|
+
const v = clone[key];
|
|
339
|
+
if (Array.isArray(v)) {
|
|
340
|
+
clone[key] = v.map((item) => this._substituteIota(item, val));
|
|
341
|
+
} else if (v && typeof v === "object" && v.kind) {
|
|
342
|
+
clone[key] = this._substituteIota(v, val);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return clone;
|
|
346
|
+
},
|
|
347
|
+
};
|