gofront 1.8.0 → 1.9.0

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 (177) hide show
  1. package/README.md +3 -3
  2. package/package.json +5 -4
  3. package/src/backend/js/expressions.js +849 -371
  4. package/src/backend/js/index.js +241 -147
  5. package/src/backend/js/natives.js +383 -0
  6. package/src/backend/js/runtime.js +512 -2547
  7. package/src/backend/js/statements.js +506 -103
  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 +177 -13
  11. package/src/backend/shared/fixed-float.js +20 -0
  12. package/src/backend/shared/fmt-types.js +63 -171
  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 +643 -256
  16. package/src/backend/wasm/emit-exprs.js +124 -57
  17. package/src/backend/wasm/emit-maps.js +17 -367
  18. package/src/backend/wasm/emit-stmts.js +325 -75
  19. package/src/backend/wasm/emit.js +104 -13
  20. package/src/backend/wasm/globals.js +47 -8
  21. package/src/backend/wasm/glue.js +2 -4
  22. package/src/backend/wasm/index.js +36 -530
  23. package/src/backend/wasm/method-expr.js +12 -1
  24. package/src/backend/wasm/module-emitter.js +92 -717
  25. package/src/backend/wasm/monomorph.js +103 -6
  26. package/src/backend/wasm/natives.js +596 -0
  27. package/src/backend/wasm/prescan.js +5 -153
  28. package/src/backend/wasm/prune-funcs.js +89 -0
  29. package/src/backend/wasm/stdlib/index.js +2 -32
  30. package/src/backend/wasm/types.js +68 -20
  31. package/src/backend/wasm/variadic.js +3 -3
  32. package/src/cli/cli-core.js +5 -1
  33. package/src/cli/test-runner.js +115 -342
  34. package/src/compiler.js +108 -130
  35. package/src/lexer/lexer.js +105 -11
  36. package/src/lexer/templ-lexer.js +61 -18
  37. package/src/lexer/tokens.js +1 -0
  38. package/src/link/stdlib.js +354 -132
  39. package/src/link/type-names.js +148 -0
  40. package/src/link/wasm.js +30 -6
  41. package/src/lower/bounds.js +153 -0
  42. package/src/lower/captures.js +92 -33
  43. package/src/lower/embedding.js +32 -3
  44. package/src/lower/errors-as.js +196 -0
  45. package/src/lower/fmt-formatters.js +905 -0
  46. package/src/lower/functions.js +2 -0
  47. package/src/lower/gen-types.js +274 -0
  48. package/src/lower/index.js +7 -1
  49. package/src/lower/json-codecs.js +228 -247
  50. package/src/lower/ownership.js +25 -7
  51. package/src/lower/reachability.js +69 -51
  52. package/src/lower/walk.js +12 -0
  53. package/src/parser/declarations.js +28 -5
  54. package/src/parser/expressions.js +86 -16
  55. package/src/parser/statements.js +1 -0
  56. package/src/parser/templ.js +44 -5
  57. package/src/parser/types.js +1 -0
  58. package/src/resolver.js +1 -2
  59. package/src/typechecker/assignability.js +124 -11
  60. package/src/typechecker/const-fold.js +70 -27
  61. package/src/typechecker/constants.js +32 -6
  62. package/src/typechecker/expressions.js +326 -98
  63. package/src/typechecker/index.js +208 -33
  64. package/src/typechecker/init-order.js +136 -40
  65. package/src/typechecker/operand-text.js +40 -0
  66. package/src/typechecker/resolve.js +192 -36
  67. package/src/typechecker/statements.js +122 -11
  68. package/src/typechecker/stdlib/excluded.js +9 -5
  69. package/src/typechecker/stdlib/index.js +25 -9
  70. package/src/typechecker/termination.js +10 -1
  71. package/src/typechecker/types.js +203 -573
  72. package/stdlib/bytes/buffer.go +479 -0
  73. package/stdlib/bytes/bytes.go +1010 -0
  74. package/stdlib/bytes/reader.go +161 -0
  75. package/stdlib/context/context.go +115 -0
  76. package/stdlib/crypto/rand/rand.go +34 -0
  77. package/stdlib/crypto/sha256/sha256.go +249 -0
  78. package/stdlib/encoding/base64/base64.go +331 -0
  79. package/stdlib/encoding/binary/binary.go +332 -0
  80. package/stdlib/encoding/hex/hex.go +140 -0
  81. package/stdlib/encoding/json/json.go +62 -0
  82. package/stdlib/errors/errors.go +174 -0
  83. package/stdlib/fmt/errors.go +110 -0
  84. package/stdlib/fmt/format.go +732 -0
  85. package/stdlib/fmt/print.go +1232 -0
  86. package/stdlib/gom/gom.go +293 -0
  87. package/stdlib/html/html.go +251 -0
  88. package/stdlib/internal/fmtrt/fmtrt.go +163 -0
  89. package/stdlib/internal/jsonrt/dec.go +171 -38
  90. package/stdlib/internal/jsonrt/enc.go +186 -36
  91. package/stdlib/internal/jsonrt/scanner.go +17 -8
  92. package/stdlib/io/io.go +157 -1
  93. package/stdlib/maps/maps.go +131 -0
  94. package/stdlib/math/bits/bits.go +507 -0
  95. package/stdlib/math/extra.go +16 -14
  96. package/stdlib/math/math.go +439 -0
  97. package/stdlib/math/rand/rand.go +297 -0
  98. package/stdlib/os/os.go +93 -0
  99. package/stdlib/path/filepath/filepath.go +47 -0
  100. package/stdlib/path/path.go +394 -0
  101. package/stdlib/regexp/regexp.go +231 -0
  102. package/stdlib/slices/slices.go +526 -0
  103. package/stdlib/slices/sort.go +209 -0
  104. package/stdlib/slices/zsort_cmp.go +480 -0
  105. package/stdlib/slices/zsort_ordered.go +482 -0
  106. package/stdlib/sort/sort.go +391 -452
  107. package/stdlib/sort/zsort_func.go +480 -0
  108. package/stdlib/sort/zsort_iface.go +480 -0
  109. package/stdlib/strconv/float.go +1699 -0
  110. package/stdlib/strconv/isprint.go +752 -0
  111. package/stdlib/strconv/quote.go +235 -50
  112. package/stdlib/strconv/strconv.go +616 -0
  113. package/stdlib/strings/builder.go +95 -0
  114. package/stdlib/strings/reader.go +163 -0
  115. package/stdlib/strings/replacer.go +102 -0
  116. package/stdlib/strings/strings.go +807 -0
  117. package/stdlib/sync/sync.go +268 -0
  118. package/stdlib/testing/async_js.go +61 -0
  119. package/stdlib/testing/benchmark.go +297 -0
  120. package/stdlib/testing/testing.go +523 -0
  121. package/stdlib/time/duration.go +233 -0
  122. package/stdlib/time/format.go +745 -0
  123. package/stdlib/time/parse.go +790 -0
  124. package/stdlib/time/sleep_js.go +63 -0
  125. package/stdlib/time/time.go +834 -0
  126. package/stdlib/unicode/unicode.go +141 -3
  127. package/stdlib/unicode/utf8/utf8.go +464 -0
  128. package/src/backend/js/fmt-runtime.js +0 -573
  129. package/src/backend/js/stdlib/base64.js +0 -12
  130. package/src/backend/js/stdlib/binary.js +0 -102
  131. package/src/backend/js/stdlib/bits.js +0 -119
  132. package/src/backend/js/stdlib/builder.js +0 -98
  133. package/src/backend/js/stdlib/bytes.js +0 -368
  134. package/src/backend/js/stdlib/errors.js +0 -45
  135. package/src/backend/js/stdlib/fmt.js +0 -517
  136. package/src/backend/js/stdlib/gom.js +0 -155
  137. package/src/backend/js/stdlib/hex.js +0 -17
  138. package/src/backend/js/stdlib/html.js +0 -20
  139. package/src/backend/js/stdlib/io.js +0 -137
  140. package/src/backend/js/stdlib/json.js +0 -1167
  141. package/src/backend/js/stdlib/maps.js +0 -37
  142. package/src/backend/js/stdlib/math.js +0 -85
  143. package/src/backend/js/stdlib/os.js +0 -13
  144. package/src/backend/js/stdlib/path.js +0 -48
  145. package/src/backend/js/stdlib/rand.js +0 -32
  146. package/src/backend/js/stdlib/regexp.js +0 -54
  147. package/src/backend/js/stdlib/sha256.js +0 -12
  148. package/src/backend/js/stdlib/shared.js +0 -17
  149. package/src/backend/js/stdlib/simd.js +0 -34
  150. package/src/backend/js/stdlib/slices.js +0 -107
  151. package/src/backend/js/stdlib/sort.js +0 -30
  152. package/src/backend/js/stdlib/strconv.js +0 -79
  153. package/src/backend/js/stdlib/strings.js +0 -301
  154. package/src/backend/js/stdlib/sync.js +0 -13
  155. package/src/backend/js/stdlib/testing.js +0 -41
  156. package/src/backend/js/stdlib/time.js +0 -152
  157. package/src/backend/js/stdlib/unicode.js +0 -25
  158. package/src/backend/js/stdlib/utf8.js +0 -26
  159. package/src/backend/wasm/stdlib/bytes.js +0 -6716
  160. package/src/backend/wasm/stdlib/core.js +0 -10278
  161. package/src/backend/wasm/stdlib/io.js +0 -3578
  162. package/src/backend/wasm/stdlib/packages.js +0 -2402
  163. package/src/backend/wasm/stdlib/pure.js +0 -720
  164. package/src/backend/wasm/stdlib/time.js +0 -2229
  165. package/src/typechecker/stdlib/core.js +0 -1079
  166. package/src/typechecker/stdlib/extended.js +0 -1210
  167. package/stdlib/bytes/iter.go +0 -192
  168. package/stdlib/bytes/search.go +0 -240
  169. package/stdlib/bytes/split.go +0 -218
  170. package/stdlib/errors/overlay.go +0 -12
  171. package/stdlib/maps/iter.go +0 -65
  172. package/stdlib/slices/extra.go +0 -73
  173. package/stdlib/slices/iter.go +0 -113
  174. package/stdlib/strings/extra.go +0 -35
  175. package/stdlib/strings/iter.go +0 -122
  176. package/stdlib/sync/extra.go +0 -105
  177. package/stdlib/unicode/utf8/bytes.go +0 -239
