gofront 1.9.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 (42) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/src/backend/js/expressions.js +329 -58
  4. package/src/backend/js/index.js +90 -10
  5. package/src/backend/js/runtime.js +51 -9
  6. package/src/backend/js/statements.js +193 -84
  7. package/src/backend/js/type-info.js +123 -8
  8. package/src/backend/shared/fmt-types.js +30 -1
  9. package/src/backend/wasm/emit-builtins.js +250 -9
  10. package/src/backend/wasm/emit-exprs.js +63 -22
  11. package/src/backend/wasm/emit-shared.js +4 -1
  12. package/src/backend/wasm/emit-stmts.js +239 -205
  13. package/src/backend/wasm/emit.js +73 -26
  14. package/src/backend/wasm/globals.js +1 -0
  15. package/src/backend/wasm/index.js +8 -0
  16. package/src/backend/wasm/module-emitter.js +211 -57
  17. package/src/backend/wasm/monomorph.js +36 -2
  18. package/src/backend/wasm/natives.js +4 -0
  19. package/src/backend/wasm/types.js +32 -3
  20. package/src/backend/wasm/variadic.js +6 -1
  21. package/src/compiler.js +6 -1
  22. package/src/lexer/lexer.js +1 -0
  23. package/src/link/stdlib.js +1 -0
  24. package/src/link/type-names.js +72 -16
  25. package/src/lower/embedding.js +43 -15
  26. package/src/lower/fmt-formatters.js +38 -14
  27. package/src/lower/functions.js +223 -7
  28. package/src/lower/gen-types.js +21 -2
  29. package/src/lower/index.js +7 -1
  30. package/src/lower/json-codecs.js +114 -6
  31. package/src/parser/declarations.js +4 -2
  32. package/src/parser/expressions.js +25 -41
  33. package/src/parser/statements.js +107 -0
  34. package/src/parser/types.js +11 -0
  35. package/src/typechecker/assignability.js +94 -12
  36. package/src/typechecker/constants.js +124 -2
  37. package/src/typechecker/expressions.js +171 -17
  38. package/src/typechecker/index.js +158 -29
  39. package/src/typechecker/operand-text.js +1 -0
  40. package/src/typechecker/resolve.js +308 -39
  41. package/src/typechecker/statements.js +171 -21
  42. package/src/typechecker/types.js +84 -2
