gofront 0.0.3 → 0.0.4

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.
package/README.md CHANGED
@@ -145,7 +145,7 @@ readable and debuggable — no name mangling, no opaque wrappers.
145
145
  | `for {}` | `while (true) {}` |
146
146
  | `for i, v := range slice` | `for (const [i, v] of arr.entries())` |
147
147
  | `for k, v := range map` | `for (const [k, v] of Object.entries(m))` |
148
- | `for i, ch := range str` | `for (const [i, ch] of Array.from(s).entries())` |
148
+ | `for i, r := range str` | `for (const [i, r] of Array.from(s, (c, i) => [i, c.codePointAt(0)]))` — `r` is a rune integer |
149
149
  | `for i := range n` | `for (let i = 0; i < n; i++)` |
150
150
  | `switch` / `fallthrough` | `switch` / case fall-through |
151
151
  | `switch v := x.(type)` | `if/else if` with `typeof` / `instanceof` checks |
@@ -387,6 +387,8 @@ signatures into GoFront's internal type representation.
387
387
  | `math` package | ✓ — `Sqrt`, `Pow`, `Abs`, `Floor`, `Ceil`, `Round`, `Log`, trig functions + `Pi`, `E`, `MaxInt` constants |
388
388
  | `errors` package | ✓ — `errors.New` (returns plain string) |
389
389
  | `time` package (partial) | ✓ — `Now`, `Since`, `Sleep` + `Millisecond`, `Second`, `Minute`, `Hour` constants |
390
+ | `unicode` package | ✓ — `IsLetter`, `IsDigit`, `IsSpace`, `IsUpper`, `IsLower`, `IsPunct`, `IsControl`, `IsPrint`, `IsGraphic`, `ToUpper`, `ToLower` |
391
+ | `os` package (partial) | ✓ — `Exit`, `Args`, `Getenv` |
390
392
  | External `.d.ts` types | ✓ |
391
393
  | npm package types | ✓ |
392
394
  | Multi-file packages | ✓ |
@@ -408,6 +410,12 @@ signatures into GoFront's internal type representation.
408
410
  | Binary / octal literals (`0b1010`, `0o777`) | ✓ — passed through to JS which supports them natively |
409
411
  | Hex float literals (`0x1.8p1`) | ✓ — evaluated at lex time to a decimal value |
410
412
  | Untyped constants | ✓ — `const x = 5` produces an untyped int that coerces to `int` or `float64` on use (Go spec §Constants) |
413
+ | Positional (unkeyed) struct literals (`Point{1, 2}`) | ✓ |
414
+ | Method expressions (`T.Method`) | ✓ — compiles to `(recv, ...args) => recv.Method(...args)` |
415
+ | Method values (`x.Method`) | ✓ — compiles to `x.Method.bind(x)` |
416
+ | Struct and array equality (`a == b`) | ✓ — deep comparison via tree-shaken `__equal` helper |
417
+ | Multi-value function forwarding (`f(g())`) | ✓ — single multi-return call forwarded as spread |
418
+ | Terminating statement analysis | ✓ — missing `return` in non-void functions is a type error |
411
419
 
412
420
  ---
413
421
 
@@ -435,13 +443,15 @@ developer would expect:
435
443
  - **Expressions**: all arithmetic, comparison, logical, and bitwise operators (including
436
444
  `&^`), compound assignment, increment/decrement, type conversions, type assertions
437
445
  (plain and comma-ok), slice expressions (`a[lo:hi]`, `a[lo:hi:max]`), composite
438
- literals, `[...]T{...}` inference, rune literals, raw string literals, variadic
439
- spread, blank identifier `_`.
446
+ literals (keyed and positional), `[...]T{...}` inference, rune literals, raw string
447
+ literals, variadic spread, blank identifier `_`, method expressions (`T.Method`),
448
+ method values (`x.Method`), struct/array equality, multi-value function forwarding.
440
449
  - **Builtins**: `len`, `cap`, `append`, `copy`, `make`, `delete`, `new`, `clear`, `min`,
441
450
  `max`, `print`, `println`, `panic`, `recover`, `fmt.Sprintf`/`Printf`/`Println`/`Errorf`.
442
451
  - **Standard library shims**: `strings` (20 functions), `strconv` (8 functions),
443
452
  `sort` (6 functions), `math` (20 functions + 6 constants), `errors.New`,
444
- `time.Now`/`Since`/`Sleep` + duration constants.
453
+ `time.Now`/`Since`/`Sleep` + duration constants, `unicode` (11 functions),
454
+ `os.Exit`/`Args`/`Getenv`.
445
455
  - **Packages**: multi-file packages, cross-package imports, import aliases,
446
456
  dot imports, blank imports, unused import detection.
447
457
 
@@ -464,7 +474,6 @@ These features are intentional additions for the JavaScript platform:
464
474
  | Range over iterator functions (Go 1.23) | `func(yield func(K, V) bool)` protocol. | Feasible. |
465
475
  | Complex numbers (`complex64`, `complex128`, `3i`) | Would need a runtime complex type. Rarely used in frontend. | Feasible but low priority. |
466
476
  | `goto` | No clean JS translation. Rare in idiomatic Go. | Not planned. |
467
- | Method expressions (`T.Method`) | Requires function-wrapping with explicit receiver. | Feasible. |
468
477
  | Goroutines / channels / `select` | Go's concurrency model has no JS equivalent. A userland scheduler defeats the "no runtime" goal. | Out of scope. |
469
478
  | `unsafe`, `reflect`, `cgo` | Require memory model or runtime type metadata that JS cannot provide. | Out of scope. |
470
479
 
@@ -483,34 +492,22 @@ you know exactly what to expect.
483
492
  | Fixed-size arrays (`[n]T`) | Plain JS arrays (no length enforcement) | Fixed at compile time | Runtime enforcement adds overhead for no JS benefit. |
484
493
  | `nil` | Maps to `null` | Typed nil (distinct per type) | JS has no typed nil concept. |
485
494
  | `rune` / `byte` | Treated as `int` | Distinct types with UTF-8 encoding | JS strings are UTF-16. |
486
- | `range` over string | JS characters (UTF-16 via `Array.from`) | Runes (UTF-8 code points) | UTF-16 vs UTF-8 mismatch. |
495
+ | `range` over string | Rune integers via `.codePointAt()` | Runes (UTF-8 code points) | Close match — indices are byte-sequential, values are code points. |
487
496
  | `len()` on strings | JS `.length` (UTF-16 code units) | Byte count (UTF-8) | Matching Go would require `TextEncoder` on every call. |
488
497
  | `error` type | Plain string | Interface `{ Error() string }` | Practical simplification for JS. |
489
498
  | `defer` | `try`/`finally` with a defer stack | Runtime stack unwinding | Covers most cases but not identical to Go internals. |
490
499
  | Struct field tags | Parsed, silently discarded | Available via `reflect` | No reflection = no use for tag values. |
491
500
  | Pointers (`&x`, `*p`) | Syntax accepted, semantically transparent | True memory indirection | JS has no pointer model. |
