gofront 1.7.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 (194) hide show
  1. package/README.md +14 -14
  2. package/package.json +10 -38
  3. package/src/backend/js/expressions.js +2060 -389
  4. package/src/backend/js/index.js +586 -174
  5. package/src/backend/js/natives.js +383 -0
  6. package/src/backend/js/runtime.js +936 -1644
  7. package/src/backend/js/statements.js +1349 -132
  8. package/src/backend/js/stdlib/index.js +51 -94
  9. package/src/backend/js/templ.js +67 -118
  10. package/src/backend/js/type-info.js +436 -0
  11. package/src/backend/shared/fixed-float.js +20 -0
  12. package/src/backend/shared/fmt-types.js +80 -0
  13. package/src/backend/shared/host-unicode.js +36 -0
  14. package/src/backend/wasm/boundary.js +753 -872
  15. package/src/backend/wasm/emit-builtins.js +914 -237
  16. package/src/backend/wasm/emit-exprs.js +1456 -195
  17. package/src/backend/wasm/emit-maps.js +78 -380
  18. package/src/backend/wasm/emit-stmts.js +1659 -214
  19. package/src/backend/wasm/emit.js +614 -88
  20. package/src/backend/wasm/encode.js +43 -13
  21. package/src/backend/wasm/globals.js +491 -0
  22. package/src/backend/wasm/glue.js +2 -4
  23. package/src/backend/wasm/index.js +406 -3195
  24. package/src/backend/wasm/method-expr.js +392 -0
  25. package/src/backend/wasm/module-emitter.js +1615 -0
  26. package/src/backend/wasm/monomorph.js +164 -9
  27. package/src/backend/wasm/natives.js +596 -0
  28. package/src/backend/wasm/prescan.js +249 -0
  29. package/src/backend/wasm/prune-funcs.js +89 -0
  30. package/src/backend/wasm/stdlib/index.js +2 -37
  31. package/src/backend/wasm/stdlib/simd.js +293 -28
  32. package/src/backend/wasm/types.js +260 -27
  33. package/src/backend/wasm/variadic.js +93 -0
  34. package/src/backend/wasm/wat.js +47 -20
  35. package/src/{cli-core.js → cli/cli-core.js} +241 -11
  36. package/src/{test-runner.js → cli/test-runner.js} +188 -122
  37. package/src/{vendor.js → cli/vendor.js} +3 -4
  38. package/src/compiler.js +202 -386
  39. package/src/index.js +15 -169
  40. package/src/{lexer.js → lexer/lexer.js} +203 -62
  41. package/src/{templ-lexer.js → lexer/templ-lexer.js} +61 -18
  42. package/src/{tokens.js → lexer/tokens.js} +2 -0
  43. package/src/link/sourcemap.js +97 -0
  44. package/src/link/stdlib.js +737 -0
  45. package/src/link/type-names.js +148 -0
  46. package/src/link/wasm.js +318 -0
  47. package/src/lower/bounds.js +547 -0
  48. package/src/lower/boxing.js +1 -1
  49. package/src/lower/captures.js +267 -64
  50. package/src/lower/embedding.js +32 -3
  51. package/src/lower/errors-as.js +196 -0
  52. package/src/lower/fmt-formatters.js +905 -0
  53. package/src/lower/functions.js +2 -0
  54. package/src/lower/gen-types.js +274 -0
  55. package/src/lower/index.js +33 -19
  56. package/src/lower/json-codecs.js +1379 -0
  57. package/src/lower/ownership.js +47 -8
  58. package/src/lower/range.js +86 -1
  59. package/src/lower/reachability.js +150 -0
  60. package/src/lower/walk.js +12 -0
  61. package/src/parser/declarations.js +83 -15
  62. package/src/parser/expressions.js +110 -18
  63. package/src/parser/index.js +2 -1
  64. package/src/parser/statements.js +38 -22
  65. package/src/{templ-parser.js → parser/templ.js} +179 -17
  66. package/src/parser/types.js +70 -9
  67. package/src/resolver.js +21 -1
  68. package/src/typechecker/assignability.js +291 -19
  69. package/src/typechecker/const-fold.js +399 -0
  70. package/src/typechecker/constants.js +282 -0
  71. package/src/typechecker/expressions.js +1220 -183
  72. package/src/typechecker/index.js +659 -92
  73. package/src/typechecker/init-order.js +426 -0
  74. package/src/typechecker/operand-text.js +40 -0
  75. package/src/typechecker/resolve.js +613 -62
  76. package/src/typechecker/statements.js +212 -20
  77. package/src/typechecker/stdlib/excluded.js +902 -0
  78. package/src/typechecker/stdlib/index.js +38 -0
  79. package/src/typechecker/stdlib/simd.js +65 -15
  80. package/src/typechecker/termination.js +11 -2
  81. package/src/typechecker/types.js +268 -514
  82. package/stdlib/bytes/buffer.go +479 -0
  83. package/stdlib/bytes/bytes.go +1010 -0
  84. package/stdlib/bytes/reader.go +161 -0
  85. package/stdlib/cmp/cmp.go +65 -0
  86. package/stdlib/context/context.go +115 -0
  87. package/stdlib/crypto/rand/rand.go +34 -0
  88. package/stdlib/crypto/sha256/sha256.go +249 -0
  89. package/stdlib/encoding/base64/base64.go +331 -0
  90. package/stdlib/encoding/binary/binary.go +332 -0
  91. package/stdlib/encoding/hex/hex.go +140 -0
  92. package/stdlib/encoding/json/errors.go +31 -0
  93. package/stdlib/encoding/json/indent.go +71 -0
  94. package/stdlib/encoding/json/json.go +62 -0
  95. package/stdlib/encoding/json/number.go +12 -0
  96. package/stdlib/encoding/json/stream.go +173 -0
  97. package/stdlib/errors/errors.go +174 -0
  98. package/stdlib/fmt/errors.go +110 -0
  99. package/stdlib/fmt/format.go +732 -0
  100. package/stdlib/fmt/print.go +1232 -0
  101. package/stdlib/gom/gom.go +293 -0
  102. package/stdlib/hash/hash.go +53 -0
  103. package/stdlib/html/html.go +251 -0
  104. package/stdlib/internal/fmtrt/fmtrt.go +163 -0
  105. package/stdlib/internal/jsonrt/dec.go +871 -0
  106. package/stdlib/internal/jsonrt/enc.go +516 -0
  107. package/stdlib/internal/jsonrt/indent.go +135 -0
  108. package/stdlib/internal/jsonrt/number.go +27 -0
  109. package/stdlib/internal/jsonrt/reader.go +73 -0
  110. package/stdlib/internal/jsonrt/scanner.go +658 -0
  111. package/stdlib/io/io.go +502 -0
  112. package/stdlib/iter/iter.go +18 -0
  113. package/stdlib/maps/maps.go +131 -0
  114. package/stdlib/math/bits/bits.go +507 -0
  115. package/stdlib/math/extra.go +272 -0
  116. package/stdlib/math/math.go +439 -0
  117. package/stdlib/math/rand/rand.go +297 -0
  118. package/stdlib/os/os.go +93 -0
  119. package/stdlib/path/filepath/filepath.go +47 -0
  120. package/stdlib/path/path.go +394 -0
  121. package/stdlib/regexp/regexp.go +231 -0
  122. package/stdlib/slices/slices.go +526 -0
  123. package/stdlib/slices/sort.go +209 -0
  124. package/stdlib/slices/zsort_cmp.go +480 -0
  125. package/stdlib/slices/zsort_ordered.go +482 -0
  126. package/stdlib/sort/sort.go +500 -0
  127. package/stdlib/sort/zsort_func.go +480 -0
  128. package/stdlib/sort/zsort_iface.go +480 -0
  129. package/stdlib/strconv/float.go +1699 -0
  130. package/stdlib/strconv/isprint.go +752 -0
  131. package/stdlib/strconv/quote.go +526 -0
  132. package/stdlib/strconv/strconv.go +616 -0
  133. package/stdlib/strings/builder.go +95 -0
  134. package/stdlib/strings/reader.go +163 -0
  135. package/stdlib/strings/replacer.go +102 -0
  136. package/stdlib/strings/strings.go +807 -0
  137. package/stdlib/sync/sync.go +268 -0
  138. package/stdlib/testing/async_js.go +61 -0
  139. package/stdlib/testing/benchmark.go +297 -0
  140. package/stdlib/testing/testing.go +523 -0
  141. package/stdlib/time/duration.go +233 -0
  142. package/stdlib/time/format.go +745 -0
  143. package/stdlib/time/parse.go +790 -0
  144. package/stdlib/time/sleep_js.go +63 -0
  145. package/stdlib/time/time.go +834 -0
  146. package/stdlib/unicode/unicode.go +250 -0
  147. package/stdlib/unicode/utf8/utf8.go +464 -0
  148. package/src/backend/js/stdlib/base64.js +0 -12
  149. package/src/backend/js/stdlib/binary.js +0 -102
  150. package/src/backend/js/stdlib/bits.js +0 -46
  151. package/src/backend/js/stdlib/builder.js +0 -63
  152. package/src/backend/js/stdlib/bytes.js +0 -122
  153. package/src/backend/js/stdlib/errors.js +0 -44
  154. package/src/backend/js/stdlib/fmt.js +0 -178
  155. package/src/backend/js/stdlib/gom.js +0 -155
  156. package/src/backend/js/stdlib/hex.js +0 -17
  157. package/src/backend/js/stdlib/html.js +0 -20
  158. package/src/backend/js/stdlib/io.js +0 -137
  159. package/src/backend/js/stdlib/json.js +0 -972
  160. package/src/backend/js/stdlib/maps.js +0 -45
  161. package/src/backend/js/stdlib/math.js +0 -69
  162. package/src/backend/js/stdlib/os.js +0 -13
  163. package/src/backend/js/stdlib/path.js +0 -48
  164. package/src/backend/js/stdlib/rand.js +0 -32
  165. package/src/backend/js/stdlib/regexp.js +0 -54
  166. package/src/backend/js/stdlib/sha256.js +0 -12
  167. package/src/backend/js/stdlib/shared.js +0 -17
  168. package/src/backend/js/stdlib/simd.js +0 -24
  169. package/src/backend/js/stdlib/slices.js +0 -77
  170. package/src/backend/js/stdlib/sort.js +0 -30
  171. package/src/backend/js/stdlib/strconv.js +0 -74
  172. package/src/backend/js/stdlib/strings.js +0 -222
  173. package/src/backend/js/stdlib/sync.js +0 -13
  174. package/src/backend/js/stdlib/testing.js +0 -34
  175. package/src/backend/js/stdlib/time.js +0 -128
  176. package/src/backend/js/stdlib/unicode.js +0 -25
  177. package/src/backend/js/stdlib/utf8.js +0 -26
  178. package/src/backend/wasm/const-fold.js +0 -205
  179. package/src/backend/wasm/emit-stdlib.js +0 -13
  180. package/src/backend/wasm/stdlib/bytes.js +0 -5039
  181. package/src/backend/wasm/stdlib/core.js +0 -8903
  182. package/src/backend/wasm/stdlib/io.js +0 -2161
  183. package/src/backend/wasm/stdlib/packages.js +0 -2400
  184. package/src/backend/wasm/stdlib/pure.js +0 -720
  185. package/src/backend/wasm/stdlib/time.js +0 -1200
  186. package/src/typechecker/stdlib/core.js +0 -801
  187. package/src/typechecker/stdlib/extended.js +0 -914
  188. package/src/typechecker/stdlib.js +0 -22
  189. /package/src/{asset-manager.js → cli/asset-manager.js} +0 -0
  190. /package/src/{colors.js → cli/colors.js} +0 -0
  191. /package/src/{dev-server.js → cli/dev-server.js} +0 -0
  192. /package/src/{minifier.js → cli/minifier.js} +0 -0
  193. /package/src/{project-config.js → cli/project-config.js} +0 -0
  194. /package/src/{pwa.js → cli/pwa.js} +0 -0