@@ -24,6 +24,7 @@ import {
24
24
  isAny,
25
25
  isBool,
26
26
  isNil,
27
+ isString,
27
28
  isUntyped,
28
29
  isVoid,
29
30
  iteratorYieldParams,
@@ -33,6 +34,9 @@ import {
33
34
 
34
35
  /** @typedef {import('./index.js').TypeChecker} TypeChecker */
35
36
 
37
+ // Tag kinds with no nil value: `case nil` against them cannot compile.
38
+ const NON_NILABLE_TAG_KINDS = new Set(["basic", "struct", "array"]);
39
+
36
40
  /** @type {ThisType<TypeChecker>} */
37
41
  export const statementCheckMethods = {
38
42
  checkBlock(block, scope, returnType) {
@@ -79,11 +83,34 @@ export const statementCheckMethods = {
79
83
  return true;
80
84
  },
81
85
 
86
+ _assignOperand(stmt, i) {
87
+ if (stmt.rhs.length === stmt.lhs.length) return stmt.rhs[i];
88
+ return stmt.rhs.length === 1 ? stmt.rhs[0] : undefined;
89
+ },
90
+
91
+ _checkRedeclAssign(target, source, stmt, i) {
92
+ if (!target || !source || !this._needsAssignCheck(target, source)) return;
93
+ this._skipIfaceMark = true;
94
+ this.assertAssignable(
95
+ target,
96
+ source,
97
+ stmt.lhs[i],
98
+ "assignment",
99
+ this._assignOperand(stmt, i),
100
+ );
101
+ this._skipIfaceMark = false;
102
+ },
103
+
82
104
  _declareDefineVars(stmt, rhsFlat, scope) {
83
105
  for (let i = 0; i < stmt.lhs.length; i++) {
84
106
  const name = stmt.lhs[i].name ?? stmt.lhs[i];
85
107
  if (name === "_") continue;
86
- if (scope.symbols.has(name)) stmt.lhs[i]._redecl = true;
108
+ if (scope.symbols.has(name)) {
109
+ stmt.lhs[i]._redecl = true;
110
+ // Redeclared variables keep their type; the value must be assignable.
111
+ this._checkRedeclAssign(scope.symbols.get(name), rhsFlat[i], stmt, i);
112
+ continue;
113
+ }
87
114
  if (stmt.rhs.length === stmt.lhs.length)
88
115
  this._checkConstDefault(
89
116
  rhsFlat[i],
@@ -195,10 +222,37 @@ export const statementCheckMethods = {
195
222
  return this.checkExpr(e, scope);
196
223
  },
197
224
 
225
+ // A source-written `any` operand still goes through assertAssignable so a
226
+ // concrete target reports `need type assertion`; interop `any` stays exempt.
227
+ _needsAssignCheck(target, source) {
228
+ return !isAny(target) && (!isAny(source) || !!source._declared);
229
+ },
230
+
231
+ // A range assignment (`for _, s = range xs`) stores each key and element
232
+ // into an existing variable; one of interface type boxes the value as
233
+ // any other conversion does. The box is recorded on a holder, not on the
234
+ // target, which codegen also reads as an assignable expression.
235
+ _markRangeAssignBox(stmt, i, target, source) {
236
+ if (stmt.rhs[0]?.kind !== "RangeExpr") return;
237
+ this._markRangeTargetBox(stmt.lhs[i], target, source);
238
+ },
239
+
240
+ _markRangeTargetBox(node, target, source) {
241
+ if (!target || !source) return;
242
+ const holder = {};
243
+ this._markIfaceBox(
244
+ this.resolveType(target),
245
+ this.resolveType(source),
246
+ holder,
247
+ );
248
+ if (holder._ifaceBox) node._rangeBox = holder;
249
+ },
250
+
198
251
  _checkAssignTypes(stmt, rhsFlat, lhsTypes) {
199
252
  for (let i = 0; i < lhsTypes.length; i++) {
200
253
  if (stmt.lhs[i]?.kind === "Ident" && stmt.lhs[i]?.name === "_") continue;
201
254
  const r = rhsFlat[i] ?? ANY;
255
+ this._markRangeAssignBox(stmt, i, lhsTypes[i], r);
202
256
  if (stmt.rhs.length === lhsTypes.length && lhsTypes[i] && r)
203
257
  this._markIfaceBox(
204
258
  this.resolveType(lhsTypes[i]),
@@ -215,14 +269,14 @@ export const statementCheckMethods = {
215
269
  this._checkConstOverflow(lhsTypes[i], r, stmt.rhs[i], "assignment")
216
270
  )
217
271
  continue;
218
- if (!isAny(lhsTypes[i]) && !isAny(r)) {
272
+ if (this._needsAssignCheck(lhsTypes[i], r)) {
219
273
  this._skipIfaceMark = true;
220
274
  this.assertAssignable(
221
275
  lhsTypes[i],
222
276
  r,
223
277
  stmt.lhs[i],
224
278
  "assignment",
225
- stmt.rhs.length === lhsTypes.length ? stmt.rhs[i] : undefined,
279
+ this._assignOperand(stmt, i),
226
280
  );
227
281
  this._skipIfaceMark = false;
228
282
  }
@@ -260,9 +314,34 @@ export const statementCheckMethods = {
260
314
  }
261
315
  }
262
316
  const lhsTypes = stmt.lhs.map((e) => this._checkAssignLhsType(e, scope));
317
+ // A rejected range index is not checked again as an ordinary value.
318
+ if (this._checkRangeIndexAssign(stmt, lhsTypes))
319
+ rhsFlat = [ANY, ...rhsFlat.slice(1)];
263
320
  this._checkAssignTypes(stmt, rhsFlat, lhsTypes);
264
321
  },
265
322
 
323
+ // The index of a string, slice or array range is a typed `int`: Go assigns
324
+ // it only to `int` (or an interface), never to another integer type.
325
+ _checkRangeIndexAssign(stmt, lhsTypes) {
326
+ const range = stmt.rhs[0];
327
+ const lhs = stmt.lhs[0];
328
+ if (range?.kind !== "RangeExpr" || range._isIterator) return false;
329
+ if (!lhs || (lhs.kind === "Ident" && lhs.name === "_")) return false;
330
+ const target = lhsTypes[0];
331
+ if (!target || isAny(target) || typeStr(target) === "int") return false;
332
+ const coll = this.resolveType(range.expr._coreType ?? range.expr._type);
333
+ const cu =
334
+ coll?.kind === "named" ? this.resolveType(coll.underlying) : coll;
335
+ if (!isString(cu) && cu?.kind !== "slice" && cu?.kind !== "array")
336
+ return false;
337
+ const tu = this.resolveType(
338
+ target.kind === "named" ? target.underlying : target,
339
+ );
340
+ if (tu?.kind === "interface") return false;
341
+ this.err(`Cannot assign int to ${typeStr(target)}`, lhs);
342
+ return true;
343
+ },
344
+
266
345
  _checkReturnStmt(stmt, scope, returnType) {
267
346
  const types = stmt.values.map((v) => this.checkExpr(v, scope));
268
347
  stmt._types = types;
@@ -287,14 +366,12 @@ export const statementCheckMethods = {
287
366
  );
288
367
  return;
289
368
  }
290
- for (let i = 0; i < expected.length; i++) {
291
- this.assertAssignable(
292
- expected[i],
293
- forwarded[i],
294
- stmt.values[0],
295
- "return statement",
296
- );
297
- }
369
+ this._checkForwardedValues(
370
+ expected,
371
+ forwarded,
372
+ stmt.values[0],
373
+ "return statement",
374
+ );
298
375
  return;
299
376
  }
300
377
  for (let i = 0; i < expected.length; i++) {
@@ -309,6 +386,35 @@ export const statementCheckMethods = {
309
386
  }
310
387
  },
311
388
 
389
+ // Checks each result of a forwarded multi-value call (`return f()`,
390
+ // `g(f())`) against its target. Every element gets its own interface-box
391
+ // marks in `call._fwdIface` (null where nothing is boxed): tagging the
392
+ // shared call node would give every element the marks of the last one.
393
+ // Lowering splits such forwards into temps that carry these marks.
394
+ _checkForwardedValues(expected, forwarded, call, ctx) {
395
+ const skip = this._skipIfaceMark;
396
+ this._skipIfaceMark = true;
397
+ try {
398
+ for (let i = 0; i < expected.length; i++)
399
+ this.assertAssignable(expected[i], forwarded[i], call, ctx);
400
+ } finally {
401
+ this._skipIfaceMark = skip;
402
+ }
403
+ if (!skip) this._markForwardedElems(expected, forwarded, call);
404
+ },
405
+
406
+ // Records in `call._fwdIface` one interface-box mark holder per forwarded
407
+ // element (null where the element is not boxed), or clears it when no
408
+ // element enters an interface.
409
+ _markForwardedElems(expected, forwarded, call) {
410
+ const holders = expected.map((t, i) => {
411
+ const holder = {};
412
+ this._markIfaceBox(t, forwarded[i], holder);
413
+ return "_ifaceSrc" in holder ? holder : null;
414
+ });
415
+ call._fwdIface = holders.some(Boolean) ? holders : undefined;
416
+ },
417
+
312
418
  _checkIfStmt(stmt, scope, returnType) {
313
419
  const inner = new Scope(scope);
314
420
  if (stmt.init) this.checkStmt(stmt.init, inner, returnType);
@@ -382,6 +488,7 @@ export const statementCheckMethods = {
382
488
  for (const e of c.list) {
383
489
  this.checkExpr(e, caseScope);
384
490
  this._checkCaseValue(stmt.tag, e);
491
+ if (stmt.tag) this._tagFloat32Const(stmt.tag._type, e._type, e);
385
492
  if (tagIs64 && e._type?.kind === "untyped") e._is64Bit = true;
386
493
  }
387
494
  }
@@ -402,9 +509,12 @@ export const statementCheckMethods = {
402
509
  }
403
510
  let tagType = this.resolveType(tag._type);
404
511
  // `switch "foo" {` compares as the constant's default type.
405
- if (isUntyped(tagType) && !isNil(tagType) && tagType.base !== "bool")
406
- tagType = defaultType(tagType);
512
+ if (isUntyped(tagType) && !isNil(tagType)) tagType = defaultType(tagType);
407
513
  if (this._caseValueUnchecked(tagType, caseType)) return;
514
+ if (isNil(caseType)) {
515
+ this.err(`cannot convert nil to type ${typeStr(tagType)}`, e);
516
+ return;
517
+ }
408
518
  if (isUntyped(caseType)) {
409
519
  if (this._isUntypedAssignable(tagType, caseType)) return;
410
520
  this.err(
@@ -427,7 +537,11 @@ export const statementCheckMethods = {
427
537
  // A tagless `switch` is `switch true`, so every case must be boolean.
428
538
  _checkTaglessCase(e, caseType) {
429
539
  if (!caseType || caseType._tainted || caseType.kind === "typeParam") return;
430
- if (isAny(caseType) || isNil(caseType) || isBool(caseType)) return;
540
+ if (isNil(caseType)) {
541
+ this.err(`cannot convert nil to type bool`, e);
542
+ return;
543
+ }
544
+ if (isAny(caseType) || isBool(caseType)) return;
431
545
  if (isUntyped(caseType) && caseType.base === "bool") return;
432
546
  const under = this.resolveType(caseType.underlying ?? caseType);
433
547
  if (isBool(under)) return;
@@ -442,13 +556,22 @@ export const statementCheckMethods = {
442
556
  _caseValueUnchecked(tagType, caseType) {
443
557
  if (!tagType || !caseType || tagType._tainted || caseType._tainted)
444
558
  return true;
445
- if (isAny(tagType) || isAny(caseType) || isNil(caseType)) return true;
559
+ if (isAny(tagType) || isAny(caseType)) return true;
446
560
  if (isUntyped(tagType) || tagType.kind === "typeParam") return true;
561
+ if (isNil(caseType)) return this._nilCaseOk(tagType);
447
562
  return (
448
563
  this.resolveType(tagType.underlying ?? tagType)?.kind === "interface"
449
564
  );
450
565
  },
451
566
 
567
+ // `case nil` needs a tag with a nil value (pointer, slice, map, func,
568
+ // chan, interface); a basic, struct or array tag makes the caller report
569
+ // `cannot convert nil`.
570
+ _nilCaseOk(tagType) {
571
+ const kind = this._deepUnderlying(tagType)?.kind;
572
+ return !NON_NILABLE_TAG_KINDS.has(kind);
573
+ },
574
+
452
575
  // Whether assertAssignable(target, source) reports nothing.
453
576
  _assignableQuietly(target, source) {
454
577
  const saved = this.errors;
@@ -509,7 +632,8 @@ export const statementCheckMethods = {
509
632
  this._markDynamicType(t, this.resolveTypeNode(t, inner));
510
633
  }
511
634
  if (stmt.assign) {
512
- // Bind the variable to the single case type, or any for multi/default.
635
+ // Bind the variable to the single case type, or the switch operand's type
636
+ // for multi-type and default clauses, as Go does.
513
637
  // `case nil` binds the switch expression's own type, as in Go.
514
638
  const only = c.types?.length === 1 ? c.types[0] : null;
515
639
  const isNilCase = only?.kind === "TypeName" && only.name === "nil";
@@ -517,7 +641,7 @@ export const statementCheckMethods = {
517
641
  ? (stmt.expr._type ?? ANY)
518
642
  : only
519
643
  ? this.resolveTypeNode(only, inner)
520
- : ANY;
644
+ : (stmt.expr._type ?? ANY);
521
645
  caseScope.defineLocal(stmt.assign, bindType);
522
646
  // The capture variable is always considered used — it's the whole
523
647
  // point of the switch-with-assign form. Each case body uses its own
@@ -564,9 +688,35 @@ export const statementCheckMethods = {
564
688
  }
565
689
  },
566
690
 
567
- _checkRangeIterAssignVars(lhs, scope) {
568
- for (const e of lhs) {
569
- if (e.kind === "Ident" && e.name !== "_") this.checkExpr(e, scope);
691
+ // Each yielded value is stored into an existing variable; one of
692
+ // interface type boxes the value as any other conversion does.
693
+ _checkRangeIterAssignVars(lhs, scope, yieldParams = []) {
694
+ lhs.forEach((e, i) => {
695
+ if (e.kind === "Ident" && e.name === "_") return;
696
+ const target = this.checkExpr(e, scope);
697
+ this._markRangeTargetBox(e, target, yieldParams[i]);
698
+ this._checkRangeIterAssignable(e, target, yieldParams[i]);
699
+ });
700
+ },
701
+
702
+ // The yielded value is assigned to the target, so it must be assignable
703
+ // to it (`for s = range seq` with `s string` and `seq` yielding `int`).
704
+ // The lenient `float64 = int` pair of plain assignments is reported here:
705
+ // the iterator loop stores the yielded value without converting it.
706
+ _checkRangeIterAssignable(node, target, source) {
707
+ if (!target || !source) return;
708
+ const t = this.resolveType(target);
709
+ const v = this.resolveType(source);
710
+ if (this._isNumericCoercible(t, v)) {
711
+ this.err(`Cannot assign ${typeStr(v)} to ${typeStr(t)}`, node);
712
+ return;
713
+ }
714
+ if (!this._needsAssignCheck(target, source)) return;
715
+ this._skipIfaceMark = true;
716
+ try {
717
+ this.assertAssignable(target, source, node, "assignment");
718
+ } finally {
719
+ this._skipIfaceMark = false;
570
720
  }
571
721
  },
572
722
 
@@ -592,7 +742,7 @@ export const statementCheckMethods = {
592
742
 
593
743
  if (initStmt.kind === "DefineStmt")
594
744
  this._bindRangeIterDefineVars(lhs, info, scope);
595
- else this._checkRangeIterAssignVars(lhs, scope);
745
+ else this._checkRangeIterAssignVars(lhs, scope, info.yieldParams);
596
746
  return info;
597
747
  },
598
748
  };
@@ -323,6 +323,9 @@ export const UNTYPED_INT = { kind: "untyped", base: "int" };
323
323
  export const UNTYPED_FLOAT = { kind: "untyped", base: "float64" };
324
324
  export const UNTYPED_STRING = { kind: "untyped", base: "string" };
325
325
  export const UNTYPED_BOOL = { kind: "untyped", base: "bool" };
326
+ // An untyped rune constant is an untyped integer whose default type is
327
+ // `rune`; every `base === "int"` rule applies to it unchanged.
328
+ export const UNTYPED_RUNE = { kind: "untyped", base: "int", rune: true };
326
329
 
327
330
  // ── Complex types ────────────────────────────────────────────
328
331
  export const COMPLEX128 = { kind: "basic", name: "complex128" };
@@ -530,7 +533,7 @@ export function defaultType(t) {
530
533
  if (t?.kind !== "untyped") return t;
531
534
  switch (t.base) {
532
535
  case "int":
533
- return INT;
536
+ return t.rune ? RUNE : INT;
534
537
  case "float64":
535
538
  return FLOAT64;
536
539
  case "string":
@@ -551,7 +554,7 @@ export function typeStr(t) {
551
554
  case "basic":
552
555
  return t.name;
553
556
  case "untyped":
554
- return `untyped ${t.base}`;
557
+ return t.rune ? "untyped rune" : `untyped ${t.base}`;
555
558
  case "slice":
556
559
  return `[]${typeStr(t.elem)}`;
557
560
  case "shared":
@@ -596,6 +599,27 @@ export function typeStr(t) {
596
599
 
597
600
  // The field an embedded type declares is named after the type as written, so
598
601
  // an alias (`type A = Base; struct{ A }`) gives the field `A`.
602
+ // Per struct type, the methods promoted into its method set from embedded
603
+ // fields: name -> { depth, viaPtr, source, mType }. `depth` counts embeds
604
+ // from the struct, `viaPtr` is set when an embed on the path is a pointer
605
+ // and `source` is the method as declared. Kept outside the struct object so
606
+ // a copy of the struct (`type S Base`) does not share it.
607
+ export const promotedMethods = new WeakMap();
608
+
609
+ // Two embedded fields at the same, shallowest depth declare method `name`,
610
+ // so it is in neither method set of `struct` and selecting it is an error.
611
+ export function isAmbiguousPromotion(struct, name) {
612
+ return promotedMethods.get(struct)?.get(name)?.ambiguous === true;
613
+ }
614
+
615
+ // The ambiguous method names of `struct` (see isAmbiguousPromotion).
616
+ export function ambiguousPromotions(struct) {
617
+ const names = [];
618
+ for (const [name, p] of promotedMethods.get(struct) ?? [])
619
+ if (p.ambiguous) names.push(name);
620
+ return names;
621
+ }
622
+
599
623
  export function embeddedFieldName(f) {
600
624
  const ast = f.astType?.kind === "PointerType" ? f.astType.base : f.astType;
601
625
  const t = f.type?.kind === "pointer" ? f.type.base : f.type;
@@ -605,6 +629,64 @@ export function embeddedFieldName(f) {
605
629
  .replace(/\$\$.*$/, "");
606
630
  }
607
631
 
632
+ // Whether `st` declares `name` itself: a field of that name or an embedded
633
+ // field named after its type.
634
+ function declaresField(st, name) {
635
+ return (st._structFields ?? []).some((f) =>
636
+ f.embedded ? embeddedFieldName(f) === name : f.names?.includes(name),
637
+ );
638
+ }
639
+
640
+ // The shallowest depth at which `struct` promotes a field `name` from its
641
+ // embedded types, and how many promotion paths reach it at that depth:
642
+ // { depth, count }, or null when no embedded type offers one.
643
+ function promotedFieldDepth(struct, name, path = new Set()) {
644
+ if (path.has(struct)) return null;
645
+ path.add(struct);
646
+ let best = null;
647
+ for (const e of struct._embeds ?? []) {
648
+ const inner = e?.kind === "pointer" ? e.base : e;
649
+ const st = inner?.kind === "named" ? inner.underlying : inner;
650
+ if (st?.kind !== "struct" || !st._structFields) continue;
651
+ const sub = declaresField(st, name)
652
+ ? { depth: 0, count: 1 }
653
+ : promotedFieldDepth(st, name, path);
654
+ if (!sub) continue;
655
+ const depth = 1 + sub.depth;
656
+ if (!best || depth < best.depth) best = { depth, count: sub.count };
657
+ else if (depth === best.depth) best.count += sub.count;
658
+ }
659
+ path.delete(struct);
660
+ return best;
661
+ }
662
+
663
+ // Whether selecting `name` on `struct`, whose flattened field map has it, is
664
+ // ambiguous: two promotion paths reach a field of that name at the same
665
+ // shallowest depth, or a field and a method promoted at that depth do. A
666
+ // field declared on the struct itself and a method declared on it always win.
667
+ export function isAmbiguousFieldSelector(struct, name) {
668
+ if (declaresField(struct, name)) return false;
669
+ const promoted = promotedMethods.get(struct)?.get(name);
670
+ if (struct.methods?.has(name) && promoted?.mType !== struct.methods.get(name))
671
+ return false;
672
+ const field = promotedFieldDepth(struct, name);
673
+ if (!field) return false;
674
+ if (!promoted || field.depth < promoted.depth) return field.count > 1;
675
+ return field.depth === promoted.depth || promoted.ambiguous === true;
676
+ }
677
+
678
+ // Whether selecting `name` on `struct`, whose flattened field map has it,
679
+ // reaches a method promoted at a shallower depth than the field (Go's depth
680
+ // rule): the method is selected and the deeper field is shadowed.
681
+ export function isMethodPromotedOverField(struct, name) {
682
+ if (declaresField(struct, name)) return false;
683
+ const promoted = promotedMethods.get(struct)?.get(name);
684
+ if (!promoted?.mType || promoted.ambiguous) return false;
685
+ if (struct.methods?.get(name) !== promoted.mType) return false;
686
+ const field = promotedFieldDepth(struct, name);
687
+ return field !== null && promoted.depth < field.depth;
688
+ }
689
+
608
690
  // A function-local type shadowed by another type is renamed `Name__L<n>`.
609
691
  export const LOCAL_TYPE_SUFFIX = /__L\d+/;
610
692