492
501
  | Three-index slice (`a[lo:hi:max]`) | `max` is parsed but ignored | Sets result capacity | JS arrays have no capacity. |
493
- | Exported / unexported | Uppercase names exported across packages | Access enforced per-identifier | Cross-package access to lowercase names is not yet an error. |
502
+ | Exported / unexported | Enforced for GoFront packages; external `.d.ts`/npm namespaces are exempt | Access enforced uniformly | External JS APIs use lowercase names by convention. |
494
503
 
495
504
  ---
496
505
 
497
506
  ## Roadmap
498
507
 
499
- These are the open items roughly in priority order. GoFront's guiding principle is
500
- **no runtime** — features that need a scheduler, runtime type descriptors, or raw memory
501
- access are out of scope.
502
-
503
- | Feature | Difficulty | Notes |
504
- |---|---|---|
505
- | Generics (`[T any]`) | High | Biggest modern Go feature gap. Touches every compiler stage but has low runtime pressure. |
506
- | Range over iterator functions | Medium | Go 1.23 `func(yield func(K, V) bool)` protocol. Low runtime pressure. |
507
- | Complex number types | Medium | New type kind + builtins (`complex`, `real`, `imag`). Shimmed with a two-field object. |
508
- | Richer error values | Medium | Move `error` from plain string to an interface-like value. |
509
- | Better array semantics | Medium | Arrays are currently indistinguishable from slices at runtime. |
510
- | Better pointer model | High | Current `{ value: T }` boxing is useful but shallow. |
511
-
512
- Explicitly **out of scope**: goroutines, channels, `select`, `unsafe`, full `reflect`,
513
- `cgo`, exact integer overflow, Go-equivalent map semantics.
508
+ See [`docs/ROADMAP.md`](docs/ROADMAP.md) for the full roadmap and release history.
509
+ Design documents for planned features are organised by release under `docs/v*/`
510
+ (e.g. [`docs/v0.0.5/`](docs/v0.0.5/)).
514
511
 
515
512
  ---
516
513
 
@@ -520,7 +517,7 @@ Explicitly **out of scope**: goroutines, channels, `select`, `unsafe`, full `ref
520
517
  npm test
521
518
  ```
522
519
 
523
- 611 tests covering language features, type errors, edge cases, DOM (jsdom), external
520
+ 655 tests covering language features, type errors, edge cases, DOM (jsdom), external
524
521
  `.d.ts`, npm resolver, multi-file compilation, embedded structs, string formatting, map
525
522
  iteration order, integer overflow semantics, unused variable detection, unused import