package/src/index.js CHANGED
@@ -14,30 +14,30 @@
14
14
  // gofront init [dir] — scaffold a new GoFront project
15
15
 
16
16
  import { statSync } from "node:fs";
17
- import { dirname, relative, resolve } from "node:path";
18
- import { copyAssets } from "./asset-manager.js";
17
+ import { dirname, resolve } from "node:path";
19
18
  import {
20
- formatBuildSummary,
19
+ devReporters,
21
20
  formatPrepSummary,
22
21
  formatWrittenDesc,
23
22
  GOFRONT_VERSION,
23
+ HELP_TEXT,
24
24
  handleBuild,
25
25
  handleCheck,
26
26
  handleDev,
27
27
  handleInit,
28
28
  handlePrep,
29
29
  handleTest,
30
- maybeMinify,
31
30
  parseBuildArgs,
32
31
  parseCheckArgs,
33
32
  parseDevArgs,
34
33
  parseLegacyArgs,
35
34
  parsePrepArgs,
36
35
  parseTestArgs,
37
- runCompile,
38
- writeCompileOutput,
39
- } from "./cli-core.js";
40
- import { colors, formatDiagnostic, log, ms, stamp } from "./colors.js";
36
+ reportBuild,
37
+ reportCheck,
38
+ runSingleShot,
39
+ } from "./cli/cli-core.js";
40
+ import { colors, log, stamp } from "./cli/colors.js";
41
41
 
