gofront 1.3.12 → 1.5.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 (63) hide show
  1. package/README.md +90 -20
  2. package/package.json +2 -1
  3. package/src/{codegen → backend/js}/expressions.js +217 -60
  4. package/src/{codegen → backend/js}/index.js +47 -36
  5. package/src/{codegen → backend/js}/runtime.js +11 -0
  6. package/src/{codegen → backend/js}/statements.js +43 -48
  7. package/src/backend/js/stdlib/bits.js +32 -0
  8. package/src/{codegen → backend/js}/stdlib/index.js +3 -0
  9. package/src/{codegen → backend/js}/templ.js +31 -2
  10. package/src/backend/wasm/boundary.js +1345 -0
  11. package/src/backend/wasm/emit.js +5779 -0
  12. package/src/backend/wasm/encode.js +789 -0
  13. package/src/backend/wasm/glue.js +130 -0
  14. package/src/backend/wasm/index.js +1413 -0
  15. package/src/backend/wasm/types.js +420 -0
  16. package/src/backend/wasm/wat.js +255 -0
  17. package/src/cli-core.js +52 -7
  18. package/src/compiler.js +328 -12
  19. package/src/dev-server.js +56 -15
  20. package/src/index.js +17 -9
  21. package/src/lexer.js +18 -0
  22. package/src/lower/boxing.js +36 -0
  23. package/src/lower/captures.js +179 -0
  24. package/src/lower/embedding.js +30 -0
  25. package/src/lower/escape.js +169 -0
  26. package/src/lower/functions.js +30 -0
  27. package/src/lower/globals.js +324 -0
  28. package/src/lower/index.js +115 -0
  29. package/src/lower/ownership.js +142 -0
  30. package/src/lower/range.js +70 -0
  31. package/src/minifier.js +2 -0
  32. package/src/parser/index.js +8 -3
  33. package/src/resolver.js +4 -1
  34. package/src/templ-lexer.js +107 -3
  35. package/src/templ-parser.js +29 -0
  36. package/src/test-runner.js +74 -20
  37. package/src/typechecker/expressions.js +40 -1
  38. package/src/typechecker/index.js +301 -6
  39. package/src/typechecker/statements.js +7 -0
  40. package/src/typechecker/stdlib/core.js +17 -0
  41. package/src/typechecker/types.js +56 -2
  42. /package/src/{codegen → backend/js}/source-map.js +0 -0
  43. /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
  44. /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
  45. /package/src/{codegen → backend/js}/stdlib/errors.js +0 -0
  46. /package/src/{codegen → backend/js}/stdlib/fmt.js +0 -0
  47. /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
  48. /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
  49. /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
  50. /package/src/{codegen → backend/js}/stdlib/maps.js +0 -0
  51. /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
  52. /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
  53. /package/src/{codegen → backend/js}/stdlib/path.js +0 -0
  54. /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
  55. /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
  56. /package/src/{codegen → backend/js}/stdlib/slices.js +0 -0
  57. /package/src/{codegen → backend/js}/stdlib/sort.js +0 -0
  58. /package/src/{codegen → backend/js}/stdlib/strconv.js +0 -0
  59. /package/src/{codegen → backend/js}/stdlib/strings.js +0 -0
  60. /package/src/{codegen → backend/js}/stdlib/testing.js +0 -0
  61. /package/src/{codegen → backend/js}/stdlib/time.js +0 -0
  62. /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
  63. /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
@@ -25,9 +25,18 @@ import {
25
25
  TAINTED_ANY,
26
26
  TypeCheckError,
27
27
  typeStr,
28
+ VALID_TARGETS,
28
29
  VOID,
29
30
  } from "./types.js";
30
31
 
32
+ function rootIdentName(e) {
33
+ if (!e) return null;
34
+ if (e.kind === "Ident") return e.name;
35
+ if (e.kind === "SelectorExpr" || e.kind === "IndexExpr")
36
+ return rootIdentName(e.expr);
37
+ return null;
38
+ }
39
+
31
40
  // Re-export for consumers that import from typechecker.js
