gofront 1.3.5 → 1.3.7

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.
@@ -1,6 +1,6 @@
1
1
  // CodeGen statement methods — installed as a mixin on CodeGen.prototype.
2
2
 
3
- import { isComplex } from "../typechecker/types.js";
3
+ import { isComplex, isNumeric } from "../typechecker/types.js";
4
4
 
5
5
  /** @typedef {import('./index.js').CodeGen} CodeGen */
6
6
 
@@ -122,14 +122,20 @@ export const statementGenMethods = {
122
122
 
123
123
  _genDefineMixedRedecl(lhs, rhs, redecls) {
124
124
  if (rhs.length === 1 && lhs.length > 1) {
125
- const tmp = "__t";
126
- this.line(`let ${tmp} = ${rhs[0]};`);
125
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
126
+ const tmp = `__t${this._tmpCounter}`;
127
+ this.line(`const ${tmp} = ${rhs[0]};`);
127
128
  for (let i = 0; i < lhs.length; i++) {
129
+ if (lhs[i] === "_") continue;
128
130
  const prefix = redecls[i] ? "" : "let ";
129
131
  this.line(`${prefix}${lhs[i]} = ${tmp}[${i}];`);
130
132
  }
131
133
  } else {
132
134
  for (let i = 0; i < lhs.length; i++) {
135
+ if (lhs[i] === "_") {
136
+ this.line(`${rhs[i]};`);
137
+ continue;
138
+ }
133
139
  const prefix = redecls[i] ? "" : "let ";
134
140
  this.line(`${prefix}${lhs[i]} = ${rhs[i]};`);
135
141
  }
@@ -142,7 +148,13 @@ export const statementGenMethods = {
142
148
  this._genCommaOkMapIndex(stmt, rhsNode, true);
143
149
  return;
144
150
  }
145
- const rhs = stmt.rhs.map((e) => this.genExpr(e));
151
+ const paired = stmt.rhs.length === stmt.lhs.length;
152
+ const rhs = stmt.rhs.map((e, i) => {
153
+ const l = paired ? stmt.lhs[i] : null;
154
+ if (!l || l.name === "_") return this.genExpr(e);
155
+ if (l._redecl) return this.genValueExpr(e);
156
+ return this._genDeclValue(l.name, e);
157
+ });
146
158
  const lhs = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
147
159
  const redecls = stmt.lhs.map((e) => !!e._redecl);
148
160
  const anyRedecl = redecls.some((r) => r);
@@ -150,7 +162,8 @@ export const statementGenMethods = {
150
162
  this._genDefineSingleVar(lhs, rhs, redecls);
151
163
  } else if (!anyRedecl) {
152
164
  const rhsStr = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
153
- this.line(`let [${lhs.join(", ")}] = ${rhsStr};`);
165
+ const names = lhs.map((n) => (n === "_" ? "" : n));
166
+ this.line(`let [${names.join(", ")}] = ${rhsStr};`);
154
167
  } else {
155
168
  this._genDefineMixedRedecl(lhs, rhs, redecls);
156
169
  }
@@ -218,7 +231,12 @@ export const statementGenMethods = {
218
231
  this._genStructDerefAssign(stmt);
219
232
  return;
220
233
  }
221
- const rhs = stmt.rhs.map((e) => this.genExpr(e));
234
+ const paired = stmt.rhs.length === stmt.lhs.length && stmt.op === "=";
235
+ const rhs = stmt.rhs.map((e, i) =>
236
+ paired && stmt.lhs[i].name !== "_"
237
+ ? this.genValueExpr(e)
238
+ : this.genExpr(e),
239
+ );
222
240
  this._markIndexLvalues(stmt.lhs);
223
241
  const lhs = stmt.lhs.map((e) => this._genAssignLhsExpr(e));
224
242
  const pairs = lhs.map((l, i) => ({ l, r: rhs[i] ?? rhs[0] }));
@@ -243,7 +261,14 @@ export const statementGenMethods = {
243
261
 
244
262
  _genStructDerefAssign(stmt) {
245
263
  const target = this.genExpr(stmt.lhs[0].operand);
246
- const source = this.genExpr(stmt.rhs[0]);
264
+ const rhsNode = stmt.rhs[0];
265
+ const fields = this._aggregateBase(rhsNode._type)?.fields;
266
+ const shallowOk =
267
+ fields instanceof Map &&
268
+ ![...fields.values()].some((t) => this._aggregateBase(t));
269
+ const source = shallowOk
270
+ ? this.genExpr(rhsNode)
271
+ : this.genValueExpr(rhsNode);
247
272
  this.line(`Object.assign(${target}, ${source});`);
248
273
  },
249
274
 
@@ -293,7 +318,7 @@ export const statementGenMethods = {
293
318
  }
294
319
  const vals =
295
320
  stmt.values.length > 0
296
- ? stmt.values.map((v) => this.genExpr(v))
321
+ ? stmt.values.map((v) => this._genReturnValue(v))
297
322
  : (this.namedReturnVars ?? []);
298
323
  const stored = vals.length === 1 ? vals[0] : `[${vals.join(", ")}]`;
299
324
  this.line(
@@ -318,10 +343,10 @@ export const statementGenMethods = {
318
343
  this.line("return;");
319
344
  }
320
345
  } else if (stmt.values.length === 1) {
321
- this.line(`return ${this.genExpr(stmt.values[0])};`);
346
+ this.line(`return ${this._genReturnValue(stmt.values[0])};`);
322
347
  } else {
323
348
  this.line(
324
- `return [${stmt.values.map((v) => this.genExpr(v)).join(", ")}];`,
349
+ `return [${stmt.values.map((v) => this._genReturnValue(v)).join(", ")}];`,
325
350
  );
326
351
  }
327
352
  },
@@ -425,12 +450,7 @@ export const statementGenMethods = {
425
450
  },
426
451
 
427
452
  _isIntRangeType(iterType) {
428
- return (
429
- (iterType?.kind === "basic" &&
430
- (iterType.name === "int" || iterType.name === "float64")) ||
431
- (iterType?.kind === "untyped" &&
432
- (iterType.base === "int" || iterType.base === "float64"))
433
- );
453
+ return isNumeric(iterType);
434
454
  },
435
455
 
436
456
  _isMapRangeType(iterType) {
@@ -447,47 +467,152 @@ export const statementGenMethods = {
447
467
  );
448
468
  },
449
469
 
450
- _emitSliceLoopHeader(rawIdx, iteree, isAssign, d) {
470
+ _rangeVarTarget(e, isAssign) {
471
+ if (e.kind === "Ident") {
472
+ if (e.name === "_") return null;
473
+ const boxed = this._boxedVars.has(e.name);
474
+ if (isAssign) return boxed ? `${e.name}.value = ` : `${e.name} = `;
475
+ return boxed ? { name: e.name, boxed } : { name: e.name };
476
+ }
477
+ this._markIndexLvalues([e]);
478
+ return `${this._genAssignLhsExpr(e)} = `;
479
+ },
480
+
481
+ _emitRangeVar(e, valueJs, isAssign) {
482
+ if (!e) return;
483
+ const target = this._rangeVarTarget(e, isAssign);
484
+ if (target === null) return;
485
+ if (typeof target === "string") {
486
+ this.line(`${target}${valueJs};`);
487
+ return;
488
+ }
489
+ const v = target.boxed ? `{ value: ${valueJs} }` : valueJs;
490
+ this.line(`let ${target.name} = ${v};`);
491
+ },
492
+
493
+ // True when the body re-declares one of `names` at its top level (legal Go shadowing).
494
+ _bodyRedeclares(body, names) {
495
+ if (names.length === 0) return false;
496
+ for (const s of body.stmts ?? []) {
497
+ if (s.kind === "DefineStmt" && s.lhs.some((e) => names.includes(e.name)))
498
+ return true;
499
+ if (
500
+ (s.kind === "VarDecl" || s.kind === "ConstDecl") &&
501
+ s.decls?.some((d) => d.names?.some((n) => names.includes(n)))
502
+ )
503
+ return true;
504
+ }
505
+ return false;
506
+ },
507
+
508
+ // Range value variable: aliases the element unless the body writes the variable or the collection.
509
+ _rangeElemValue(stmt, valNode, js, isAssign) {
510
+ const iterNode = stmt.init.rhs[0].expr;
511
+ const iterType = iterNode._type;
512
+ const base = iterType?.kind === "named" ? iterType.underlying : iterType;
513
+ const elemType = base?.elem;
514
+ if (!valNode || !this._aggregateBase(elemType)) return js;
515
+ if (isAssign || valNode.kind !== "Ident")
516
+ return this._cloneJs(elemType, js);
517
+ if (valNode.name === "_") return js;
518
+ const root = this._rootIdentName(iterNode);
519
+ const mutated =
520
+ this._fnMutates(valNode.name) ||
521
+ (root !== null && this._nodeMutatesVar(stmt.body, root));
522
+ this._markOwnership(valNode.name, mutated);
523
+ return mutated ? this._cloneJs(elemType, js) : js;
524
+ },
525
+
526
+ _genSliceRangeFor(stmt, iteree, isAssign) {
527
+ this._loopDepth = this._loopDepth ?? 0;
528
+ const d = this._loopDepth++;
529
+ const [idxNode, valNode] = stmt.init.lhs;
530
+ const i = `__i${d}`;
451
531
  const arr = `__arr${d}`;
452
532
  const len = `__len${d}`;
453
- const hasIdx = rawIdx && rawIdx !== "_";
454
- if (isAssign && hasIdx) {
455
- this.line(`const ${arr} = ${iteree};`);
456
- this.line(`const ${len} = ${arr} ? ${arr}.length : 0;`);
457
- this.line(`for (${rawIdx} = 0; ${rawIdx} < ${len}; ${rawIdx}++) {`);
458
- } else {
459
- const idx = hasIdx ? rawIdx : `__i${d}`;
460
- this.line(
461
- `for (let ${idx} = 0, ${arr} = ${iteree}, ${len} = ${arr} ? ${arr}.length : 0; ${idx} < ${len}; ${idx}++) {`,
533
+ this.line(
534
+ `for (let ${i} = 0, ${arr} = ${iteree}, ${len} = ${arr} ? ${arr}.length : 0; ${i} < ${len}; ${i}++) {`,
535
+ );
536
+ this.indented(() => {
537
+ this._emitRangeVar(idxNode, i, isAssign);
538
+ this._emitRangeVar(
539
+ valNode,
540
+ this._rangeElemValue(stmt, valNode, `${arr}[${i}]`, isAssign),
541
+ isAssign,
462
542
  );
463
- }
543
+ const declared = isAssign
544
+ ? []
545
+ : [idxNode, valNode]
546
+ .filter((e) => e?.kind === "Ident" && e.name !== "_")
547
+ .map((e) => e.name);
548
+ if (this._bodyRedeclares(stmt.body, declared)) {
549
+ this.line("{");
550
+ this.indented(() => this.genBlock(stmt.body));
551
+ this.line("}");
552
+ } else {
553
+ this.genBlock(stmt.body);
554
+ }
555
+ });
556
+ this.line("}");
557
+ this._loopDepth--;
464
558
  },
465
559
 
466
- _emitSliceValAssign(rawIdx, rawVal, isAssign, d) {
467
- if (!rawVal || rawVal === "_") return;
468
- const idx = rawIdx && rawIdx !== "_" ? rawIdx : `__i${d}`;
469
- const kw = isAssign ? "" : "let ";
470
- this.line(`${kw}${rawVal} = __arr${d}[${idx}];`);
560
+ // Whether `for i := range n` can be emitted as a plain `for (let i = 0; …)` without hidden registers.
561
+ _isSimpleIntRange(stmt, name, isAssign) {
562
+ if (isAssign) return false;
563
+ if (!name || name === "_") return true;
564
+ return !this._boxedVars.has(name) && !this._nodeAssigns(stmt.body, name);
471
565
  },
472
566
 
473
- _genSliceRangeFor(stmt, iteree, lhs, isAssign) {
567
+ _genIntRangeBody(stmt, name, isAssign) {
568
+ const wrap = !isAssign && name && this._bodyRedeclares(stmt.body, [name]);
569
+ if (!wrap) return this.genBlock(stmt.body);
570
+ this.line("{");
571
+ this.indented(() => this.genBlock(stmt.body));
572
+ this.line("}");
573
+ },
574
+
575
+ _genIntRangeFor(stmt, iteree, isAssign) {
576
+ const varNode = stmt.init.lhs[0];
577
+ const name = varNode?.kind === "Ident" ? varNode.name : null;
578
+ if (this._isSimpleIntRange(stmt, name, isAssign)) {
579
+ const v = !name || name === "_" ? "_$" : name;
580
+ this.line(`for (let ${v} = 0; ${v} < ${iteree}; ${v}++) {`);
581
+ this.indented(() => this.genBlock(stmt.body));
582
+ this.line("}");
583
+ return;
584
+ }
474
585
  this._loopDepth = this._loopDepth ?? 0;
475
- const d = this._loopDepth;
476
- this._loopDepth++;
477
- this._emitSliceLoopHeader(lhs[0], iteree, isAssign, d);
586
+ const d = this._loopDepth++;
587
+ const i = `__i${d}`;
588
+ const n = `__n${d}`;
589
+ this.line(`for (let ${i} = 0, ${n} = ${iteree}; ${i} < ${n}; ${i}++) {`);
478
590
  this.indented(() => {
479
- this._emitSliceValAssign(lhs[0], lhs[1], isAssign, d);
480
- this.genBlock(stmt.body);
591
+ this._emitRangeVar(varNode, i, isAssign);
592
+ this._genIntRangeBody(stmt, name, isAssign);
481
593
  });
482
594
  this.line("}");
483
595
  this._loopDepth--;
484
596
  },
485
597
 
486
- _genIntRangeFor(rawVar, iteree, body) {
487
- const v = rawVar === "_" ? "_$" : rawVar;
488
- this.line(`for (let ${v} = 0; ${v} < ${iteree}; ${v}++) {`);
489
- this.indented(() => this.genBlock(body));
490
- this.line("}");
598
+ // True when `node` contains an assignment or ++/-- targeting identifier `name`.
599
+ _nodeAssigns(node, name) {
600
+ if (!node || typeof node !== "object") return false;
601
+ if (Array.isArray(node))
602
+ return node.some((n) => this._nodeAssigns(n, name));
603
+ if (
604
+ (node.kind === "AssignStmt" &&
605
+ node.lhs.some((e) => e.kind === "Ident" && e.name === name)) ||
606
+ (node.kind === "IncDecStmt" &&
607
+ node.expr.kind === "Ident" &&
608
+ node.expr.name === name)
609
+ )
610
+ return true;
611
+ for (const key of Object.keys(node)) {
612
+ if (key.startsWith("_")) continue;
613
+ if (this._nodeAssigns(node[key], name)) return true;
614
+ }
615
+ return false;
491
616
  },
492
617
 
493
618
  _genIterFor(init, iterType, iteree, lhs, body) {
@@ -515,12 +640,12 @@ export const statementGenMethods = {
515
640
  : this.genExpr(range.expr);
516
641
 
517
642
  if (lhs.length <= 1 && this._isIntRangeType(iterType)) {
518
- this._genIntRangeFor(lhs[0], iteree, stmt.body);
643
+ this._genIntRangeFor(stmt, iteree, init.kind === "AssignStmt");
519
644
  return;
520
645
  }
521
646
 
522
647
  if (!this._isMapRangeType(iterType) && !this._isStringRangeType(iterType)) {
523
- this._genSliceRangeFor(stmt, iteree, lhs, init.kind === "AssignStmt");
648
+ this._genSliceRangeFor(stmt, iteree, init.kind === "AssignStmt");
524
649
  return;
525
650
  }
526
651
 
@@ -28,12 +28,17 @@ const BUILDER_COMMON = {
28
28
  export const builderMethods = {
29
29
  _genBuilderCall(typeName, method, expr) {
30
30
  const recv = expr.func.expr;
31
- const recvJs = this.genExpr(recv);
32
- const base = `(${recvJs}?.value ?? ${recvJs})`;
31
+ const base = this._unboxWriter(this.genExpr(recv));
33
32
  const args = expr.args.map((a) => this.genExpr(a));
34
33
  const isStr = typeName === "strings.Builder";
35
34
  const table = isStr ? BUILDER_STR : BUILDER_BYTES;
36
35
  const gen = table[method] ?? BUILDER_COMMON[method];
37
36
  return gen ? gen(base, args) : undefined;
38
37
  },
38
+
39
+ // Accepts a Builder/Buffer or a pointer box around one; evaluates `js` once.
40
+ _unboxWriter(js) {
41
+ if (/^[\w$.]+$/.test(js)) return `(${js}?.value ?? ${js})`;
42
+ return `((__w) => __w?.value ?? __w)(${js})`;
43
+ },
39
44
  };
@@ -80,12 +80,12 @@ export const fmtMethods = {
80
80
  const sprintfCall = this._buildFprintSprintfCall(fn, rest.length, restJs);
81
81
  if (targetTypeName === "strings.Builder") {
82
82
  const w = this.genExpr(writerArg);
83
- const base = `(${w}?.value ?? ${w})`;
83
+ const base = this._unboxWriter(w);
84
84
  return `(${base}._buf += ${sprintfCall})`;
85
85
  }
86
86
  if (targetTypeName === "bytes.Buffer") {
87
87
  const w = this.genExpr(writerArg);
88
- const base = `(${w}?.value ?? ${w})`;
88
+ const base = this._unboxWriter(w);
89
89
  return `((__b,__s)=>{ __b._buf.push(...new TextEncoder().encode(__s)); })(${base}, ${sprintfCall})`;
90
90
  }
91
91
  // Generic io.Writer fallback — call .WriteString if available
@@ -14,7 +14,7 @@ export const ioMethods = {
14
14
  (writerType?.kind === "pointer" ? writerType.base?.name : null);
15
15
  const w = this.genExpr(writerArg);
16
16
  const s = this.genExpr(expr.args[1]);
17
- const base = `(${w}?.value ?? ${w})`;
17
+ const base = this._unboxWriter(w);
18
18
  if (typeName === "strings.Builder") {
19
19
  return `((b,s) => { b._buf += s; return [s.length, null]; })(${base}, ${s})`;
20
20
  }
package/src/dev-server.js CHANGED
@@ -140,13 +140,15 @@ function isInsideDir(root, target) {
140
140
  return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
141
141
  }
142
142
 
143
- function handleSseRequest(req, res, clients) {
143
+ function handleSseRequest(req, res, clients, lastError = null) {
144
144
  res.writeHead(200, {
145
145
  "Content-Type": "text/event-stream",
146
146
  "Cache-Control": "no-cache",
147
147
  Connection: "keep-alive",
148
148
  });
149
149
  res.write(": connected\n\n");
150
+ if (lastError)
151
+ res.write(`event: build-error\ndata: ${JSON.stringify(lastError)}\n\n`);
150
152
  clients.add(res);
151
153
  req.on?.("close", () => clients.delete(res));
152
154
  }
@@ -216,7 +218,7 @@ export function handleDevRequest(
216
218
  options = {},
217
219
  ) {
218
220
  if (req.url === "/_gofront/events") {
219
- handleSseRequest(req, res, clients);
221
+ handleSseRequest(req, res, clients, options.getLastError?.() ?? null);
220
222
  return;
221
223
  }
222
224
 
@@ -312,15 +314,19 @@ function closeServer(server, clients, timer) {
312
314
  export function createDevServer(serveDir, port = 3000, options = {}) {
313
315
  const clients = new Set();
314
316
  const heartbeatMs = options.heartbeatInterval ?? 15000;
317
+ const host = options.host ?? "localhost";
318
+ // Kept so browsers that connect (or reload) after a failed build still see the overlay.
319
+ let lastError = null;
320
+ const requestOptions = { ...options, getLastError: () => lastError };
315
321
 
316
322
  const server = createServer((req, res) => {
317
- handleDevRequest(req, res, serveDir, clients, options);
323
+ handleDevRequest(req, res, serveDir, clients, requestOptions);
318
324
  });
319
325
 
320
326
  server.on("error", (err) => handleServerError(err, port));
321
- server.listen(port, () => {
327
+ server.listen(port, host, () => {
322
328
  const actualPort = server.address()?.port ?? port;
323
- if (port !== 0) {
329
+ if (port !== 0 && !options.silent) {
324
330
  console.error(`gofront: dev server → http://localhost:${actualPort}`);
325
331
  }
326
332
  });
@@ -329,17 +335,23 @@ export function createDevServer(serveDir, port = 3000, options = {}) {
329
335
  heartbeatMs > 0 ? setInterval(() => sendPings(clients), heartbeatMs) : null;
330
336
  heartbeatTimer?.unref?.();
331
337
 
332
- const notify = createNotify(clients);
338
+ const broadcastNotify = createNotify(clients);
339
+ const notify = (arg) => {
340
+ if (!arg?.type) lastError = null;
341
+ broadcastNotify(arg);
342
+ };
343
+ const notifyError = (err) => {
344
+ lastError = buildErrorPayload(err);
345
+ broadcastToClients(clients, "build-error", lastError);
346
+ };
333
347
 
334
348
  return {
335
349
  broadcast: (event, data) => broadcastToClients(clients, event, data),
336
350
  notify,
337
351
  notifyCss: (file) =>
338
352
  broadcastToClients(clients, "css-update", { type: "css-update", file }),
339
- notifyError: (err) =>
340
- broadcastToClients(clients, "build-error", buildErrorPayload(err)),
341
- notifyBuildError: (err) =>
342
- broadcastToClients(clients, "build-error", buildErrorPayload(err)),
353
+ notifyError,
354
+ notifyBuildError: notifyError,
343
355
  ping: () => sendPings(clients),
344
356
  clients,
345
357
  server,
package/src/index.js CHANGED
@@ -162,8 +162,14 @@ if (args[0] === "dev") {
162
162
  const devOptions = parseDevArgs(args.slice(1));
163
163
  try {
164
164
  const dev = await handleDev(devOptions.targetDir, devOptions);
165
- const port = dev.port ?? devOptions.port ?? 3000;
166
- console.error(`gofront: dev server running → http://localhost:${port}`);
165
+ if (dev.initialError) {
166
+ console.error("gofront: ERROR");
167
+ for (const line of dev.initialError.message.split("\n"))
168
+ console.error(` ${line}`);
169
+ } else {
170
+ console.error(`gofront: OK — wrote ${dev.outputFile}`);
171
+ }
172
+ console.error(`gofront: dev server running → http://localhost:${dev.port}`);
167
173
  console.error(`gofront: watching ${dev.srcDir} for changes...`);
168
174
 
169
175
  const shutdown = async () => {
@@ -29,7 +29,7 @@ export const declarationParseMethods = {
29
29
  this.check(T.IDENT) && !this.isReceiverTerminator(this.peek2())
30
30
  ? this.advance().value
31
31
  : "_";
32
- this.match(T.STAR); // pointer receiver: (c *Counter) — strip *, treat same as value receiver
32
+ const recvPointer = Boolean(this.match(T.STAR));
33
33
  const recvType = this.parseTypeName();
34
34
  this.expect(T.RPAREN);
35
35
  const name = this.expect(T.IDENT).value;
@@ -40,6 +40,7 @@ export const declarationParseMethods = {
40
40
  kind: "MethodDecl",
41
41
  recvName,
42
42
  recvType,
43
+ recvPointer,
43
44
  name,
44
45
  params,
45
46
  returnType,
package/src/pwa.js CHANGED
@@ -86,7 +86,9 @@ self.addEventListener("fetch", (event) => {
86
86
  return response;
87
87
  }).catch(() => {
88
88
  if (event.request.headers?.get?.("accept")?.includes("text/html")) {
89
- return caches.match("./") || caches.match("./index.html");
89
+ return caches
90
+ .match("./")
91
+ .then((r) => r || caches.match("./index.html"));
90
92
  }
91
93
  });
92
94
  }),
@@ -14,6 +14,7 @@ import {
14
14
  isNumeric,
15
15
  isPointer,
16
16
  isString,
17
+ isTypedArraySlice,
17
18
  isUntyped,
18
19
  LOG_OPS,
19
20
  SIZED_FLOAT_NAMES,
@@ -157,6 +158,7 @@ export const assignabilityMethods = {
157
158
  if (!target || !source) return;
158
159
  target = this.resolveType(target);
159
160
  source = this.resolveType(source);
161
+ this._markIfaceBox(target, source, node);
160
162
  if (this._assertAssignableEarlyReturn(target, source)) return;
161
163
  if (isPointer(target) && isPointer(source)) {
162
164
  this.assertAssignable(
@@ -170,10 +172,34 @@ export const assignabilityMethods = {
170
172
  if (this._isNumericCoercible(target, source)) return;
171
173
  if (this._checkArrayAssignable(target, source, node)) return;
172
174
  if (this._checkTypedArrayAssignable(target, source, node)) return;
175
+ if (this._checkFuncAssignable(target, source, node)) return;
173
176
  if (typeStr(target) !== typeStr(source))
174
177
  this._assertAssignableTypeMismatch(target, source, node);
175
178
  },
176
179
 
180
+ // Tags struct values / struct pointers flowing into an interface so codegen can
181
+ // tell `T` from `*T` at runtime (both compile to the same class instance).
182
+ _markIfaceBox(target, source, node) {
183
+ if (this._skipIfaceMark) return;
184
+ if (!node || typeof node !== "object" || !target || !source) return;
185
+ if (target.kind === "typeParam") return;
186
+ const tBase = this.resolveType(
187
+ target.kind === "named" ? target.underlying : target,
188
+ );
189
+ if (!isAny(target) && tBase?.kind !== "interface") return;
190
+ const structOf = (t) => {
191
+ const r = this.resolveType(t);
192
+ const b = r?.kind === "named" ? this.resolveType(r.underlying) : r;
193
+ return b?.kind === "struct";
194
+ };
195
+ const sBase = this.resolveType(
196
+ source.kind === "named" ? source.underlying : source,
197
+ );
198
+ if (sBase?.kind === "struct") node._ifaceBox = "value";
199
+ else if (sBase?.kind === "pointer" && structOf(sBase.base))
200
+ node._ifaceBox = "ptr";
201
+ },
202
+
177
203
  _assertAssignableEarlyReturn(target, source) {
178
204
  return (
179
205
  target?.kind === "typeParam" ||
@@ -211,9 +237,13 @@ export const assignabilityMethods = {
211
237
 
212
238
  _isNumericCoercible(target, source) {
213
239
  if (target.kind !== "basic" || source.kind !== "basic") return false;
240
+ const pair = `${target.name}:${source.name}`;
214
241
  return (
215
- (target.name === "float64" && source.name === "int") ||
216
- (target.name === "int" && source.name === "float64")
242
+ pair === "float64:int" ||
243
+ pair === "int:float64" ||
244
+ // rune was historically int in GoFront; keep rune/int interchangeable.
245
+ pair === "int:int32" ||
246
+ pair === "int32:int"
217
247
  );
218
248
  },
219
249
 
@@ -258,8 +288,9 @@ export const assignabilityMethods = {
258
288
  const taElem = targetTa
259
289
  ? (target.underlying?._elemType ?? target._elemType)
260
290
  : (source.underlying?._elemType ?? source._elemType);
261
- const slElem = targetSlice ? targetSlice.elem : sourceSlice.elem;
262
- if (typeStr(taElem) === typeStr(slElem)) return true;
291
+ const slice = targetSlice ?? sourceSlice;
292
+ if (isTypedArraySlice(slice) && typeStr(taElem) === typeStr(slice.elem))
293
+ return true;
263
294
  this.err(`Cannot assign ${typeStr(source)} to ${typeStr(target)}`, node);
264
295
  return true;
265
296
  }
@@ -275,6 +306,21 @@ export const assignabilityMethods = {
275
306
  return false;
276
307
  },
277
308
 
309
+ _checkFuncAssignable(target, source, node) {
310
+ const tFunc = target.kind === "named" ? target.underlying : target;
311
+ const sFunc = source.kind === "named" ? source.underlying : source;
312
+ if (tFunc?.kind === "func" && sFunc?.kind === "func") {
313
+ if (!this._implementsMethod(tFunc, sFunc)) {
314
+ this.err(
315
+ `Cannot assign ${typeStr(source)} to ${typeStr(target)}`,
316
+ node,
317
+ );
318
+ }
319
+ return true;
320
+ }
321
+ return false;
322
+ },
323
+
278
324
  // ── Interface satisfaction ────────────────────────────────────────
279
325
 
280
326
  _sigParamsMatch(reqList, actList) {
@@ -15,6 +15,7 @@ import {
15
15
  isNumeric,
16
16
  isString,
17
17
  NIL,
18
+ RUNE,
18
19
  Scope,
19
20
  STRING,
20
21
  TAINTED_ANY,
@@ -40,6 +41,17 @@ const BUILTIN_CHECK = {
40
41
  s.err(`cannot append to array (type ${typeStr(st)})`, e);
41
42
  return st;
42
43
  }
44
+ const sb = s.resolveType(st?.kind === "named" ? st.underlying : st);
45
+ if (sb?.kind === "slice") {
46
+ for (let i = 1; i < e.args.length; i++) {
47
+ if (!e.args[i]._spread && at[i])
48
+ s._markIfaceBox(
49
+ s.resolveType(sb.elem),
50
+ s.resolveType(at[i]),
51
+ e.args[i],
52
+ );
53
+ }
54
+ }
43
55
  return st;
44
56
  },
45
57
  copy: () => INT,
@@ -379,7 +391,7 @@ export const expressionCheckMethods = {
379
391
  },
380
392
 
381
393
  _rangeCollTypeTuple(resolved, collType) {
382
- if (isString(resolved)) return { kind: "tuple", types: [INT, INT] };
394
+ if (isString(resolved)) return { kind: "tuple", types: [INT, RUNE] };
383
395
  if (resolved?.kind === "slice" || resolved?.kind === "array")
384
396
  return { kind: "tuple", types: [INT, resolved.elem ?? ANY] };
385
397
  if (resolved?.kind === "map")