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 +20 -23
- package/package.json +1 -1
- package/src/codegen/expressions.js +130 -27
- package/src/codegen/statements.js +67 -9
- package/src/codegen.js +9 -6
- package/src/parser.js +36 -14
- package/src/typechecker/expressions.js +92 -4
- package/src/typechecker/statements.js +39 -2
- package/src/typechecker/types.js +0 -18
- package/src/typechecker.js +176 -6
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,
|
|
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
|
|
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 |
|
|
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 |
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
541
|
-
// Handles embedded-spread (_isEmbedInit) and
|
|
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
|
-
.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
559
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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 ${
|
|
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.
|
|
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
|
|
212
|
-
|
|
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
|
|
392
|
+
let rawExprs; // before iota substitution
|
|
391
393
|
if (this.check(T.ASSIGN)) {
|
|
392
394
|
this.advance();
|
|
393
|
-
|
|
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
|
-
|
|
403
|
+
rawExprs = this.parseExprList();
|
|
404
|
+
prevRawExprs = rawExprs;
|
|
399
405
|
} else {
|
|
400
|
-
//
|
|
401
|
-
|
|
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
|
-
//
|
|
406
|
-
value =
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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);
|
package/src/typechecker/types.js
CHANGED
|
@@ -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;
|
package/src/typechecker.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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))
|
|
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)}`,
|