42
42
  // ── Parse CLI args ───────────────────────────────────────────
43
43
 
@@ -48,35 +48,7 @@ if (args[0] === "--version" || args[0] === "-v") {
48
48
  }
49
49
 
50
50
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
51
- console.log(
52
- `
53
- GoFront — a Go-inspired language that compiles to JavaScript and WebAssembly
54
-
55
- Usage:
56
- gofront dev [dir] [options] Start dev server with live reload (default port 3000)
57
- gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify, --emit-wat, --release, --wasm-opt, --js-only)
58
- gofront check [dir|dir/...] Type-check only (dir/... recurses into every package)
59
- gofront test [dir|dir/...] [--dom] Run unit tests (-v verbose, -run <regex>)
60
- gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
61
- gofront init [dir] Scaffold a new GoFront project
62
- gofront <file.go> Compile single file and print to stdout
63
- gofront <dir> (or gofront .) Compile all *.go in directory as one bundle
64
- gofront <input> -o out.js Compile and write to file (wasm packages also emit app.wasm)
65
- gofront <input> --release Production build with minification and Binaryen wasm optimization
66
- gofront <input> --wasm-opt Optimize wasm output with Binaryen (-O3 + GUFA)
67
- gofront <input> --check Type-check only
68
- gofront <input> --watch Watch for changes and recompile
69
- gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
70
- gofront <input> -o out.js --serve --port 8080 Use a custom port
71
- gofront <input> -o out.js --copy-assets Compile and synchronize static assets
72
- gofront <input> --source-map Append inline source map to output
73
- gofront <input> --minify Minify output
74
- gofront <input> --minify --mangle Minify and mangle identifiers
75
- gofront <file.go> --ast Dump AST (debug)
76
- gofront <file.go> --tokens Dump tokens (debug)
77
- gofront --version Print version
78
- `.trim(),
79
- );
51
+ console.log(HELP_TEXT);
80
52
  process.exit(0);
