gofront 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +174 -403
  2. package/package.json +16 -19
  3. package/src/{codegen → backend/js}/expressions.js +265 -77
  4. package/src/{codegen → backend/js}/index.js +120 -89
  5. package/src/{codegen → backend/js}/runtime.js +102 -0
  6. package/src/{codegen → backend/js}/source-map.js +32 -0
  7. package/src/{codegen → backend/js}/statements.js +139 -64
  8. package/src/backend/js/stdlib/bits.js +32 -0
  9. package/src/{codegen → backend/js}/stdlib/errors.js +2 -2
  10. package/src/{codegen → backend/js}/stdlib/fmt.js +26 -8
  11. package/src/{codegen → backend/js}/stdlib/index.js +6 -0
  12. package/src/{codegen → backend/js}/stdlib/maps.js +1 -1
  13. package/src/{codegen → backend/js}/stdlib/path.js +2 -2
  14. package/src/backend/js/stdlib/shared.js +17 -0
  15. package/src/{codegen → backend/js}/stdlib/slices.js +1 -1
  16. package/src/{codegen → backend/js}/stdlib/sort.js +1 -1
  17. package/src/backend/js/stdlib/strconv.js +58 -0
  18. package/src/{codegen → backend/js}/stdlib/strings.js +4 -1
  19. package/src/{codegen → backend/js}/stdlib/testing.js +2 -2
  20. package/src/{codegen → backend/js}/stdlib/time.js +3 -3
  21. package/src/{codegen → backend/js}/templ.js +3 -3
  22. package/src/backend/wasm/boundary.js +1798 -0
  23. package/src/backend/wasm/emit-builtins.js +1103 -0
  24. package/src/backend/wasm/emit-exprs.js +1908 -0
  25. package/src/backend/wasm/emit-maps.js +1863 -0
  26. package/src/backend/wasm/emit-shared.js +519 -0
  27. package/src/backend/wasm/emit-stdlib.js +3006 -0
  28. package/src/backend/wasm/emit-stmts.js +2472 -0
  29. package/src/backend/wasm/emit.js +1376 -0
  30. package/src/backend/wasm/encode.js +937 -0
  31. package/src/backend/wasm/glue.js +144 -0
  32. package/src/backend/wasm/index.js +2790 -0
  33. package/src/backend/wasm/monomorph.js +711 -0
  34. package/src/backend/wasm/optimize.js +227 -0
  35. package/src/backend/wasm/types.js +544 -0
  36. package/src/backend/wasm/wat.js +302 -0
  37. package/src/cli-core.js +323 -75
  38. package/src/compiler.js +447 -14
  39. package/src/dev-server.js +56 -15
  40. package/src/index.js +99 -120
  41. package/src/lexer.js +18 -0
  42. package/src/lower/boxing.js +35 -0
  43. package/src/lower/captures.js +172 -0
  44. package/src/lower/embedding.js +30 -0
  45. package/src/lower/escape.js +164 -0
  46. package/src/lower/functions.js +302 -0
  47. package/src/lower/globals.js +316 -0
  48. package/src/lower/index.js +118 -0
  49. package/src/lower/ownership.js +137 -0
  50. package/src/lower/range.js +67 -0
  51. package/src/lower/walk.js +25 -0
  52. package/src/minifier.js +2 -0
  53. package/src/parser/index.js +8 -3
  54. package/src/resolver.js +3 -1
  55. package/src/test-runner.js +94 -20
  56. package/src/typechecker/assignability.js +42 -1
  57. package/src/typechecker/expressions.js +182 -3
  58. package/src/typechecker/index.js +261 -5
  59. package/src/typechecker/resolve.js +35 -6
  60. package/src/typechecker/stdlib/core.js +34 -19
  61. package/src/typechecker/stdlib/extended.js +18 -3
  62. package/src/typechecker/stdlib/shared.js +53 -0
  63. package/src/typechecker/stdlib.js +2 -0
  64. package/src/typechecker/types.js +259 -6
  65. package/src/vendor.js +2 -2
  66. package/src/codegen/stdlib/strconv.js +0 -33
  67. /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
  68. /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
  69. /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
  70. /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
  71. /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
  72. /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
  73. /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
  74. /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
  75. /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
  76. /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
  77. /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
