gofront 1.4.0 → 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 (61) hide show
  1. package/README.md +72 -9
  2. package/package.json +2 -1
  3. package/src/{codegen → backend/js}/expressions.js +217 -60
  4. package/src/{codegen → backend/js}/index.js +21 -35
  5. package/src/{codegen → backend/js}/statements.js +43 -48
  6. package/src/backend/js/stdlib/bits.js +32 -0
  7. package/src/{codegen → backend/js}/stdlib/index.js +3 -0
  8. package/src/{codegen → backend/js}/templ.js +2 -2
  9. package/src/backend/wasm/boundary.js +1345 -0
  10. package/src/backend/wasm/emit.js +5779 -0
  11. package/src/backend/wasm/encode.js +789 -0
  12. package/src/backend/wasm/glue.js +130 -0
  13. package/src/backend/wasm/index.js +1413 -0
  14. package/src/backend/wasm/types.js +420 -0
  15. package/src/backend/wasm/wat.js +255 -0
  16. package/src/cli-core.js +52 -7
  17. package/src/compiler.js +322 -12
  18. package/src/dev-server.js +56 -15
  19. package/src/index.js +17 -9
  20. package/src/lexer.js +18 -0
  21. package/src/lower/boxing.js +36 -0
  22. package/src/lower/captures.js +179 -0
  23. package/src/lower/embedding.js +30 -0
  24. package/src/lower/escape.js +169 -0
  25. package/src/lower/functions.js +30 -0
  26. package/src/lower/globals.js +324 -0
  27. package/src/lower/index.js +115 -0
  28. package/src/lower/ownership.js +142 -0
  29. package/src/lower/range.js +70 -0
  30. package/src/minifier.js +2 -0
  31. package/src/parser/index.js +8 -3
  32. package/src/resolver.js +1 -0
  33. package/src/test-runner.js +74 -20
  34. package/src/typechecker/expressions.js +40 -1
  35. package/src/typechecker/index.js +234 -4
  36. package/src/typechecker/statements.js +7 -0
  37. package/src/typechecker/stdlib/core.js +17 -0
  38. package/src/typechecker/types.js +56 -2
  39. /package/src/{codegen → backend/js}/runtime.js +0 -0
  40. /package/src/{codegen → backend/js}/source-map.js +0 -0
  41. /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
  42. /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
  43. /package/src/{codegen → backend/js}/stdlib/errors.js +0 -0
  44. /package/src/{codegen → backend/js}/stdlib/fmt.js +0 -0
  45. /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
  46. /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
  47. /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
  48. /package/src/{codegen → backend/js}/stdlib/maps.js +0 -0
  49. /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
  50. /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
  51. /package/src/{codegen → backend/js}/stdlib/path.js +0 -0
  52. /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
  53. /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
  54. /package/src/{codegen → backend/js}/stdlib/slices.js +0 -0
  55. /package/src/{codegen → backend/js}/stdlib/sort.js +0 -0
  56. /package/src/{codegen → backend/js}/stdlib/strconv.js +0 -0
  57. /package/src/{codegen → backend/js}/stdlib/strings.js +0 -0
  58. /package/src/{codegen → backend/js}/stdlib/testing.js +0 -0
  59. /package/src/{codegen → backend/js}/stdlib/time.js +0 -0
  60. /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
  61. /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
@@ -114,6 +114,8 @@ export class Parser {
114
114
  pkg,
115
115
  imports,
116
116
  decls,
117
+ target: this.tokens?.target ?? null,
118
+ _targetDirective: this.tokens?.targetDirective ?? null,
117
119
  _filename: this.filename,
118
120
  _source: this.source,
119
121
  };
