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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/cli-core.js +65 -0
  4. package/src/codegen/expressions.js +3 -0
  5. package/src/codegen/statements.js +3 -0
  6. package/src/codegen/stdlib/builder.js +3 -0
  7. package/src/codegen/stdlib/bytes.js +3 -0
  8. package/src/codegen/stdlib/errors.js +3 -0
  9. package/src/codegen/stdlib/fmt.js +3 -0
  10. package/src/codegen/stdlib/gom.js +3 -0
  11. package/src/codegen/stdlib/html.js +3 -0
  12. package/src/codegen/stdlib/index.js +3 -0
  13. package/src/codegen/stdlib/io.js +3 -0
  14. package/src/codegen/stdlib/maps.js +3 -0
  15. package/src/codegen/stdlib/math.js +3 -0
  16. package/src/codegen/stdlib/os.js +3 -0
  17. package/src/codegen/stdlib/path.js +3 -0
  18. package/src/codegen/stdlib/rand.js +3 -0
  19. package/src/codegen/stdlib/regexp.js +3 -0
  20. package/src/codegen/stdlib/slices.js +3 -0
  21. package/src/codegen/stdlib/sort.js +3 -0
  22. package/src/codegen/stdlib/strconv.js +3 -0
  23. package/src/codegen/stdlib/strings.js +3 -0
  24. package/src/codegen/stdlib/time.js +3 -0
  25. package/src/codegen/stdlib/unicode.js +3 -0
  26. package/src/codegen/stdlib/utf8.js +3 -0
  27. package/src/codegen/templ.js +3 -0
  28. package/src/compiler.js +63 -4
  29. package/src/index.js +32 -126
  30. package/src/parser/declarations.js +347 -0
  31. package/src/parser/index.js +3 -346
  32. package/src/typechecker/assignability.js +3 -0
  33. package/src/typechecker/expressions.js +9 -0
  34. package/src/typechecker/index.js +2 -1
  35. package/src/typechecker/resolve.js +9 -2
  36. package/src/typechecker/statements.js +3 -0
  37. package/src/typechecker/termination.js +3 -0
  38. 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 { basename, dirname, join, relative, resolve } from "node:path";
30
- import { CodeGen } from "./codegen/index.js";
31
- import { compileDir, resolveImports } from "./compiler.js";
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
- writeFileSync(mainPath, template);
66
+ ({ mainPath } = handleInit(targetDir));
105
67
  } catch (e) {
106
- console.error(`gofront: cannot write '${mainPath}': ${e.message}`);
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
+ };