@@ -0,0 +1,172 @@
1
+ // src/lower/captures.js
2
+ // Identifies free variables captured by closures and determines which ones
3
+ // are mutated and thus require heap-allocated environment cells for WASM closures.
4
+
5
+ import { collectMutatedVars, nodeMutatesVar } from "./ownership.js";
6
+ import { forEachChild } from "./walk.js";
7
+
8
+ /**
9
+ * Result of capture analysis on a function or closure.
10
+ */
11
+ export class CaptureAnalysis {
12
+ constructor() {
13
+ // Map<FuncLit, Set<string>>: free variables captured by each closure
14
+ this.capturesByClosure = new Map();
15
+ // Set<string>: variables in the enclosing scope that are captured AND mutated
16
+ this.mutatedCaptures = new Set();
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Finds all variable declarations in a block or list of statements.
22
+ */
23
+ export function collectDeclaredNames(node, names = new Set()) {
24
+ if (!node || typeof node !== "object") return names;
25
+ if (Array.isArray(node)) {
26
+ for (const item of node) collectDeclaredNames(item, names);
27
+ return names;
28
+ }
29
+
30
+ if (node.kind === "VarDecl" || node.kind === "ConstDecl") {
31
+ for (const spec of node.decls ?? []) {
32
+ for (const name of spec.names ?? []) names.add(name);
33
+ }
34
+ } else if (node.kind === "DefineStmt") {
35
+ for (const lhs of node.lhs ?? []) {
36
+ if (lhs.kind === "Ident") names.add(lhs.name);
37
+ }
38
+ } else if (node.kind === "TypeSwitchStmt" && node.assign) {
39
+ names.add(node.assign);
40
+ }
41
+
42
+ // Do not descend into nested functions when collecting local declarations
43
+ if (node.kind === "FuncLit") return names;
44
+
45
+ forEachChild(node, (child) => collectDeclaredNames(child, names));
46
+ return names;
47
+ }
48
+
49
+ /**
50
+ * Finds all identifier references in a node that are not shadowed by inner declarations.
51
+ */
52
+ function collectReferencedNames(node, localDecls, refs = new Set()) {
53
+ if (!node || typeof node !== "object") return refs;
54
+ if (Array.isArray(node)) {
55
+ for (const item of node) collectReferencedNames(item, localDecls, refs);
56
+ return refs;
57
+ }
58
+
59
+ // In struct literals, field names in KeyValueExpr are not variable references
60
+ if (node.kind === "KeyValueExpr") {
61
+ collectReferencedNames(node.value, localDecls, refs);
62
+ return refs;
63
+ }
64
+
65
+ if (node.kind === "FuncLit") {
66
+ const innerLocals = new Set(localDecls);
67
+ for (const p of node.params ?? []) {
68
+ if (p.name && p.name !== "_") innerLocals.add(p.name);
69
+ }
70
+ for (const r of node.returnType?._namedReturns ?? []) {
71
+ if (r.name && r.name !== "_") innerLocals.add(r.name);
72
+ }
73
+ collectDeclaredNames(node.body, innerLocals);
74
+ forEachChild(node, (child) =>
75
+ collectReferencedNames(child, innerLocals, refs),
76
+ );
77
+ return refs;
78
+ }
79
+
80
+ if (
81
+ node.kind === "Ident" &&
82
+ !localDecls.has(node.name) &&
83
+ node.name !== "_"
84
+ ) {
85
+ refs.add(node.name);
86
+ }
87
+
88
+ // For nested closures, free variables are also referenced
89
+ forEachChild(node, (child) =>
90
+ collectReferencedNames(child, localDecls, refs),
91
+ );
92
+ return refs;
93
+ }
94
+
95
+ /**
96
+ * Analyzes a function or method declaration for closures and their captured variables.
97
+ * @param {Object} fnDecl AST FuncDecl or FuncLit node
98
+ * @returns {CaptureAnalysis}
99
+ */
100
+ export function analyzeCaptures(fnDecl) {
101
+ const result = new CaptureAnalysis();
102
+ if (!fnDecl?.body) return result;
103
+
104
+ // Collect parameters, receiver and named returns of the root function
105
+ const rootScope = new Set();
106
+ if (fnDecl.recvName && fnDecl.recvName !== "_") {
107
+ rootScope.add(fnDecl.recvName);
108
+ }
109
+ for (const p of fnDecl.params ?? []) {
110
+ if (p.name && p.name !== "_") rootScope.add(p.name);
111
+ }
112
+ for (const r of fnDecl.returnType?._namedReturns ?? []) {
113
+ if (r.name && r.name !== "_") rootScope.add(r.name);
114
+ }
115
+ collectDeclaredNames(fnDecl.body, rootScope);
116
+
117
+ // Fast pre-collected set of all mutated variables in the entire function
118
+ const mutatedVars = collectMutatedVars(fnDecl.body);
119
+
120
+ // Traverse closures with lexical scope stack to properly handle arbitrary nesting
121
+ const walkClosures = (node, scopeStack) => {
122
+ if (!node || typeof node !== "object") return;
123
+ if (Array.isArray(node)) {
124
+ for (const item of node) walkClosures(item, scopeStack);
125
+ return;
126
+ }
127
+
128
+ if (node.kind === "FuncLit") {
129
+ const closureLocals = new Set();
130
+ for (const p of node.params ?? []) {
131
+ if (p.name && p.name !== "_") closureLocals.add(p.name);
132
+ }
133
+ for (const r of node.returnType?._namedReturns ?? []) {
134
+ if (r.name && r.name !== "_") closureLocals.add(r.name);
135
+ }
136
+ collectDeclaredNames(node.body, closureLocals);
137
+
138
+ // Visible outer variables from all enclosing ancestor scopes
139
+ const allOuterScope = new Set();
140
+ for (const scope of scopeStack) {
141
+ for (const name of scope) allOuterScope.add(name);
142
+ }
143
+
144
+ const allRefs = collectReferencedNames(node.body, closureLocals);
145
+ const captured = new Set();
146
+ for (const ref of allRefs) {
147
+ if (allOuterScope.has(ref)) {
148
+ captured.add(ref);
149
+ }
150
+ }
151
+ result.capturesByClosure.set(node, captured);
152
+
153
+ for (const varName of captured) {
154
+ if (
155
+ mutatedVars.has(varName) ||
156
+ nodeMutatesVar(fnDecl.body, varName) ||
157
+ nodeMutatesVar(node.body, varName)
158
+ ) {
159
+ result.mutatedCaptures.add(varName);
160
+ }
161
+ }
162
+
163
+ walkClosures(node.body, [...scopeStack, closureLocals]);
164
+ return;
165
+ }
166
+
167
+ forEachChild(node, (child) => walkClosures(child, scopeStack));
168
+ };
169
+
170
+ walkClosures(fnDecl.body, [rootScope]);
171
+ return result;
172
+ }
@@ -0,0 +1,30 @@
1
+ // src/lower/embedding.js
2
+ // Computes promoted methods and delegation stubs for embedded structs.
3
+
4
+ export function computeEmbeddedStubs(name, methodDecls, checker) {
5
+ if (!checker) return [];
6
+ const resolvedType = checker.types.get(name)?.underlying;
7
+ if (resolvedType?.kind !== "struct" || !resolvedType._embeds) return [];
8
+
9
+ const declared = new Set(methodDecls.map((m) => m.name));
10
+ const stubs = [];
11
+
12
+ for (const embed of resolvedType._embeds) {
13
+ const embedName = embed.kind === "named" ? embed.name : null;
14
+ if (!embedName) continue;
15
+ const embedBase = embed.kind === "named" ? embed.underlying : embed;
16
+ if (embedBase?.kind !== "struct" || !embedBase.methods) continue;
17
+
18
+ for (const [mName] of embedBase.methods.entries()) {
19
+ if (!declared.has(mName)) {
20
+ stubs.push({
21
+ methodName: mName,
22
+ embedName,
23
+ });
24
+ declared.add(mName); // Prevent duplicate stubs for diamond embeds
25
+ }
26
+ }
27
+ }
28
+
29
+ return stubs;
30
+ }
@@ -0,0 +1,164 @@
1
+ // src/lower/escape.js
2
+ // Analyzes pointer retention / escaping for arguments across the WASM boundary.
3
+ // In GoFront's hybrid model, `*T` where T is a `both` type (e.g. `*mathx.Vec3`) uses
4
+ // copy-in/copy-out per call. If the WASM side retains the pointer beyond the call,
5
+ // it is flagged as an escape error.
6
+
7
+ import { collectDeclaredNames } from "./captures.js";
8
+ import { rootIdentName } from "./ownership.js";
9
+ import { forEachChild } from "./walk.js";
10
+
11
+ /**
12
+ * Checks whether an expression evaluates to one of the target pointer variables
13
+ * (or an interior pointer into them).
14
+ *
15
+ * @param {Object} expr AST expression node
16
+ * @param {Set<string>} targetNames Set of identifier names being tracked
17
+ * @returns {boolean}
18
+ */
19
+ function evaluatesToPointer(expr, targetNames) {
20
+ if (!expr || typeof expr !== "object") return false;
21
+ if (expr.kind === "Ident") {
22
+ return targetNames.has(expr.name);
23
+ }
24
+ if (expr.kind === "ParenExpr" || expr.kind === "TypeAssertExpr") {
25
+ return evaluatesToPointer(expr.expr, targetNames);
26
+ }
27
+ // Type conversions: (*Vec3)(p)
28
+ if (expr.kind === "CallExpr" && expr.args?.length === 1) {
29
+ return evaluatesToPointer(expr.args[0], targetNames);
30
+ }
31
+ // Interior pointer: &p.Field or &p[i] retains a pointer into p, or &CompositeLit holding p
32
+ if (expr.kind === "UnaryExpr" && expr.op === "&") {
33
+ const root = rootIdentName(expr.operand);
34
+ if (root != null && targetNames.has(root)) return true;
35
+ return evaluatesToPointer(expr.operand, targetNames);
36
+ }
37
+ // Composite literal holding pointer: Holder{ P: p } or []*Vec{ p }
38
+ if (expr.kind === "CompositeLit") {
39
+ for (const elem of expr.elems ?? []) {
40
+ const val = elem.kind === "KeyValueExpr" ? elem.value : elem;
41
+ if (evaluatesToPointer(val, targetNames)) return true;
42
+ }
43
+ }
44
+ return false;
45
+ }
46
+
47
+ /**
48
+ * Checks if a parameter identifier escapes or is retained in `body`.
49
+ * A parameter `name` is considered retained if it:
50
+ * - is returned from the function
51
+ * - is assigned to a package global or external object field
52
+ * - is stored into a struct field or slice/map element (e.g., `obj.field = p`, `arr[i] = p`)
53
+ * - is captured by a closure that outlives or is returned from the function
54
+ *
55
+ * @param {Object} body AST block of the function
56
+ * @param {string} paramName Name of the parameter to check
57
+ * @returns {boolean} True if param escapes/is retained, false if only used locally in-place
58
+ */
59
+ export function paramEscapes(body, paramName) {
60
+ if (!body || !paramName || paramName === "_") return false;
61
+
62
+ const aliases = new Set([paramName]);
63
+ const localNames = new Set([paramName]);
64
+ collectDeclaredNames(body, localNames);
65
+
66
+ let escapes = false;
67
+
68
+ const walk = (node) => {
69
+ if (escapes || !node || typeof node !== "object") return;
70
+ if (Array.isArray(node)) {
71
+ for (const item of node) walk(item);
72
+ return;
73
+ }
74
+
75
+ // 1. Returning the pointer
76
+ if (node.kind === "ReturnStmt") {
77
+ for (const res of node.values ?? node.results ?? []) {
78
+ if (evaluatesToPointer(res, aliases)) {
79
+ escapes = true;
80
+ return;
81
+ }
82
+ }
83
+ }
84
+
85
+ // 2. Assigning the pointer:
86
+ // Storing into a field, index, or package-level global escapes.
87
+ // Storing into a local variable aliases the pointer.
88
+ if (node.kind === "AssignStmt") {
89
+ for (let i = 0; i < node.lhs.length; i++) {
90
+ const lhs = node.lhs[i];
91
+ const rhs = node.rhs?.[i];
92
+ if (!rhs) continue;
93
+ if (evaluatesToPointer(rhs, aliases)) {
94
+ // Storing into a field or index: `target.field = p` or `arr[i] = p`
95
+ if (lhs.kind === "SelectorExpr" || lhs.kind === "IndexExpr") {
96
+ escapes = true;
97
+ return;
98
+ }
99
+ // If LHS is not a local variable declared in body, it's package-level / global
100
+ if (lhs.kind === "Ident") {
101
+ if (!localNames.has(lhs.name)) {
102
+ escapes = true;
103
+ return;
104
+ }
105
+ aliases.add(lhs.name);
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ // 3. Short variable definition: local alias `tmp := p`
112
+ if (node.kind === "DefineStmt") {
113
+ for (let i = 0; i < node.lhs.length; i++) {
114
+ const lhs = node.lhs[i];
115
+ const rhs = node.rhs?.[i];
116
+ if (rhs && evaluatesToPointer(rhs, aliases)) {
117
+ if (lhs.kind === "Ident") {
118
+ aliases.add(lhs.name);
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ // 4. Storing into append call: `slice = append(slice, p)`
125
+ const fnName = node.func?.name ?? node.fun?.name;
126
+ if (node.kind === "CallExpr" && fnName === "append") {
127
+ for (const arg of node.args?.slice(1) ?? []) {
128
+ if (evaluatesToPointer(arg, aliases)) {
129
+ escapes = true;
130
+ return;
131
+ }
132
+ }
133
+ }
134
+
135
+ // 5. Storing into a closure (FuncLit)
136
+ if (node.kind === "FuncLit") {
137
+ let captured = false;
138
+ const checkClosure = (cNode) => {
139
+ if (captured || !cNode || typeof cNode !== "object") return;
140
+ if (Array.isArray(cNode)) {
141
+ for (const item of cNode) checkClosure(item);
142
+ return;
143
+ }
144
+ if (cNode.kind === "Ident" && aliases.has(cNode.name)) {
145
+ captured = true;
146
+ return;
147
+ }
148
+ forEachChild(cNode, checkClosure);
149
+ };
150
+ checkClosure(node.body);
151
+ if (captured) {
152
+ escapes = true;
153
+ return;
154
+ }
155
+ // Do not descend redundantly into closure body
156
+ return;
157
+ }
158
+
159
+ forEachChild(node, walk);
160
+ };
161
+
162
+ walk(body);
163
+ return escapes;
164
+ }
@@ -0,0 +1,302 @@
1
+ // src/lower/functions.js
2
+ // Analyzes function signatures, named returns and defer structures.
3
+
4
+ import { forEachChild, someChild } from "./walk.js";
5
+
6
+ export function extractNamedReturns(decl) {
7
+ const named = decl?.returnType?._namedReturns;
8
+ if (!named || named.length === 0) return null;
9
+ return {
10
+ entries: named, // [{ name, type }]
11
+ names: named.map((r) => r.name).filter(Boolean),
12
+ };
13
+ }
14
+
15
+ export function hasDefer(body) {
16
+ if (!body) return false;
17
+ if (body._hasDefer != null) return Boolean(body._hasDefer);
18
+
19
+ const walk = (node) => {
20
+ if (!node || typeof node !== "object") return false;
21
+ if (Array.isArray(node)) return node.some(walk);
22
+ if (node.kind === "DeferStmt") return true;
23
+ if (node.kind === "FuncLit") return false; // separate function boundary
24
+ return someChild(node, walk);
25
+ };
26
+
27
+ return walk(body);
28
+ }
29
+
30
+ let defargCounter = 0;
31
+
32
+ /**
33
+ * True when `body` contains a direct `recover()` call (not nested inside a
34
+ * FuncLit). Used to decide which functions may legitimately observe an
35
+ * in-flight panic: Go only honours recover() when it is called directly by
36
+ * a deferred function.
37
+ */
38
+ export function hasDirectRecover(body) {
39
+ if (!body) return false;
40
+ if (body._hasDirectRecover != null) return Boolean(body._hasDirectRecover);
41
+ const walk = (node) => {
42
+ if (!node || typeof node !== "object") return false;
43
+ if (Array.isArray(node)) return node.some(walk);
44
+ if (node.kind === "FuncLit") return false;
45
+ if (
46
+ node.kind === "CallExpr" &&
47
+ node.func?.kind === "Ident" &&
48
+ node.func.name === "recover"
49
+ )
50
+ return true;
51
+ return someChild(node, walk);
52
+ };
53
+ const result = walk(body);
54
+ body._hasDirectRecover = result;
55
+ return result;
56
+ }
57
+
58
+ const BUILTIN_FUNC_NAMES = new Set([
59
+ "println",
60
+ "print",
61
+ "panic",
62
+ "recover",
63
+ "make",
64
+ "new",
65
+ "len",
66
+ "cap",
67
+ "append",
68
+ "copy",
69
+ "delete",
70
+ "close",
71
+ "clear",
72
+ "min",
73
+ "max",
74
+ ]);
75
+
76
+ export function normalizeDefers(programOrPrograms) {
77
+ defargCounter = 0;
78
+ const programs = Array.isArray(programOrPrograms)
79
+ ? programOrPrograms
80
+ : [programOrPrograms];
81
+
82
+ const pkgFuncNames = new Set(BUILTIN_FUNC_NAMES);
83
+ // Package-level funcs/methods whose body calls recover() directly. A
84
+ // `defer f()` whose callee is one of these must keep recover armed when the
85
+ // synthesized wrapper forwards the call.
86
+ const recoverFuncs = new Set();
87
+ for (const program of programs) {
88
+ for (const d of program?.decls ?? []) {
89
+ if (d.kind === "FuncDecl") pkgFuncNames.add(d.name);
90
+ if (
91
+ (d.kind === "FuncDecl" || d.kind === "MethodDecl") &&
92
+ hasDirectRecover(d.body)
93
+ )
94
+ recoverFuncs.add(d.name);
95
+ }
96
+ }
97
+ const ctx = { pkgFuncNames, recoverFuncs };
98
+
99
+ for (const program of programs) {
100
+ if (!program?.decls) continue;
101
+ for (const d of program.decls) {
102
+ if (d.body) normalizeBlockDefers(d.body, ctx);
103
+ }
104
+ }
105
+ }
106
+
107
+ function normalizeBlockDefers(node, ctx) {
108
+ if (!node || typeof node !== "object") return;
109
+ if (Array.isArray(node)) {
110
+ for (let i = 0; i < node.length; i++) {
111
+ const item = node[i];
112
+ if (item && item.kind === "DeferStmt") {
113
+ const inserted = normalizeDeferStmt(item, node, i, ctx);
114
+ i += inserted;
115
+ } else {
116
+ normalizeBlockDefers(item, ctx);
117
+ }
118
+ }
119
+ return;
120
+ }
121
+
122
+ if (node.kind === "FuncLit") {
123
+ normalizeBlockDefers(node.body, ctx);
124
+ return;
125
+ }
126
+
127
+ forEachChild(node, (child) => normalizeBlockDefers(child, ctx));
128
+ }
129
+
130
+ // Does the deferred callee possibly call recover() directly? Known package
131
+ // funcs/methods are looked up; unknown callees (closure variables, func
132
+ // values) are assumed to, so recover stays armed through the wrapper.
133
+ function deferCalleeMayRecover(funcExpr, ctx) {
134
+ if (!ctx) return false;
135
+ if (funcExpr.kind === "Ident") {
136
+ if (BUILTIN_FUNC_NAMES.has(funcExpr.name)) return false;
137
+ if (ctx.pkgFuncNames.has(funcExpr.name))
138
+ return ctx.recoverFuncs.has(funcExpr.name);
139
+ return true;
140
+ }
141
+ if (funcExpr.kind === "SelectorExpr") {
142
+ if (funcExpr.expr?._type?.kind === "namespace") return false;
143
+ return ctx.recoverFuncs.has(funcExpr.field);
144
+ }
145
+ if (funcExpr.kind === "FuncLit") return hasDirectRecover(funcExpr.body);
146
+ return true;
147
+ }
148
+
149
+ function normalizeDeferStmt(stmt, parentList = null, index = -1, ctx = null) {
150
+ if (stmt.call?.kind !== "CallExpr") return 0;
151
+ const call = stmt.call;
152
+ const isZeroArgFuncLit =
153
+ call.func?.kind === "FuncLit" && (!call.args || call.args.length === 0);
154
+ if (isZeroArgFuncLit) {
155
+ // The FuncLit itself is the deferred function: recover() inside it is
156
+ // honoured.
157
+ call.func._isDeferTarget = true;
158
+ normalizeBlockDefers(call.func.body, ctx);
159
+ return 0;
160
+ }
161
+ const pkgFuncNames = ctx?.pkgFuncNames ?? null;
162
+
163
+ const tempStmts = [];
164
+ let funcExpr = call.func;
165
+
166
+ // Hoist method receiver if call.func is a method call (e.g. defer r.Close()).
167
+ // Skipped for pointer-receiver methods on an addressable (non-pointer)
168
+ // operand: Go evaluates `&recv` at defer time, so the deferred call must
169
+ // observe later mutations of the original variable — copying the struct
170
+ // into a temp would break that. The receiver expression itself is then
171
+ // re-evaluated at run time, which matches Go for the common `defer x.M()`
172
+ // / `defer s.f.M()` cases.
173
+ const recvIsAddressableValue =
174
+ call.func?._type?._ptrRecv === true &&
175
+ call.func.expr?._type?.kind !== "pointer";
176
+ if (
177
+ parentList &&
178
+ index >= 0 &&
179
+ call.func?.kind === "SelectorExpr" &&
180
+ call.func.expr?._type?.kind !== "namespace" &&
181
+ !recvIsAddressableValue
182
+ ) {
183
+ const recv = call.func.expr;
184
+ defargCounter++;
185
+ const recvName = `__defrecv$${defargCounter}`;
186
+ const recvIdent = {
187
+ kind: "Ident",
188
+ name: recvName,
189
+ _type: recv._type,
190
+ };
191
+ tempStmts.push({
192
+ kind: "DefineStmt",
193
+ lhs: [{ kind: "Ident", name: recvName, _type: recv._type }],
194
+ rhs: [recv],
195
+ });
196
+ funcExpr = {
197
+ ...call.func,
198
+ expr: recvIdent,
199
+ };
200
+ } else if (
201
+ parentList &&
202
+ index >= 0 &&
203
+ call.func?.kind === "Ident" &&
204
+ pkgFuncNames &&
205
+ !pkgFuncNames.has(call.func.name)
206
+ ) {
207
+ // Hoist local function variable (e.g. fn := f1; defer fn(); fn = f2)
208
+ defargCounter++;
209
+ const fnName = `__deffn$${defargCounter}`;
210
+ const fnIdent = {
211
+ kind: "Ident",
212
+ name: fnName,
213
+ _type: call.func._type,
214
+ };
215
+ tempStmts.push({
216
+ kind: "DefineStmt",
217
+ lhs: [{ kind: "Ident", name: fnName, _type: call.func._type }],
218
+ rhs: [call.func],
219
+ });
220
+ funcExpr = fnIdent;
221
+ }
222
+
223
+ const args = call.args ?? [];
224
+ const tempIdents = [];
225
+
226
+ if (parentList && index >= 0 && args.length > 0) {
227
+ for (let a = 0; a < args.length; a++) {
228
+ const arg = args[a];
229
+ if (arg._type?.kind === "tuple" && Array.isArray(arg._type.types)) {
230
+ const tupleTypes = arg._type.types;
231
+ const tupleLhs = [];
232
+ for (let t = 0; t < tupleTypes.length; t++) {
233
+ defargCounter++;
234
+ const tempName = `__defarg$${defargCounter}`;
235
+ const ident = {
236
+ kind: "Ident",
237
+ name: tempName,
238
+ _type: tupleTypes[t],
239
+ };
240
+ tempIdents.push(ident);
241
+ tupleLhs.push(ident);
242
+ }
243
+ tempStmts.push({
244
+ kind: "DefineStmt",
245
+ lhs: tupleLhs,
246
+ rhs: [arg],
247
+ });
248
+ } else {
249
+ defargCounter++;
250
+ const tempName = `__defarg$${defargCounter}`;
251
+ tempIdents.push({
252
+ kind: "Ident",
253
+ name: tempName,
254
+ _type: arg._type,
255
+ });
256
+ tempStmts.push({
257
+ kind: "DefineStmt",
258
+ lhs: [{ kind: "Ident", name: tempName, _type: arg._type }],
259
+ rhs: [arg],
260
+ });
261
+ }
262
+ }
263
+ }
264
+
265
+ if (parentList && index >= 0 && tempStmts.length > 0) {
266
+ parentList.splice(index, 0, ...tempStmts);
267
+ }
268
+
269
+ const callArgs = tempIdents.length > 0 ? tempIdents : args;
270
+ const innerCall = {
271
+ ...call,
272
+ func: funcExpr,
273
+ args: callArgs,
274
+ _multiForward: tempIdents.length > 0 ? false : call._multiForward,
275
+ };
276
+ const synthFuncLit = {
277
+ kind: "FuncLit",
278
+ params: [],
279
+ returnType: null,
280
+ body: {
281
+ kind: "Block",
282
+ stmts: [{ kind: "ExprStmt", expr: innerCall }],
283
+ list: [{ kind: "ExprStmt", expr: innerCall }],
284
+ _hasDefer: false,
285
+ },
286
+ _type: { kind: "FuncType", params: [], returnType: null },
287
+ _isDeferTarget: true,
288
+ // The wrapper only forwards to the real deferred callee; when that
289
+ // callee may call recover() directly, recover must stay armed across
290
+ // the wrapper frame.
291
+ _deferForward: deferCalleeMayRecover(call.func, ctx),
292
+ };
293
+ if (call.func?.kind === "FuncLit") normalizeBlockDefers(call.func.body, ctx);
294
+ stmt.call = {
295
+ kind: "CallExpr",
296
+ func: synthFuncLit,
297
+ args: [],
298
+ _type: { kind: "basic", name: "void" },
299
+ };
300
+
301
+ return tempStmts.length;
302
+ }