gofront 1.8.0 → 1.9.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 (178) hide show
  1. package/README.md +3 -3
  2. package/package.json +6 -5
  3. package/src/backend/js/expressions.js +1157 -408
  4. package/src/backend/js/index.js +327 -153
  5. package/src/backend/js/natives.js +383 -0
  6. package/src/backend/js/runtime.js +559 -2552
  7. package/src/backend/js/statements.js +670 -158
  8. package/src/backend/js/stdlib/index.js +45 -93
  9. package/src/backend/js/templ.js +32 -45
  10. package/src/backend/js/type-info.js +298 -19
  11. package/src/backend/shared/fixed-float.js +20 -0
  12. package/src/backend/shared/fmt-types.js +91 -170
  13. package/src/backend/shared/host-unicode.js +36 -0
  14. package/src/backend/wasm/boundary.js +490 -965
  15. package/src/backend/wasm/emit-builtins.js +884 -256
  16. package/src/backend/wasm/emit-exprs.js +185 -77
  17. package/src/backend/wasm/emit-maps.js +17 -367
  18. package/src/backend/wasm/emit-shared.js +4 -1
  19. package/src/backend/wasm/emit-stmts.js +524 -240
  20. package/src/backend/wasm/emit.js +176 -38
  21. package/src/backend/wasm/globals.js +48 -8
  22. package/src/backend/wasm/glue.js +2 -4
  23. package/src/backend/wasm/index.js +44 -530
  24. package/src/backend/wasm/method-expr.js +12 -1
  25. package/src/backend/wasm/module-emitter.js +299 -770
  26. package/src/backend/wasm/monomorph.js +139 -8
  27. package/src/backend/wasm/natives.js +600 -0
  28. package/src/backend/wasm/prescan.js +5 -153
  29. package/src/backend/wasm/prune-funcs.js +89 -0
  30. package/src/backend/wasm/stdlib/index.js +2 -32
  31. package/src/backend/wasm/types.js +97 -20
  32. package/src/backend/wasm/variadic.js +9 -4
  33. package/src/cli/cli-core.js +5 -1
  34. package/src/cli/test-runner.js +115 -342
  35. package/src/compiler.js +113 -130
  36. package/src/lexer/lexer.js +106 -11
  37. package/src/lexer/templ-lexer.js +61 -18
  38. package/src/lexer/tokens.js +1 -0
  39. package/src/link/stdlib.js +355 -132
  40. package/src/link/type-names.js +204 -0
  41. package/src/link/wasm.js +30 -6
  42. package/src/lower/bounds.js +153 -0
  43. package/src/lower/captures.js +92 -33
  44. package/src/lower/embedding.js +62 -5
  45. package/src/lower/errors-as.js +196 -0
  46. package/src/lower/fmt-formatters.js +929 -0
  47. package/src/lower/functions.js +225 -7
  48. package/src/lower/gen-types.js +293 -0
  49. package/src/lower/index.js +14 -2
  50. package/src/lower/json-codecs.js +334 -245
  51. package/src/lower/ownership.js +25 -7
  52. package/src/lower/reachability.js +69 -51
  53. package/src/lower/walk.js +12 -0
  54. package/src/parser/declarations.js +30 -5
  55. package/src/parser/expressions.js +111 -57
  56. package/src/parser/statements.js +108 -0
  57. package/src/parser/templ.js +44 -5
  58. package/src/parser/types.js +12 -0
  59. package/src/resolver.js +1 -2
  60. package/src/typechecker/assignability.js +214 -19
  61. package/src/typechecker/const-fold.js +70 -27
  62. package/src/typechecker/constants.js +156 -8
  63. package/src/typechecker/expressions.js +490 -108
  64. package/src/typechecker/index.js +346 -42
  65. package/src/typechecker/init-order.js +136 -40
  66. package/src/typechecker/operand-text.js +41 -0
  67. package/src/typechecker/resolve.js +490 -65
  68. package/src/typechecker/statements.js +288 -27
  69. package/src/typechecker/stdlib/excluded.js +9 -5
  70. package/src/typechecker/stdlib/index.js +25 -9
  71. package/src/typechecker/termination.js +10 -1
  72. package/src/typechecker/types.js +287 -575
  73. package/stdlib/bytes/buffer.go +479 -0
  74. package/stdlib/bytes/bytes.go +1010 -0
  75. package/stdlib/bytes/reader.go +161 -0
  76. package/stdlib/context/context.go +115 -0
  77. package/stdlib/crypto/rand/rand.go +34 -0
  78. package/stdlib/crypto/sha256/sha256.go +249 -0
  79. package/stdlib/encoding/base64/base64.go +331 -0
  80. package/stdlib/encoding/binary/binary.go +332 -0
  81. package/stdlib/encoding/hex/hex.go +140 -0
  82. package/stdlib/encoding/json/json.go +62 -0
  83. package/stdlib/errors/errors.go +174 -0
  84. package/stdlib/fmt/errors.go +110 -0
  85. package/stdlib/fmt/format.go +732 -0
  86. package/stdlib/fmt/print.go +1232 -0
  87. package/stdlib/gom/gom.go +293 -0
  88. package/stdlib/html/html.go +251 -0
  89. package/stdlib/internal/fmtrt/fmtrt.go +163 -0
  90. package/stdlib/internal/jsonrt/dec.go +171 -38
  91. package/stdlib/internal/jsonrt/enc.go +186 -36
  92. package/stdlib/internal/jsonrt/scanner.go +17 -8
  93. package/stdlib/io/io.go +157 -1
  94. package/stdlib/maps/maps.go +131 -0
  95. package/stdlib/math/bits/bits.go +507 -0
  96. package/stdlib/math/extra.go +16 -14
  97. package/stdlib/math/math.go +439 -0
  98. package/stdlib/math/rand/rand.go +297 -0
  99. package/stdlib/os/os.go +93 -0
  100. package/stdlib/path/filepath/filepath.go +47 -0
  101. package/stdlib/path/path.go +394 -0
  102. package/stdlib/regexp/regexp.go +231 -0
  103. package/stdlib/slices/slices.go +526 -0
  104. package/stdlib/slices/sort.go +209 -0
  105. package/stdlib/slices/zsort_cmp.go +480 -0
  106. package/stdlib/slices/zsort_ordered.go +482 -0
  107. package/stdlib/sort/sort.go +391 -452
  108. package/stdlib/sort/zsort_func.go +480 -0
  109. package/stdlib/sort/zsort_iface.go +480 -0
  110. package/stdlib/strconv/float.go +1699 -0
  111. package/stdlib/strconv/isprint.go +752 -0
  112. package/stdlib/strconv/quote.go +235 -50
  113. package/stdlib/strconv/strconv.go +616 -0
  114. package/stdlib/strings/builder.go +95 -0
  115. package/stdlib/strings/reader.go +163 -0
  116. package/stdlib/strings/replacer.go +102 -0
  117. package/stdlib/strings/strings.go +807 -0
  118. package/stdlib/sync/sync.go +268 -0
  119. package/stdlib/testing/async_js.go +61 -0
  120. package/stdlib/testing/benchmark.go +297 -0
  121. package/stdlib/testing/testing.go +523 -0
  122. package/stdlib/time/duration.go +233 -0
  123. package/stdlib/time/format.go +745 -0
  124. package/stdlib/time/parse.go +790 -0
  125. package/stdlib/time/sleep_js.go +63 -0
  126. package/stdlib/time/time.go +834 -0
  127. package/stdlib/unicode/unicode.go +141 -3
  128. package/stdlib/unicode/utf8/utf8.go +464 -0
  129. package/src/backend/js/fmt-runtime.js +0 -573
  130. package/src/backend/js/stdlib/base64.js +0 -12
  131. package/src/backend/js/stdlib/binary.js +0 -102
  132. package/src/backend/js/stdlib/bits.js +0 -119
  133. package/src/backend/js/stdlib/builder.js +0 -98
  134. package/src/backend/js/stdlib/bytes.js +0 -368
  135. package/src/backend/js/stdlib/errors.js +0 -45
  136. package/src/backend/js/stdlib/fmt.js +0 -517
  137. package/src/backend/js/stdlib/gom.js +0 -155
  138. package/src/backend/js/stdlib/hex.js +0 -17
  139. package/src/backend/js/stdlib/html.js +0 -20
  140. package/src/backend/js/stdlib/io.js +0 -137
  141. package/src/backend/js/stdlib/json.js +0 -1167
  142. package/src/backend/js/stdlib/maps.js +0 -37
  143. package/src/backend/js/stdlib/math.js +0 -85
  144. package/src/backend/js/stdlib/os.js +0 -13
  145. package/src/backend/js/stdlib/path.js +0 -48
  146. package/src/backend/js/stdlib/rand.js +0 -32
  147. package/src/backend/js/stdlib/regexp.js +0 -54
  148. package/src/backend/js/stdlib/sha256.js +0 -12
  149. package/src/backend/js/stdlib/shared.js +0 -17
  150. package/src/backend/js/stdlib/simd.js +0 -34
  151. package/src/backend/js/stdlib/slices.js +0 -107
  152. package/src/backend/js/stdlib/sort.js +0 -30
  153. package/src/backend/js/stdlib/strconv.js +0 -79
  154. package/src/backend/js/stdlib/strings.js +0 -301
  155. package/src/backend/js/stdlib/sync.js +0 -13
  156. package/src/backend/js/stdlib/testing.js +0 -41
  157. package/src/backend/js/stdlib/time.js +0 -152
  158. package/src/backend/js/stdlib/unicode.js +0 -25
  159. package/src/backend/js/stdlib/utf8.js +0 -26
  160. package/src/backend/wasm/stdlib/bytes.js +0 -6716
  161. package/src/backend/wasm/stdlib/core.js +0 -10278
  162. package/src/backend/wasm/stdlib/io.js +0 -3578
  163. package/src/backend/wasm/stdlib/packages.js +0 -2402
  164. package/src/backend/wasm/stdlib/pure.js +0 -720
  165. package/src/backend/wasm/stdlib/time.js +0 -2229
  166. package/src/typechecker/stdlib/core.js +0 -1079
  167. package/src/typechecker/stdlib/extended.js +0 -1210
  168. package/stdlib/bytes/iter.go +0 -192
  169. package/stdlib/bytes/search.go +0 -240
  170. package/stdlib/bytes/split.go +0 -218
  171. package/stdlib/errors/overlay.go +0 -12
  172. package/stdlib/maps/iter.go +0 -65
  173. package/stdlib/slices/extra.go +0 -73
  174. package/stdlib/slices/iter.go +0 -113
  175. package/stdlib/strings/extra.go +0 -35
  176. package/stdlib/strings/iter.go +0 -122
  177. package/stdlib/sync/extra.go +0 -105
  178. package/stdlib/unicode/utf8/bytes.go +0 -239