526
523
  detection, semantic difference verification, stdlib shim packages, and both example apps.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,6 +48,15 @@ export const expressionGenMethods = {
48
48
  if (expr.op === "&^") {
49
49
  return `${l} & ~${r}`;
50
50
  }
51
+ // Struct/array equality: use __equal for value comparison
52
+ if (expr.op === "==" || expr.op === "!=") {
53
+ const lt = expr.left._type;
54
+ if (this._isStructOrArrayType(lt)) {
55
+ this._usesEqual = true;
56
+ const cmp = `__equal(${l}, ${r})`;
57
+ return expr.op === "==" ? cmp : `!${cmp}`;
58
+ }
59
+ }
51
60
  // Go == and != are strict — map to JS === / !==
52
61
  const op =
53
62
  expr.op === "==" ? "===" : expr.op === "!=" ? "!==" : expr.op;
@@ -58,6 +67,11 @@ export const expressionGenMethods = {
58
67
  return this.genCall(expr);
59
68
 
60
69
  case "SelectorExpr": {
70
+ // Method expression: TypeName.MethodName → (recv, ...args) => recv.method(...args)
71
+ if (expr._isMethodExpr) {
72
+ const method = expr.field;
73
+ return `((recv, ...args) => recv.${method}(...args))`;
74
+ }
61
75
  // math constants
62
76
  if (expr.expr.kind === "Ident" && expr.expr.name === "math") {
63
77
  switch (expr.field) {
@@ -75,6 +89,13 @@ export const expressionGenMethods = {
75
89
  return "Number.MIN_SAFE_INTEGER";
76
90
  }
77
91
  }
92
+ // os constants/vars
93
+ if (expr.expr.kind === "Ident" && expr.expr.name === "os") {
94
+ switch (expr.field) {
95
+ case "Args":
96
+ return "process.argv";
97
+ }
98
+ }
78
99
  // time constants (nanosecond durations)
79
100
  if (expr.expr.kind === "Ident" && expr.expr.name === "time") {
80
101
  switch (expr.field) {
@@ -93,9 +114,15 @@ export const expressionGenMethods = {
93
114
  if (this.bundledPackages.has(base)) return expr.field;
94
115
  // Pointer types are wrapped as { value: T }; route through .value
95
116
  if (expr.expr._type?.kind === "pointer" && expr.field !== "value") {
96
- return `${base}.value.${expr.field}`;
117
+ const sel = `${base}.value.${expr.field}`;
118
+ if (expr._isMethodValue && !expr._callee)
119
+ return `${sel}.bind(${base}.value)`;
120
+ return sel;
97
121
  }
98
- return `${base}.${expr.field}`;
122
+ const sel = `${base}.${expr.field}`;
123
+ // Method used as value (not called) — bind the receiver
124
+ if (expr._isMethodValue && !expr._callee) return `${sel}.bind(${base})`;
125
+ return sel;
99
126
  }
100
127
 
101
128
  case "IndexExpr": {
@@ -110,6 +137,14 @@ export const expressionGenMethods = {
110
137
  }
111
138
  return `(${base}[${idx}] ?? ${zero})`;
112
139
  }
140
+ // String indexing returns a byte (charCodeAt), not a JS character
141
+ const exprType = expr.expr._type;
142
+ const isStr =
143
+ (exprType?.kind === "basic" && exprType.name === "string") ||
144
+ (exprType?.kind === "untyped" && exprType.base === "string");
145
+ if (isStr && !expr._lvalue) {
146
+ return `${base}.charCodeAt(${idx})`;
147
+ }
113
148
  return `${base}[${idx}]`;
114
149
  }
115
150
 
@@ -295,20 +330,16 @@ export const expressionGenMethods = {
295
330
  const fmtArgs = expr.args.map((a) => this.genExpr(a)).join(", ");
296
331
  switch (expr.func.field) {
297
332
  case "Sprintf":
298
- this._usesSprintf = true;
299
- return `__sprintf(${fmtArgs})`;
300
333
  case "Errorf":
301
334
  this._usesSprintf = true;
302
335
  return `__sprintf(${fmtArgs})`;
303
336
  case "Printf":
337
+ case "Print":
304
338
  this._usesSprintf = true;
305
339
  return `process?.stdout?.write(__sprintf(${fmtArgs}))`;
306
340
  case "Println":
307
341
  this._usesSprintf = true;
308
342
  return `console.log(__sprintf(${fmtArgs}))`;
309
- case "Print":
310
- this._usesSprintf = true;
311
- return `process?.stdout?.write(__sprintf(${fmtArgs}))`;
312
343
  }
313
344
  }
314
345
 
@@ -463,6 +494,54 @@ export const expressionGenMethods = {
463
494
  }
464
495
  }
465
496
 
497
+ // unicode.* — rune/character classification
498
+ if (
499
+ expr.func.kind === "SelectorExpr" &&
500
+ expr.func.expr.kind === "Ident" &&
501
+ expr.func.expr.name === "unicode"
502
+ ) {
503
+ const a = expr.args.map((e) => this.genExpr(e));
504
+ switch (expr.func.field) {
505
+ case "IsLetter":
506
+ return `/\\p{L}/u.test(String.fromCodePoint(${a[0]}))`;
507
+ case "IsDigit":
508
+ return `/\\p{Nd}/u.test(String.fromCodePoint(${a[0]}))`;
509
+ case "IsSpace":
510
+ return `/\\s/.test(String.fromCodePoint(${a[0]}))`;
511
+ case "IsUpper":
512
+ return `((__c) => __c === __c.toUpperCase() && /\\p{L}/u.test(__c))(String.fromCodePoint(${a[0]}))`;
513
+ case "IsLower":
514
+ return `((__c) => __c === __c.toLowerCase() && /\\p{L}/u.test(__c))(String.fromCodePoint(${a[0]}))`;
515
+ case "IsPunct":
516
+ return `/\\p{P}/u.test(String.fromCodePoint(${a[0]}))`;
517
+ case "IsControl":
518
+ return `/\\p{Cc}/u.test(String.fromCodePoint(${a[0]}))`;
519
+ case "IsPrint":
520
+ return `!/\\p{Cc}/u.test(String.fromCodePoint(${a[0]}))`;
521
+ case "IsGraphic":
522
+ return `!/\\p{Cc}/u.test(String.fromCodePoint(${a[0]}))`;
523
+ case "ToUpper":
524
+ return `String.fromCodePoint(${a[0]}).toUpperCase().codePointAt(0)`;
525
+ case "ToLower":
526
+ return `String.fromCodePoint(${a[0]}).toLowerCase().codePointAt(0)`;
527
+ }
528
+ }
529
+
530
+ // os.* — process/environment access
531
+ if (
532
+ expr.func.kind === "SelectorExpr" &&
533
+ expr.func.expr.kind === "Ident" &&
534
+ expr.func.expr.name === "os"
535
+ ) {
536
+ const a = expr.args.map((e) => this.genExpr(e));
537
+ switch (expr.func.field) {
538
+ case "Exit":
539
+ return `process.exit(${a[0]})`;
540
+ case "Getenv":
541
+ return `(process.env[${a[0]}] ?? "")`;
542
+ }
543
+ }
544
+
466
545
  // errors.New — identity (errors are plain strings)
467
546
  if (
468
547
  expr.func.kind === "SelectorExpr" &&
@@ -490,9 +569,15 @@ export const expressionGenMethods = {
490
569
  }
491
570
  }
492
571
 
572
+ // Mark the callee so SelectorExpr knows it's being called, not used as a value
573
+ expr.func._callee = true;
493
574
  const rawFn = this.genExpr(expr.func);
494
575
  // Wrap function literals in parens so `function(){}()` → `(function(){})()`
495
576
  const fn = expr.func.kind === "FuncLit" ? `(${rawFn})` : rawFn;
577
+ // Multi-value forwarding: f(g()) where g() returns multiple values → f(...g())
578
+ if (expr._multiForward) {
579
+ return `${fn}(...${this.genExpr(expr.args[0])})`;
580
+ }
496
581
  const args = expr.args
497
582
  .map((a) => (a._spread ? `...${this.genExpr(a)}` : this.genExpr(a)))
498
583
  .join(", ");
@@ -537,34 +622,40 @@ export const expressionGenMethods = {
537
622
  return "{}";
538
623
  },
539
624
 
540
- // Render KeyValueExpr elements as a JS field list string.
541
- // Handles embedded-spread (_isEmbedInit) and plain key: value pairs.
625
+ // Render struct elements as a JS field list string.
626
+ // Handles positional (_positionalField), embedded-spread (_isEmbedInit), and keyed (KeyValueExpr).
542
627
  _genStructFields(elems) {
543
628
  return elems
544
- .filter((e) => e.kind === "KeyValueExpr")
545
- .map((e) =>
546
- e._isEmbedInit
547
- ? `...${this.genExpr(e.value)}`
548
- : `${e.key.name ?? this.genExpr(e.key)}: ${this.genExpr(e.value)}`,
549
- )
629
+ .map((e) => {
630
+ if (e._positionalField) {
631
+ return `${e._positionalField}: ${this.genExpr(e)}`;
632
+ }
633
+ if (e.kind !== "KeyValueExpr") return null;
634
+ if (e._isEmbedInit) return `...${this.genExpr(e.value)}`;
635
+ return `${e.key.name ?? this.genExpr(e.key)}: ${this.genExpr(e.value)}`;
636
+ })
637
+ .filter((s) => s !== null)
550
638
  .join(", ");
551
639
  },
552
640
 
553
641
  genCompositeLit(expr) {
554
642
  const t = expr.typeExpr;
555
643
 
556
- // Implicit composite literal: {X: 1} inside a slice/map — type inferred from context
644
+ // Implicit composite literal: {X: 1} or {1, 2} inside a slice/map — type inferred from context
557
645
  if (t === null) {
558
- if (expr.elems.length > 0 && expr.elems[0]?.kind === "KeyValueExpr") {
559
- const typeName = expr._type?.name ?? expr._type?.underlying?.name;
646
+ const typeName = expr._type?.name ?? expr._type?.underlying?.name;
647
+ const hasPositional = expr.elems.some((e) => e._positionalField);
648
+ const hasKeyed = expr.elems.some((e) => e.kind === "KeyValueExpr");
649
+ if (hasPositional || hasKeyed) {
560
650
  if (typeName && this.structNames.has(typeName)) {
561
651
  return `new ${typeName}({ ${this._genStructFields(expr.elems)} })`;
562
652
  }
563
653
  const fields = expr.elems
564
- .map(
565
- (e) =>
566
- `${e.key.name ?? this.genExpr(e.key)}: ${this.genExpr(e.value)}`,
567
- )
654
+ .map((e) => {
655
+ if (e._positionalField)
656
+ return `${e._positionalField}: ${this.genExpr(e)}`;
657
+ return `${e.key.name ?? this.genExpr(e.key)}: ${this.genExpr(e.value)}`;
658
+ })
568
659
  .join(", ");
569
660
  return `{ ${fields} }`;
570
661
  }
@@ -573,11 +664,18 @@ export const expressionGenMethods = {
573
664
 
574
665
  const typeName = this.getTypeName(t);
575
666
 
576
- // Struct: new Foo({ X: 1, Y: 2 })
667
+ // Struct: new Foo({ X: 1, Y: 2 }) — keyed or positional
577
668
  if (typeName && this.structNames.has(typeName)) {
578
669
  return `new ${typeName}({ ${this._genStructFields(expr.elems)} })`;
579
670
  }
580
671
 
672
+ // Fallback: named type that's not a struct but has positional elements → treat as struct
673
+ if (expr.elems.some((e) => e._positionalField)) {
674
+ if (typeName) {
675
+ return `new ${typeName}({ ${this._genStructFields(expr.elems)} })`;
676
+ }
677
+ }
678
+
581
679
  // Slice/array: [1, 2, 3]
582
680
  if (t?.kind === "SliceType" || t?.kind === "ArrayType") {
583
681
  const elems = expr.elems
@@ -710,10 +808,8 @@ export const expressionGenMethods = {
710
808
  zeroValueForType(t) {
711
809
  if (!t) return "null";
712
810
  switch (t.kind) {
713
- case "basic": {
714
- const basic = this._zeroForBasicName(t.name);
715
- return basic !== null ? basic : "null";
716
- }
811
+ case "basic":
812
+ return this._zeroForBasicName(t.name) ?? "null";
717
813
  case "slice":
718
814
  return "null";
719
815
  case "map":
@@ -788,6 +884,13 @@ export const expressionGenMethods = {
788
884
  return "null";
789
885
  },
790
886
 
887
+ // Returns true if the type is a struct or array (uses value comparison with __equal).
888
+ _isStructOrArrayType(t) {
889
+ if (!t) return false;
890
+ const base = t.kind === "named" ? t.underlying : t;
891
+ return base?.kind === "struct" || base?.kind === "array";
892
+ },
893
+
791
894
  typeComment(typeNode) {
792
895
  if (!typeNode) return "unknown";
793
896
  switch (typeNode.kind) {
@@ -75,14 +75,69 @@ export const statementGenMethods = {
75
75
  }
76
76
 
77
77
  case "AssignStmt": {
78
+ // comma-ok map index: v, ok = m["key"]
79
+ if (stmt._commaOkMap) {
80
+ const rhsNode = stmt.rhs[0];
81
+ const mapExpr = this.genExpr(rhsNode.expr);
82
+ const keyExpr = this.genExpr(rhsNode.index);
83
+ const [vName, okName] = stmt.lhs.map(
84
+ (e) => e.name ?? this.genExpr(e),
85
+ );
86
+ if (vName !== "_") {
87
+ const zero = rhsNode._mapValueType
88
+ ? this.zeroValueForType(rhsNode._mapValueType)
89
+ : "undefined";
90
+ this.line(
91
+ `${vName} = (${keyExpr}) in ${mapExpr} ? ${mapExpr}[${keyExpr}] : ${zero};`,
92
+ );
93
+ }
94
+ if (okName !== "_")
95
+ this.line(`${okName} = (${keyExpr}) in ${mapExpr};`);
96
+ break;
97
+ }
98
+ // comma-ok type assertion: v, ok = x.(T)
99
+ if (
100
+ stmt.lhs.length === 2 &&
101
+ stmt.rhs.length === 1 &&
102
+ stmt.rhs[0]._commaOk
103
+ ) {
104
+ const val = this.genExpr(stmt.rhs[0]);
105
+ const [vName, okName] = stmt.lhs.map(
106
+ (e) => e.name ?? this.genExpr(e),
107
+ );
108
+ const tmp = "__ta";
109
+ this.line(`let ${tmp} = ${val};`);
110
+ if (vName !== "_") this.line(`${vName} = ${tmp}[0];`);
111
+ if (okName !== "_") this.line(`${okName} = ${tmp}[1];`);
112
+ break;
113
+ }
78
114
  const rhs = stmt.rhs.map((e) => this.genExpr(e));
79
115
  for (const e of stmt.lhs) if (e.kind === "IndexExpr") e._lvalue = true;
80
- const lhs = stmt.lhs.map((e) => this.genExpr(e));
81
- if (lhs.length === 1) {
82
- this.line(`${lhs[0]} ${stmt.op} ${rhs[0]};`);
116
+ const lhs = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
117
+ // Filter out blank identifier assignments entirely
118
+ const pairs = lhs.map((l, i) => ({ l, r: rhs[i] ?? rhs[0] }));
119
+ const active = pairs.filter((p) => p.l !== "_");
120
+ if (active.length === 0) {
121
+ // All _ — still need to evaluate rhs for side effects
122
+ if (rhs.length > 0) this.line(`${rhs[0]};`);
123
+ } else if (lhs.length === 1) {
124
+ this.line(`${active[0].l} ${stmt.op} ${active[0].r};`);
83
125
  } else {
126
+ // multi-assign: emit only non-blank targets
84
127
  const rhsStr = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
85
- this.line(`[${lhs.join(", ")}] = ${rhsStr};`);
128
+ if (active.length === pairs.length) {
129
+ this.line(`[${lhs.join(", ")}] = ${rhsStr};`);
130
+ } else {
131
+ // Some blanks — use temp and assign only non-blank
132
+ const tmp = "__t";
133
+ this.line(`let ${tmp} = ${rhsStr};`);
134
+ for (let i = 0; i < lhs.length; i++) {
135
+ if (lhs[i] !== "_") {
136
+ const src = rhs.length === 1 ? `${tmp}[${i}]` : rhs[i];
137
+ this.line(`${lhs[i]} ${stmt.op} ${src};`);
138
+ }
139
+ }
140
+ }
86
141
  }
87
142
  break;
88
143
  }
@@ -275,11 +330,14 @@ export const statementGenMethods = {
275
330
  (iterType?.kind === "basic" && iterType.name === "string") ||
276
331
  (iterType?.kind === "untyped" && iterType.base === "string")
277
332
  ) {
278
- // string: for i, ch := range s → for (const [i, ch] of s.split('').entries())
279
- iterExpr =
280
- lhs.length === 1
281
- ? `Array.from(${iteree}).keys()`
282
- : `Array.from(${iteree}).entries()`;
333
+ // string: for i, r := range s → rune (code point), not JS character
334
+ // Go spec: iterating a string yields (byte-index, rune) pairs
335
+ if (lhs.length === 1) {
336
+ iterExpr = `Array.from(${iteree}).keys()`;
337
+ } else {
338
+ // Wrap code points so the value is an integer (rune), not a JS string
339
+ iterExpr = `Array.from(${iteree}, (__c, __i) => [__i, __c.codePointAt(0)])`;
340
+ }
283
341
  } else {
284
342
  // slice/array: for i, v := range arr → for (const [i, v] of __s(arr).entries())
285
343
  // null-guard handles nil slices (zero value) gracefully.
package/src/codegen.js CHANGED
@@ -43,6 +43,7 @@ export class CodeGen {
43
43
  this._usesAppend = false;
44
44
  this._usesSliceGuard = false;
45
45
  this._usesSprintf = false;
46
+ this._usesEqual = false;
46
47
  }
47
48
 
48
49
  // ── Output helpers ───────────────────────────────────────────
@@ -120,10 +121,7 @@ export class CodeGen {
120
121
  if (d.name === "init") {
121
122
  const renamed = initCount === 0 ? "init" : `init$${initCount}`;
122
123
  initNames.push(renamed);
123
- const saved = d.name;
124
- d.name = renamed;
125
- this.genFuncDecl(d);
126
- d.name = saved;
124
+ this.genFuncDecl(d, renamed);
127
125
  initCount++;
128
126
  } else {
129
127
  this.genFuncDecl(d);
@@ -149,6 +147,10 @@ export class CodeGen {
149
147
  );
150
148
  if (this._usesSliceGuard)
151
149
  helpers.push("function __s(a) { return a || []; }");
150
+ if (this._usesEqual)
151
+ helpers.push(
152
+ 'function __equal(a,b){if(a===b)return true;if(a===null||b===null)return false;if(Array.isArray(a)&&Array.isArray(b)){if(a.length!==b.length)return false;for(let i=0;i<a.length;i++)if(!__equal(a[i],b[i]))return false;return true;}if(typeof a==="object"&&typeof b==="object"){const ka=Object.keys(a),kb=Object.keys(b);if(ka.length!==kb.length)return false;for(const k of ka)if(!__equal(a[k],b[k]))return false;return true;}return false;}',
153
+ );
152
154
  if (this._usesSprintf)
153
155
  helpers.push(
154
156
  [
@@ -287,7 +289,8 @@ export class CodeGen {
287
289
 
288
290
  // ── Function declarations ────────────────────────────────────
289
291
 
290
- genFuncDecl(decl) {
292
+ genFuncDecl(decl, nameOverride) {
293
+ const name = nameOverride ?? decl.name;
291
294
  const params = decl.params
292
295
  .map((p, i) =>
293
296
  p.variadic && i === decl.params.length - 1 ? `...${p.name}` : p.name,
@@ -296,7 +299,7 @@ export class CodeGen {
296
299
  const asyncPrefix = decl.async ? "async " : "";
297
300
  const srcLine = decl._line ?? null;
298
301
  this.line(
299
- `${asyncPrefix}function ${decl.name}(${params}) {`,
302
+ `${asyncPrefix}function ${name}(${params}) {`,
300
303
  srcLine ? srcLine - 1 : null,
301
304
  );
302
305
  this.indented(() =>
package/src/parser.js CHANGED
@@ -172,7 +172,7 @@ export class Parser {
172
172
  if (this.check(T.LPAREN)) {
173
173
  this.expect(T.LPAREN);
174
174
  const recvName =
175
- this.check(T.IDENT) && !this.isTypeName(this.peek2())
175
+ this.check(T.IDENT) && !this.isReceiverTerminator(this.peek2())
176
176
  ? this.advance().value
177
177
  : "_";
178
178
  this.match(T.STAR); // pointer receiver: (c *Counter) — strip *, treat same as value receiver
@@ -208,8 +208,8 @@ export class Parser {
208
208
  return decl;
209
209
  }
210
210
 
211
- // Returns true if tok looks like a type name token (for receiver parsing)
212
- isTypeName(tok) {
211
+ // Returns true if tok is a receiver-list terminator (i.e. not a type name)
212
+ isReceiverTerminator(tok) {
213
213
  return (
214
214
  tok &&
215
215
  (tok.type === T.RPAREN ||
@@ -383,31 +383,34 @@ export class Parser {
383
383
  const decls = [];
384
384
  if (this.match(T.LPAREN)) {
385
385
  let iotaVal = 0;
386
+ let prevRawExprs = null; // raw AST (with iota Idents) from the previous spec
387
+ let prevType = null;
386
388
  while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
387
389
  const names = [this.expect(T.IDENT).value];
388
390
  while (this.match(T.COMMA)) names.push(this.expect(T.IDENT).value);
389
391
  let type = null;
390
- let value;
392
+ let rawExprs; // before iota substitution
391
393
  if (this.check(T.ASSIGN)) {
392
394
  this.advance();
393
- value = this.parseExprList();
395
+ rawExprs = this.parseExprList();
396
+ prevRawExprs = rawExprs;
397
+ prevType = null;
394
398
  } else if (!this.check(T.SEMICOLON) && !this.check(T.RPAREN)) {
395
399
  // optional type annotation before =
396
400
  type = this.parseType();
401
+ prevType = type;
397
402
  this.expect(T.ASSIGN);
398
- value = this.parseExprList();
403
+ rawExprs = this.parseExprList();
404
+ prevRawExprs = rawExprs;
399
405
  } else {
400
- // implicit iota
401
- value = [
406
+ // Omitted expression — repeat previous expression with updated iota
407
+ rawExprs = prevRawExprs ?? [
402
408
  { kind: "BasicLit", litKind: "INT", value: String(iotaVal) },
403
409
  ];
410
+ type = prevType ?? null;
404
411
  }
405
- // Replace iota identifier with current counter value
406
- value = value.map((v) =>
407
- v.kind === "Ident" && v.name === "iota"
408
- ? { kind: "BasicLit", litKind: "INT", value: String(iotaVal) }
409
- : v,
410
- );
412
+ // Substitute iota deeply in a cloned expression AST
413
+ const value = rawExprs.map((v) => this._substituteIota(v, iotaVal));
411
414
  decls.push({ names, type, value });
412
415
  iotaVal++;
413
416
  this.semi();
@@ -425,6 +428,25 @@ export class Parser {
425
428
  this.semi();
426
429
  return { kind: "ConstDecl", decls };
427
430
  }
431
+
432
+ // Deep-clone an expression AST, replacing `iota` identifiers with the given integer.
433
+ _substituteIota(node, val) {
434
+ if (!node || typeof node !== "object") return node;
435
+ if (node.kind === "Ident" && node.name === "iota") {
436
+ return { kind: "BasicLit", litKind: "INT", value: String(val) };
437
+ }
438
+ // Shallow-clone the node, then recursively substitute in all child properties
439
+ const clone = { ...node };
440
+ for (const key of Object.keys(clone)) {
441
+ const v = clone[key];
442
+ if (Array.isArray(v)) {
443
+ clone[key] = v.map((item) => this._substituteIota(item, val));
444
+ } else if (v && typeof v === "object" && v.kind) {
445
+ clone[key] = this._substituteIota(v, val);
446
+ }
447
+ }
448
+ return clone;
449
+ }
428
450
  }
429
451
 
430
452
  Object.assign(Parser.prototype, typeParserMethods);
@@ -48,6 +48,8 @@ export const expressionCheckMethods = {
48
48
  case "Ident": {
49
49
  const t = scope.lookup(expr.name);
50
50
  if (!t) return this.err(`Undefined: '${expr.name}'`, expr);
51
+ // Mark type-name identifiers so SelectorExpr can detect method expressions
52
+ if (this.types.has(expr.name)) expr._isTypeRef = true;
51
53
  return t;
52
54
  }
53
55
 
@@ -68,7 +70,8 @@ export const expressionCheckMethods = {
68
70
  case "BinaryExpr": {
69
71
  const lt = this.checkExpr(expr.left, scope);
70
72
  const rt = this.checkExpr(expr.right, scope);
71
- if (isAny(lt) || isAny(rt)) return this.binaryResultType(expr.op, ANY);
73
+ if (isAny(lt) || isAny(rt))
74
+ return this.binaryResultType(expr.op, ANY, ANY, expr);
72
75
  return this.binaryResultType(expr.op, lt, rt, expr);
73
76
  }
74
77
 
@@ -78,7 +81,33 @@ export const expressionCheckMethods = {
78
81
 
79
82
  case "SelectorExpr": {
80
83
  const baseType = this.checkExpr(expr.expr, scope);
81
- return this.fieldType(baseType, expr.field, expr);
84
+ // Method expression: TypeName.MethodName → func(ReceiverType, ...params) ret
85
+ if (expr.expr._isTypeRef) {
86
+ let base = baseType.kind === "named" ? baseType.underlying : baseType;
87
+ base = this.resolveType(base);
88
+ if (base?.kind === "struct" && base.methods?.has(expr.field)) {
89
+ const methodType = base.methods.get(expr.field);
90
+ // Return a func type with the receiver prepended to params
91
+ expr._isMethodExpr = true;
92
+ expr._methodExprRecv = baseType;
93
+ return {
94
+ kind: "func",
95
+ params: [baseType, ...(methodType.params ?? [])],
96
+ returns: methodType.returns ?? [],
97
+ variadic: methodType.variadic,
98
+ };
99
+ }
100
+ }
101
+ const ft = this.fieldType(baseType, expr.field, expr);
102
+ // Mark method selectors so codegen can emit .bind() when used as a value
103
+ if (ft?.kind === "func") {
104
+ let base = baseType.kind === "named" ? baseType.underlying : baseType;
105
+ base = this.resolveType(base);
106
+ if (base?.kind === "struct" && base.methods?.has(expr.field)) {
107
+ expr._isMethodValue = true;
108
+ }
109
+ }
110
+ return ft;
82
111
  }
83
112
 
84
113
  case "IndexExpr": {
@@ -170,7 +199,29 @@ export const expressionCheckMethods = {
170
199
 
171
200
  case "RangeExpr": {
172
201
  // Type-check the iterated expression so its _type is annotated
173
- return this.checkExpr(expr.expr, scope);
202
+ const collType = this.checkExpr(expr.expr, scope);
203
+ const resolved =
204
+ collType?.kind === "named" ? collType.underlying : collType;
205
+ // Return a tuple of (index/key type, value type) so DefineStmt can
206
+ // assign the correct types to range variables.
207
+ const isString =
208
+ (resolved?.kind === "basic" && resolved.name === "string") ||
209
+ (resolved?.kind === "untyped" && resolved.base === "string");
210
+ if (isString) {
211
+ // string range: (int index, rune value) — rune is an alias for int
212
+ return { kind: "tuple", types: [INT, INT] };
213
+ }
214
+ if (resolved?.kind === "slice") {
215
+ return { kind: "tuple", types: [INT, resolved.elem ?? ANY] };
216
+ }
217
+ if (resolved?.kind === "map") {
218
+ return {
219
+ kind: "tuple",
220
+ types: [resolved.key ?? ANY, resolved.value ?? ANY],
221
+ };
222
+ }
223
+ // integer range or unknown — return single-element tuple
224
+ return { kind: "tuple", types: [collType] };
174
225
  }
175
226
 
176
227
  default:
@@ -193,7 +244,20 @@ export const expressionCheckMethods = {
193
244
  return this.checkBuiltin(fnType.name, expr, argTypes, scope);
194
245
  }
195
246
 
196
- const argTypes = expr.args.map((a) => this.checkExpr(a, scope));
247
+ // Multi-value forwarding: f(g()) where g() returns multiple values
248
+ // When a single argument is a multi-return call, flatten its tuple into args.
249
+ let argTypes;
250
+ if (expr.args.length === 1 && expr.args[0].kind === "CallExpr") {
251
+ const argType = this.checkExpr(expr.args[0], scope);
252
+ if (argType?.kind === "tuple") {
253
+ expr._multiForward = true; // flag for codegen
254
+ argTypes = argType.types;
255
+ } else {
256
+ argTypes = [argType];
257
+ }
258
+ } else {
259
+ argTypes = expr.args.map((a) => this.checkExpr(a, scope));
260
+ }
197
261
 
198
262
  if (isAny(fnType)) return ANY;
199
263
 
@@ -284,6 +348,30 @@ export const expressionCheckMethods = {
284
348
  const base = t.kind === "named" ? t.underlying : t;
285
349
 
286
350
  if (base?.kind === "struct") {
351
+ // Detect positional (unkeyed) literal: all elements must be non-KeyValueExpr
352
+ const hasPositional = expr.elems.some((e) => e.kind !== "KeyValueExpr");
353
+ const hasKeyed = expr.elems.some((e) => e.kind === "KeyValueExpr");
354
+ if (hasPositional && hasKeyed) {
355
+ this.err("mixture of field:value and value initializers", expr);
356
+ }
357
+ if (hasPositional) {
358
+ // Map positional elements to struct fields by declaration order
359
+ const fieldNames = [...(base.fields?.keys() ?? [])];
360
+ for (let i = 0; i < expr.elems.length; i++) {
361
+ const elem = expr.elems[i];
362
+ const fieldName = fieldNames[i];
363
+ if (!fieldName) {
364
+ this.err("too many values in struct literal", elem);
365
+ continue;
366
+ }
367
+ const fieldType = base.fields.get(fieldName);
368
+ const vt = this.checkExpr(elem, scope);
369
+ if (fieldType) this.assertAssignable(fieldType, vt, elem);
370
+ // Annotate for codegen
371
+ elem._positionalField = fieldName;
372
+ }
373
+ return t;
374
+ }
287
375
  for (const elem of expr.elems) {
288
376
  if (elem.kind === "KeyValueExpr") {
289
377
  const keyName = elem.key.name;
@@ -72,9 +72,37 @@ export const statementCheckMethods = {
72
72
  }
73
73
 
74
74
  case "AssignStmt": {
75
+ // comma-ok map index: v, ok = m["key"]
76
+ // Evaluate map expr first so _type is populated, then detect the pattern.
77
+ if (
78
+ stmt.lhs.length === 2 &&
79
+ stmt.rhs.length === 1 &&
80
+ stmt.rhs[0].kind === "IndexExpr"
81
+ ) {
82
+ const mapBaseType = this.checkExpr(stmt.rhs[0].expr, scope);
83
+ const resolvedBase =
84
+ mapBaseType?.kind === "named"
85
+ ? mapBaseType.underlying
86
+ : mapBaseType;
87
+ if (resolvedBase?.kind === "map") {
88
+ this.checkExpr(stmt.rhs[0].index, scope);
89
+ stmt._commaOkMap = true;
90
+ // Store the map value type so codegen can emit the zero-value fallback
91
+ stmt.rhs[0]._mapValueType = resolvedBase.value;
92
+ break;
93
+ }
94
+ }
95
+ // comma-ok type assertion: v, ok = x.(T)
96
+ if (
97
+ stmt.lhs.length === 2 &&
98
+ stmt.rhs.length === 1 &&
99
+ stmt.rhs[0].kind === "TypeAssertExpr"
100
+ ) {
101
+ stmt.rhs[0]._commaOk = true;
102
+ }
75
103
  // Check for const reassignment before evaluating RHS
76
104
  for (const lhs of stmt.lhs) {
77
- if (lhs.kind === "Ident") {
105
+ if (lhs.kind === "Ident" && lhs.name !== "_") {
78
106
  const ownerScope = scope.lookupScope(lhs.name);
79
107
  if (ownerScope?.isConst(lhs.name))
80
108
  this.err(`cannot assign to const '${lhs.name}'`, lhs);
@@ -83,8 +111,13 @@ export const statementCheckMethods = {
83
111
  const rhs = stmt.rhs.map((e) => this.checkExpr(e, scope));
84
112
  const rhsFlat =
85
113
  rhs.length === 1 && rhs[0]?.kind === "tuple" ? rhs[0].types : rhs;
86
- const lhsTypes = stmt.lhs.map((e) => this.checkExpr(e, scope));
114
+ const lhsTypes = stmt.lhs.map((e) => {
115
+ if (e.kind === "Ident" && e.name === "_") return ANY;
116
+ return this.checkExpr(e, scope);
117
+ });
87
118
  for (let i = 0; i < lhsTypes.length; i++) {
119
+ if (stmt.lhs[i]?.kind === "Ident" && stmt.lhs[i]?.name === "_")
120
+ continue;
88
121
  const r = rhsFlat[i] ?? ANY;
89
122
  if (!isAny(lhsTypes[i]) && !isAny(r)) {
90
123
  this.assertAssignable(lhsTypes[i], r, stmt.lhs[i]);
@@ -200,6 +233,10 @@ export const statementCheckMethods = {
200
233
  ? this.resolveTypeNode(c.types[0], inner)
201
234
  : ANY;
202
235
  caseScope.defineLocal(stmt.assign, bindType);
236
+ // The capture variable is always considered used — it's the whole
237
+ // point of the switch-with-assign form. Each case body uses its own
238
+ // typed copy of the variable.
239
+ caseScope.lookup(stmt.assign);
203
240
  }
204
241
  for (const s of c.stmts) this.checkStmt(s, caseScope, returnType);
205
242
  this._reportUnused(caseScope, stmt);
@@ -124,24 +124,6 @@ export function defaultType(t) {
124
124
  }
125
125
  }
126
126
 
127
- /** Convert a concrete basic type to its untyped equivalent (for const inference). */
128
- export function toUntyped(t) {
129
- if (t?.kind === "untyped") return t;
130
- if (t?.kind === "basic") {
131
- switch (t.name) {
132
- case "int":
133
- return UNTYPED_INT;
134
- case "float64":
135
- return UNTYPED_FLOAT;
136
- case "string":
137
- return UNTYPED_STRING;
138
- case "bool":
139
- return UNTYPED_BOOL;
140
- }
141
- }
142
- return t;
143
- }
144
-
145
127
  export function typeStr(t) {
146
128
  if (!t) return "void";
147
129
  if (t.kind === "basic" && t.alias) return t.alias;
@@ -23,6 +23,7 @@ import {
23
23
  isNumeric,
24
24
  isString,
25
25
  isUntyped,
26
+ isVoid,
26
27
  LOG_OPS,
27
28
  Scope,
28
29
  STRING,
@@ -346,6 +347,41 @@ export class TypeChecker {
346
347
  },
347
348
  });
348
349
 
350
+ // unicode package
351
+ const runeToStr = { kind: "func", params: [INT], returns: [BOOL] };
352
+ const runeTrans = { kind: "func", params: [INT], returns: [INT] };
353
+ this.globals.define("unicode", {
354
+ kind: "namespace",
355
+ name: "unicode",
356
+ members: {
357
+ IsLetter: runeToStr,
358
+ IsDigit: runeToStr,
359
+ IsSpace: runeToStr,
360
+ IsUpper: runeToStr,
361
+ IsLower: runeToStr,
362
+ IsPunct: runeToStr,
363
+ IsControl: runeToStr,
364
+ IsPrint: runeToStr,
365
+ IsGraphic: runeToStr,
366
+ ToUpper: runeTrans,
367
+ ToLower: runeTrans,
368
+ },
369
+ });
370
+
371
+ // os package (JS-friendly subset)
372
+ this.globals.define("os", {
373
+ kind: "namespace",
374
+ name: "os",
375
+ members: {
376
+ Exit: { kind: "func", params: [INT], returns: [VOID] },
377
+ Args: { kind: "slice", elem: STRING },
378
+ Getenv: { kind: "func", params: [STRING], returns: [STRING] },
379
+ Stdout: ANY,
380
+ Stderr: ANY,
381
+ Stdin: ANY,
382
+ },
383
+ });
384
+
349
385
  this.globals.define("append", { kind: "builtin", name: "append" });
350
386
  this.globals.define("len", { kind: "builtin", name: "len" });
351
387
  this.globals.define("cap", { kind: "builtin", name: "cap" });
@@ -378,7 +414,13 @@ export class TypeChecker {
378
414
  addPackageNamespace(pkgName, symbols, types) {
379
415
  const members = {};
380
416
  for (const [name, type] of symbols) members[name] = type;
381
- this.globals.define(pkgName, { kind: "namespace", name: pkgName, members });
417
+ // _gofront: true marks this as a GoFront package — exported identifier rules apply
418
+ this.globals.define(pkgName, {
419
+ kind: "namespace",
420
+ name: pkgName,
421
+ members,
422
+ _gofront: true,
423
+ });
382
424
  for (const [name, type] of types) this.types.set(name, type);
383
425
  }
384
426
 
@@ -618,7 +660,7 @@ export class TypeChecker {
618
660
  for (const p of decl.params) {
619
661
  inner.define(p.name, this.resolveTypeNode(p.type, outer));
620
662
  }
621
- const returnType = decl.returnType
663
+ let returnType = decl.returnType
622
664
  ? this.resolveTypeNode(decl.returnType, outer)
623
665
  : VOID;
624
666
  const hasNamedReturns = this._injectNamedReturns(
@@ -626,7 +668,9 @@ export class TypeChecker {
626
668
  inner,
627
669
  outer,
628
670
  );
629
- if (hasNamedReturns && returnType) returnType._hasNamedReturns = true;
671
+ // Wrap the return type to avoid mutating the shared singleton
672
+ if (hasNamedReturns && returnType)
673
+ returnType = { ...returnType, _hasNamedReturns: true };
630
674
  const savedDefer = this._deferCount;
631
675
  this._deferCount = 0;
632
676
  this.checkBlock(decl.body, inner, returnType);
@@ -634,6 +678,12 @@ export class TypeChecker {
634
678
  if (this._deferCount > 0) decl.body._hasDefer = true;
635
679
  this._deferCount = savedDefer;
636
680
  decl._returnType = returnType;
681
+ // Missing return check: non-void functions must terminate on all paths
682
+ if (!isVoid(returnType) && !returnType._hasNamedReturns) {
683
+ if (!this._isTerminating(decl.body)) {
684
+ this.err(`missing return at end of function '${decl.name}'`, decl);
685
+ }
686
+ }
637
687
  }
638
688
 
639
689
  checkMethodDecl(decl, outer) {
@@ -643,7 +693,7 @@ export class TypeChecker {
643
693
  for (const p of decl.params) {
644
694
  inner.define(p.name, this.resolveTypeNode(p.type, outer));
645
695
  }
646
- const returnType = decl.returnType
696
+ let returnType = decl.returnType
647
697
  ? this.resolveTypeNode(decl.returnType, outer)
648
698
  : VOID;
649
699
  const hasNamedReturns = this._injectNamedReturns(
@@ -651,7 +701,8 @@ export class TypeChecker {
651
701
  inner,
652
702
  outer,
653
703
  );
654
- if (hasNamedReturns && returnType) returnType._hasNamedReturns = true;
704
+ if (hasNamedReturns && returnType)
705
+ returnType = { ...returnType, _hasNamedReturns: true };
655
706
  const savedDefer = this._deferCount;
656
707
  this._deferCount = 0;
657
708
  this.checkBlock(decl.body, inner, returnType);
@@ -659,6 +710,92 @@ export class TypeChecker {
659
710
  if (this._deferCount > 0) decl.body._hasDefer = true;
660
711
  this._deferCount = savedDefer;
661
712
  decl._returnType = returnType;
713
+ // Missing return check
714
+ if (!isVoid(returnType) && !returnType._hasNamedReturns) {
715
+ if (!this._isTerminating(decl.body)) {
716
+ this.err(`missing return at end of method '${decl.name}'`, decl);
717
+ }
718
+ }
719
+ }
720
+
721
+ // Go spec §Terminating statements — returns true if the statement always terminates.
722
+ _isTerminating(stmt) {
723
+ if (!stmt) return false;
724
+ switch (stmt.kind) {
725
+ case "ReturnStmt":
726
+ return true;
727
+ case "BranchStmt":
728
+ // panic() is in ExprStmt, not BranchStmt — but "goto" could be here
729
+ return false;
730
+ case "ExprStmt":
731
+ // panic(...) is a terminating call
732
+ if (
733
+ stmt.expr?.kind === "CallExpr" &&
734
+ stmt.expr?.func?.kind === "Ident" &&
735
+ stmt.expr?.func?.name === "panic"
736
+ )
737
+ return true;
738
+ return false;
739
+ case "Block":
740
+ return this._isTerminatingBlock(stmt);
741
+ case "IfStmt":
742
+ // Terminating if: has else, and both branches terminate
743
+ if (!stmt.elseBody) return false;
744
+ return (
745
+ this._isTerminatingBlock(stmt.body) &&
746
+ (stmt.elseBody.kind === "Block"
747
+ ? this._isTerminatingBlock(stmt.elseBody)
748
+ : this._isTerminating(stmt.elseBody))
749
+ );
750
+ case "ForStmt":
751
+ // Infinite loop (no condition) with no break — terminating
752
+ if (!stmt.cond && !this._blockHasBreak(stmt.body)) return true;
753
+ return false;
754
+ case "SwitchStmt":
755
+ // Terminating switch: has default and every case terminates
756
+ return this._isTerminatingSwitch(stmt);
757
+ case "TypeSwitchStmt":
758
+ return this._isTerminatingTypeSwitch(stmt);
759
+ case "LabeledStmt":
760
+ return this._isTerminating(stmt.body);
761
+ default:
762
+ return false;
763
+ }
764
+ }
765
+
766
+ _isTerminatingBlock(block) {
767
+ if (!block?.stmts?.length) return false;
768
+ return this._isTerminating(block.stmts[block.stmts.length - 1]);
769
+ }
770
+
771
+ _blockHasBreak(block) {
772
+ if (!block?.stmts) return false;
773
+ for (const s of block.stmts) {
774
+ if (s.kind === "BranchStmt" && s.keyword === "break") return true;
775
+ }
776
+ return false;
777
+ }
778
+
779
+ _isTerminatingSwitch(stmt) {
780
+ let hasDefault = false;
781
+ for (const c of stmt.cases ?? []) {
782
+ if (!c.list) hasDefault = true;
783
+ if (!c.stmts?.length) return false;
784
+ const last = c.stmts[c.stmts.length - 1];
785
+ if (!this._isTerminating(last)) return false;
786
+ }
787
+ return hasDefault;
788
+ }
789
+
790
+ _isTerminatingTypeSwitch(stmt) {
791
+ let hasDefault = false;
792
+ for (const c of stmt.cases ?? []) {
793
+ if (!c.types) hasDefault = true;
794
+ if (!c.stmts?.length) return false;
795
+ const last = c.stmts[c.stmts.length - 1];
796
+ if (!this._isTerminating(last)) return false;
797
+ }
798
+ return hasDefault;
662
799
  }
663
800
 
664
801
  _injectNamedReturns(returnTypeNode, scope, outer) {
@@ -852,6 +989,18 @@ export class TypeChecker {
852
989
  return this.err(`No field '${field}' on ${typeStr(baseType)}`, node);
853
990
  }
854
991
  if (base?.kind === "namespace") {
992
+ // Enforce exported identifier rule for GoFront packages only
993
+ if (
994
+ base._gofront &&
995
+ field.length > 0 &&
996
+ field[0] >= "a" &&
997
+ field[0] <= "z"
998
+ ) {
999
+ return this.err(
1000
+ `cannot refer to unexported name ${base.name}.${field}`,
1001
+ node,
1002
+ );
1003
+ }
855
1004
  if (field in base.members) return base.members[field];
856
1005
  return this.err(`No member '${field}' in namespace ${base.name}`, node);
857
1006
  }
@@ -875,7 +1024,26 @@ export class TypeChecker {
875
1024
  }
876
1025
 
877
1026
  binaryResultType(op, lt, rt, node) {
878
- if (CMP_OPS.has(op)) return BOOL;
1027
+ if (CMP_OPS.has(op)) {
1028
+ // Reject slice/map/func comparisons unless one side is nil (Go spec)
1029
+ if (op === "==" || op === "!=") {
1030
+ const lNil = isNil(lt);
1031
+ const rNil = isNil(rt);
1032
+ if (!lNil && !rNil) {
1033
+ for (const t of [lt, rt]) {
1034
+ const base = t?.kind === "named" ? t.underlying : t;
1035
+ if (
1036
+ base?.kind === "slice" ||
1037
+ base?.kind === "map" ||
1038
+ base?.kind === "func"
1039
+ ) {
1040
+ this.err(`operator ${op} not defined on ${typeStr(t)}`, node);
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ return BOOL;
1046
+ }
879
1047
  if (LOG_OPS.has(op)) return BOOL;
880
1048
  if (isAny(lt) || isAny(rt)) return ANY;
881
1049
  if (isNumeric(lt) && isNumeric(rt)) {
@@ -946,6 +1114,8 @@ export class TypeChecker {
946
1114
  tBase = this.resolveType(tBase);
947
1115
  sBase = this.resolveType(sBase);
948
1116
  if (tBase?.kind === "interface") {
1117
+ // Empty interface (interface{}) accepts any type
1118
+ if (tBase.methods.size === 0) return;
949
1119
  if (!this.implements(source, tBase, node)) {
950
1120
  this.err(
951
1121
  `${typeStr(source)} does not implement ${typeStr(target)}`,