gofront 0.0.5 → 0.0.7
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 +104 -37
- package/package.json +7 -4
- package/src/codegen/expressions.js +589 -7
- package/src/codegen/source-map.js +13 -5
- package/src/codegen/statements.js +30 -26
- package/src/codegen.js +99 -22
- package/src/compiler.js +10 -3
- package/src/dev-server.js +83 -0
- package/src/index.js +35 -5
- package/src/parser.js +0 -10
- package/src/typechecker/expressions.js +38 -9
- package/src/typechecker/stdlib.js +764 -0
- package/src/typechecker/types.js +15 -20
- package/src/typechecker.js +34 -394
|
@@ -16,21 +16,29 @@ function vlqEncode(value) {
|
|
|
16
16
|
return result;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function buildSourceMap(
|
|
20
|
-
//
|
|
19
|
+
export function buildSourceMap(sources, mappings) {
|
|
20
|
+
// sources: string[]
|
|
21
|
+
// mappings: Array<{ genLine: number, srcLine: number, srcFileIdx?: number }> (0-based)
|
|
21
22
|
// Emits one segment per generated line, column 0 → source line (delta-encoded).
|
|
22
23
|
const lines = [];
|
|
23
24
|
let prevSrcLine = 0;
|
|
25
|
+
let prevSrcFile = 0;
|
|
24
26
|
const maxGen = mappings.reduce((m, e) => Math.max(m, e.genLine), -1);
|
|
25
27
|
const byLine = new Map(mappings.map((e) => [e.genLine, e]));
|
|
26
28
|
for (let g = 0; g <= maxGen; g++) {
|
|
27
29
|
const entry = byLine.get(g);
|
|
28
30
|
if (entry) {
|
|
31
|
+
const fileIdx = entry.srcFileIdx ?? 0;
|
|
32
|
+
const fileDelta = fileIdx - prevSrcFile;
|
|
29
33
|
const srcDelta = entry.srcLine - prevSrcLine;
|
|
30
|
-
// Segment: [genCol=0,
|
|
34
|
+
// Segment: [genCol=0, srcFileIdxDelta, srcLineDelta, srcCol=0]
|
|
31
35
|
lines.push(
|
|
32
|
-
vlqEncode(0) +
|
|
36
|
+
vlqEncode(0) +
|
|
37
|
+
vlqEncode(fileDelta) +
|
|
38
|
+
vlqEncode(srcDelta) +
|
|
39
|
+
vlqEncode(0),
|
|
33
40
|
);
|
|
41
|
+
prevSrcFile = fileIdx;
|
|
34
42
|
prevSrcLine = entry.srcLine;
|
|
35
43
|
} else {
|
|
36
44
|
lines.push(""); // no mapping for this generated line
|
|
@@ -38,7 +46,7 @@ export function buildSourceMap(sourceName, mappings) {
|
|
|
38
46
|
}
|
|
39
47
|
return JSON.stringify({
|
|
40
48
|
version: 3,
|
|
41
|
-
sources
|
|
49
|
+
sources,
|
|
42
50
|
names: [],
|
|
43
51
|
mappings: lines.join(";"),
|
|
44
52
|
});
|
|
@@ -7,6 +7,30 @@ export const statementGenMethods = {
|
|
|
7
7
|
for (const stmt of block.stmts) this.genStmt(stmt);
|
|
8
8
|
},
|
|
9
9
|
|
|
10
|
+
// Shared helper for comma-ok map index in both DefineStmt and AssignStmt.
|
|
11
|
+
// isDefine=true → emit `let v = m[k]; let ok = k in m;`
|
|
12
|
+
// isDefine=false → emit `v = k in m ? m[k] : zero; ok = k in m;`
|
|
13
|
+
_genCommaOkMapIndex(stmt, rhsNode, isDefine) {
|
|
14
|
+
const mapExpr = this.genExpr(rhsNode.expr);
|
|
15
|
+
const keyExpr = this.genExpr(rhsNode.index);
|
|
16
|
+
const [vName, okName] = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
|
|
17
|
+
const decl = isDefine ? "let " : "";
|
|
18
|
+
if (vName !== "_") {
|
|
19
|
+
if (isDefine) {
|
|
20
|
+
this.line(`let ${vName} = ${mapExpr}[${keyExpr}];`);
|
|
21
|
+
} else {
|
|
22
|
+
const zero = rhsNode._mapValueType
|
|
23
|
+
? this.zeroValueForType(rhsNode._mapValueType)
|
|
24
|
+
: "undefined";
|
|
25
|
+
this.line(
|
|
26
|
+
`${vName} = (${keyExpr}) in ${mapExpr} ? ${mapExpr}[${keyExpr}] : ${zero};`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (okName !== "_")
|
|
31
|
+
this.line(`${decl}${okName} = (${keyExpr}) in ${mapExpr};`);
|
|
32
|
+
},
|
|
33
|
+
|
|
10
34
|
genStmt(stmt) {
|
|
11
35
|
// Record source mapping for the first line this statement produces
|
|
12
36
|
const srcLine = stmt._line ?? null;
|
|
@@ -31,15 +55,7 @@ export const statementGenMethods = {
|
|
|
31
55
|
rhsNode?.kind === "IndexExpr" &&
|
|
32
56
|
rhsNode.expr?._type?.kind === "map"
|
|
33
57
|
) {
|
|
34
|
-
|
|
35
|
-
const keyExpr = this.genExpr(rhsNode.index);
|
|
36
|
-
const [vName, okName] = stmt.lhs.map(
|
|
37
|
-
(e) => e.name ?? this.genExpr(e),
|
|
38
|
-
);
|
|
39
|
-
if (vName !== "_")
|
|
40
|
-
this.line(`let ${vName} = ${mapExpr}[${keyExpr}];`);
|
|
41
|
-
if (okName !== "_")
|
|
42
|
-
this.line(`let ${okName} = (${keyExpr}) in ${mapExpr};`);
|
|
58
|
+
this._genCommaOkMapIndex(stmt, rhsNode, true);
|
|
43
59
|
break;
|
|
44
60
|
}
|
|
45
61
|
const rhs = stmt.rhs.map((e) => this.genExpr(e));
|
|
@@ -117,22 +133,7 @@ export const statementGenMethods = {
|
|
|
117
133
|
}
|
|
118
134
|
// comma-ok map index: v, ok = m["key"]
|
|
119
135
|
if (stmt._commaOkMap) {
|
|
120
|
-
|
|
121
|
-
const mapExpr = this.genExpr(rhsNode.expr);
|
|
122
|
-
const keyExpr = this.genExpr(rhsNode.index);
|
|
123
|
-
const [vName, okName] = stmt.lhs.map(
|
|
124
|
-
(e) => e.name ?? this.genExpr(e),
|
|
125
|
-
);
|
|
126
|
-
if (vName !== "_") {
|
|
127
|
-
const zero = rhsNode._mapValueType
|
|
128
|
-
? this.zeroValueForType(rhsNode._mapValueType)
|
|
129
|
-
: "undefined";
|
|
130
|
-
this.line(
|
|
131
|
-
`${vName} = (${keyExpr}) in ${mapExpr} ? ${mapExpr}[${keyExpr}] : ${zero};`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
if (okName !== "_")
|
|
135
|
-
this.line(`${okName} = (${keyExpr}) in ${mapExpr};`);
|
|
136
|
+
this._genCommaOkMapIndex(stmt, stmt.rhs[0], false);
|
|
136
137
|
break;
|
|
137
138
|
}
|
|
138
139
|
// comma-ok type assertion: v, ok = x.(T)
|
|
@@ -383,8 +384,11 @@ export const statementGenMethods = {
|
|
|
383
384
|
const init = stmt.init;
|
|
384
385
|
const range = init.rhs[0];
|
|
385
386
|
const lhs = init.lhs.map((e) => e.name ?? this.genExpr(e));
|
|
386
|
-
const iteree = this.genExpr(range.expr);
|
|
387
387
|
const iterType = range.expr._type;
|
|
388
|
+
const wrapField = this._namedWrapperField(iterType, range.expr);
|
|
389
|
+
const iteree = wrapField
|
|
390
|
+
? `${this.genExpr(range.expr)}.${wrapField}`
|
|
391
|
+
: this.genExpr(range.expr);
|
|
388
392
|
|
|
389
393
|
let iterExpr;
|
|
390
394
|
if (
|
package/src/codegen.js
CHANGED
|
@@ -35,10 +35,12 @@ export class CodeGen {
|
|
|
35
35
|
this.out = [];
|
|
36
36
|
this.indent = 0;
|
|
37
37
|
this.structNames = new Set();
|
|
38
|
+
this.namedWrapperNames = new Set();
|
|
38
39
|
this.jsImports = jsImports;
|
|
39
40
|
this.bundledPackages = bundledPackages;
|
|
40
41
|
this.namedReturnVars = null; // names of current function's named return vars
|
|
41
|
-
this._srcMappings = []; // { genLine, srcLine } for source map
|
|
42
|
+
this._srcMappings = []; // { genLine, srcLine, srcFileIdx } for source map
|
|
43
|
+
this._currentSrcFileIdx = 0; // updated as each top-level decl is generated
|
|
42
44
|
this._boxedVars = new Set(); // address-taken scalar variables that need boxing
|
|
43
45
|
// Runtime helper usage tracking — only emit helpers that are actually used
|
|
44
46
|
this._usesLen = false;
|
|
@@ -67,8 +69,9 @@ export class CodeGen {
|
|
|
67
69
|
if (srcLine != null) {
|
|
68
70
|
this._srcMappings.push({
|
|
69
71
|
genLine: this.out.length,
|
|
70
|
-
srcLine: srcLine - 1,
|
|
71
|
-
|
|
72
|
+
srcLine: srcLine - 1, // 0-based
|
|
73
|
+
srcFileIdx: this._currentSrcFileIdx,
|
|
74
|
+
});
|
|
72
75
|
}
|
|
73
76
|
this.out.push(" ".repeat(this.indent) + s);
|
|
74
77
|
}
|
|
@@ -90,7 +93,7 @@ export class CodeGen {
|
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
// Build a map:
|
|
96
|
+
// Build a map: typeName → [MethodDecl, ...]
|
|
94
97
|
const methods = new Map();
|
|
95
98
|
for (const d of program.decls) {
|
|
96
99
|
if (d.kind === "MethodDecl") {
|
|
@@ -100,6 +103,18 @@ export class CodeGen {
|
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
// Collect named non-struct types that have methods (emitted as wrapper classes)
|
|
107
|
+
for (const d of program.decls) {
|
|
108
|
+
if (
|
|
109
|
+
d.kind === "TypeDecl" &&
|
|
110
|
+
d.type.kind !== "StructType" &&
|
|
111
|
+
d.type.kind !== "InterfaceType" &&
|
|
112
|
+
(methods.get(d.name) ?? []).length > 0
|
|
113
|
+
) {
|
|
114
|
+
this.namedWrapperNames.add(d.name);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
// Emit ESM import statements for npm packages
|
|
104
119
|
for (const [importPath, names] of this.jsImports) {
|
|
105
120
|
if (names.length === 0) continue;
|
|
@@ -109,6 +124,7 @@ export class CodeGen {
|
|
|
109
124
|
|
|
110
125
|
for (const d of program.decls) {
|
|
111
126
|
if (d.kind === "TypeDecl") {
|
|
127
|
+
this._currentSrcFileIdx = d._srcFileIdx ?? 0;
|
|
112
128
|
this.genTypeDeclWithMethods(d, methods.get(d.name) ?? []);
|
|
113
129
|
this.blank();
|
|
114
130
|
}
|
|
@@ -116,10 +132,12 @@ export class CodeGen {
|
|
|
116
132
|
|
|
117
133
|
for (const d of program.decls) {
|
|
118
134
|
if (d.kind === "VarDecl") {
|
|
135
|
+
this._currentSrcFileIdx = d._srcFileIdx ?? 0;
|
|
119
136
|
this.genVarDecl(d);
|
|
120
137
|
this.blank();
|
|
121
138
|
}
|
|
122
139
|
if (d.kind === "ConstDecl") {
|
|
140
|
+
this._currentSrcFileIdx = d._srcFileIdx ?? 0;
|
|
123
141
|
this.genConstDecl(d);
|
|
124
142
|
this.blank();
|
|
125
143
|
}
|
|
@@ -130,6 +148,7 @@ export class CodeGen {
|
|
|
130
148
|
const initNames = [];
|
|
131
149
|
for (const d of program.decls) {
|
|
132
150
|
if (d.kind === "FuncDecl") {
|
|
151
|
+
this._currentSrcFileIdx = d._srcFileIdx ?? 0;
|
|
133
152
|
if (d.name === "init") {
|
|
134
153
|
const renamed = initCount === 0 ? "init" : `init$${initCount}`;
|
|
135
154
|
initNames.push(renamed);
|
|
@@ -152,29 +171,31 @@ export class CodeGen {
|
|
|
152
171
|
// Prepend runtime helpers that were actually used
|
|
153
172
|
const helpers = [];
|
|
154
173
|
if (this._usesLen)
|
|
155
|
-
helpers.push(
|
|
174
|
+
helpers.push(
|
|
175
|
+
"var __len = __len || function(a) { if (a && typeof a === 'object' && !Array.isArray(a)) return Object.keys(a).length; return a?.length ?? 0; };",
|
|
176
|
+
);
|
|
156
177
|
if (this._usesAppend)
|
|
157
178
|
helpers.push(
|
|
158
|
-
"
|
|
179
|
+
"var __append = __append || function(a, ...b) { return a ? [...a, ...b] : b; };",
|
|
159
180
|
);
|
|
160
181
|
if (this._usesSliceGuard)
|
|
161
|
-
helpers.push("
|
|
182
|
+
helpers.push("var __s = __s || function(a) { return a || []; };");
|
|
162
183
|
if (this._usesEqual)
|
|
163
184
|
helpers.push(
|
|
164
|
-
'function __equal(a,b){if(a===b)return true;if(a===null||b===null)return false;if(Array.isArray(a)&&Array.isArray(b)){if(a.length!==b.length)return false;for(let i=0;i<a.length;i++)if(!__equal(a[i],b[i]))return false;return true;}if(typeof a==="object"&&typeof b==="object"){const ka=Object.keys(a),kb=Object.keys(b);if(ka.length!==kb.length)return false;for(const k of ka)if(!__equal(a[k],b[k]))return false;return true;}return false;}',
|
|
185
|
+
'var __equal = __equal || function __equal(a,b){if(a===b)return true;if(a===null||b===null)return false;if(Array.isArray(a)&&Array.isArray(b)){if(a.length!==b.length)return false;for(let i=0;i<a.length;i++)if(!__equal(a[i],b[i]))return false;return true;}if(typeof a==="object"&&typeof b==="object"){const ka=Object.keys(a),kb=Object.keys(b);if(ka.length!==kb.length)return false;for(const k of ka)if(!__equal(a[k],b[k]))return false;return true;}return false;};',
|
|
165
186
|
);
|
|
166
187
|
if (this._usesCmul)
|
|
167
188
|
helpers.push(
|
|
168
|
-
"
|
|
189
|
+
"var __cmul = __cmul || function(a, b) { return { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re }; };",
|
|
169
190
|
);
|
|
170
191
|
if (this._usesCdiv)
|
|
171
192
|
helpers.push(
|
|
172
|
-
"
|
|
193
|
+
"var __cdiv = __cdiv || function(a, b) { const d = b.re * b.re + b.im * b.im; return { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d }; };",
|
|
173
194
|
);
|
|
174
195
|
if (this._usesSprintf)
|
|
175
196
|
helpers.push(
|
|
176
197
|
[
|
|
177
|
-
"
|
|
198
|
+
"var __sprintf = __sprintf || function(f,...a){let i=0;return f.replace(/%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)[sdvftxXqobeEgGw%]/g,(m)=>{",
|
|
178
199
|
"if(m==='%%')return'%';const fl=m.slice(1,-1),verb=m.slice(-1),v=a[i++];",
|
|
179
200
|
"const pad=(s,w,z)=>{w=parseInt(w)||0;if(!w)return s;const p=(z?'0':' ').repeat(Math.max(0,w-s.length));return fl.includes('-')?s+p:p+s;};",
|
|
180
201
|
"const [,flags,width,prec]=m.match(/^%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)/)||[];",
|
|
@@ -194,16 +215,16 @@ export class CodeGen {
|
|
|
194
215
|
"case'g':case'G':{const n=Number(v);return pad(prec!==''?n.toPrecision(parseInt(prec)):String(n),width,zero);}",
|
|
195
216
|
"case'w':return pad(String(v==null?'<nil>':typeof v==='object'&&v.Error?v.Error():v),width,false);",
|
|
196
217
|
"default:return m;}});",
|
|
197
|
-
"}",
|
|
218
|
+
"};",
|
|
198
219
|
].join(""),
|
|
199
220
|
);
|
|
200
221
|
if (this._usesError)
|
|
201
222
|
helpers.push(
|
|
202
|
-
"
|
|
223
|
+
"var __error = __error || function(msg, cause) { return { Error() { return msg; }, toString() { return msg; }, _msg: msg, _cause: cause ?? null }; };",
|
|
203
224
|
);
|
|
204
225
|
if (this._usesErrorIs)
|
|
205
226
|
helpers.push(
|
|
206
|
-
'
|
|
227
|
+
'var __errorIs = __errorIs || function(err, target) { while (err !== null && err !== undefined) { if (err === target) return true; if (typeof err === "object" && typeof target === "object" && err._msg !== undefined && target._msg !== undefined && err._msg === target._msg) return true; err = err?._cause ?? null; } return false; };',
|
|
207
228
|
);
|
|
208
229
|
|
|
209
230
|
if (helpers.length > 0) this.out.unshift(...helpers, "");
|
|
@@ -214,16 +235,22 @@ export class CodeGen {
|
|
|
214
235
|
}
|
|
215
236
|
|
|
216
237
|
// Generate a single bundle from multiple programs (same-package multi-file).
|
|
217
|
-
//
|
|
238
|
+
// Annotates each decl with its source file index before merging.
|
|
218
239
|
generateAll(programs) {
|
|
240
|
+
for (let i = 0; i < programs.length; i++) {
|
|
241
|
+
for (const decl of programs[i].decls) {
|
|
242
|
+
decl._srcFileIdx = i;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
219
245
|
const merged = { decls: programs.flatMap((p) => p.decls) };
|
|
220
246
|
return this.generate(merged);
|
|
221
247
|
}
|
|
222
248
|
|
|
223
249
|
// Returns a source map JSON string for the last generate() call.
|
|
224
|
-
//
|
|
225
|
-
getSourceMap(
|
|
226
|
-
|
|
250
|
+
// sources: string[] of source filenames (relative to the output file).
|
|
251
|
+
getSourceMap(sources) {
|
|
252
|
+
const srcArray = Array.isArray(sources) ? sources : [sources];
|
|
253
|
+
return buildSourceMap(srcArray, this._srcMappings);
|
|
227
254
|
}
|
|
228
255
|
|
|
229
256
|
// ── Type declarations ────────────────────────────────────────
|
|
@@ -234,13 +261,41 @@ export class CodeGen {
|
|
|
234
261
|
} else if (decl.type.kind === "InterfaceType") {
|
|
235
262
|
// Interfaces are compile-time only — no JS output needed.
|
|
236
263
|
this.line(`// interface ${decl.name} (compile-time only)`);
|
|
264
|
+
} else if (methodDecls.length > 0) {
|
|
265
|
+
// Named non-struct type with methods — emit an ES6 wrapper class.
|
|
266
|
+
this.genNamedTypeClass(decl.name, methodDecls);
|
|
237
267
|
} else {
|
|
238
|
-
// Type alias — no JS equivalent needed unless it's used as a constructor.
|
|
239
|
-
// e.g. type MyInt int → just a comment
|
|
240
268
|
this.line(`// type ${decl.name} = ${this.typeComment(decl.type)}`);
|
|
241
269
|
}
|
|
242
270
|
}
|
|
243
271
|
|
|
272
|
+
genNamedTypeClass(name, methodDecls) {
|
|
273
|
+
const namedType = this.checker?.types.get(name);
|
|
274
|
+
const underlying = namedType?.underlying;
|
|
275
|
+
let field, ctorDefault;
|
|
276
|
+
if (underlying?.kind === "func") {
|
|
277
|
+
field = "_fn";
|
|
278
|
+
ctorDefault = "null";
|
|
279
|
+
} else if (underlying?.kind === "map") {
|
|
280
|
+
field = "_map";
|
|
281
|
+
ctorDefault = "{}";
|
|
282
|
+
} else {
|
|
283
|
+
field = "_items";
|
|
284
|
+
ctorDefault = "[]";
|
|
285
|
+
}
|
|
286
|
+
this.line(`class ${name} {`);
|
|
287
|
+
this.indented(() => {
|
|
288
|
+
this.line(
|
|
289
|
+
`constructor(${field} = ${ctorDefault}) { this.${field} = ${field}; }`,
|
|
290
|
+
);
|
|
291
|
+
for (const m of methodDecls) {
|
|
292
|
+
this.blank();
|
|
293
|
+
this.genMethod(m, field);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
this.line("}");
|
|
297
|
+
}
|
|
298
|
+
|
|
244
299
|
genStruct(name, structTypeAst, methodDecls) {
|
|
245
300
|
const fields = [];
|
|
246
301
|
if (this.checker) {
|
|
@@ -303,23 +358,45 @@ export class CodeGen {
|
|
|
303
358
|
this.line("}");
|
|
304
359
|
}
|
|
305
360
|
|
|
306
|
-
genMethod(decl) {
|
|
361
|
+
genMethod(decl, recvField = null) {
|
|
307
362
|
const params = decl.params.map((p) => p.name).join(", ");
|
|
308
363
|
const asyncPrefix = decl.async ? "async " : "";
|
|
309
364
|
this.line(`${asyncPrefix}${decl.name}(${params}) {`);
|
|
310
365
|
const prevBoxed = this._boxedVars;
|
|
311
366
|
this._boxedVars = new Set();
|
|
312
367
|
this._scanAddressTaken(decl.body);
|
|
368
|
+
const prevUnwrapped = this._unwrappedRecv;
|
|
313
369
|
this.indented(() => {
|
|
314
370
|
if (decl.recvName && decl.recvName !== "_") {
|
|
315
|
-
|
|
371
|
+
if (recvField) {
|
|
372
|
+
this.line(`const ${decl.recvName} = this.${recvField};`);
|
|
373
|
+
this._unwrappedRecv = decl.recvName;
|
|
374
|
+
} else {
|
|
375
|
+
this.line(`const ${decl.recvName} = this;`);
|
|
376
|
+
}
|
|
316
377
|
}
|
|
317
378
|
this._withNamedReturns(decl, () => this._genBody(decl.body));
|
|
318
379
|
});
|
|
380
|
+
this._unwrappedRecv = prevUnwrapped;
|
|
319
381
|
this._boxedVars = prevBoxed;
|
|
320
382
|
this.line("}");
|
|
321
383
|
}
|
|
322
384
|
|
|
385
|
+
// Returns the wrapper field name ("_fn", "_items", "_map") if `type` is a named
|
|
386
|
+
// non-struct type emitted as a wrapper class, or null otherwise.
|
|
387
|
+
// Pass `expr` so we can skip unwrapping when the expression is the method receiver
|
|
388
|
+
// (which was already unwrapped to `this.<field>` at the top of the method body).
|
|
389
|
+
_namedWrapperField(type, expr = null) {
|
|
390
|
+
if (type?.kind !== "named") return null;
|
|
391
|
+
if (!this.namedWrapperNames.has(type.name)) return null;
|
|
392
|
+
if (expr?.kind === "Ident" && expr.name === this._unwrappedRecv)
|
|
393
|
+
return null;
|
|
394
|
+
const u = type.underlying;
|
|
395
|
+
if (u?.kind === "func") return "_fn";
|
|
396
|
+
if (u?.kind === "map") return "_map";
|
|
397
|
+
return "_items";
|
|
398
|
+
}
|
|
399
|
+
|
|
323
400
|
// ── Function declarations ────────────────────────────────────
|
|
324
401
|
|
|
325
402
|
genFuncDecl(decl, nameOverride) {
|
package/src/compiler.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// de-qualifies it because the dependency is inlined.
|
|
17
17
|
|
|
18
18
|
import { readdirSync, readFileSync } from "node:fs";
|
|
19
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
19
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
20
20
|
import { CodeGen } from "./codegen.js";
|
|
21
21
|
import { parseDts } from "./dts-parser.js";
|
|
22
22
|
import { Lexer } from "./lexer.js";
|
|
@@ -165,8 +165,15 @@ export function compileFiles(files, options = {}) {
|
|
|
165
165
|
const codegen = new CodeGen(checker, jsImports, bundledPackages);
|
|
166
166
|
const mainJs = codegen.generateAll(programs);
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
let js = preambles.length > 0 ? `${preambles.join("\n")}\n${mainJs}` : mainJs;
|
|
169
|
+
|
|
170
|
+
if (options.sourceMap) {
|
|
171
|
+
const outputDir = options.outputDir ?? fromDir;
|
|
172
|
+
const sourceFiles = files.map((f) => relative(outputDir, f));
|
|
173
|
+
const map = codegen.getSourceMap(sourceFiles);
|
|
174
|
+
const b64 = Buffer.from(map).toString("base64");
|
|
175
|
+
js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
|
|
176
|
+
}
|
|
170
177
|
|
|
171
178
|
return {
|
|
172
179
|
pkgName,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Minimal dev server for gofront --serve watch mode.
|
|
2
|
+
// Serves static files and pushes a reload event via SSE after each build.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { extname, join } from "node:path";
|
|
7
|
+
|
|
8
|
+
const MIME = {
|
|
9
|
+
".html": "text/html; charset=utf-8",
|
|
10
|
+
".js": "application/javascript; charset=utf-8",
|
|
11
|
+
".css": "text/css; charset=utf-8",
|
|
12
|
+
".json": "application/json; charset=utf-8",
|
|
13
|
+
".svg": "image/svg+xml",
|
|
14
|
+
".png": "image/png",
|
|
15
|
+
".jpg": "image/jpeg",
|
|
16
|
+
".ico": "image/x-icon",
|
|
17
|
+
".woff2": "font/woff2",
|
|
18
|
+
".woff": "font/woff",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Injected at the bottom of compiled JS in serve mode.
|
|
22
|
+
export const liveReloadClient = `(function(){var es=new EventSource('/_gofront/events');es.addEventListener('reload',function(){location.reload();});})();`;
|
|
23
|
+
|
|
24
|
+
export function createDevServer(serveDir, port) {
|
|
25
|
+
const clients = new Set();
|
|
26
|
+
|
|
27
|
+
const server = createServer((req, res) => {
|
|
28
|
+
// SSE endpoint — browser connects here to receive reload events
|
|
29
|
+
if (req.url === "/_gofront/events") {
|
|
30
|
+
res.writeHead(200, {
|
|
31
|
+
"Content-Type": "text/event-stream",
|
|
32
|
+
"Cache-Control": "no-cache",
|
|
33
|
+
Connection: "keep-alive",
|
|
34
|
+
});
|
|
35
|
+
res.write(": connected\n\n");
|
|
36
|
+
clients.add(res);
|
|
37
|
+
req.on("close", () => clients.delete(res));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let urlPath = req.url.split("?")[0];
|
|
42
|
+
if (urlPath === "/" || urlPath === "") urlPath = "/index.html";
|
|
43
|
+
|
|
44
|
+
const filePath = join(serveDir, urlPath);
|
|
45
|
+
if (!existsSync(filePath)) {
|
|
46
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
47
|
+
res.end("Not found");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const data = readFileSync(filePath);
|
|
53
|
+
const mime = MIME[extname(filePath)] ?? "application/octet-stream";
|
|
54
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
55
|
+
res.end(data);
|
|
56
|
+
} catch {
|
|
57
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
58
|
+
res.end("Server error");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
server.on("error", (err) => {
|
|
63
|
+
if (err.code === "EADDRINUSE") {
|
|
64
|
+
console.error(
|
|
65
|
+
`gofront: port ${port} already in use — try --port <number>`,
|
|
66
|
+
);
|
|
67
|
+
} else {
|
|
68
|
+
console.error(`gofront: dev server error: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
72
|
+
server.listen(port, () => {
|
|
73
|
+
console.error(`gofront: dev server → http://localhost:${port}`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function notify() {
|
|
77
|
+
for (const client of clients) {
|
|
78
|
+
client.write("event: reload\ndata: {}\n\n");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { notify };
|
|
83
|
+
}
|
package/src/index.js
CHANGED
|
@@ -26,9 +26,10 @@ import { createRequire } from "node:module";
|
|
|
26
26
|
const _require = createRequire(import.meta.url);
|
|
27
27
|
const { version } = _require("../package.json");
|
|
28
28
|
|
|
29
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
29
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
30
30
|
import { CodeGen } from "./codegen.js";
|
|
31
31
|
import { compileDir } from "./compiler.js";
|
|
32
|
+
import { createDevServer, liveReloadClient } from "./dev-server.js";
|
|
32
33
|
import { parseDts } from "./dts-parser.js";
|
|
33
34
|
import { Lexer } from "./lexer.js";
|
|
34
35
|
import { minify } from "./minifier.js";
|
|
@@ -55,6 +56,8 @@ Usage:
|
|
|
55
56
|
gofront <input> -o out.js Compile and write to file
|
|
56
57
|
gofront <input> --check Type-check only
|
|
57
58
|
gofront <input> --watch Watch for changes and recompile
|
|
59
|
+
gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
|
|
60
|
+
gofront <input> -o out.js --serve --port 8080 Use a custom port
|
|
58
61
|
gofront <input> --source-map Append inline source map to output
|
|
59
62
|
gofront <input> --minify Minify output
|
|
60
63
|
gofront <input> --minify --mangle Minify and mangle identifiers
|
|
@@ -120,9 +123,12 @@ const checkOnly = args.includes("--check");
|
|
|
120
123
|
const dumpAst = args.includes("--ast");
|
|
121
124
|
const dumpTokens = args.includes("--tokens");
|
|
122
125
|
const sourceMap = args.includes("--source-map");
|
|
123
|
-
const
|
|
126
|
+
const serveMode = args.includes("--serve");
|
|
127
|
+
const watchMode = args.includes("--watch") || serveMode;
|
|
124
128
|
const minifyOutput = args.includes("--minify");
|
|
125
129
|
const mangleOutput = args.includes("--mangle");
|
|
130
|
+
const portFlag = args.indexOf("--port");
|
|
131
|
+
const servePort = portFlag !== -1 ? parseInt(args[portFlag + 1], 10) : 3000;
|
|
126
132
|
|
|
127
133
|
// ── Determine input mode ─────────────────────────────────────
|
|
128
134
|
|
|
@@ -139,7 +145,8 @@ try {
|
|
|
139
145
|
|
|
140
146
|
function runCompile() {
|
|
141
147
|
if (isDir) {
|
|
142
|
-
|
|
148
|
+
const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
|
|
149
|
+
return compileDir(inputPath, { sourceMap, outputDir });
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
// Single-file mode
|
|
@@ -223,7 +230,9 @@ function runCompile() {
|
|
|
223
230
|
let js = cg.generate(ast);
|
|
224
231
|
|
|
225
232
|
if (sourceMap) {
|
|
226
|
-
const
|
|
233
|
+
const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
|
|
234
|
+
const srcPath = relative(outputDir, inputPath);
|
|
235
|
+
const map = cg.getSourceMap(srcPath);
|
|
227
236
|
const b64 = Buffer.from(map).toString("base64");
|
|
228
237
|
js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
|
|
229
238
|
}
|
|
@@ -291,11 +300,31 @@ function timestamp() {
|
|
|
291
300
|
return new Date().toLocaleTimeString();
|
|
292
301
|
}
|
|
293
302
|
|
|
303
|
+
// Start dev server before first build so the browser can connect immediately
|
|
304
|
+
let devServer = null;
|
|
305
|
+
if (serveMode) {
|
|
306
|
+
if (!outputFile) {
|
|
307
|
+
console.error("gofront: --serve requires -o <output file>");
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
const serveDir = dirname(resolve(outputFile));
|
|
311
|
+
devServer = createDevServer(serveDir, servePort);
|
|
312
|
+
}
|
|
313
|
+
|
|
294
314
|
function buildOnce() {
|
|
295
315
|
try {
|
|
296
316
|
const startMs = performance.now();
|
|
297
317
|
const result = runCompile();
|
|
298
|
-
|
|
318
|
+
let js = maybeMinify(result.js);
|
|
319
|
+
if (serveMode) {
|
|
320
|
+
// Insert live reload client before the source map comment so it stays last
|
|
321
|
+
const smIdx = js.lastIndexOf("\n//# sourceMappingURL=");
|
|
322
|
+
if (smIdx !== -1) {
|
|
323
|
+
js = `${js.slice(0, smIdx)}\n${liveReloadClient}${js.slice(smIdx)}`;
|
|
324
|
+
} else {
|
|
325
|
+
js += `\n${liveReloadClient}`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
299
328
|
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
300
329
|
if (outputFile) {
|
|
301
330
|
writeFileSync(outputFile, `${js}\n`);
|
|
@@ -308,6 +337,7 @@ function buildOnce() {
|
|
|
308
337
|
console.log(js);
|
|
309
338
|
console.error(`[${timestamp()}] gofront: OK (${elapsedMs}ms)`);
|
|
310
339
|
}
|
|
340
|
+
devServer?.notify();
|
|
311
341
|
} catch (e) {
|
|
312
342
|
console.error(`[${timestamp()}] gofront: ERROR`);
|
|
313
343
|
for (const line of e.message.split("\n")) console.error(` ${line}`);
|
package/src/parser.js
CHANGED
|
@@ -308,16 +308,6 @@ export class Parser {
|
|
|
308
308
|
return this.looksLikeType(next) || next.type === T.ELLIPSIS;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
// Heuristic: peek2 is a param-name separator if it's a comma, rparen, or a type token
|
|
312
|
-
isParamType(tok) {
|
|
313
|
-
if (!tok) return false;
|
|
314
|
-
// T.RPAREN is intentionally excluded: when peek2 is ")", the current IDENT
|
|
315
|
-
// is the type of an unnamed param (e.g. func(int)), not a param name.
|
|
316
|
-
return (
|
|
317
|
-
tok.type === T.COMMA || tok.type === T.ELLIPSIS || this.looksLikeType(tok)
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
311
|
looksLikeType(tok) {
|
|
322
312
|
return (
|
|
323
313
|
tok.type === T.LBRACKET ||
|