package/src/compiler.js CHANGED
@@ -32,17 +32,20 @@ import {
32
32
  WASM_MAP_URL,
33
33
  } from "./link/sourcemap.js";
34
34
  import {
35
- checkJsonCodecs,
35
+ checkGeneratedGo,
36
36
  finishStdlibJs,
37
37
  importStdlibPackages,
38
+ mangleCollidingTypes,
39
+ registerLocalTypeOwners,
40
+ renameLocalTypes,
41
+ withOriginalTypeNames,
38
42
  } from "./link/stdlib.js";
39
43
  import {
40
44
  linkIntoBundle,
41
45
  registerPackageUnit,
42
- registerWasmUnit,
43
46
  wasmMarker,
44
47
  } from "./link/wasm.js";
45
- import { lower } from "./lower/index.js";
48
+ import { lower, takesElementAddress } from "./lower/index.js";
46
49
  import { Parser } from "./parser/index.js";
47
50
  import { TemplParser } from "./parser/templ.js";
48
51
  import {
@@ -60,10 +63,21 @@ import { WASM_SUPPORTED_STDLIB } from "./typechecker/types.js";
60
63
  // Map<filePath, { mtime: number, ast: object }>
61
64
  const _parseCache = new Map();
62
65
 
66
+ // A cached AST keeps everything the build attached to it (checked types,
67
+ // lowered stdlib programs), so a build of a Go-source stdlib importer pins
68
+ // megabytes. The cache is therefore bounded; the least recently used file goes
69
+ // first.
70
+ const PARSE_CACHE_LIMIT = 16;
71
+
63
72
  export function clearParseCache() {
64
73
  _parseCache.clear();
65
74
  }
66
75
 
76
+ // Drops one file, e.g. a throwaway source that was deleted after its build.
77
+ export function evictParseCache(filePath) {
78
+ _parseCache.delete(resolve(filePath));
79
+ }
80
+
67
81
  export function parseCacheSize() {
68
82
  return _parseCache.size;
69
83
  }
@@ -71,9 +85,14 @@ export function parseCacheSize() {
71
85
  // ── Helpers ──────────────────────────────────────────────────
72
86
 
73
87
  function parseGoFrontFile(filePath) {
88
+ const key = resolve(filePath);
74
89
  const mtime = statSync(filePath).mtimeMs;
75
- const cached = _parseCache.get(filePath);
76
- if (cached && cached.mtime === mtime) return cached.ast;
90
+ const cached = _parseCache.get(key);
91
+ if (cached && cached.mtime === mtime) {
92
+ _parseCache.delete(key);
93
+ _parseCache.set(key, cached);
94
+ return cached.ast;
95
+ }
77
96
 
78
97
  const source = readFileSync(filePath, "utf8");
79
98
  const filename = basename(filePath);
@@ -88,7 +107,9 @@ function parseGoFrontFile(filePath) {
88
107
  ast._path = resolve(filePath);
89
108
  // compileFiles overwrites `target` with the resolved one; keep the directive.
90
109
  ast._declaredTarget = ast.target;
91
- _parseCache.set(filePath, { mtime, ast });
110
+ _parseCache.set(key, { mtime, ast });
111
+ if (_parseCache.size > PARSE_CACHE_LIMIT)
112
+ _parseCache.delete(_parseCache.keys().next().value);
92
113
  return ast;
93
114
  }
94
115
 
@@ -107,7 +128,7 @@ export function gwFilesIn(dir, { includeTests = false } = {}) {
107
128
  // wasm-opt when requested and produce the wasm source map.
108
129
  function finishWasm(options, merged, pkgTarget, outputDir) {
109
130
  const wantMap = Boolean(options.sourceMap && !options.isDependency);
110
- let { wasm, wat, sourceMapInfo } = linkIntoBundle(
131
+ let { wasm, wat, sourceMapInfo, stringTable } = linkIntoBundle(
111
132
  options,
112
133
  merged,
113
134
  pkgTarget,
@@ -132,7 +153,7 @@ function finishWasm(options, merged, pkgTarget, outputDir) {
132
153
  wasmOptInfo = opt;
133
154
  if (opt.sourceMap) wasmSourceMap = opt.sourceMap;
134
155
  }
135
- return { wasm, wat, wasmOptInfo, wasmSourceMap };
156
+ return { wasm, wat, wasmOptInfo, wasmSourceMap, stringTable };
136
157
  }
137
158
 
138
159
  // Prints non-fatal diagnostics (e.g. JS Number precision of a constant).
@@ -184,12 +205,6 @@ function resolveImports(
184
205
  "js: imports are not allowed in wasm packages",
185
206
  impNode,
186
207
  );
187
- } else if (path === "gom") {
188
- checker.recordBlocker("gom usage", "gom");
189
- checker.err(
190
- "package 'gom' is not available in wasm packages",
191
- impNode,
192
- );
193
208
  } else if (path === "gofront/shared" && pkgTarget === "both") {
194
209
  checker.err(
195
210
  "'gofront/shared' requires a wasm-only package (//gofront:target wasm): a both package has no linear memory on the JS side",
@@ -287,6 +302,9 @@ function resolveImports(
287
302
  isDependency: true,
288
303
  });
289
304
  if (!alreadyBundled) preambles.push(dep);
305
+ // A dependency that hands out `&xs[i]` makes whole-element
306
+ // stores in this package write in place too.
307
+ if (dep.elemAddrTaken) checker.depElemAddrTaken = true;
290
308
 
291
309
  // Target import rules for local packages
292
310
  const impNode = {
@@ -374,120 +392,29 @@ export function resolvePackageTarget(declared, pkgDir, options = {}) {
374
392
  }
375
393
 
376
394
  export function compileSingleFile(inputPath, options = {}) {
377
- const {
378
- sourceMap = false,
379
- outputFile = null,
380
- dumpTokens = false,
381
- dumpAst = false,
382
- } = options;
383
- options = {
384
- ...options,
385
- wasmUnits: options.wasmUnits ?? [],
386
- stdlibCache: options.stdlibCache ?? new Map(),
387
- jsRootPrograms: options.jsRootPrograms ?? [],
388
- bundledDirs: options.bundledDirs ?? new Set(),
389
- };
390
-
391
- let source;
392
- try {
393
- source = readFileSync(inputPath, "utf8");
394
- } catch (e) {
395
- throw new Error(`cannot read '${inputPath}': ${e.message}`);
396
- }
397
-
398
- const tokens = new Lexer(source, basename(inputPath)).tokenize();
399
- if (dumpTokens) return { tokens };
400
-
401
- const ast = new Parser(tokens, basename(inputPath), source).parse();
402
- ast._source = source;
403
- ast._path = resolve(inputPath);
404
- if (dumpAst) return { ast };
405
-
406
- const outputDir =
407
- options.outputDir ??
408
- (outputFile ? dirname(resolve(outputFile)) : resolve("."));
409
-
410
- const checker = new TypeChecker();
411
- const jsImports = new Map();
412
- const bundledPackages = new Set();
413
- const preambles = [];
414
-
415
- const pkgTarget = resolvePackageTarget(
416
- ast.target ?? options.target ?? "js",
417
- dirname(resolve(inputPath)),
418
- options,
419
- );
420
- ast.target = pkgTarget;
421
- checker.target = pkgTarget;
422
- checker.pkgName = ast.pkg?.name ?? "main";
423
-
424
- resolveImports(
425
- [ast],
426
- inputPath,
427
- checker,
428
- jsImports,
429
- bundledPackages,
430
- preambles,
431
- options.bundledDirs ?? new Set(),
432
- { ...options, outputDir },
433
- );
434
-
435
- const errors = checker.check(ast);
436
- checker.reportUnusedImports();
437
- if (errors.length > 0) {
438
- throw new Error(errors.map((e) => e.message).join("\n"));
439
- }
440
- for (const w of checker.warnings) log.warn(w);
395
+ const { outputFile = null, dumpTokens = false, dumpAst = false } = options;
441
396
 
442
- if (pkgTarget === "wasm" || pkgTarget === "both") {
443
- registerWasmUnit(options, {
444
- key: resolve(inputPath),
445
- pkgName: checker.pkgName,
446
- target: pkgTarget,
447
- programs: [ast],
448
- types: checker.types,
449
- bundledPackages,
450
- });
397
+ if (dumpTokens || dumpAst) {
398
+ let source;
399
+ try {
400
+ source = readFileSync(inputPath, "utf8");
401
+ } catch (e) {
402
+ throw new Error(`cannot read '${inputPath}': ${e.message}`);
403
+ }
404
+ const tokens = new Lexer(source, basename(inputPath)).tokenize();
405
+ if (dumpTokens) return { tokens };
406
+ const ast = new Parser(tokens, basename(inputPath), source).parse();
407
+ ast._source = source;
408
+ ast._path = resolve(inputPath);
409
+ return { ast };
451
410
  }
452
411
 
453
- const isStrict = Boolean(options.strict || pkgTarget === "both");
454
- const cg = new CodeGen(checker, jsImports, bundledPackages, {
455
- target: pkgTarget,
456
- strict: isStrict,
412
+ return compileFiles([resolve(inputPath)], {
413
+ ...options,
414
+ outputDir:
415
+ options.outputDir ??
416
+ (outputFile ? dirname(resolve(outputFile)) : resolve(".")),
457
417
  });
458
- const mainJs =
459
- pkgTarget === "wasm" ? wasmMarker(checker.pkgName) : cg.generate(ast);
460
- logWarnings(cg.warnings);
461
-
462
- const mainChunk = {
463
- js: mainJs,
464
- sourceFiles: [resolve(inputPath)],
465
- sourcesContent: [source],
466
- mappings: pkgTarget === "wasm" ? [] : cg.getMappings(),
467
- };
468
-
469
- const merged = mergeCompilationChunks([...preambles, mainChunk]);
470
- finishStdlibJs(merged, options, [ast], pkgTarget);
471
- const { wasm, wat, wasmOptInfo, wasmSourceMap } = finishWasm(
472
- { ...options, sourceMap },
473
- merged,
474
- pkgTarget,
475
- outputDir,
476
- );
477
-
478
- if (sourceMap && !options.isDependency) {
479
- attachSourceMap(merged, outputDir);
480
- }
481
-
482
- return {
483
- js: merged.js,
484
- css: cg.getCss(),
485
- target: pkgTarget,
486
- wasm,
487
- wat,
488
- wasmOptInfo,
489
- wasmSourceMap,
490
- };
491
418
  }
492
419
 
493
420
  export function compileDir(dir, options = {}) {
@@ -518,6 +445,8 @@ export function compileFiles(files, options = {}) {
518
445
  jsRootPrograms: options.jsRootPrograms ?? [],
519
446
  rootDir: options.rootDir ?? fromDir,
520
447
  bundledDirs: options.bundledDirs ?? new Set(),
448
+ typeOwners: options.typeOwners ?? new Map(),
449
+ jsonBuild: options.jsonBuild ?? { js: false, wasm: false },
521
450
  };
522
451
  const outputDir =
523
452
  options.outputDir ??
@@ -545,6 +474,15 @@ export function compileFiles(files, options = {}) {
545
474
  );
546
475
  const pkgName = pkgNames[0];
547
476
 
477
+ // Local packages share one flat JS module: a type name another package
478
+ // already declared is renamed here (`Pt` -> `Pt$$b`).
479
+ const renamedTypes = mangleLocalTypes(
480
+ programs,
481
+ resolve(fromDir),
482
+ pkgName,
483
+ options.typeOwners,
484
+ );
485
+
548
486
  // Validate target directives across files in this package
549
487
  const declaredTargets = new Map();
550
488
  for (const p of programs) {
@@ -571,6 +509,11 @@ export function compileFiles(files, options = {}) {
571
509
  );
572
510
  for (const p of programs) p.target = pkgTarget;
573
511
 
512
+ // The test runner's driver is a source that exists only in memory: it is
513
+ // parsed and checked with the package but never written next to it.
514
+ const driver = testDriverProgram(options, programs, pkgTarget, fromDir);
515
+ if (driver) programs.push(driver);
516
+
574
517
  // ── 2. Resolve imports ────────────────────────────────────────
575
518
  const checker = new TypeChecker();
576
519
  checker.target = pkgTarget;
@@ -593,7 +536,7 @@ export function compileFiles(files, options = {}) {
593
536
 
594
537
  // ── 3. Type-check ─────────────────────────────────────────────
595
538
  const errors = checker.checkAll(programs);
596
- const codecs = checkJsonCodecs(
539
+ const codecs = checkGeneratedGo(
597
540
  programs,
598
541
  checker,
599
542
  preambles,
@@ -621,11 +564,12 @@ export function compileFiles(files, options = {}) {
621
564
  const codegen = new CodeGen(checker, jsImports, bundledPackages, {
622
565
  target: pkgTarget,
623
566
  strict: isStrict,
567
+ natives: options.natives,
624
568
  });
625
569
  const mainJs =
626
570
  emitTarget === "wasm"
627
571
  ? wasmMarker(pkgName)
628
- : codegen.generateAll(programs, {
572
+ : codegen.generateAll([...programs, ...codecs], {
629
573
  isTest: options.isTest ?? false,
630
574
  });
631
575
  logWarnings(codegen.warnings);
@@ -638,8 +582,8 @@ export function compileFiles(files, options = {}) {
638
582
  };
639
583
 
640
584
  const merged = mergeCompilationChunks([...preambles, mainChunk]);
641
- finishStdlibJs(merged, options, programs, emitTarget);
642
- const { wasm, wat, wasmOptInfo, wasmSourceMap } = finishWasm(
585
+ finishStdlibJs(merged, options, [...programs, ...codecs], emitTarget);
586
+ const { wasm, wat, wasmOptInfo, wasmSourceMap, stringTable } = finishWasm(
643
587
  options,
644
588
  merged,
645
589
  pkgTarget,
@@ -658,15 +602,22 @@ export function compileFiles(files, options = {}) {
658
602
  return {
659
603
  pkgName,
660
604
  target: pkgTarget,
605
+ elemAddrTaken:
606
+ checker.depElemAddrTaken === true ||
607
+ programs.some((p) => takesElementAddress(p.decls)),
661
608
  js: merged.js,
662
609
  css: allCss,
663
610
  wasm,
664
611
  wat,
665
612
  wasmOptInfo,
666
613
  wasmSourceMap,
667
- programs,
614
+ stringTable,
615
+ programs: driver ? programs.filter((p) => p !== driver) : programs,
668
616
  exportedSymbols: checker.getExportedSymbols(),
669
- exportedTypes: checker.getExportedTypes(),
617
+ exportedTypes: withOriginalTypeNames(
618
+ checker.getExportedTypes(),
619
+ renamedTypes,
620
+ ),
670
621
  exportedConsts: checker.getExportedConsts(),
671
622
  sourceFiles: merged.sourceFiles,
672
623
  sourcesContent: merged.sourcesContent,
@@ -674,6 +625,38 @@ export function compileFiles(files, options = {}) {
674
625
  };
675
626
  }
676
627
 
628
+ // Parses the in-memory driver source `options.testDriver(programs, target)`
629
+ // returns (null: no driver) as a `_test.go` file of the package.
630
+ function testDriverProgram(options, programs, pkgTarget, fromDir) {
631
+ if (!options.testDriver || options.isDependency) return null;
632
+ const source = options.testDriver(programs, pkgTarget);
633
+ if (!source) return null;
634
+ const filename = "gofront_testmain_test.go";
635
+ const ast = new Parser(
636
+ new Lexer(source, filename).tokenize(),
637
+ filename,
638
+ source,
639
+ ).parse();
640
+ ast._source = source;
641
+ ast._path = join(fromDir, filename);
642
+ ast._declaredTarget = ast.target;
643
+ ast.target = pkgTarget;
644
+ return ast;
645
+ }
646
+
647
+ // The renamed ASTs must not be reused from the parse cache by a later build.
648
+ function mangleLocalTypes(programs, pkgKey, pkgName, owners) {
649
+ const renamed = mangleCollidingTypes(programs, pkgKey, pkgName, owners);
650
+ const foreign = new Set();
651
+ for (const [name, owner] of owners ?? [])
652
+ if (owner !== pkgKey) foreign.add(name);
653
+ const locals = renameLocalTypes(programs, foreign);
654
+ registerLocalTypeOwners(programs, pkgKey, owners);
655
+ if (renamed.size > 0 || locals)
656
+ for (const p of programs) _parseCache.delete(p._path);
657
+ return renamed;
658
+ }
659
+
677
660
  export function compileWasm(source, options = {}) {
678
661
  const filename = options.filename ?? "main.go";
679
662
  const tokens = new Lexer(source, filename).tokenize();
@@ -119,6 +119,31 @@ const SIMPLE_ESCAPES = {
119
119
  "\\": 92,
120
120
  };
121
121
 
122
+ // Decodes the raw bytes of consecutive \xNN / octal escapes in a string
123
+ // literal: valid UTF-8 sequences become their code point, every other byte
124
+ // 0x80-0xff becomes the lone surrogate U+DC00+byte so the runtime can turn it
125
+ // back into that byte when the string is converted to []byte.
126
+ function decodeRawBytes(bytes) {
127
+ const dec = new TextDecoder("utf-8", { fatal: true });
128
+ let out = "";
129
+ for (let i = 0; i < bytes.length; ) {
130
+ const b = bytes[i];
131
+ const n = b < 0x80 ? 1 : b >= 0xf0 ? 4 : b >= 0xe0 ? 3 : b >= 0xc0 ? 2 : 0;
132
+ if (n > 0 && i + n <= bytes.length) {
133
+ try {
134
+ out += dec.decode(Uint8Array.from(bytes.slice(i, i + n)));
135
+ i += n;
136
+ continue;
137
+ } catch {
138
+ // invalid sequence: fall through to the lone-byte case
139
+ }
140
+ }
141
+ out += String.fromCharCode(b < 0x80 ? b : 0xdc00 + b);
142
+ i++;
143
+ }
144
+ return out;
145
+ }
146
+
122
147
  export class Lexer {
123
148
  constructor(source, filename = null) {
124
149
  this.src = source;
@@ -130,6 +155,7 @@ export class Lexer {
130
155
  this.target = null;
131
156
  this.targetDirective = null;
132
157
  this.intrinsics = new Map(); // line -> name of a //gofront:intrinsic
158
+ this.natives = new Map(); // line -> name of a //gofront:native
133
159
  this.embedDirectives = []; // {line, col} of each //go:embed line comment
134
160
  }
135
161
 
@@ -185,6 +211,9 @@ export class Lexer {
185
211
  this.embedDirectives.push({ line, col });
186
212
  const intrinsic = comment.match(/^\/\/gofront:intrinsic\s+(\S+)/);
187
213
  if (intrinsic) this.intrinsics.set(line, intrinsic[1]);
214
+ const native = comment.match(/^\/\/gofront:native\s+(\S+)/);
215
+ if (native) this.natives.set(line, native[1]);
216
+ if (!intrinsic && !native) this.forwardDirectives(line, start);
188
217
  const match = comment.match(/^\/\/gofront:target\s+(\S+)/);
189
218
  if (match) {
190
219
  this.target = match[1];
@@ -249,7 +278,7 @@ export class Lexer {
249
278
  if (parts === null) parts = [];
250
279
  if (this.pos > start) parts.push(this.src.slice(start, this.pos));
251
280
  this.advance(); // skip \
252
- parts.push(String.fromCodePoint(this.readEscape('"')));
281
+ parts.push(this.readStringEscape());
253
282
  start = this.pos;
254
283
  } else {
255
284
  this.advance();
@@ -258,10 +287,41 @@ export class Lexer {
258
287
  throw new LexError("Unterminated string", l, c, this.filename, this.src);
259
288
  }
260
289
 
290
+ // The escape after a consumed backslash in a string literal. \xNN and octal
291
+ // escapes are raw bytes; a run of them is decoded together so "\xc3\xa9" is
292
+ // "é" and "\xff" is one invalid byte (a lone surrogate, see decodeRawBytes).
293
+ readStringEscape() {
294
+ if (!this.isByteEscapeAt(this.pos))
295
+ return String.fromCodePoint(this.readEscape('"'));
296
+ const bytes = [this.readEscape('"')];
297
+ while (this.peek() === "\\" && this.isByteEscapeAt(this.pos + 1)) {
298
+ this.advance();
299
+ bytes.push(this.readEscape('"'));
300
+ }
301
+ return decodeRawBytes(bytes);
302
+ }
303
+
304
+ // A doc comment between a directive and its func carries the directive down
305
+ // so the parser still finds it on the line above the func.
306
+ forwardDirectives(line, start) {
307
+ const before = this.src.slice(
308
+ this.src.lastIndexOf("\n", start - 1) + 1,
309
+ start,
310
+ );
311
+ if (!/^[ \t]*$/.test(before)) return;
312
+ for (const map of [this.intrinsics, this.natives])
313
+ if (map.has(line - 1)) map.set(line, map.get(line - 1));
314
+ }
315
+
316
+ isByteEscapeAt(i) {
317
+ const n = this.src[i];
318
+ return n === "x" || (n >= "0" && n <= "7");
319
+ }
320
+
261
321
  // Reads the escape after a consumed backslash and returns its code point.
262
322
  // `quote` is the enclosing delimiter: `\'` is valid only in runes and
263
- // `\"` only in strings. `\xNN` and octal escapes give U+0000-U+00FF (see
264
- // plan §1.1: strings are JS strings, so \x80-\xff are code points, not raw bytes).
323
+ // `\"` only in strings. `\xNN` and octal escapes give 0-255 (a code point in
324
+ // rune literals; string literals decode runs of them as UTF-8 bytes).
265
325
  readEscape(quote) {
266
326
  const l = this.line,
267
327
  c = this.col;
@@ -328,6 +388,7 @@ export class Lexer {
328
388
  } else if (this.peek() !== "'") {
329
389
  code = this.src.codePointAt(this.pos);
330
390
  this.advance();
391
+ if (code > 0xffff) this.pos++; // astral: skip the low surrogate
331
392
  } else {
332
393
  throw new LexError("Empty rune literal", l, c, this.filename, this.src);
333
394
  }
@@ -451,12 +512,10 @@ export class Lexer {
451
512
  break;
452
513
  }
453
514
  }
454
- if (
455
- this.pos + 1 < this.src.length &&
456
- this.src.charCodeAt(this.pos) === 46
457
- ) {
515
+ if (this.src.charCodeAt(this.pos) === 46) {
516
+ // `1.` is a float literal; `1..` and `1.x` keep the dot separate.
458
517
  const next = this.src.charCodeAt(this.pos + 1);
459
- if (next >= 48 && next <= 57) {
518
+ if (this._dotContinuesNumber(next)) {
460
519
  isFloat = true;
461
520
  n += this.advance();
462
521
  while (this.pos < this.src.length) {
@@ -469,6 +528,7 @@ export class Lexer {
469
528
  break;
470
529
  }
471
530
  }
531
+ if (n.endsWith(".")) n += "0";
472
532
  }
473
533
  }
474
534
  if (this.pos < this.src.length) {
@@ -481,6 +541,32 @@ export class Lexer {
481
541
  return { n, isFloat };
482
542
  }
483
543
 
544
+ // Whether the "." at the cursor belongs to the decimal literal before it.
545
+ // `next` is the code after the dot: a digit, an exponent (`1.e3`), an
546
+ // imaginary suffix (`1.i`) or anything that cannot start a selector or a
547
+ // range-style `..` makes `1.` a float; `1.x` and `1..` keep the dot apart.
548
+ _dotContinuesNumber(next) {
549
+ if (Number.isNaN(next)) return true;
550
+ if (next >= 48 && next <= 57) return true;
551
+ if (next === 46) return false;
552
+ const after = this.src.charCodeAt(this.pos + 2);
553
+ const identAfter =
554
+ (after >= 97 && after <= 122) ||
555
+ (after >= 65 && after <= 90) ||
556
+ after === 95 ||
557
+ (after >= 48 && after <= 57);
558
+ if (next === 101 || next === 69) {
559
+ return (after >= 48 && after <= 57) || after === 43 || after === 45;
560
+ }
561
+ if (next === 105) return !identAfter;
562
+ return !(
563
+ (next >= 97 && next <= 122) ||
564
+ (next >= 65 && next <= 90) ||
565
+ next === 95 ||
566
+ next > 127
567
+ );
568
+ }
569
+
484
570
  _startsUnicodeIdent() {
485
571
  return (
486
572
  this.peek().charCodeAt(0) > 127 &&
@@ -551,6 +637,7 @@ export class Lexer {
551
637
  }
552
638
  if (ch === "'") {
553
639
  this.push(T.INT, this.readRuneLiteral(), l, c);
640
+ this.tokens[this.tokens.length - 1].rune = true;
554
641
  continue;
555
642
  }
556
643
 
@@ -584,6 +671,7 @@ export class Lexer {
584
671
  this.tokens.target = this.target;
585
672
  this.tokens.targetDirective = this.targetDirective;
586
673
  this.tokens.intrinsics = this.intrinsics;
674
+ this.tokens.natives = this.natives;
587
675
  this.tokens.embedDirectives = this.embedDirectives;
588
676
  return this.tokens;
589
677
  }
@@ -617,8 +705,10 @@ export class Lexer {
617
705
 
618
706
  _readAmpOp(l, c) {
619
707
  if (this.match("&")) this.push(T.AND, "&&", l, c);
620
- else if (this.match("^")) this.push(T.AND_NOT, "&^", l, c);
621
- else if (this.match("=")) this.push(T.AMP_ASSIGN, "&=", l, c);
708
+ else if (this.match("^")) {
709
+ if (this.match("=")) this.push(T.AND_NOT_ASSIGN, "&^=", l, c);
710
+ else this.push(T.AND_NOT, "&^", l, c);
711
+ } else if (this.match("=")) this.push(T.AMP_ASSIGN, "&=", l, c);
622
712
  else this.push(T.AMP, "&", l, c);
623
713
  }
624
714
 
@@ -660,7 +750,12 @@ export class Lexer {
660
750
  case ".":
661
751
  if (/[0-9]/.test(this.peek() ?? "")) {
662
752
  const { n } = this._readLeadingDotFloat();
663
- this.push(T.FLOAT, n, l, c);
753
+ if (this.src[this.pos] === "i") {
754
+ this.advance();
755
+ this.push(T.IMAG, n, l, c);
756
+ } else {
757
+ this.push(T.FLOAT, n, l, c);
758
+ }
664
759
  } else if (this.peek() === "." && this.peek(1) === ".") {
665
760
  this.advance();
666
761
  this.advance();