@@ -130,7 +132,9 @@ export class Parser {
130
132
  this.expect(T.IMPORT);
131
133
  const imports = [];
132
134
  const parseOne = () => {
133
- const _line = this.peek().line;
135
+ const tok = this.peek();
136
+ const _line = tok.line;
137
+ const _col = tok.col;
134
138
  let alias = null;
135
139
  if (this.check(T.IDENT)) {
136
140
  alias = this.advance().value;
@@ -138,8 +142,9 @@ export class Parser {
138
142
  alias = ".";
139
143
  this.advance();
140
144
  }
141
- const path = this.expect(T.STRING).value;
142
- return { path, alias, _line };
145
+ const pathTok = this.expect(T.STRING);
146
+ const path = pathTok.value;
147
+ return { path, alias, _line, _col, line: pathTok.line, col: pathTok.col };
143
148
  };
144
149
  if (this.match(T.LPAREN)) {
145
150
  while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
package/src/resolver.js CHANGED
@@ -33,6 +33,7 @@ const BUILTIN_PACKAGES = new Set([
33
33
  "strconv",
34
34
  "sort",
35
35
  "math",
36
+ "math/bits",
36
37
  "math/rand",
37
38
  "errors",
38
39
  "time",
@@ -6,8 +6,13 @@ import { statSync } from "node:fs";
6
6
  import { createRequire } from "node:module";
7
7
  import { basename, dirname, join, resolve } from "node:path";
8
8
  import { pathToFileURL } from "node:url";
9
+ import { HELPER_SPRINTF, HELPER_TESTING } from "./backend/js/runtime.js";
9
10
  import { colorEnabled, createColors, formatDiagnostic } from "./colors.js";
10
- import { compilePackageTests, gwFilesIn } from "./compiler.js";
11
+ import {
12
+ compilePackageTests,
13
+ compilePackageTestsWasm,
14
+ gwFilesIn,
15
+ } from "./compiler.js";
11
16
 
12
17
  export function isTestFunc(decl) {
13
18
  if (decl.kind !== "FuncDecl") return false;
@@ -78,7 +83,16 @@ globalThis.document = globalThis.document ?? null;
78
83
  `
79
84
  }
80
85
  globalThis.__gofront_verbose = ${verbose};
81
-
86
+ ${
87
+ options.wasmBytes
88
+ ? `
89
+ // ── WASM module (tests run inside app.wasm; *testing.T stays in JS) ──
90
+ ${HELPER_SPRINTF}
91
+ ${HELPER_TESTING}
92
+ globalThis.__GOFRONT_WASM_BYTES = Uint8Array.from(atob(${JSON.stringify(Buffer.from(options.wasmBytes).toString("base64"))}), (c) => c.charCodeAt(0));
93
+ `
94
+ : ""
95
+ }
82
96
  // ── Compiled Package Bundle ──
83
97
  ${bundleJs}
84
98
  // ─────────────────────────────
@@ -281,6 +295,44 @@ function spawnTestRunner(resolvedDir, harnessJs, testCount, options) {
281
295
  });
282
296
  }
283
297
 
298
+ function reportBuildFailure(err, resolvedDir, options) {
299
+ const msg = err?.message ?? String(err);
300
+ if (options.captureOutput) {
301
+ const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
302
+ return { exitCode: 1, stdout: "", stderr: out };
303
+ }
304
+ const dest = options.stderr ?? process.stderr;
305
+ const c = colorsFor(options, dest);
306
+ dest.write(
307
+ `${c.bold(c.red("FAIL"))}\t${basename(resolvedDir)} ${c.red("[build failed]")}\n${formatDiagnostic(msg, c)}\n`,
308
+ );
309
+ return { exitCode: 1 };
310
+ }
311
+
312
+ // `both` packages run twice (JS and wasm) so the two backends are kept in
313
+ // agreement; output of both runs is concatenated.
314
+ async function runDualTarget(compiled, resolvedDir, runHarness, options) {
315
+ let wasmCompiled;
316
+ try {
317
+ wasmCompiled = compilePackageTestsWasm(resolvedDir, options);
318
+ } catch (err) {
319
+ return reportBuildFailure(err, resolvedDir, options);
320
+ }
321
+ const label = compiled.pkgName;
322
+ const jsRun = await runHarness(compiled.js, `${label} [js]`, null);
323
+ const wasmRun = await runHarness(
324
+ wasmCompiled.js,
325
+ `${label} [wasm]`,
326
+ wasmCompiled.wasm,
327
+ );
328
+ return {
329
+ exitCode: jsRun.exitCode !== 0 ? jsRun.exitCode : wasmRun.exitCode,
330
+ stdout: jsRun.stdout + wasmRun.stdout,
331
+ stderr: jsRun.stderr + wasmRun.stderr,
332
+ testCount: jsRun.testCount + wasmRun.testCount,
333
+ };
334
+ }
335
+
284
336
  export async function runTests(targetDir, options = {}) {
285
337
  let resolvedDir = resolve(targetDir);
286
338
  try {
@@ -304,17 +356,7 @@ export async function runTests(targetDir, options = {}) {
304
356
  try {
305
357
  compiled = compilePackageTests(resolvedDir, options);
306
358
  } catch (err) {
307
- const msg = err?.message ?? String(err);
308
- if (options.captureOutput) {
309
- const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
310
- return { exitCode: 1, stdout: "", stderr: out };
311
- }
312
- const dest = options.stderr ?? process.stderr;
313
- const c = colorsFor(options, dest);
314
- dest.write(
315
- `${c.bold(c.red("FAIL"))}\t${basename(resolvedDir)} ${c.red("[build failed]")}\n${formatDiagnostic(msg, c)}\n`,
316
- );
317
- return { exitCode: 1 };
359
+ return reportBuildFailure(err, resolvedDir, options);
318
360
  }
319
361
 
320
362
  const { pkgName, js, programs } = compiled;
@@ -324,12 +366,24 @@ export async function runTests(targetDir, options = {}) {
324
366
  return reportEmptyTests(pkgName, "no tests to run", options);
325
367
  }
326
368
 
327
- const harnessJs = generateTestHarness(js, testNames, {
328
- ...options,
329
- pkgName,
330
- jsdomPath,
331
- color: colorsFor(options, options.stdout ?? process.stdout).enabled,
332
- });
369
+ const runHarness = (bundleJs, label, wasmBytes) =>
370
+ spawnTestRunner(
371
+ resolvedDir,
372
+ generateTestHarness(bundleJs, testNames, {
373
+ ...options,
374
+ pkgName: label,
375
+ jsdomPath,
376
+ wasmBytes,
377
+ color: colorsFor(options, options.stdout ?? process.stdout).enabled,
378
+ }),
379
+ testNames.length,
380
+ options,
381
+ );
333
382
 
334
- return spawnTestRunner(resolvedDir, harnessJs, testNames.length, options);
383
+ // `wasm` packages run inside the linked app.wasm; `js` packages that import
384
+ // wasm packages need the linked module too (hybrid packages).
385
+ if (compiled.target === "both") {
386
+ return runDualTarget(compiled, resolvedDir, runHarness, options);
387
+ }
388
+ return runHarness(js, pkgName, compiled.wasm ?? null);
335
389
  }
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  ANY,
5
5
  BOOL,
6
+ BROWSER_GLOBALS,
6
7
  COMPLEX128,
7
8
  ERROR,
8
9
  FLOAT64,
@@ -126,6 +127,26 @@ export const expressionCheckMethods = {
126
127
  const t = scope.lookup(expr.name);
127
128
  if (!t) return this.err(`Undefined: '${expr.name}'`, expr);
128
129
  if (this.types.has(expr.name)) expr._isTypeRef = true;
130
+ if (
131
+ (this.target === "wasm" || this.target === "both") &&
132
+ BROWSER_GLOBALS.has(expr.name) &&
133
+ scope.lookup(expr.name) === this.globals.lookup(expr.name)
134
+ ) {
135
+ this.recordBlocker("browser global", expr.name);
136
+ this.err(
137
+ `'${expr.name}' is not available in wasm packages`,
138
+ expr,
139
+ `(package '${this.pkgName}' is //gofront:target ${this.target} — browser globals are only available in js packages)`,
140
+ );
141
+ }
142
+ if (
143
+ (this.target === "wasm" || this.target === "both") &&
144
+ expr.name === "gom" &&
145
+ scope.lookup(expr.name) === this.globals.lookup(expr.name)
146
+ ) {
147
+ this.recordBlocker("gom usage", "gom");
148
+ this.err("package 'gom' is not available in wasm packages", expr);
149
+ }
129
150
  return t;
130
151
  }
131
152
  case "BinaryExpr": {
@@ -136,8 +157,16 @@ export const expressionCheckMethods = {
136
157
  return this.binaryResultType(expr.op, ANY, ANY, expr);
137
158
  return this.binaryResultType(expr.op, lt, rt, expr);
138
159
  }
139
- case "AwaitExpr":
160
+ case "AwaitExpr": {
161
+ if (this.target === "wasm" || this.target === "both") {
162
+ this.recordBlocker("async function");
163
+ this.err(
164
+ "async functions are not supported in wasm packages; keep async code in a js package",
165
+ expr,
166
+ );
167
+ }
140
168
  return this.checkExpr(expr.expr, scope);
169
+ }
141
170
  case "InstantiationExpr":
142
171
  return this._checkInstantiationExpr(expr, scope);
143
172
  default: {
@@ -280,6 +309,16 @@ export const expressionCheckMethods = {
280
309
  },
281
310
 
282
311
  _checkFuncLit(expr, scope) {
312
+ if (
313
+ (this.target === "wasm" || this.target === "both") &&
314
+ (expr.async || expr.isAsync)
315
+ ) {
316
+ this.recordBlocker("async function");
317
+ this.err(
318
+ "async functions are not supported in wasm packages; keep async code in a js package",
319
+ expr,
320
+ );
321
+ }
283
322
  const inner = new Scope(scope);
284
323
  for (const p of expr.params)
285
324
  inner.define(p.name, p.type ? this.resolveTypeNode(p.type, scope) : ANY);
@@ -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) {
@@ -507,6 +713,10 @@ export class TypeChecker {
507
713
  }
508
714
 
509
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
+ }
510
720
  const paramTypes = (decl.params || []).map((p) =>
511
721
  this.resolveTypeNode(p.type, this.globals),
512
722
  );
@@ -551,6 +761,16 @@ export class TypeChecker {
551
761
  }
552
762
 
553
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
+ }
554
774
  const inner = new Scope(outer);
555
775
  this._injectTypeParams(decl, inner, outer);
556
776
  for (const p of decl.params) {
@@ -562,6 +782,16 @@ export class TypeChecker {
562
782
  }
563
783
 
564
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
+ }
565
795
  const inner = new Scope(outer);
566
796
  const recvTypeName =
567
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