81
53
  }
82
54
 
@@ -131,19 +103,7 @@ if (args[0] === "test") {
131
103
  if (args[0] === "check") {
132
104
  const { targetDir, forceTarget } = parseCheckArgs(args.slice(1));
133
105
  try {
134
- const { elapsedMs, packages } = handleCheck(targetDir, { forceTarget });
135
- const okLine = (label, t) =>
136
- log.info(`${colors.cyan(label)} — ${colors.green("OK")} ${ms(t)}`);
137
- if (packages) {
138
- for (const pkg of packages) {
139
- okLine(relative(process.cwd(), pkg.dir) || ".", pkg.elapsedMs);
140
- }
141
- log.info(
142
- `${colors.bold(`${packages.length} packages`)} — ${colors.bold(colors.green("OK"))} ${ms(elapsedMs)}`,
143
- );
144
- } else {
145
- okLine(targetDir, elapsedMs);
146
- }
106
+ reportCheck(handleCheck(targetDir, { forceTarget }), targetDir);
147
107
  process.exit(0);
148
108
  } catch (e) {
149
109
  log.fail(e.message);
@@ -157,13 +117,7 @@ if (args[0] === "build") {
157
117
  const buildOptions = parseBuildArgs(args.slice(1));
158
118
  try {
159
119
  const result = await handleBuild(buildOptions.targetDir, buildOptions);
160
- for (const line of formatBuildSummary(result)) {
161
- log.info(
162
- line
163
- .replace(/^build complete/, colors.green("build complete"))
164
- .replace(/\(([\d.]+ kB)\)$/, (_, s) => colors.dim(`(${s})`)),
165
- );
166
- }
120
+ reportBuild(result);
167
121
  process.exit(0);
168
122
  } catch (e) {
169
123
  log.fail(e.message);
@@ -200,22 +154,19 @@ if (args[0] === "dev") {
200
154
  }
201
155
  }
202
156
 
157
+ const flags = parseLegacyArgs(args);
203
158
  const {
204
159
  inputArg,
205
160
  outputFile,
206
- checkOnly,
207
- dumpAst,
208
- dumpTokens,
209
161
  sourceMap,
210
162
  serve: serveMode,
211
163
  watch: watchMode,
212
164
  copyAssets: copyAssetsFlag,
213
- wasmOpt,
214
165
  minify: minifyOutput,
215
166
  mangle: mangleOutput,
216
167
  port: servePort,
217
168
  forceTarget,
218
- } = parseLegacyArgs(args);
169
+ } = flags;
219
170
 
220
171
  // ── Determine input mode ─────────────────────────────────────
221
172
 
@@ -228,96 +179,10 @@ try {
228
179
  process.exit(1);
229
180
  }
230
181
 
231
- function copyProjectAssets(verbose) {
232
- try {
233
- const { copied, skipped } = copyAssets(resolve("."));
234
- if (verbose && (copied > 0 || skipped > 0)) {
235
- log.info(`copied ${copied} assets (${skipped} skipped)`);
236
- }
237
- } catch (e) {
238
- log.warn(`asset copy failed: ${e.message}`);
239
- }
240
- }
241
-
242
182
  // ── Single-shot mode ─────────────────────────────────────────
243
183
 
244
184
  if (!watchMode) {
245
- let result;
246
- const startMs = performance.now();
247
- try {
248
- result = runCompile(inputPath, isDir, {
249
- sourceMap,
250
- outputFile,
251
- dumpTokens,
252
- dumpAst,
253
- wasmOpt,
254
- forceTarget,
255
- });
256
- } catch (e) {
257
- log.fail(e.message);
258
- process.exit(1);
259
- }
260
-
261
- if (result.tokens) {
262
- for (const tok of result.tokens) console.log(tok.toString());
263
- process.exit(0);
264
- }
265
- if (result.ast) {
266
- console.log(JSON.stringify(result.ast, null, 2));
267
- process.exit(0);
268
- }
269
-
270
- if (checkOnly) {
271
- const elapsedMs = (performance.now() - startMs).toFixed(0);
272
- log.info(
273
- `${colors.cyan(inputArg)} — ${colors.green("OK")} ${ms(elapsedMs)}`,
274
- );
275
- process.exit(0);
276
- }
277
-
278
- let js;
279
- try {
280
- js = maybeMinify(result.js, {
281
- minify: minifyOutput,
282
- mangle: mangleOutput,
283
- sourceMap,
284
- });
285
- } catch (e) {
286
- log.fail(`minify failed: ${e.message}`);
287
- process.exit(1);
288
- }
289
-
290
- const elapsedMs = (performance.now() - startMs).toFixed(0);
291
-
292
- if (outputFile) {
293
- try {
294
- const written = writeCompileOutput(resolve(outputFile), result, js);
295
- log.info(
296
- `wrote ${formatWrittenDesc(written, outputFile)} ${ms(elapsedMs)}`,
297
- );
298
- if (result.wasmOptInfo && result.wasmOptInfo.originalSize > 0) {
299
- const origKb = (result.wasmOptInfo.originalSize / 1024).toFixed(1);
300
- const optKb = (result.wasmOptInfo.optimizedSize / 1024).toFixed(1);
301
- log.info(
302
- `wasm-opt: app.wasm ${origKb} kB → ${optKb} kB (-${result.wasmOptInfo.percentSaved}%) [${result.wasmOptInfo.engine}]`,
303
- );
304
- }
305
- } catch (e) {
306
- log.fail(`cannot write '${outputFile}': ${e.message}`);
307
- process.exit(1);
308
- }
309
- } else {
310
- console.log(js);
311
- if (result.wasm) {
312
- log.warn(
313
- "wasm packages were linked but app.wasm needs -o <file> to be written",
314
- );
315
- }
316
- }
317
-
318
- if (copyAssetsFlag) copyProjectAssets(true);
319
-
320
- process.exit(0);
185
+ process.exit(runSingleShot(inputPath, isDir, flags));
321
186
  }
322
187
 
323
188
  // ── Watch mode ───────────────────────────────────────────────
@@ -342,26 +207,7 @@ try {
342
207
  mangle: mangleOutput,
343
208
  copyAssets: copyAssetsFlag,
344
209
  forceTarget,
345
- onBuild: ({ js, written, elapsedMs, changedFile }) => {
346
- const changeNote = changedFile ? ` — ${changedFile} changed` : "";
347
- const timing = colors.dim(`(${elapsedMs}ms${changeNote})`);
348
- if (outputFile) {
349
- console.error(
350
- `${prefix()} ${colors.green("OK")} — wrote ${formatWrittenDesc(written, outputFile)} ${timing}`,
351
- );
352
- } else {
353
- // Clear screen then print
354
- process.stdout.write("\x1Bc");
355
- console.log(js);
356
- console.error(`${prefix()} ${colors.green("OK")} ${timing}`);
357
- }
358
- if (copyAssetsFlag) copyProjectAssets(false);
359
- },
360
- onError: (e) => {
361
- console.error(`${prefix()} ${colors.bold(colors.red("ERROR"))}`);
362
- for (const line of formatDiagnostic(e.message).split("\n"))
363
- console.error(` ${line}`);
364
- },
210
+ ...devReporters({ outputFile, copyAssets: copyAssetsFlag }),
365
211
  });
366
212
  } catch (e) {
367
213
  log.fail(e.message);
@@ -52,8 +52,8 @@ const KEYWORDS = new Set([
52
52
  "case",
53
53
  "default",
54
54
  "map",
55
- "new",
56
55
  "fallthrough",
56
+ "goto",
57
57
  "defer",
58
58
  "async",
59
59
  "await",
@@ -86,6 +86,11 @@ const ASSIGN_OP = {
86
86
  ":": [T.DEFINE, ":=", T.COLON, ":"],
87
87
  };
88
88
 
89
+ const UNICODE_LETTER = /^\p{L}$/u;
90
+ const UNICODE_DIGIT = /^\p{Nd}$/u;
91
+ const isUnicodeLetter = (cp) => UNICODE_LETTER.test(String.fromCodePoint(cp));
92
+ const isUnicodeDigit = (cp) => UNICODE_DIGIT.test(String.fromCodePoint(cp));
93
+
89
94
  export class LexError extends Error {
90
95
  constructor(msg, line, col, filename, sourceCode) {
91
96
  const loc = filename ? `${filename}:${line}:${col}` : `${line}:${col}`;
@@ -103,6 +108,42 @@ export class LexError extends Error {
103
108
  }
104
109
  }
105
110
 
111
+ const SIMPLE_ESCAPES = {
112
+ a: 7,
113
+ b: 8,
114
+ f: 12,
115
+ n: 10,
116
+ r: 13,
117
+ t: 9,
118
+ v: 11,
119
+ "\\": 92,
120
+ };
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
+
106
147
  export class Lexer {
107
148
  constructor(source, filename = null) {
108
149
  this.src = source;
@@ -113,6 +154,9 @@ export class Lexer {
113
154
  this.filename = filename;
114
155
  this.target = null;
115
156
  this.targetDirective = null;
157
+ this.intrinsics = new Map(); // line -> name of a //gofront:intrinsic
158
+ this.natives = new Map(); // line -> name of a //gofront:native
159
+ this.embedDirectives = []; // {line, col} of each //go:embed line comment
116
160
  }
117
161
 
118
162
  // ── Primitives ──────────────────────────────────────────────
@@ -158,6 +202,18 @@ export class Lexer {
158
202
  while (this.pos < this.src.length && this.peek() !== "\n")
159
203
  this.advance();
160
204
  const comment = this.src.slice(start, this.pos);
205
+ if (
206
+ /^\/\/go:embed(?:\s|$)/.test(comment) &&
207
+ /^[ \t]*$/.test(
208
+ this.src.slice(this.src.lastIndexOf("\n", start - 1) + 1, start),
209
+ )
210
+ )
211
+ this.embedDirectives.push({ line, col });
212
+ const intrinsic = comment.match(/^\/\/gofront:intrinsic\s+(\S+)/);
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);
161
217
  const match = comment.match(/^\/\/gofront:target\s+(\S+)/);
162
218
  if (match) {
163
219
  this.target = match[1];
@@ -222,29 +278,7 @@ export class Lexer {
222
278
  if (parts === null) parts = [];
223
279
  if (this.pos > start) parts.push(this.src.slice(start, this.pos));
224
280
  this.advance(); // skip \
225
- const esc = this.advance();
226
- switch (esc) {
227
- case "n":
228
- parts.push("\n");
229
- break;
230
- case "t":
231
- parts.push("\t");
232
- break;
233
- case "r":
234
- parts.push("\r");
235
- break;
236
- case '"':
237
- parts.push('"');
238
- break;
239
- case "\\":
240
- parts.push("\\");
241
- break;
242
- case "0":
243
- parts.push("\0");
244
- break;
245
- default:
246
- parts.push(`\\${esc}`);
247
- }
281
+ parts.push(this.readStringEscape());
248
282
  start = this.pos;
249
283
  } else {
250
284
  this.advance();
@@ -253,6 +287,77 @@ export class Lexer {
253
287
  throw new LexError("Unterminated string", l, c, this.filename, this.src);
254
288
  }
255
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
+
321
+ // Reads the escape after a consumed backslash and returns its code point.
322
+ // `quote` is the enclosing delimiter: `\'` is valid only in runes and
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).
325
+ readEscape(quote) {
326
+ const l = this.line,
327
+ c = this.col;
328
+ const fail = (msg) => {
329
+ throw new LexError(msg, l, c, this.filename, this.src);
330
+ };
331
+ const esc = this.advance();
332
+ const simple = SIMPLE_ESCAPES[esc];
333
+ if (simple !== undefined) return simple;
334
+ if (esc === quote) return esc.charCodeAt(0);
335
+ if (esc === "x") return this.readEscapeDigits(2, 16, fail);
336
+ if (esc === "u" || esc === "U") {
337
+ const v = this.readEscapeDigits(esc === "u" ? 4 : 8, 16, fail);
338
+ if (v > 0x10ffff || (v >= 0xd800 && v < 0xe000))
339
+ fail("escape is invalid Unicode code point");
340
+ return v;
341
+ }
342
+ if (esc >= "0" && esc <= "7") {
343
+ const v = Number(esc) * 64 + this.readEscapeDigits(2, 8, fail);
344
+ if (v > 255) fail("octal escape value exceeds 255");
345
+ return v;
346
+ }
347
+ return fail(`unknown escape sequence: \\${esc}`);
348
+ }
349
+
350
+ readEscapeDigits(n, radix, fail) {
351
+ let v = 0;
352
+ for (let i = 0; i < n; i++) {
353
+ const d = Number.parseInt(this.src[this.pos] ?? "", radix);
354
+ if (Number.isNaN(d)) fail("invalid character in escape sequence");
355
+ v = v * radix + d;
356
+ this.advance();
357
+ }
358
+ return v;
359
+ }
360
+
256
361
  readRawString() {
257
362
  const l = this.line,
258
363
  c = this.col;
@@ -279,38 +384,11 @@ export class Lexer {
279
384
  let code;
280
385
  if (this.peek() === "\\") {
281
386
  this.advance();
282
- const esc = this.advance();
283
- switch (esc) {
284
- case "n":
285
- code = 10;
286
- break;
287
- case "t":
288
- code = 9;
289
- break;
290
- case "r":
291
- code = 13;
292
- break;
293
- case "'":
294
- code = 39;
295
- break;
296
- case "\\":
297
- code = 92;
298
- break;
299
- case "0":
300
- code = 0;
301
- break;
302
- default:
303
- throw new LexError(
304
- `Unknown escape in rune literal: \\${esc}`,
305
- l,
306
- c,
307
- this.filename,
308
- this.src,
309
- );
310
- }
387
+ code = this.readEscape("'");
311
388
  } else if (this.peek() !== "'") {
312
389
  code = this.src.codePointAt(this.pos);
313
390
  this.advance();
391
+ if (code > 0xffff) this.pos++; // astral: skip the low surrogate
314
392
  } else {
315
393
  throw new LexError("Empty rune literal", l, c, this.filename, this.src);
316
394
  }
@@ -392,6 +470,20 @@ export class Lexer {
392
470
  return { n, isFloat: false };
393
471
  }
394
472
 
473
+ // The leading "." is already consumed; reads digits and an optional exponent.
474
+ _readLeadingDotFloat() {
475
+ let n = "0.";
476
+ while (this.pos < this.src.length) {
477
+ const c = this.src.charCodeAt(this.pos);
478
+ if (c >= 48 && c <= 57) n += this.advance();
479
+ else if (c === 95) this.advance();
480
+ else break;
481
+ }
482
+ const c = this.src.charCodeAt(this.pos);
483
+ if (c === 101 || c === 69) n = this._readDecimalExponent(n);
484
+ return { n };
485
+ }
486
+
395
487
  _readDecimalExponent(n) {
396
488
  n += this.advance(); // consume e/E
397
489
  if (this.peek() === "+" || this.peek() === "-") n += this.advance();
@@ -420,12 +512,10 @@ export class Lexer {
420
512
  break;
421
513
  }
422
514
  }
423
- if (
424
- this.pos + 1 < this.src.length &&
425
- this.src.charCodeAt(this.pos) === 46
426
- ) {
515
+ if (this.src.charCodeAt(this.pos) === 46) {
516
+ // `1.` is a float literal; `1..` and `1.x` keep the dot separate.
427
517
  const next = this.src.charCodeAt(this.pos + 1);
428
- if (next >= 48 && next <= 57) {
518
+ if (this._dotContinuesNumber(next)) {
429
519
  isFloat = true;
430
520
  n += this.advance();
431
521
  while (this.pos < this.src.length) {
@@ -438,6 +528,7 @@ export class Lexer {
438
528
  break;
439
529
  }
440
530
  }
531
+ if (n.endsWith(".")) n += "0";
441
532
  }
442
533
  }
443
534
  if (this.pos < this.src.length) {
@@ -450,6 +541,39 @@ export class Lexer {
450
541
  return { n, isFloat };
451
542
  }
452
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
+
570
+ _startsUnicodeIdent() {
571
+ return (
572
+ this.peek().charCodeAt(0) > 127 &&
573
+ isUnicodeLetter(this.src.codePointAt(this.pos))
574
+ );
575
+ }
576
+
453
577
  readIdent() {
454
578
  const start = this.pos;
455
579
  while (this.pos < this.src.length) {
@@ -461,6 +585,10 @@ export class Lexer {
461
585
  code === 95
462
586
  ) {
463
587
  this.pos++;
588
+ } else if (code > 127) {
589
+ const cp = this.src.codePointAt(this.pos);
590
+ if (!isUnicodeLetter(cp) && !isUnicodeDigit(cp)) break;
591
+ this.pos += cp > 0xffff ? 2 : 1;
464
592
  } else {
465
593
  break;
466
594
  }
@@ -526,7 +654,7 @@ export class Lexer {
526
654
  }
527
655
 
528
656
  // Identifiers & keywords
529
- if (/[a-zA-Z_]/.test(ch)) {
657
+ if (/[a-zA-Z_]/.test(ch) || this._startsUnicodeIdent()) {
530
658
  const id = this.readIdent();
531
659
  this.push(KEYWORDS.has(id) ? id : T.IDENT, id, l, c);
532
660
  continue;
@@ -541,6 +669,9 @@ export class Lexer {
541
669
  this.push(T.EOF, "", this.line, this.col);
542
670
  this.tokens.target = this.target;
543
671
  this.tokens.targetDirective = this.targetDirective;
672
+ this.tokens.intrinsics = this.intrinsics;
673
+ this.tokens.natives = this.natives;
674
+ this.tokens.embedDirectives = this.embedDirectives;
544
675
  return this.tokens;
545
676
  }
546
677
  _readPlusOp(l, c) {
@@ -573,8 +704,10 @@ export class Lexer {
573
704
 
574
705
  _readAmpOp(l, c) {
575
706
  if (this.match("&")) this.push(T.AND, "&&", l, c);
576
- else if (this.match("^")) this.push(T.AND_NOT, "&^", l, c);
577
- 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);
578
711
  else this.push(T.AMP, "&", l, c);
579
712
  }
580
713
 
@@ -614,7 +747,15 @@ export class Lexer {
614
747
  else this.push(T.PIPE, "|", l, c);
615
748
  break;
616
749
  case ".":
617
- if (this.peek() === "." && this.peek(1) === ".") {
750
+ if (/[0-9]/.test(this.peek() ?? "")) {
751
+ const { n } = this._readLeadingDotFloat();
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
+ }
758
+ } else if (this.peek() === "." && this.peek(1) === ".") {
618
759
  this.advance();
619
760
  this.advance();
620
761
  this.push(T.ELLIPSIS, "...", l, c);