package/src/compiler.js CHANGED
@@ -32,17 +32,19 @@ 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
+ renameLocalTypes,
40
+ withOriginalTypeNames,
38
41
  } from "./link/stdlib.js";
39
42
  import {
40
43
  linkIntoBundle,
41
44
  registerPackageUnit,
42
- registerWasmUnit,
43
45
  wasmMarker,
44
46
  } from "./link/wasm.js";
45
- import { lower } from "./lower/index.js";
47
+ import { lower, takesElementAddress } from "./lower/index.js";
46
48
  import { Parser } from "./parser/index.js";
47
49
  import { TemplParser } from "./parser/templ.js";
48
50
  import {
@@ -60,10 +62,21 @@ import { WASM_SUPPORTED_STDLIB } from "./typechecker/types.js";
60
62
  // Map<filePath, { mtime: number, ast: object }>
61
63
  const _parseCache = new Map();
62
64
 
65
+ // A cached AST keeps everything the build attached to it (checked types,
66
+ // lowered stdlib programs), so a build of a Go-source stdlib importer pins
67
+ // megabytes. The cache is therefore bounded; the least recently used file goes
68
+ // first.
69
+ const PARSE_CACHE_LIMIT = 16;
70
+
63
71
  export function clearParseCache() {
64
72
  _parseCache.clear();
65
73
  }
66
74
 
75
+ // Drops one file, e.g. a throwaway source that was deleted after its build.
76
+ export function evictParseCache(filePath) {
77
+ _parseCache.delete(resolve(filePath));
78
+ }
79
+
67
80
  export function parseCacheSize() {
68
81
  return _parseCache.size;
69
82
  }
@@ -71,9 +84,14 @@ export function parseCacheSize() {
71
84
  // ── Helpers ──────────────────────────────────────────────────
72
85
 
73
86
  function parseGoFrontFile(filePath) {
87
+ const key = resolve(filePath);
74
88
  const mtime = statSync(filePath).mtimeMs;
75
- const cached = _parseCache.get(filePath);
76
- if (cached && cached.mtime === mtime) return cached.ast;
89
+ const cached = _parseCache.get(key);
90
+ if (cached && cached.mtime === mtime) {
91
+ _parseCache.delete(key);
92
+ _parseCache.set(key, cached);
93
+ return cached.ast;
94
+ }
77
95
 
78
96
  const source = readFileSync(filePath, "utf8");
79
97
  const filename = basename(filePath);
@@ -88,7 +106,9 @@ function parseGoFrontFile(filePath) {
88
106
  ast._path = resolve(filePath);
89
107
  // compileFiles overwrites `target` with the resolved one; keep the directive.
90
108
  ast._declaredTarget = ast.target;
91
- _parseCache.set(filePath, { mtime, ast });
109
+ _parseCache.set(key, { mtime, ast });
110
+ if (_parseCache.size > PARSE_CACHE_LIMIT)
111
+ _parseCache.delete(_parseCache.keys().next().value);
92
112
  return ast;
93
113
  }
94
114
 
@@ -107,7 +127,7 @@ export function gwFilesIn(dir, { includeTests = false } = {}) {
107
127
  // wasm-opt when requested and produce the wasm source map.
108
128
  function finishWasm(options, merged, pkgTarget, outputDir) {
109
129
  const wantMap = Boolean(options.sourceMap && !options.isDependency);
110
- let { wasm, wat, sourceMapInfo } = linkIntoBundle(
130
+ let { wasm, wat, sourceMapInfo, stringTable } = linkIntoBundle(
111
131
  options,
112
132
  merged,
113
133
  pkgTarget,
@@ -132,7 +152,7 @@ function finishWasm(options, merged, pkgTarget, outputDir) {
132
152
  wasmOptInfo = opt;
133
153
  if (opt.sourceMap) wasmSourceMap = opt.sourceMap;
134
154
  }
135
- return { wasm, wat, wasmOptInfo, wasmSourceMap };
155
+ return { wasm, wat, wasmOptInfo, wasmSourceMap, stringTable };
136
156
  }
137
157
 
138
158
  // Prints non-fatal diagnostics (e.g. JS Number precision of a constant).
@@ -184,12 +204,6 @@ function resolveImports(
184
204
  "js: imports are not allowed in wasm packages",
185
205
  impNode,
186
206
  );
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
207
  } else if (path === "gofront/shared" && pkgTarget === "both") {
194
208
  checker.err(
195
209
  "'gofront/shared' requires a wasm-only package (//gofront:target wasm): a both package has no linear memory on the JS side",
@@ -287,6 +301,9 @@ function resolveImports(
287
301
  isDependency: true,
288
302
  });
289
303
  if (!alreadyBundled) preambles.push(dep);
304
+ // A dependency that hands out `&xs[i]` makes whole-element
305
+ // stores in this package write in place too.
306
+ if (dep.elemAddrTaken) checker.depElemAddrTaken = true;
290
307
 
291
308
  // Target import rules for local packages
292
309
  const impNode = {
@@ -374,120 +391,29 @@ export function resolvePackageTarget(declared, pkgDir, options = {}) {
374
391
  }
375
392
 
376
393
  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);
394
+ const { outputFile = null, dumpTokens = false, dumpAst = false } = options;
441
395
 
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
- });
396
+ if (dumpTokens || dumpAst) {
397
+ let source;
398
+ try {
399
+ source = readFileSync(inputPath, "utf8");
400
+ } catch (e) {
401
+ throw new Error(`cannot read '${inputPath}': ${e.message}`);
402
+ }
403
+ const tokens = new Lexer(source, basename(inputPath)).tokenize();
404
+ if (dumpTokens) return { tokens };
405
+ const ast = new Parser(tokens, basename(inputPath), source).parse();
406
+ ast._source = source;
407
+ ast._path = resolve(inputPath);
408
+ return { ast };
451
409
  }
452
410
 
453
- const isStrict = Boolean(options.strict || pkgTarget === "both");
454
- const cg = new CodeGen(checker, jsImports, bundledPackages, {
455
- target: pkgTarget,
456
- strict: isStrict,
411
+ return compileFiles([resolve(inputPath)], {
412
+ ...options,
413
+ outputDir:
414
+ options.outputDir ??
415
+ (outputFile ? dirname(resolve(outputFile)) : resolve(".")),
457
416
  });
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
417
  }
492
418
 
493
419
  export function compileDir(dir, options = {}) {
@@ -518,6 +444,8 @@ export function compileFiles(files, options = {}) {
518
444
  jsRootPrograms: options.jsRootPrograms ?? [],
519
445
  rootDir: options.rootDir ?? fromDir,
520
446
  bundledDirs: options.bundledDirs ?? new Set(),
447
+ typeOwners: options.typeOwners ?? new Map(),
448
+ jsonBuild: options.jsonBuild ?? { js: false, wasm: false },
521
449
  };
522
450
  const outputDir =
523
451
  options.outputDir ??
@@ -545,6 +473,15 @@ export function compileFiles(files, options = {}) {
545
473
  );
546
474
  const pkgName = pkgNames[0];
547
475
 
476
+ // Local packages share one flat JS module: a type name another package
477
+ // already declared is renamed here (`Pt` -> `Pt$$b`).
478
+ const renamedTypes = mangleLocalTypes(
479
+ programs,
480
+ resolve(fromDir),
481
+ pkgName,
482
+ options.typeOwners,
483
+ );
484
+
548
485
  // Validate target directives across files in this package
549
486
  const declaredTargets = new Map();
550
487
  for (const p of programs) {
@@ -571,6 +508,11 @@ export function compileFiles(files, options = {}) {
571
508
  );
572
509
  for (const p of programs) p.target = pkgTarget;
573
510
 
511
+ // The test runner's driver is a source that exists only in memory: it is
512
+ // parsed and checked with the package but never written next to it.
513
+ const driver = testDriverProgram(options, programs, pkgTarget, fromDir);
514
+ if (driver) programs.push(driver);
515
+
574
516
  // ── 2. Resolve imports ────────────────────────────────────────
575
517
  const checker = new TypeChecker();
576
518
  checker.target = pkgTarget;
@@ -593,7 +535,7 @@ export function compileFiles(files, options = {}) {
593
535
 
594
536
  // ── 3. Type-check ─────────────────────────────────────────────
595
537
  const errors = checker.checkAll(programs);
596
- const codecs = checkJsonCodecs(
538
+ const codecs = checkGeneratedGo(
597
539
  programs,
598
540
  checker,
599
541
  preambles,
@@ -621,11 +563,12 @@ export function compileFiles(files, options = {}) {
621
563
  const codegen = new CodeGen(checker, jsImports, bundledPackages, {
622
564
  target: pkgTarget,
623
565
  strict: isStrict,
566
+ natives: options.natives,
624
567
  });
625
568
  const mainJs =
626
569
  emitTarget === "wasm"
627
570
  ? wasmMarker(pkgName)
628
- : codegen.generateAll(programs, {
571
+ : codegen.generateAll([...programs, ...codecs], {
629
572
  isTest: options.isTest ?? false,
630
573
  });
631
574
  logWarnings(codegen.warnings);
@@ -638,8 +581,8 @@ export function compileFiles(files, options = {}) {
638
581
  };
639
582
 
640
583
  const merged = mergeCompilationChunks([...preambles, mainChunk]);
641
- finishStdlibJs(merged, options, programs, emitTarget);
642
- const { wasm, wat, wasmOptInfo, wasmSourceMap } = finishWasm(
584
+ finishStdlibJs(merged, options, [...programs, ...codecs], emitTarget);
585
+ const { wasm, wat, wasmOptInfo, wasmSourceMap, stringTable } = finishWasm(
643
586
  options,
644
587
  merged,
645
588
  pkgTarget,
@@ -658,15 +601,22 @@ export function compileFiles(files, options = {}) {
658
601
  return {
659
602
  pkgName,
660
603
  target: pkgTarget,
604
+ elemAddrTaken:
605
+ checker.depElemAddrTaken === true ||
606
+ programs.some((p) => takesElementAddress(p.decls)),
661
607
  js: merged.js,
662
608
  css: allCss,
663
609
  wasm,
664
610
  wat,
665
611
  wasmOptInfo,
666
612
  wasmSourceMap,
667
- programs,
613
+ stringTable,
614
+ programs: driver ? programs.filter((p) => p !== driver) : programs,
668
615
  exportedSymbols: checker.getExportedSymbols(),
669
- exportedTypes: checker.getExportedTypes(),
616
+ exportedTypes: withOriginalTypeNames(
617
+ checker.getExportedTypes(),
618
+ renamedTypes,
619
+ ),
670
620
  exportedConsts: checker.getExportedConsts(),
671
621
  sourceFiles: merged.sourceFiles,
672
622
  sourcesContent: merged.sourcesContent,
@@ -674,6 +624,34 @@ export function compileFiles(files, options = {}) {
674
624
  };
675
625
  }
676
626
 
627
+ // Parses the in-memory driver source `options.testDriver(programs, target)`
628
+ // returns (null: no driver) as a `_test.go` file of the package.
629
+ function testDriverProgram(options, programs, pkgTarget, fromDir) {
630
+ if (!options.testDriver || options.isDependency) return null;
631
+ const source = options.testDriver(programs, pkgTarget);
632
+ if (!source) return null;
633
+ const filename = "gofront_testmain_test.go";
634
+ const ast = new Parser(
635
+ new Lexer(source, filename).tokenize(),
636
+ filename,
637
+ source,
638
+ ).parse();
639
+ ast._source = source;
640
+ ast._path = join(fromDir, filename);
641
+ ast._declaredTarget = ast.target;
642
+ ast.target = pkgTarget;
643
+ return ast;
644
+ }
645
+
646
+ // The renamed ASTs must not be reused from the parse cache by a later build.
647
+ function mangleLocalTypes(programs, pkgKey, pkgName, owners) {
648
+ const renamed = mangleCollidingTypes(programs, pkgKey, pkgName, owners);
649
+ const locals = renameLocalTypes(programs);
650
+ if (renamed.size > 0 || locals)
651
+ for (const p of programs) _parseCache.delete(p._path);
652
+ return renamed;
653
+ }
654
+
677
655
  export function compileWasm(source, options = {}) {
678
656
  const filename = options.filename ?? "main.go";
679
657
  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 &&
@@ -584,6 +670,7 @@ export class Lexer {
584
670
  this.tokens.target = this.target;
585
671
  this.tokens.targetDirective = this.targetDirective;
586
672
  this.tokens.intrinsics = this.intrinsics;
673
+ this.tokens.natives = this.natives;
587
674
  this.tokens.embedDirectives = this.embedDirectives;
588
675
  return this.tokens;
589
676
  }
@@ -617,8 +704,10 @@ export class Lexer {
617
704
 
618
705
  _readAmpOp(l, c) {
619
706
  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);
707
+ else if (this.match("^")) {
708
+ if (this.match("=")) this.push(T.AND_NOT_ASSIGN, "&^=", l, c);
709
+ else this.push(T.AND_NOT, "&^", l, c);
710
+ } else if (this.match("=")) this.push(T.AMP_ASSIGN, "&=", l, c);
622
711
  else this.push(T.AMP, "&", l, c);
623
712
  }
624
713
 
@@ -660,7 +749,12 @@ export class Lexer {
660
749
  case ".":
661
750
  if (/[0-9]/.test(this.peek() ?? "")) {
662
751
  const { n } = this._readLeadingDotFloat();
663
- this.push(T.FLOAT, n, l, c);
752
+ if (this.src[this.pos] === "i") {
753
+ this.advance();
754
+ this.push(T.IMAG, n, l, c);
755
+ } else {
756
+ this.push(T.FLOAT, n, l, c);
757
+ }
664
758
  } else if (this.peek() === "." && this.peek(1) === ".") {
665
759
  this.advance();
666
760
  this.advance();