32
41
  export { TypeCheckError, typeStr };
33
42
 
@@ -36,6 +45,10 @@ export class TypeChecker {
36
45
  this.types = new Map(); // named types
37
46
  this.globals = new Scope();
38
47
  this.errors = [];
48
+ this.target = "js";
49
+ this.pkgName = null;
50
+ this.blockers = new Map();
51
+ this.summary = null;
39
52
  this._currentFile = null; // filename of file currently being checked
40
53
  this._currentSource = null;
41
54
  this._loopDepth = 0; // for break/continue validation
@@ -117,12 +130,42 @@ export class TypeChecker {
117
130
  return new Map(this.types);
118
131
  }
119
132
 
120
- err(msg, node) {
133
+ recordBlocker(category, item = null) {
134
+ if (!this.blockers.has(category)) {
135
+ this.blockers.set(category, []);
136
+ }
137
+ this.blockers.get(category).push(item);
138
+ }
139
+
140
+ getSummary() {
141
+ if (this.blockers.size === 0) return null;
142
+ let total = 0;
143
+ const parts = [];
144
+ for (const [category, items] of this.blockers) {
145
+ const uniqueItems = [
146
+ ...new Set(items.filter((x) => x != null && x !== "")),
147
+ ];
148
+ const count = uniqueItems.length > 0 ? uniqueItems.length : items.length;
149
+ total += count;
150
+ const itemsStr =
151
+ uniqueItems.length > 0 ? ` (${uniqueItems.join(", ")})` : "";
152
+ // "gom usage" is a mass noun; everything else pluralises on the last word.
153
+ const label =
154
+ count === 1 || category === "gom usage" ? category : `${category}s`;
155
+ parts.push(`${count} ${label}${itemsStr}`);
156
+ }
157
+ const noun = total === 1 ? "blocker" : "blockers";
158
+ const what = this.target === "both" ? "both" : "wasm";
159
+ return `package '${this.pkgName}' cannot be ${what}: ${total} ${noun} — ${parts.join(", ")}`;
160
+ }
161
+
162
+ err(msg, node, hint = null) {
121
163
  const e = new TypeCheckError(
122
164
  msg,
123
165
  node,
124
166
  this._currentFile,
125
167
  this._currentSource,
168
+ hint,
126
169
  );
127
170
  this.errors.push(e);
128
171
  return TAINTED_ANY; // tainted recovery type — suppresses downstream cascade errors
@@ -163,18 +206,181 @@ export class TypeChecker {
163
206
  }
164
207
  }
165
208
 
166
- // Like check() but operates over multiple programs (same-package multi-file).
167
- // All passes run across all files before moving to the next pass,
168
- // so every file sees every other file's declarations.
169
209
  checkAll(programs) {
210
+ for (const p of programs) {
211
+ const d = p._targetDirective;
212
+ if (!d) continue;
213
+ this._setCurrentFile(p);
214
+ if (!VALID_TARGETS.has(d.value)) {
215
+ this.err(
216
+ `unknown //gofront:target '${d.value}' (expected js, wasm or both)`,
217
+ d,
218
+ );
219
+ p.target = null;
220
+ if (this.target === d.value) this.target = "js";
221
+ } else if (!d.beforePackage) {
222
+ this.err("//gofront:target must appear before the package clause", d);
223
+ }
224
+ }
225
+ if (programs.length > 0) {
226
+ if (!this.target || this.target === "js") {
227
+ const pTarget = programs.find((p) => p.target)?.target;
228
+ if (pTarget) this.target = pTarget;
229
+ }
230
+ if (!this.pkgName) {
231
+ this.pkgName = programs[0].pkg?.name ?? null;
232
+ }
233
+ }
234
+
235
+ if (this.target === "wasm" || this.target === "both") {
236
+ for (const p of programs) {
237
+ if (p._filename?.endsWith(".templ")) {
238
+ this._setCurrentFile(p);
239
+ const fileName = p._filename.split("/").pop();
240
+ this.recordBlocker(".templ file", fileName);
241
+ this.err(
242
+ `.templ files are not allowed in wasm packages; move '${fileName}' to a js package`,
243
+ p.pkg,
244
+ );
245
+ }
246
+ }
247
+ }
248
+
170
249
  this._collectTypesPass(programs);
171
250
  this._collectFuncsPass(programs);
172
251
  this._promoteEmbeddedMethods();
173
252
  this._collectVarsConstsPass(programs);
174
253
  this._checkTopDeclsPass(programs);
254
+
255
+ if (this.target === "both") {
256
+ this._checkMutablePackageVars(programs);
257
+ }
258
+
259
+ if (this.blockers.size > 0) {
260
+ const summaryLine = this.getSummary();
261
+ this.summary = summaryLine;
262
+ const summaryErr = new Error(summaryLine);
263
+ summaryErr.isSummary = true;
264
+ this.errors.push(summaryErr);
265
+ }
266
+
175
267
  return this.errors;
176
268
  }
177
269
 
270
+ _checkMutablePackageVars(programs) {
271
+ const pkgVars = new Set();
272
+ for (const p of programs) {
273
+ for (const d of p.decls) {
274
+ if (d.kind === "VarDecl") {
275
+ for (const spec of d.decls) {
276
+ for (const name of spec.names) {
277
+ if (name !== "_") pkgVars.add(name);
278
+ }
279
+ }
280
+ }
281
+ }
282
+ }
283
+ if (pkgVars.size === 0) return;
284
+
285
+ const SCOPE_KINDS = new Set([
286
+ "Block",
287
+ "FuncLit",
288
+ "ForStmt",
289
+ "IfStmt",
290
+ "SwitchStmt",
291
+ "TypeSwitchStmt",
292
+ "CaseClause",
293
+ ]);
294
+
295
+ const report = (root, node) => {
296
+ this.recordBlocker("mutable package variable", root);
297
+ this.err(
298
+ `package-level variable '${root}' is mutated; not allowed in 'both' packages (each target gets its own copy)`,
299
+ node,
300
+ );
301
+ };
302
+
303
+ const checkBody = (body, initialLocals) => {
304
+ const scopes = [new Set(initialLocals)];
305
+ const isLocal = (name) => scopes.some((s) => s.has(name));
306
+ const declare = (name) => {
307
+ if (name && name !== "_") scopes[scopes.length - 1].add(name);
308
+ };
309
+ const flag = (e, node) => {
310
+ const root = rootIdentName(e);
311
+ if (root && pkgVars.has(root) && !isLocal(root)) report(root, node);
312
+ };
313
+
314
+ const walk = (node) => {
315
+ if (!node || typeof node !== "object") return;
316
+ if (Array.isArray(node)) {
317
+ for (const item of node) walk(item);
318
+ return;
319
+ }
320
+ const opensScope = SCOPE_KINDS.has(node.kind);
321
+ if (opensScope) scopes.push(new Set());
322
+ switch (node.kind) {
323
+ case "FuncLit":
324
+ for (const p of node.params ?? []) declare(p.name);
325
+ for (const r of node.returnType?._namedReturns ?? [])
326
+ declare(r.name);
327
+ break;
328
+ case "VarDecl":
329
+ for (const spec of node.decls ?? [])
330
+ for (const name of spec.names ?? []) declare(name);
331
+ break;
332
+ case "DefineStmt":
333
+ for (const e of node.lhs ?? [])
334
+ if (e.kind === "Ident") declare(e.name);
335
+ break;
336
+ case "TypeSwitchStmt":
337
+ declare(node.assign);
338
+ break;
339
+ case "AssignStmt":
340
+ for (const e of node.lhs) flag(e, node);
341
+ break;
342
+ case "IncDecStmt":
343
+ flag(node.expr, node);
344
+ break;
345
+ case "UnaryExpr":
346
+ if (node.op === "&") flag(node.operand, node);
347
+ break;
348
+ case "SelectorExpr":
349
+ // Pointer-receiver method call or value: `G.Bump()` mutates G.
350
+ if (node._isMethodValue && node._type?._ptrRecv)
351
+ flag(node.expr, node);
352
+ break;
353
+ }
354
+ for (const key of Object.keys(node)) {
355
+ if (key.startsWith("_")) continue;
356
+ walk(node[key]);
357
+ }
358
+ if (opensScope) scopes.pop();
359
+ };
360
+ walk(body);
361
+ };
362
+
363
+ for (const p of programs) {
364
+ if (p._filename?.endsWith("_test.go")) continue;
365
+ this._currentFile = p._filename;
366
+ this._currentSource = p._source;
367
+ for (const d of p.decls) {
368
+ if ((d.kind === "FuncDecl" || d.kind === "MethodDecl") && d.body) {
369
+ if (d.name === "init" && d.kind === "FuncDecl") continue;
370
+ const locals = [];
371
+ if (d.recvName) locals.push(d.recvName);
372
+ for (const prm of d.params ?? []) if (prm.name) locals.push(prm.name);
373
+ for (const r of d.returnType?._namedReturns ?? [])
374
+ if (r.name) locals.push(r.name);
375
+ checkBody(d.body, locals);
376
+ } else if (d.kind === "VarDecl") {
377
+ // Closures in package-level initializers run after init.
378
+ for (const spec of d.decls ?? []) checkBody(spec.value, []);
379
+ }
380
+ }
381
+ }
382
+ }
383
+
178
384
  _collectTypesPass(programs) {
179
385
  for (const p of programs) {
180
386
  for (const d of p.decls) {
@@ -241,6 +447,7 @@ export class TypeChecker {
241
447
  if (d.kind === "FuncDecl" || d.kind === "MethodDecl")
242
448
  this.collectFunc(d);
243
449
  else if (d.kind === "TemplDecl") this._collectTemplDecl(d);
450
+ else if (d.kind === "CssDecl") this._collectCssDecl(d);
244
451
  }
245
452
  }
246
453
  }
@@ -267,7 +474,11 @@ export class TypeChecker {
267
474
  for (const p of programs) {
268
475
  this._setCurrentFile(p);
269
476
  for (const d of p.decls) {
270
- if (d.kind === "FuncDecl" || d.kind === "MethodDecl") {
477
+ if (
478
+ d.kind === "FuncDecl" ||
479
+ d.kind === "MethodDecl" ||
480
+ d.kind === "TemplDecl"
481
+ ) {
271
482
  this.checkTopDecl(d, this.globals);
272
483
  }
273
484
  }
@@ -457,11 +668,75 @@ export class TypeChecker {
457
668
  this.checkConstDecl(decl, scope);
458
669
  break;
459
670
  case "TypeDecl":
671
+ case "CssDecl":
672
+ break;
460
673
  case "TemplDecl":
461
- break; // already collected / no body to check
674
+ this._markTemplNodesUsed(decl.body);
675
+ break;
676
+ }
677
+ }
678
+
679
+ _markTokensUsed(tokens) {
680
+ if (!tokens) return;
681
+ for (const t of tokens) {
682
+ if (t.type === "IDENT" || t.type?.name === "IDENT") {
683
+ this.globals.lookup(t.value);
684
+ }
685
+ }
686
+ }
687
+
688
+ _markTemplNodesUsed(nodes) {
689
+ if (!nodes) return;
690
+ for (const n of nodes) {
691
+ if (n.kind === "TemplElement") {
692
+ for (const a of n.attrs || []) {
693
+ if (a.tokens) this._markTokensUsed(a.tokens);
694
+ }
695
+ this._markTemplNodesUsed(n.children);
696
+ } else if (n.kind === "TemplExpr" || n.kind === "TemplComponent") {
697
+ this._markTokensUsed(n.tokens);
698
+ } else if (n.kind === "TemplIf") {
699
+ this._markTokensUsed(n.condTokens);
700
+ this._markTemplNodesUsed(n.then);
701
+ this._markTemplNodesUsed(n.else_);
702
+ } else if (n.kind === "TemplFor") {
703
+ this._markTokensUsed(n.stmtTokens);
704
+ this._markTemplNodesUsed(n.body);
705
+ } else if (n.kind === "TemplSwitch") {
706
+ this._markTokensUsed(n.exprTokens);
707
+ for (const c of n.cases || []) {
708
+ this._markTokensUsed(c.caseTokens);
709
+ this._markTemplNodesUsed(c.body);
710
+ }
711
+ }
462
712
  }
463
713
  }
464
714
 
715
+ _collectCssDecl(decl) {
716
+ if (this.target === "wasm" || this.target === "both") {
717
+ this.recordBlocker("css declaration");
718
+ this.err("css declarations are not allowed in wasm packages", decl);
719
+ }
720
+ const paramTypes = (decl.params || []).map((p) =>
721
+ this.resolveTypeNode(p.type, this.globals),
722
+ );
723
+ const stringType = this.types.get("string") ?? {
724
+ kind: "basic",
725
+ name: "string",
726
+ };
727
+ const funcType = {
728
+ kind: "func",
729
+ params: paramTypes,
730
+ returns: [stringType],
731
+ variadic: false,
732
+ async: false,
733
+ };
734
+ if (this.globals.symbols.has(decl.name) && decl.name !== "init") {
735
+ this.err(`${decl.name} redeclared in this block`, decl);
736
+ }
737
+ this.globals.define(decl.name, funcType);
738
+ }
739
+
465
740
  _collectTemplDecl(decl) {
466
741
  const paramTypes = decl.params.map((p) =>
467
742
  this.resolveTypeNode(p.type, this.globals),
@@ -486,6 +761,16 @@ export class TypeChecker {
486
761
  }
487
762
 
488
763
  checkFuncDecl(decl, outer) {
764
+ if (
765
+ (this.target === "wasm" || this.target === "both") &&
766
+ (decl.async || decl.isAsync)
767
+ ) {
768
+ this.recordBlocker("async function", decl.name);
769
+ this.err(
770
+ "async functions are not supported in wasm packages; keep async code in a js package",
771
+ decl,
772
+ );
773
+ }
489
774
  const inner = new Scope(outer);
490
775
  this._injectTypeParams(decl, inner, outer);
491
776
  for (const p of decl.params) {
@@ -497,6 +782,16 @@ export class TypeChecker {
497
782
  }
498
783
 
499
784
  checkMethodDecl(decl, outer) {
785
+ if (
786
+ (this.target === "wasm" || this.target === "both") &&
787
+ (decl.async || decl.isAsync)
788
+ ) {
789
+ this.recordBlocker("async function", decl.name);
790
+ this.err(
791
+ "async functions are not supported in wasm packages; keep async code in a js package",
792
+ decl,
793
+ );
794
+ }
500
795
  const inner = new Scope(outer);
501
796
  const recvTypeName =
502
797
  decl.recvType.kind === "GenericTypeName"
@@ -43,6 +43,13 @@ export const statementCheckMethods = {
43
43
  this.checkExpr(stmt.expr, scope);
44
44
  break;
45
45
  case "DeferStmt":
46
+ if (this.target === "wasm" || this.target === "both") {
47
+ this.recordBlocker("defer statement");
48
+ this.err(
49
+ "'defer' is not yet supported in wasm packages (planned)",
50
+ stmt,
51
+ );
52
+ }
46
53
  if (stmt.call.kind !== "CallExpr")
47
54
  this.err("defer requires a function call", stmt.call);
48
55
  this.checkExpr(stmt.call, scope);
@@ -9,6 +9,8 @@ import {
9
9
  INT,
10
10
  STRING,
11
11
  UINT8,
12
+ UINT32,
13
+ UINT64,
12
14
  VOID,
13
15
  } from "../types.js";
14
16
 
@@ -428,6 +430,21 @@ export function setupCoreGlobals(globals, types) {
428
430
  },
429
431
  });
430
432
 
433
+ globals.define("bits", {
434
+ kind: "namespace",
435
+ name: "bits",
436
+ members: {
437
+ LeadingZeros32: { kind: "func", params: [UINT32], returns: [INT] },
438
+ TrailingZeros32: { kind: "func", params: [UINT32], returns: [INT] },
439
+ OnesCount32: { kind: "func", params: [UINT32], returns: [INT] },
440
+ RotateLeft32: { kind: "func", params: [UINT32, INT], returns: [UINT32] },
441
+ LeadingZeros64: { kind: "func", params: [UINT64], returns: [INT] },
442
+ TrailingZeros64: { kind: "func", params: [UINT64], returns: [INT] },
443
+ OnesCount64: { kind: "func", params: [UINT64], returns: [INT] },
444
+ RotateLeft64: { kind: "func", params: [UINT64, INT], returns: [UINT64] },
445
+ },
446
+ });
447
+
431
448
  globals.define("errors", {
432
449
  kind: "namespace",
433
450
  name: "errors",
@@ -13,7 +13,7 @@
13
13
  // { kind: 'named', name: string, underlying: Type }
14
14
 
15
15
  export class TypeCheckError extends Error {
16
- constructor(msg, node, filename, sourceCode) {
16
+ constructor(msg, node, filename, sourceCode, hint = null) {
17
17
  const lineNum = node?.line || node?._line;
18
18
  const colNum = node?.col || node?._col;
19
19
  const loc = filename
@@ -23,6 +23,7 @@ export class TypeCheckError extends Error {
23
23
  : lineNum
24
24
  ? ` at line ${lineNum}:${colNum ?? 1}`
25
25
  : "";
26
+ const hintText = hint ? `\n ${hint}` : "";
26
27
  let lineContext = "";
27
28
  if (lineNum && sourceCode) {
28
29
  const lines = sourceCode.split("\n");
@@ -33,10 +34,63 @@ export class TypeCheckError extends Error {
33
34
  lineContext = `\n${prefix}${lineStr}\n${" ".repeat(prefix.length)}${caretPad}^`;
34
35
  }
35
36
  }
36
- super(`Type error${loc}: ${msg}${lineContext}`);
37
+ super(`Type error${loc}: ${msg}${hintText}${lineContext}`);
38
+ this.line = lineNum;
39
+ this.col = colNum;
37
40
  }
38
41
  }
39
42
 
43
+ export const BROWSER_GLOBALS = new Set([
44
+ "console",
45
+ "document",
46
+ "window",
47
+ "navigator",
48
+ "location",
49
+ "history",
50
+ "screen",
51
+ "performance",
52
+ "crypto",
53
+ "indexedDB",
54
+ "fetch",
55
+ "setTimeout",
56
+ "setInterval",
57
+ "clearTimeout",
58
+ "clearInterval",
59
+ "requestAnimationFrame",
60
+ "cancelAnimationFrame",
61
+ "Math",
62
+ "JSON",
63
+ "Date",
64
+ "RegExp",
65
+ "Promise",
66
+ "Error",
67
+ "Symbol",
68
+ "String",
69
+ "Number",
70
+ "Boolean",
71
+ "Array",
72
+ "Object",
73
+ "parseInt",
74
+ "parseFloat",
75
+ "isNaN",
76
+ "isFinite",
77
+ "encodeURIComponent",
78
+ "decodeURIComponent",
79
+ "atob",
80
+ "btoa",
81
+ "alert",
82
+ "confirm",
83
+ "prompt",
84
+ "localStorage",
85
+ "sessionStorage",
86
+ ]);
87
+
88
+ // Stdlib packages the WASM backend can emit. Extend as src/backend/wasm/emit.js gains support;
89
+ // anything not listed is rejected at import with "not yet available in wasm packages".
90
+ export const WASM_SUPPORTED_STDLIB = new Set(["math", "math/bits", "testing"]);
91
+
92
+ export const VALID_TARGETS = new Set(["js", "wasm", "both"]);
93
+
40
94
  // ── Static operator sets (module-level for reuse) ────────────
41
95
  export const CMP_OPS = new Set(["==", "!=", "<", ">", "<=", ">="]);
42
96
  export const LOG_OPS = new Set(["&&", "||"]);
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes