cluaupp 1.5.2 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +27 -40
  3. package/api/generated/bench-latest.json +43 -21
  4. package/api/generated/lsp-index.json +1 -1
  5. package/api/manifests/roblox-target.manifest.json +5 -5
  6. package/api/normalized/roblox-target.profile.json +8729 -267
  7. package/api/roblox/luau-builtins.d.luau +185 -0
  8. package/api/roblox/surface.json +1 -0
  9. package/api/roblox-api.lock.json +11 -2
  10. package/bin/cluaupp.js +38 -0
  11. package/editors/vscode/syntaxes/clpp.tmLanguage.json +211 -0
  12. package/examples/README.md +9 -9
  13. package/examples/game/cluaupp.config.json +2 -2
  14. package/examples/game/default.project.json +4 -15
  15. package/examples/game/out/ReplicatedStorage/Shared/GameInfo.luau +8 -0
  16. package/examples/game/out/ReplicatedStorage/Shared/GameInfo.luau.map.json +13 -0
  17. package/examples/game/out/ServerScriptService/Leaderstats.server.luau +26 -0
  18. package/examples/game/out/ServerScriptService/Leaderstats.server.luau.map.json +13 -0
  19. package/examples/game/out/ServerScriptService/Main.server.luau +15 -0
  20. package/examples/game/out/ServerScriptService/Main.server.luau.map.json +13 -0
  21. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Main.client.luau +10 -0
  22. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Main.client.luau.map.json +13 -0
  23. package/examples/game/src/ReplicatedStorage/Shared/GameInfo.clp +1 -0
  24. package/examples/game/src/ServerScriptService/Leaderstats.server.clp +18 -0
  25. package/examples/game/src/ServerScriptService/Main.server.clp +9 -0
  26. package/examples/game/src/StarterPlayer/StarterPlayerScripts/Main.client.clp +4 -0
  27. package/generated/api/build-profile.js +1 -1
  28. package/generated/api/loader.js +2 -2
  29. package/generated/api/surface/build.js +328 -0
  30. package/generated/api/surface/luau-defs.js +595 -0
  31. package/generated/api/surface/model.js +8 -0
  32. package/generated/clpp/contract.js +6 -3
  33. package/generated/clpp/handshake.js +36 -0
  34. package/generated/clpp/modules.js +20 -9
  35. package/generated/clpp/runner.js +62 -31
  36. package/generated/compile.js +18 -1
  37. package/generated/compiler/ast.js +3 -0
  38. package/generated/compiler/checker/calls.js +999 -0
  39. package/generated/compiler/checker/checker.js +1215 -0
  40. package/generated/compiler/checker/exprs.js +791 -0
  41. package/generated/compiler/checker/flow.js +168 -0
  42. package/generated/compiler/checker/members.js +763 -0
  43. package/generated/compiler/checker/statements.js +792 -0
  44. package/generated/compiler/checker/symbols.js +90 -0
  45. package/generated/compiler/checker/typeops.js +348 -0
  46. package/generated/compiler/checker/yields.js +115 -0
  47. package/generated/compiler/diagnostics.js +138 -0
  48. package/generated/compiler/emit/emitter.js +2356 -0
  49. package/generated/compiler/emit/luau-types.js +137 -0
  50. package/generated/compiler/emit/names.js +130 -0
  51. package/generated/compiler/emit/runtime.js +432 -0
  52. package/generated/compiler/lexer.js +403 -0
  53. package/generated/compiler/parser.js +1881 -0
  54. package/generated/compiler/project.js +332 -0
  55. package/generated/compiler/roblox/api.js +388 -0
  56. package/generated/compiler/roblox/context.js +100 -0
  57. package/generated/compiler/source.js +59 -0
  58. package/generated/compiler/stdlib.js +232 -0
  59. package/generated/compiler/types.js +250 -0
  60. package/generated/target/build-cache.js +4 -4
  61. package/generated/target/gen-rojo-tree.js +15 -0
  62. package/generated/target/source-map.js +61 -8
  63. package/generated/target/studio-bridge.js +1 -0
  64. package/generated/utils/project.js +83 -25
  65. package/native/bin/.gitkeep +1 -0
  66. package/package.json +17 -6
  67. package/plugins/CluauppNav/Plugin.luau +28 -4
  68. package/rust/cluaupp/Cargo.lock +795 -0
  69. package/rust/cluaupp/Cargo.toml +17 -0
  70. package/rust/cluaupp/src/main.rs +717 -0
  71. package/scripts/postinstall-native.js +56 -0
  72. package/skills/README.md +3 -3
  73. package/skills/clpp-hive-flare/SKILL.md +30 -26
  74. package/skills/clpp-language/SKILL.md +67 -74
  75. package/skills/clpp-language/compiler.md +24 -36
  76. package/skills/clpp-style/SKILL.md +31 -44
  77. package/src/api/build-profile.ts +1 -1
  78. package/src/api/loader.ts +2 -2
  79. package/src/api/surface/build.ts +374 -0
  80. package/src/api/surface/luau-defs.ts +635 -0
  81. package/src/api/surface/model.ts +96 -0
  82. package/src/clpp/contract.ts +20 -3
  83. package/src/clpp/handshake.ts +25 -0
  84. package/src/clpp/modules.ts +17 -10
  85. package/src/clpp/runner.ts +52 -34
  86. package/src/compile.ts +20 -2
  87. package/src/compiler/ast.ts +336 -0
  88. package/src/compiler/checker/calls.ts +1025 -0
  89. package/src/compiler/checker/checker.ts +1352 -0
  90. package/src/compiler/checker/exprs.ts +805 -0
  91. package/src/compiler/checker/flow.ts +174 -0
  92. package/src/compiler/checker/members.ts +793 -0
  93. package/src/compiler/checker/statements.ts +802 -0
  94. package/src/compiler/checker/symbols.ts +219 -0
  95. package/src/compiler/checker/typeops.ts +355 -0
  96. package/src/compiler/checker/yields.ts +120 -0
  97. package/src/compiler/diagnostics.ts +163 -0
  98. package/src/compiler/emit/emitter.ts +2486 -0
  99. package/src/compiler/emit/luau-types.ts +143 -0
  100. package/src/compiler/emit/names.ts +130 -0
  101. package/src/compiler/emit/runtime.ts +438 -0
  102. package/src/compiler/lexer.ts +444 -0
  103. package/src/compiler/parser.ts +1943 -0
  104. package/src/compiler/project.ts +354 -0
  105. package/src/compiler/roblox/api.ts +408 -0
  106. package/src/compiler/roblox/context.ts +108 -0
  107. package/src/compiler/source.ts +67 -0
  108. package/src/compiler/stdlib.ts +256 -0
  109. package/src/compiler/types.ts +313 -0
  110. package/src/target/build-cache.ts +5 -8
  111. package/src/target/gen-rojo-tree.ts +15 -0
  112. package/src/target/source-map.ts +65 -27
  113. package/src/target/studio-bridge.ts +3 -0
  114. package/src/types.ts +3 -0
  115. package/src/utils/project.ts +79 -31
  116. package/templates/README.md +7 -0
  117. package/templates/{game → game-modern}/cluaupp.config.json +2 -2
  118. package/templates/{game → game-modern}/package.json +1 -2
  119. package/templates/game-modern/rokit.toml +2 -0
  120. package/templates/game-modern/src/ReplicatedStorage/Shared/GameInfo.clp +1 -0
  121. package/templates/game-modern/src/ServerScriptService/Handlers/Guard.server.clp +4 -0
  122. package/templates/game-modern/src/ServerScriptService/Main.server.clp +8 -0
  123. package/templates/game-modern/src/StarterPlayer/StarterPlayerScripts/Main.client.clp +4 -0
  124. package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau +0 -29
  125. package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau.map.json +0 -13
  126. package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau +0 -83
  127. package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau.map.json +0 -13
  128. package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau +0 -18
  129. package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau.map.json +0 -13
  130. package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau +0 -14
  131. package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau.map.json +0 -13
  132. package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau +0 -15
  133. package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau.map.json +0 -13
  134. package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau +0 -50
  135. package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau.map.json +0 -13
  136. package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau +0 -12
  137. package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau.map.json +0 -13
  138. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau +0 -16
  139. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau.map.json +0 -13
  140. package/examples/game/src/Client/Controllers/DataController.client.clpp +0 -7
  141. package/examples/game/src/Client/Main.client.clpp +0 -4
  142. package/examples/game/src/Declarations/Net.flare +0 -5
  143. package/examples/game/src/Include/GameTypes.clh +0 -40
  144. package/examples/game/src/Modules/Core/Guard.server.clpp +0 -7
  145. package/examples/game/src/Modules/Core/PlayerHandler.server.clpp +0 -23
  146. package/examples/game/src/Server/Boot/DataBoot.server.clpp +0 -13
  147. package/examples/game/src/Server/Main.server.clpp +0 -4
  148. package/templates/game/.clangd +0 -25
  149. package/templates/game/.vscode/extensions.json +0 -8
  150. package/templates/game/.vscode/settings.json +0 -17
  151. package/templates/game/Packages/.gitkeep +0 -0
  152. package/templates/game/compile_flags.txt +0 -6
  153. package/templates/game/default.project.json +0 -41
  154. package/templates/game/include/clpp/datatypes.clh +0 -575
  155. package/templates/game/include/clpp/generated/enums.clh +0 -4920
  156. package/templates/game/include/clpp/generated/instances.clh +0 -87182
  157. package/templates/game/include/clpp/libs/arrayindexer.clh +0 -12
  158. package/templates/game/include/clpp/libs/axiom.clh +0 -154
  159. package/templates/game/include/clpp/libs/bloom.clh +0 -58
  160. package/templates/game/include/clpp/libs/coil.clh +0 -37
  161. package/templates/game/include/clpp/libs/crest.clh +0 -22
  162. package/templates/game/include/clpp/libs/echo.clh +0 -52
  163. package/templates/game/include/clpp/libs/ember.clh +0 -17
  164. package/templates/game/include/clpp/libs/flare.clh +0 -65
  165. package/templates/game/include/clpp/libs/gleam.clh +0 -111
  166. package/templates/game/include/clpp/libs/guide.clh +0 -42
  167. package/templates/game/include/clpp/libs/helm.clh +0 -31
  168. package/templates/game/include/clpp/libs/hive.clh +0 -20
  169. package/templates/game/include/clpp/libs/keep.clh +0 -158
  170. package/templates/game/include/clpp/libs/lens.clh +0 -24
  171. package/templates/game/include/clpp/libs/mint.clh +0 -28
  172. package/templates/game/include/clpp/libs/net.clh +0 -31
  173. package/templates/game/include/clpp/libs/occlude.clh +0 -15
  174. package/templates/game/include/clpp/libs/pin.clh +0 -28
  175. package/templates/game/include/clpp/libs/promise.clh +0 -24
  176. package/templates/game/include/clpp/libs/roster.clh +0 -123
  177. package/templates/game/include/clpp/libs/shift.clh +0 -22
  178. package/templates/game/include/clpp/libs/spark.clh +0 -28
  179. package/templates/game/include/clpp/libs/stage.clh +0 -23
  180. package/templates/game/include/clpp/libs/sweep.clh +0 -23
  181. package/templates/game/include/clpp/libs/trace.clh +0 -18
  182. package/templates/game/include/clpp/libs/ward.clh +0 -19
  183. package/templates/game/include/clpp/libs.clh +0 -31
  184. package/templates/game/include/clpp/roblox.clh +0 -41
  185. package/templates/game/rokit.toml +0 -5
  186. package/templates/game/src/Client/Controllers/DataController.client.clpp +0 -7
  187. package/templates/game/src/Client/Main.client.clpp +0 -4
  188. package/templates/game/src/Declarations/Net.flare +0 -5
  189. package/templates/game/src/Include/GameTypes.clh +0 -40
  190. package/templates/game/src/Modules/Core/Guard.server.clpp +0 -7
  191. package/templates/game/src/Modules/Core/PlayerHandler.server.clpp +0 -24
  192. package/templates/game/src/Server/Boot/DataBoot.server.clpp +0 -13
  193. package/templates/game/src/Server/Main.server.clpp +0 -4
  194. package/templates/game/stylua.toml +0 -9
  195. package/templates/game/wally.toml +0 -12
@@ -0,0 +1,2486 @@
1
+ /**
2
+ * Luau emitter. Walks the checked tree and writes idiomatic, `--!strict`-typed Luau:
3
+ * services hoisted once, `:` method calls decided by type, record-typed classes with metatables,
4
+ * numeric `for` loops, `continue`, compound assignment, `if`-expressions, string interpolation,
5
+ * and only the runtime helpers the file actually uses.
6
+ */
7
+
8
+ import type { Arg, Expr, FuncDecl, MatchArm, Param, Pattern, Program, Stmt, SwitchCase } from "../ast.js";
9
+ import type { FileInfo, LinkInfo } from "../checker/checker.js";
10
+ import type { EnumInfo, MethodInfo, StructInfo, Sym } from "../checker/symbols.js";
11
+ import type { RobloxApi } from "../roblox/api.js";
12
+ import { stdModule } from "../stdlib.js";
13
+ import { isDynamic, isValueTable, prune, T, type Type } from "../types.js";
14
+ import { luauType, type TypeNaming } from "./luau-types.js";
15
+ import { interpolationText, isIdentifier, isReservedTypeName, LUAU_KEYWORDS, luauNumber, luauString, reservedGlobals, sanitizeIdentifier } from "./names.js";
16
+ import { HELPERS, helperFunctionName, resolveHelpers } from "./runtime.js";
17
+
18
+ export interface EmitOptions {
19
+ sourcePosition?(offset: number): {line: number; column: number};
20
+ onSourceMap?(lines: import("../../clpp/contract.js").SourceMapLine[]): void;
21
+ /** Source path shown in the header (relative to the project). */
22
+ sourceLabel: string;
23
+ version: string;
24
+ /** Luau expression of the Instance a link points to (the argument of `require`), or null. */
25
+ requireTarget(link: LinkInfo, service: (name: string) => string): string | null;
26
+ /** Luau expression of the CluauppLibs folder. */
27
+ libsRoot(service: (name: string) => string): string;
28
+ }
29
+
30
+ const P = { IF: 0, OR: 1, AND: 2, CMP: 3, CONCAT: 4, ADD: 5, MUL: 6, UNARY: 7, POW: 8, ATOM: 10 } as const;
31
+
32
+ interface Code {
33
+ code: string;
34
+ prec: number;
35
+ }
36
+
37
+ interface LoopFrame {
38
+ kind: "loop" | "switch" | "try";
39
+ /** Statements to run before `continue` (C-style for step). */
40
+ step?: () => void;
41
+ /** Inside a pcall closure: control flow is returned as codes. */
42
+ usedBreak?: boolean;
43
+ usedContinue?: boolean;
44
+ usedReturn?: boolean;
45
+ /** Switch wrapped in `repeat … until true`. */
46
+ wrapped?: boolean;
47
+ continueFlag?: string;
48
+ }
49
+
50
+ const atom = (code: string): Code => ({ code, prec: P.ATOM });
51
+
52
+ export class LuauEmitter implements TypeNaming {
53
+ private lines: string[] = [];
54
+ private sourceOffset: number | null = null;
55
+ private indent = 0;
56
+ private readonly helpers = new Set<string>();
57
+ private readonly services = new Set<string>();
58
+ private readonly names = new Map<number, string>();
59
+ private readonly reserved: Set<string>;
60
+ private tmp = 0;
61
+ private readonly loops: LoopFrame[] = [];
62
+ private readonly usedLinks = new Set<LinkInfo>();
63
+ private axiomRuntime = false;
64
+ private sharedTable = false;
65
+ private readonly substitutions = new Map<number, string>();
66
+ private readonly staticSignals = new Set<number>();
67
+ private readonly enumTablesUsed = new Set<string>();
68
+ private fnDepth = 0;
69
+ private selfName = "self";
70
+ /** Function symbols referenced so far (for forward declarations). */
71
+ private readonly referencedFuncs = new Set<number>();
72
+ private readonly forwardFuncs = new Set<number>();
73
+ private readonly definedFuncs = new Set<number>();
74
+ private readonly isModule: boolean;
75
+
76
+ constructor(
77
+ private readonly program: Program,
78
+ private readonly file: FileInfo,
79
+ private readonly api: RobloxApi,
80
+ private readonly options: EmitOptions,
81
+ ) {
82
+ this.reserved = reservedGlobals(api);
83
+ for (const s of file.services) {
84
+ this.reserved.add(s);
85
+ }
86
+ this.isModule = file.kind === "module";
87
+ this.assignNames();
88
+ this.findStaticSignals();
89
+ }
90
+
91
+ // ---------- naming ----------
92
+
93
+ private assignNames(): void {
94
+ for (const s of this.file.structs) {
95
+ s.luauName = isReservedTypeName(this.api, s.name) || this.reserved.has(s.name) ? `${s.name}_` : sanitizeIdentifier(s.name);
96
+ }
97
+ for (const e of this.file.enums) {
98
+ e.luauName = this.reserved.has(e.name) || isReservedTypeName(this.api, e.name) ? `${e.name}_` : sanitizeIdentifier(e.name);
99
+ }
100
+ }
101
+
102
+ nameOf(sym: Sym): string {
103
+ const sub = this.substitutions.get(sym.id);
104
+ if (sub) {
105
+ return sub;
106
+ }
107
+ const known = this.names.get(sym.id);
108
+ if (known) {
109
+ return known;
110
+ }
111
+ let base = sanitizeIdentifier(sym.name);
112
+ if (sym.kind === "struct" && sym.typeId) {
113
+ const info = this.file.structRegistry.get(sym.typeId);
114
+ if (info) {
115
+ base = info.luauName;
116
+ }
117
+ }
118
+ if (LUAU_KEYWORDS.has(base) || this.reserved.has(base) || base.startsWith("__cl")) {
119
+ base = `${base}_`;
120
+ }
121
+ if (sym.kind === "self") {
122
+ base = this.selfName;
123
+ }
124
+ this.names.set(sym.id, base);
125
+ return base;
126
+ }
127
+
128
+ private temp(prefix = "t"): string {
129
+ this.tmp++;
130
+ return `__${prefix}${this.tmp}`;
131
+ }
132
+
133
+ struct(id: string): string | null {
134
+ const info = this.file.structRegistry.get(id);
135
+ if (!info) {
136
+ return null;
137
+ }
138
+ if (info.moduleId === this.file.moduleId) {
139
+ return info.luauName;
140
+ }
141
+ const link = this.linkFor((exp) => [...exp.structs.values()].some((s) => s.id === id));
142
+ if (!link) {
143
+ return null;
144
+ }
145
+ return `${this.linkName(link)}.${info.luauName}`;
146
+ }
147
+
148
+ enumType(id: string): string | null {
149
+ const info = this.file.enumRegistry.get(id);
150
+ if (!info || !info.hasPayload) {
151
+ return null;
152
+ }
153
+ if (info.moduleId === this.file.moduleId) {
154
+ return info.luauName;
155
+ }
156
+ const link = this.linkFor((exp) => [...exp.enums.values()].some((e) => e.id === id));
157
+ return link ? `${this.linkName(link)}.${info.luauName}` : null;
158
+ }
159
+
160
+ private linkFor(test: (exp: NonNullable<Extract<LinkInfo["target"], { kind: "clp" }>>["exports"]) => boolean): LinkInfo | null {
161
+ for (const link of this.file.links) {
162
+ const exp = link.target.kind === "clp" ? link.target.exports : link.target.kind === "luau" ? link.target.exports : undefined;
163
+ if (exp && test(exp)) {
164
+ this.usedLinks.add(link);
165
+ return link;
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+
171
+ private linkName(link: LinkInfo): string {
172
+ return link.sym ? this.nameOf(link.sym as Sym) : sanitizeIdentifier(link.decl.alias);
173
+ }
174
+
175
+ private structRef(id: string): string {
176
+ return this.struct(id) ?? "nil";
177
+ }
178
+
179
+ private typeOf(t: Type | undefined): string | null {
180
+ return t ? luauType(t, this) : null;
181
+ }
182
+
183
+ private use(helper: string): string {
184
+ this.helpers.add(helper);
185
+ return helperFunctionName(helper);
186
+ }
187
+
188
+ private service(name: string): string {
189
+ if (name === "Workspace") {
190
+ return "workspace";
191
+ }
192
+ this.services.add(name);
193
+ return name;
194
+ }
195
+
196
+ // ---------- writer ----------
197
+
198
+ private line(text: string): void {
199
+ const marker = text && this.sourceOffset !== null && this.options.sourcePosition ? `\u0000SRC${this.sourceOffset}\u0000` : "";
200
+ this.lines.push(text === "" ? "" : `${"\t".repeat(this.indent)}${text}${marker}`);
201
+ }
202
+
203
+ private block(fn: () => void): void {
204
+ this.indent++;
205
+ fn();
206
+ this.indent--;
207
+ }
208
+
209
+ /** Run fn with a fresh line buffer; returns the lines it produced. */
210
+ private capture(fn: () => void): string[] {
211
+ const saved = this.lines;
212
+ this.lines = [];
213
+ fn();
214
+ const out = this.lines;
215
+ this.lines = saved;
216
+ return out;
217
+ }
218
+
219
+ // ---------- entry ----------
220
+
221
+ emit(): string {
222
+ const types = this.capture(() => this.emitTypes());
223
+ const classes = this.capture(() => this.emitClassTables());
224
+ const functions = this.capture(() => this.emitFunctions());
225
+ const captured = this.file.topLevel.filter((s) => s.kind === "var" && s.captured);
226
+ const body = this.capture(() => {
227
+ for (const stmt of this.program.body) {
228
+ if (stmt.kind !== "Decl") {
229
+ this.stmt(stmt, true);
230
+ }
231
+ }
232
+ });
233
+ const footer = this.capture(() => this.emitReturn());
234
+
235
+ const out: string[] = [];
236
+ out.push(this.file.mode === "nocheck" ? "--!nocheck" : "--!strict");
237
+ out.push("--!optimize 2");
238
+ if (this.file.directives.includes("native")) {
239
+ out.push("--!native");
240
+ }
241
+ out.push(`-- Generated by Cluaupp ${this.options.version} from ${this.options.sourceLabel}. Do not edit: changes are overwritten on build.`);
242
+
243
+ const requireLines = this.capture(() => this.emitRequires());
244
+ const serviceLines = [...this.services].sort().map((s) => `local ${s} = game:GetService(${luauString(s)})`);
245
+ if (serviceLines.length || requireLines.length) {
246
+ out.push("", ...serviceLines);
247
+ if (requireLines.length) {
248
+ if (serviceLines.length) {
249
+ out.push("");
250
+ }
251
+ out.push(...requireLines);
252
+ }
253
+ }
254
+ const helperNames = resolveHelpers(this.helpers);
255
+ const helperTypes = helperNames.map((h) => HELPERS[h].types).filter(Boolean) as string[];
256
+ const helperCode = helperNames.map((h) => HELPERS[h].code).filter(Boolean);
257
+ if (helperTypes.length || helperCode.length) {
258
+ out.push("", ...helperTypes, ...(helperTypes.length ? [""] : []), helperCode.join("\n\n"));
259
+ }
260
+ if (types.length) {
261
+ out.push("", ...types);
262
+ }
263
+ if (this.sharedTable) {
264
+ out.push("", "local __clShared: { [string]: any } = {}");
265
+ }
266
+ if (classes.length) {
267
+ out.push("", ...classes);
268
+ }
269
+ const forward: string[] = [];
270
+ for (const s of captured) {
271
+ const t = this.typeOf(s.type);
272
+ forward.push(`local ${this.nameOf(s)}${t ? `: ${t}` : ""}`);
273
+ }
274
+ const forwardFns = this.file.topLevel.filter((s) => s.kind === "func" && this.forwardFuncs.has(s.id)).map((s) => this.nameOf(s));
275
+ if (forwardFns.length) {
276
+ forward.push(`local ${forwardFns.join(", ")}`);
277
+ }
278
+ if (forward.length) {
279
+ out.push("", ...forward);
280
+ }
281
+ const fnText = functions.map((l) => l.replace(/\u0000DECL(\d+)\u0000/g, (_m, id: string) => (this.forwardFuncs.has(Number(id)) ? "function" : "local function")));
282
+ if (fnText.length) {
283
+ out.push("", ...fnText);
284
+ }
285
+ if (body.length) {
286
+ out.push("", ...body);
287
+ }
288
+ if (footer.length) {
289
+ out.push("", ...footer);
290
+ }
291
+ const text = collapseBlankLines(out).join("\n").replace(/\s+$/, "");
292
+ const mappings: import("../../clpp/contract.js").SourceMapLine[] = [];
293
+ const clean = text.split("\n").map((line, index) => line.replace(/\u0000SRC(\d+)\u0000/g, (_m, offset: string) => {
294
+ const pos = this.options.sourcePosition!(Number(offset));
295
+ mappings.push({luauLine: index+1, clppLine: pos.line, clppColumn: pos.column, file: this.options.sourceLabel});
296
+ return "";
297
+ })).join("\n");
298
+ this.options.onSourceMap?.(mappings);
299
+ return clean + "\n";
300
+ }
301
+
302
+ private emitRequires(): void {
303
+ for (const link of this.file.links) {
304
+ const target = link.target;
305
+ if (target.kind === "roblox" || target.kind === "error") {
306
+ continue;
307
+ }
308
+ if (target.kind === "std") {
309
+ if (target.module === "axiom" && this.axiomRuntime) {
310
+ this.line(`local ${this.linkName(link)} = require(${this.options.libsRoot(this.service.bind(this))}:WaitForChild("Axiom"))`);
311
+ }
312
+ continue;
313
+ }
314
+ const used = this.usedLinks.has(link);
315
+ if (!used) {
316
+ continue;
317
+ }
318
+ const expr = this.options.requireTarget(link, (name) => this.service(name));
319
+ if (expr) {
320
+ this.line(`local ${this.linkName(link)} = require(${expr})`);
321
+ }
322
+ }
323
+ }
324
+
325
+ // ---------- declarations ----------
326
+
327
+ private orderedStructs(): StructInfo[] {
328
+ const own = this.file.structs;
329
+ const out: StructInfo[] = [];
330
+ const seen = new Set<string>();
331
+ const visit = (s: StructInfo) => {
332
+ if (seen.has(s.id)) {
333
+ return;
334
+ }
335
+ seen.add(s.id);
336
+ for (const b of s.bases) {
337
+ const base = own.find((x) => x.id === b);
338
+ if (base) {
339
+ visit(base);
340
+ }
341
+ }
342
+ out.push(s);
343
+ };
344
+ own.forEach(visit);
345
+ return out;
346
+ }
347
+
348
+ private exportKw(): string {
349
+ return this.isModule ? "export type" : "type";
350
+ }
351
+
352
+ private emitTypes(): void {
353
+ for (const e of this.file.enums) {
354
+ if (!e.hasPayload) {
355
+ continue;
356
+ }
357
+ const payloads = [...new Set(e.cases.map((k) => (k.payload ? this.typeOf(k.payload) ?? "any" : null)).filter(Boolean) as string[])];
358
+ const optional = e.cases.some((k) => !k.payload);
359
+ const value = payloads.length ? (optional ? `(${payloads.join(" | ")})?` : payloads.join(" | ")) : "nil";
360
+ this.line(`${this.exportKw()} ${e.luauName} = { tag: number, value: ${value} }`);
361
+ }
362
+ for (const s of this.orderedStructs()) {
363
+ const generics = s.typeParams.length ? `<${s.typeParams.join(", ")}>` : "";
364
+ this.line(`${this.exportKw()} ${s.luauName}${generics} = {`);
365
+ this.block(() => {
366
+ for (const f of s.fields) {
367
+ this.line(`${f.name}: ${this.typeOf(f.type) ?? "any"},`);
368
+ }
369
+ for (const m of s.methods.values()) {
370
+ if (m.isStatic) {
371
+ continue;
372
+ }
373
+ this.line(`${m.name}: ${this.methodType(m)},`);
374
+ }
375
+ });
376
+ this.line("}");
377
+ }
378
+ }
379
+
380
+ private ownerType(m: MethodInfo): string {
381
+ for (const s of this.file.structRegistry.values()) {
382
+ if (s.name === m.owner && s.methods.get(m.name) === m) {
383
+ const ref = this.struct(s.id) ?? s.luauName;
384
+ return s.typeParams.length ? `${ref}<${s.typeParams.join(", ")}>` : ref;
385
+ }
386
+ }
387
+ return "any";
388
+ }
389
+
390
+ private methodType(m: MethodInfo): string {
391
+ const params = [`self: ${this.ownerType(m)}`];
392
+ for (const p of m.type.params) {
393
+ const t = this.typeOf(p.type) ?? "any";
394
+ params.push(p.optional && !t.endsWith("?") ? optionalType(t) : t);
395
+ }
396
+ if (m.type.variadic) {
397
+ params.push(`...${this.typeOf(m.type.variadic) ?? "any"}`);
398
+ }
399
+ const ret = this.returnType(m.type.ret, m.decl.isAsync);
400
+ return `(${params.join(", ")}) -> ${ret ?? "()"}`;
401
+ }
402
+
403
+ private returnType(ret: Type, isAsync: boolean): string | null {
404
+ let r = prune(ret);
405
+ if (isAsync && r.k === "task") {
406
+ r = prune(r.result);
407
+ }
408
+ if (r.k === "void") {
409
+ return "()";
410
+ }
411
+ if (r.k === "infer" || r.k === "unknown") {
412
+ return null;
413
+ }
414
+ return this.typeOf(r);
415
+ }
416
+
417
+ private emitClassTables(): void {
418
+ for (const e of this.file.enums) {
419
+ if (this.isModule || this.enumTablesUsed.has(e.id)) {
420
+ const cases = e.cases.map((k, i) => `${k.name} = ${i}`).join(", ");
421
+ this.line(`local ${e.luauName} = table.freeze({ ${cases} })`);
422
+ }
423
+ }
424
+ for (const s of this.orderedStructs()) {
425
+ this.line(`local ${s.luauName} = {}`);
426
+ this.line(`${s.luauName}.__index = ${s.luauName}`);
427
+ this.line(`${s.luauName}.__fields = table.freeze({ ${s.fields.map((f) => luauString(f.name)).join(", ")} })`);
428
+ }
429
+ for (const stmt of this.program.body) {
430
+ if (stmt.kind === "Decl" && stmt.decl.kind === "Namespace") {
431
+ this.line(`local ${sanitizeIdentifier(stmt.decl.name)} = {}`);
432
+ }
433
+ }
434
+ }
435
+
436
+ private emitFunctions(): void {
437
+ let first = true;
438
+ const sep = () => {
439
+ if (!first) {
440
+ this.line("");
441
+ }
442
+ first = false;
443
+ };
444
+ for (const s of this.orderedStructs()) {
445
+ if (!s.isAbstract) {
446
+ sep();
447
+ this.emitConstructor(s);
448
+ }
449
+ for (const m of s.decl.methods) {
450
+ if (!m.body) {
451
+ continue;
452
+ }
453
+ sep();
454
+ this.emitFunction(m, `${s.luauName}.${m.name}`, s);
455
+ }
456
+ for (const [name, m] of s.methods) {
457
+ if (m.owner !== s.name) {
458
+ const base = [...this.file.structRegistry.values()].find((x) => x.name === m.owner && x.methods.get(name) === m);
459
+ if (base && !m.isAbstract) {
460
+ this.line(`${s.luauName}.${name} = ${this.structRef(base.id)}.${name}`);
461
+ }
462
+ }
463
+ }
464
+ }
465
+ for (const stmt of this.program.body) {
466
+ if (stmt.kind !== "Decl") {
467
+ continue;
468
+ }
469
+ const decl = stmt.decl;
470
+ if (decl.kind === "Func" && decl.body) {
471
+ sep();
472
+ const sym = this.file.funcSyms.get(decl);
473
+ this.emitFunction(decl, sym ? this.nameOf(sym) : sanitizeIdentifier(decl.name), null, sym ?? null);
474
+ } else if (decl.kind === "Namespace") {
475
+ for (const f of decl.funcs) {
476
+ if (f.body) {
477
+ sep();
478
+ this.emitFunction(f, `${sanitizeIdentifier(decl.name)}.${f.name}`, null);
479
+ }
480
+ }
481
+ }
482
+ }
483
+ }
484
+
485
+ private emitConstructor(s: StructInfo): void {
486
+ const generics = s.typeParams.length ? `<${s.typeParams.join(", ")}>` : "";
487
+ const selfType = `${s.luauName}${generics}`;
488
+ const params = s.fields.map((f) => {
489
+ const t = this.typeOf(f.type) ?? "any";
490
+ return `${safeName(f.name)}: ${f.defaultValue ? optionalType(t) : t}`;
491
+ });
492
+ this.line(`function ${s.luauName}.new${generics}(${params.join(", ")}): ${selfType}`);
493
+ this.block(() => {
494
+ const fields = s.fields.map((f) => {
495
+ const local = safeName(f.name);
496
+ if (f.defaultValue) {
497
+ const def = this.expr(f.defaultValue).code;
498
+ return `${fieldKey(f.name)} = if ${local} == nil then ${def} else ${local}`;
499
+ }
500
+ return `${fieldKey(f.name)} = ${local}`;
501
+ });
502
+ if (fields.length <= 3) {
503
+ this.line(`local self = { ${fields.join(", ")} } :: ${selfType}`);
504
+ } else {
505
+ this.line("local self = {");
506
+ this.block(() => fields.forEach((f) => this.line(`${f},`)));
507
+ this.line(`} :: ${selfType}`);
508
+ }
509
+ // __index supplies the methods described by the public struct type.
510
+ this.line(`return setmetatable(self, ${s.luauName})`);
511
+ });
512
+ this.line("end");
513
+ }
514
+
515
+ private paramList(params: Param[], fnType: Extract<Type, { k: "func" }> | null): { list: string[]; prologue: Array<() => void> } {
516
+ const list: string[] = [];
517
+ const prologue: Array<() => void> = [];
518
+ for (const p of params) {
519
+ const sym = p.sym as Sym | undefined;
520
+ const name = sym ? this.nameOf(sym) : sanitizeIdentifier(p.name);
521
+ if (p.variadic) {
522
+ const elem = p.ty && prune(p.ty).k === "list" ? this.typeOf((prune(p.ty) as Extract<Type, { k: "list" }>).elem) : null;
523
+ list.push(elem ? `...: ${elem}` : "...");
524
+ prologue.push(() => this.line(`local ${name} = { ... }`));
525
+ continue;
526
+ }
527
+ const t = this.typeOf(p.ty);
528
+ list.push(t ? `${name}: ${p.defaultValue && !t.endsWith("?") ? optionalType(t) : t}` : name);
529
+ if (p.defaultValue) {
530
+ const dv = p.defaultValue;
531
+ prologue.push(() => {
532
+ const value = this.expr(dv).code;
533
+ this.line(`if ${name} == nil then`);
534
+ this.block(() => this.line(`${name} = ${value}`));
535
+ this.line("end");
536
+ });
537
+ }
538
+ }
539
+ void fnType;
540
+ return { list, prologue };
541
+ }
542
+
543
+ private emitFunction(decl: FuncDecl, name: string, owner: StructInfo | null, sym: Sym | null = null): void {
544
+ const fnType = sym ? (sym.type as Extract<Type, { k: "func" }>) : (this.file.funcSyms.get(decl)?.type as Extract<Type, { k: "func" }> | undefined) ?? null;
545
+ const isMethod = owner !== null && !decl.isStatic;
546
+ const generics = [...(isMethod || decl.isStatic ? owner?.typeParams ?? [] : []), ...decl.typeParams];
547
+ const { list, prologue } = this.paramList(decl.params, fnType);
548
+ if (isMethod && owner) {
549
+ const ownerGenerics = owner.typeParams.length ? `<${owner.typeParams.join(", ")}>` : "";
550
+ list.unshift(`self: ${owner.luauName}${ownerGenerics}`);
551
+ }
552
+ const ret = fnType ? this.returnType(fnType.ret, decl.isAsync) : null;
553
+ const retText = ret && ret !== "()" ? `: ${ret}` : "";
554
+ if (decl.attributes.some((a) => a.name === "native")) {
555
+ this.line("@native");
556
+ }
557
+ if (decl.doc) {
558
+ for (const docLine of decl.doc.split(/\r?\n/)) {
559
+ this.line(`--- ${docLine.trim()}`);
560
+ }
561
+ }
562
+ const header = owner || name.includes(".") ? `function ${name}` : `\u0000DECL${sym?.id ?? 0}\u0000 ${name}`;
563
+ if (sym) {
564
+ if (this.referencedFuncs.has(sym.id)) {
565
+ this.forwardFuncs.add(sym.id);
566
+ }
567
+ this.definedFuncs.add(sym.id);
568
+ }
569
+ this.line(`${header}${generics.length ? `<${generics.join(", ")}>` : ""}(${list.join(", ")})${retText}`);
570
+ this.block(() => {
571
+ this.fnDepth++;
572
+ const saved = this.loops.splice(0);
573
+ for (const p of prologue) {
574
+ p();
575
+ }
576
+ this.stmts(decl.body || []);
577
+ this.loops.push(...saved);
578
+ this.fnDepth--;
579
+ });
580
+ this.line("end");
581
+ }
582
+
583
+ private emitReturn(): void {
584
+ if (!this.isModule) {
585
+ return;
586
+ }
587
+ const entries: string[] = [];
588
+ const exp = this.file.exports;
589
+ for (const [name] of exp.funcs) {
590
+ const sym = this.file.topLevel.find((s) => s.name === name && s.kind === "func");
591
+ if (sym) {
592
+ entries.push(`${fieldKey(name)} = ${this.nameOf(sym)}`);
593
+ }
594
+ }
595
+ for (const [name, k] of exp.consts) {
596
+ entries.push(`${fieldKey(name)} = ${literal(k.value)}`);
597
+ }
598
+ for (const s of this.file.structs) {
599
+ entries.push(`${fieldKey(s.luauName)} = ${s.luauName}`);
600
+ }
601
+ for (const e of this.file.enums) {
602
+ entries.push(`${fieldKey(e.luauName)} = ${e.luauName}`);
603
+ }
604
+ for (const [name] of exp.namespaces) {
605
+ entries.push(`${fieldKey(name)} = ${sanitizeIdentifier(name)}`);
606
+ }
607
+ if (entries.length === 0) {
608
+ this.line("return {}");
609
+ return;
610
+ }
611
+ this.line("return table.freeze({");
612
+ this.block(() => entries.forEach((e) => this.line(`${e},`)));
613
+ this.line("})");
614
+ }
615
+
616
+ // ---------- signals ----------
617
+
618
+ /** CL++ signals connected only at the top level resolve at compile time: firing calls the handlers directly. */
619
+ private findStaticSignals(): void {
620
+ const dynamic = new Set<number>();
621
+ const all = new Set<number>();
622
+ const scan = (stmts: Stmt[], depth: number) => {
623
+ for (const s of stmts) {
624
+ walkStmt(s, (inner, d) => {
625
+ if (inner.kind === "Connect" && inner.target === "cl" && inner.signal.kind === "Name" && inner.signal.res?.kind === "signal") {
626
+ const id = (inner.signal.res.sym as Sym).id;
627
+ all.add(id);
628
+ if (d > 0 || inner.handler.kind === "Function") {
629
+ dynamic.add(id);
630
+ }
631
+ }
632
+ }, depth);
633
+ }
634
+ };
635
+ scan(this.program.body, 0);
636
+ for (const id of all) {
637
+ if (!dynamic.has(id)) {
638
+ this.staticSignals.add(id);
639
+ }
640
+ }
641
+ for (const s of this.file.topLevel) {
642
+ if (s.kind === "signal" && !all.has(s.id)) {
643
+ this.staticSignals.add(s.id);
644
+ }
645
+ }
646
+ }
647
+
648
+ // ---------- statements ----------
649
+
650
+ private stmts(list: Stmt[]): void {
651
+ for (const s of list) {
652
+ this.stmt(s, false);
653
+ }
654
+ }
655
+
656
+ private stmt(s: Stmt, topLevel: boolean): void {
657
+ const saved = this.sourceOffset;
658
+ this.sourceOffset = s.span.start;
659
+ try {this.emitStmt(s, topLevel);} finally {this.sourceOffset = saved;}
660
+ }
661
+
662
+ private emitStmt(s: Stmt, topLevel: boolean): void {
663
+ switch (s.kind) {
664
+ case "Var":
665
+ this.varStmt(s, topLevel);
666
+ return;
667
+ case "Assign":
668
+ this.assign(s);
669
+ return;
670
+ case "Expr":
671
+ this.exprStmt(s.expr);
672
+ return;
673
+ case "If":
674
+ this.ifStmt(s);
675
+ return;
676
+ case "While": {
677
+ const cond = this.cond(s.cond);
678
+ this.line(`while ${cond} do`);
679
+ this.loopBody(s.body, {});
680
+ this.line("end");
681
+ return;
682
+ }
683
+ case "For":
684
+ this.forStmt(s);
685
+ return;
686
+ case "ForIn":
687
+ this.forIn(s);
688
+ return;
689
+ case "Break":
690
+ this.breakStmt();
691
+ return;
692
+ case "Continue":
693
+ this.continueStmt();
694
+ return;
695
+ case "Return":
696
+ this.returnStmt(s);
697
+ return;
698
+ case "Switch":
699
+ this.switchStmt(s);
700
+ return;
701
+ case "Match":
702
+ this.matchStmt(s);
703
+ return;
704
+ case "Try":
705
+ this.tryStmt(s);
706
+ return;
707
+ case "Output":
708
+ this.output(s);
709
+ return;
710
+ case "Connect":
711
+ this.connect(s);
712
+ return;
713
+ case "Signal": {
714
+ const sym = s.name.sym as Sym | undefined;
715
+ if (sym && !this.staticSignals.has(sym.id)) {
716
+ this.line(`local ${this.nameOf(sym)}: { (...any) -> () } = {}`);
717
+ }
718
+ return;
719
+ }
720
+ case "Using": {
721
+ const sym = s.sym as Sym | undefined;
722
+ if (sym && sym.constValue === undefined && s.target) {
723
+ this.line(`local ${this.nameOf(sym)} = ${this.valueOf(s.target).code}`);
724
+ }
725
+ return;
726
+ }
727
+ case "Block":
728
+ this.line("do");
729
+ this.block(() => this.stmts(s.body));
730
+ this.line("end");
731
+ return;
732
+ case "Decl":
733
+ if (s.decl.kind === "Func" && this.fnDepth > 0) {
734
+ const sym = this.file.funcSyms.get(s.decl);
735
+ const name = sym ? this.nameOf(sym) : sanitizeIdentifier(s.decl.name);
736
+ const lines = this.capture(() => this.emitFunction(s.decl as FuncDecl, name, null, null));
737
+ if (lines.length) {
738
+ lines[0] = lines[0].replace(`function ${name}`, `local function ${name}`).replace(/\u0000DECL\d+\u0000 /, "local function ");
739
+ }
740
+ this.lines.push(...lines);
741
+ }
742
+ return;
743
+ default:
744
+ return;
745
+ }
746
+ }
747
+
748
+ private varStmt(s: Extract<Stmt, { kind: "Var" }>, topLevel: boolean): void {
749
+ if (s.unpack) {
750
+ const names = s.names.map((n) => (n.sym ? this.nameOf(n.sym as Sym) : sanitizeIdentifier(n.name)));
751
+ const src = prune(s.init?.ty ?? T.any);
752
+ const init = s.init ? this.expr(s.init).code : "nil";
753
+ if (src.k === "tuple") {
754
+ this.line(`local ${names.join(", ")} = ${init}`);
755
+ } else {
756
+ this.line(`local ${names.join(", ")} = table.unpack(${init})`);
757
+ }
758
+ return;
759
+ }
760
+ const n = s.names[0];
761
+ const sym = n.sym as Sym | undefined;
762
+ if (!sym) {
763
+ return;
764
+ }
765
+ if (sym.kind === "const" && sym.constValue !== undefined) {
766
+ return; // inlined at every use
767
+ }
768
+ const name = this.nameOf(sym);
769
+ const declared = topLevel && sym.captured ? "" : "local ";
770
+ const annotation = declared ? this.annotationFor(s, sym) : "";
771
+ if (s.init && s.init.res?.kind === "call" && s.init.res.call.form === "instance") {
772
+ this.instanceInto(s.init, `${declared}${name}${annotation}`, name);
773
+ } else if (s.init && s.init.res?.kind === "call" && s.init.res.call.form === "datatype" && s.init.res.call.props.length) {
774
+ this.datatypeInto(s.init, `${declared}${name}${annotation}`, name);
775
+ } else {
776
+ const value = s.init ? this.valueOf(s.init).code : "nil";
777
+ this.line(`${declared}${name}${annotation} = ${value}`);
778
+ }
779
+ if (sym.observable) {
780
+ const t = this.typeOf(sym.type) ?? "any";
781
+ this.line(`local ${name}_changed: { (${t}) -> () } = {}`);
782
+ }
783
+ }
784
+
785
+ /** Only annotate when Luau could not infer the same type from the initializer. */
786
+ private annotationFor(s: Extract<Stmt, { kind: "Var" }>, sym: Sym): string {
787
+ const t = prune(sym.type);
788
+ const init = s.init;
789
+ const needs =
790
+ !init ||
791
+ init.kind === "Null" ||
792
+ s.declaredType !== null ||
793
+ ((t.k === "list" || t.k === "dict") && init.kind === "Call" && init.res?.kind === "call" && init.res.call.form === "builtin") ||
794
+ (t.k === "option" && !isInferredByLuau(init)) ||
795
+ t.k === "union" ||
796
+ t.k === "lit";
797
+ if (!needs) {
798
+ return "";
799
+ }
800
+ const lt = this.typeOf(sym.type);
801
+ return lt && lt !== "any" ? `: ${lt}` : "";
802
+ }
803
+
804
+ private instanceInto(e: Expr, declaration: string, name: string): void {
805
+ const res = e.res as Extract<NonNullable<Expr["res"]>, { kind: "call" }>;
806
+ const call = res.call as Extract<typeof res.call, { form: "instance" }>;
807
+ this.line(`${declaration} = Instance.new(${luauString(call.cls)})`);
808
+ for (const p of call.props) {
809
+ this.line(`${name}.${p.name} = ${this.valueOf(p.value).code}`);
810
+ }
811
+ if (call.parent) {
812
+ this.line(`${name}.Parent = ${this.expr(call.parent).code}`);
813
+ }
814
+ }
815
+
816
+ private datatypeInto(e: Expr, declaration: string, name: string): void {
817
+ const res = e.res as Extract<NonNullable<Expr["res"]>, { kind: "call" }>;
818
+ const call = res.call as Extract<typeof res.call, { form: "datatype" }>;
819
+ this.line(`${declaration} = ${call.name}.new(${this.args(res.args, res.variadicArgs)})`);
820
+ for (const p of call.props) {
821
+ this.line(`${name}.${p.name} = ${this.valueOf(p.value).code}`);
822
+ }
823
+ }
824
+
825
+ private assign(s: Extract<Stmt, { kind: "Assign" }>): void {
826
+ const target = s.target;
827
+ // Vector component: pos.y = 1 → pos = Vector3.new(pos.X, 1, pos.Z)
828
+ if (target.kind === "Member" && target.res?.kind === "vectorComponent") {
829
+ this.assignVectorComponent(target, s.op, s.value);
830
+ return;
831
+ }
832
+ // `x = f(x)`: the old x is dropped by this very statement, so passing it needs no copy.
833
+ if (s.op === "=" && target.kind === "Name" && target.res?.kind === "local" && s.value.kind === "Call" && s.value.res?.kind === "call") {
834
+ const id = (target.res.sym as Sym).id;
835
+ for (const arg of s.value.res.args) {
836
+ if (arg && arg.copy && arg.kind === "Name" && arg.res?.kind === "local" && (arg.res.sym as Sym).id === id) {
837
+ arg.copy = false;
838
+ }
839
+ }
840
+ }
841
+ const targetCode = this.place(target);
842
+ const t = prune(target.ty ?? T.any);
843
+ let value: string;
844
+ if (s.op === "=") {
845
+ value = this.valueOf(s.value).code;
846
+ this.line(`${targetCode} = ${value}`);
847
+ } else {
848
+ const op = s.op[0];
849
+ const rhs = this.expr(s.value);
850
+ const intDiv = op === "/" && isIntPair(t, prune(s.value.ty ?? T.any));
851
+ if (intDiv) {
852
+ this.line(`${targetCode} = ${this.use("__cl_idiv")}(${targetCode}, ${rhs.code})`);
853
+ } else if (op === "%") {
854
+ this.line(`${targetCode} = math.fmod(${targetCode}, ${rhs.code})`);
855
+ } else {
856
+ this.line(`${targetCode} ${s.op} ${rhs.code}`);
857
+ }
858
+ }
859
+ this.notifyObservable(target);
860
+ }
861
+
862
+ private notifyObservable(target: Expr): void {
863
+ if (target.kind === "Name" && target.res?.kind === "local" && (target.res.sym as Sym).observable) {
864
+ const name = this.nameOf(target.res.sym as Sym);
865
+ this.line(`for _, onChange in ${name}_changed do`);
866
+ this.block(() => this.line(`onChange(${name})`));
867
+ this.line("end");
868
+ }
869
+ }
870
+
871
+ private assignVectorComponent(target: Extract<Expr, { kind: "Member" }>, op: string, valueExpr: Expr): void {
872
+ const vecType = prune(target.object.ty ?? T.any);
873
+ const dt = vecType.k === "datatype" ? vecType.name : "Vector3";
874
+ const comps = dt.startsWith("Vector2") ? ["X", "Y"] : ["X", "Y", "Z"];
875
+ const objectPlace = this.place(target.object);
876
+ const simple = /^[A-Za-z_][\w.]*$/.test(objectPlace);
877
+ let src = objectPlace;
878
+ if (!simple) {
879
+ src = this.temp("v");
880
+ this.line(`local ${src} = ${objectPlace}`);
881
+ }
882
+ const comp = (target.res as { component: string }).component.toUpperCase();
883
+ const rhs = this.expr(valueExpr).code;
884
+ const newValue = op === "=" ? rhs : `${src}.${comp} ${op[0]} ${wrapCode(rhs, P.MUL)}`;
885
+ const args = comps.map((c) => (c === comp ? newValue : `${src}.${c}`));
886
+ this.line(`${objectPlace} = ${dt}.new(${args.join(", ")})`);
887
+ }
888
+
889
+ /** Assignable Luau place for a target expression. */
890
+ private place(e: Expr): string {
891
+ switch (e.kind) {
892
+ case "Name":
893
+ return e.res?.kind === "local" ? this.nameOf(e.res.sym as Sym) : sanitizeIdentifier(e.name);
894
+ case "SelfField":
895
+ return e.res?.kind === "sharedField" ? this.shared(e.name) : `${this.selfName}.${e.name}`;
896
+ case "Member": {
897
+ const obj = this.expr(e.object);
898
+ if (e.res?.kind === "vectorComponent") {
899
+ return `${wrap(obj, P.ATOM)}.${e.res.component}`;
900
+ }
901
+ return `${wrap(obj, P.ATOM)}${memberAccess(e.name)}`;
902
+ }
903
+ case "Index":
904
+ return this.index(e).code;
905
+ default:
906
+ return this.expr(e).code;
907
+ }
908
+ }
909
+
910
+ private shared(name: string): string {
911
+ this.sharedTable = true;
912
+ return `__clShared${memberAccess(name)}`;
913
+ }
914
+
915
+ private exprStmt(e: Expr): void {
916
+ if (e.kind === "Update") {
917
+ this.line(`${this.place(e.target)} ${e.delta > 0 ? "+=" : "-="} 1`);
918
+ this.notifyObservable(e.target);
919
+ return;
920
+ }
921
+ if (e.kind === "Call" && e.res?.kind === "call") {
922
+ const call = e.res.call;
923
+ if (call.form === "signalFire") {
924
+ this.fireSignal(e, call.sym as Sym, e.res.args);
925
+ return;
926
+ }
927
+ if (call.form === "instance") {
928
+ const name = this.temp("i");
929
+ this.instanceInto(e, `local ${name}`, name);
930
+ return;
931
+ }
932
+ }
933
+ const code = this.expr(e).code;
934
+ if (isCallCode(code)) {
935
+ this.line(code);
936
+ } else {
937
+ this.line(`local _ = ${code}`);
938
+ }
939
+ }
940
+
941
+ private fireSignal(e: Expr, sym: Sym, args: Array<Expr | null>): void {
942
+ const values = args.map((a) => (a ? this.valueOf(a).code : "nil"));
943
+ if (this.staticSignals.has(sym.id)) {
944
+ const handlers = sym.handlers ?? [];
945
+ const needTemps = handlers.length > 1 && values.some((v) => !/^[\w.]+$|^".*"$|^-?\d/.test(v));
946
+ const passed = needTemps
947
+ ? values.map((v) => {
948
+ const t = this.temp("a");
949
+ this.line(`local ${t} = ${v}`);
950
+ return t;
951
+ })
952
+ : values;
953
+ for (const h of handlers) {
954
+ this.line(`${wrap(this.expr(h), P.ATOM)}(${passed.join(", ")})`);
955
+ }
956
+ return;
957
+ }
958
+ this.line(`for _, handler in ${this.nameOf(sym)} do`);
959
+ this.block(() => this.line(`handler(${values.join(", ")})`));
960
+ this.line("end");
961
+ }
962
+
963
+ private connect(s: Extract<Stmt, { kind: "Connect" }>): void {
964
+ if (s.target === "cl") {
965
+ const sym = s.signal.kind === "Name" && s.signal.res?.kind === "signal" ? (s.signal.res.sym as Sym) : null;
966
+ if (sym && this.staticSignals.has(sym.id)) {
967
+ return;
968
+ }
969
+ this.line(`table.insert(${this.expr(s.signal).code}, ${this.expr(s.handler).code})`);
970
+ return;
971
+ }
972
+ this.line(`${wrap(this.expr(s.signal), P.ATOM)}:Connect(${this.expr(s.handler).code})`);
973
+ }
974
+
975
+ private output(s: Extract<Stmt, { kind: "Output" }>): void {
976
+ const value = this.stringy(s.value, s.channel === "report");
977
+ switch (s.channel) {
978
+ case "post":
979
+ case "cout":
980
+ this.line(`print(${value})`);
981
+ return;
982
+ case "warn":
983
+ this.line(`warn(${value})`);
984
+ return;
985
+ case "report":
986
+ this.line(`error(${value}, 0)`);
987
+ return;
988
+ }
989
+ }
990
+
991
+ /** Value printed the way CL++ prints it. */
992
+ private stringy(e: Expr, forceString = false): string {
993
+ const t = prune(e.ty ?? T.any);
994
+ const c = this.expr(e);
995
+ if (t.k === "string" || t.k === "lit" || ((t.k === "int" || t.k === "float" || t.k === "bool") && !forceString)) {
996
+ return c.code;
997
+ }
998
+ if (t.k === "int" || t.k === "float" || t.k === "bool" || t.k === "instance" || t.k === "rbxEnumItem" || (t.k === "datatype" && t.name !== "Vector3" && t.name !== "Vector2")) {
999
+ return `tostring(${c.code})`;
1000
+ }
1001
+ return `${this.use("__cl_str")}(${c.code})`;
1002
+ }
1003
+
1004
+ private ifStmt(s: Extract<Stmt, { kind: "If" }>): void {
1005
+ if (s.letName) {
1006
+ const sym = s.letName.sym as Sym | undefined;
1007
+ const name = sym ? this.nameOf(sym) : sanitizeIdentifier(s.letName.name);
1008
+ this.line("do");
1009
+ this.block(() => {
1010
+ this.line(`local ${name} = ${this.valueOf(s.cond).code}`);
1011
+ this.line(`if ${this.truthy({ code: name, prec: P.ATOM }, prune(s.cond.ty ?? T.any))} then`);
1012
+ this.block(() => this.stmts(s.then));
1013
+ if (s.else) {
1014
+ this.line("else");
1015
+ this.block(() => this.stmts(s.else!));
1016
+ }
1017
+ this.line("end");
1018
+ });
1019
+ this.line("end");
1020
+ return;
1021
+ }
1022
+ this.line(`if ${this.cond(s.cond)} then`);
1023
+ this.block(() => this.stmts(s.then));
1024
+ let rest = s.else;
1025
+ while (rest) {
1026
+ if (rest.length === 1 && rest[0].kind === "If" && !rest[0].letName && !rest[0].attributes) {
1027
+ const next = rest[0];
1028
+ const hoisted: string[] = [];
1029
+ let cond = "";
1030
+ hoisted.push(...this.capture(() => {
1031
+ cond = this.cond(next.cond);
1032
+ }));
1033
+ if (hoisted.length) {
1034
+ this.line("else");
1035
+ this.block(() => {
1036
+ this.lines.push(...hoisted.map((l) => `\t${l}`));
1037
+ this.line(`if ${cond} then`);
1038
+ this.block(() => this.stmts(next.then));
1039
+ if (next.else) {
1040
+ this.line("else");
1041
+ this.block(() => this.stmts(next.else!));
1042
+ }
1043
+ this.line("end");
1044
+ });
1045
+ break;
1046
+ }
1047
+ this.line(`elseif ${cond} then`);
1048
+ this.block(() => this.stmts(next.then));
1049
+ rest = next.else;
1050
+ continue;
1051
+ }
1052
+ this.line("else");
1053
+ this.block(() => this.stmts(rest!));
1054
+ break;
1055
+ }
1056
+ this.line("end");
1057
+ }
1058
+
1059
+ private loopBody(body: Stmt[], frame: Omit<LoopFrame, "kind">): void {
1060
+ this.loops.push({ kind: "loop", ...frame });
1061
+ this.block(() => this.stmts(body));
1062
+ this.loops.pop();
1063
+ }
1064
+
1065
+ private forStmt(s: Extract<Stmt, { kind: "For" }>): void {
1066
+ const numeric = this.numericFor(s);
1067
+ if (numeric) {
1068
+ this.line(`for ${numeric.name} = ${numeric.from}, ${numeric.to}${numeric.step !== "1" ? `, ${numeric.step}` : ""} do`);
1069
+ this.loopBody(s.body, {});
1070
+ this.line("end");
1071
+ return;
1072
+ }
1073
+ this.line("do");
1074
+ this.block(() => {
1075
+ if (s.init) {
1076
+ this.stmt(s.init, false);
1077
+ }
1078
+ const cond = s.cond ? this.cond(s.cond) : "true";
1079
+ this.line(`while ${cond} do`);
1080
+ const step = s.step;
1081
+ this.loopBody(s.body, { step: step ? () => this.stmt(step, false) : undefined });
1082
+ if (step) {
1083
+ this.block(() => this.stmt(step, false));
1084
+ }
1085
+ this.line("end");
1086
+ });
1087
+ this.line("end");
1088
+ }
1089
+
1090
+ /** `for (let mut i = a; i < b; i++)` with an invariant bound → `for i = a, b - 1 do`. */
1091
+ private numericFor(s: Extract<Stmt, { kind: "For" }>): { name: string; from: string; to: string; step: string } | null {
1092
+ const init = s.init;
1093
+ if (!init || init.kind !== "Var" || init.names.length !== 1 || !init.init || !s.cond || !s.step) {
1094
+ return null;
1095
+ }
1096
+ const sym = init.names[0].sym as Sym | undefined;
1097
+ if (!sym || !isIntLikeType(sym.type) || sym.captured) {
1098
+ return null;
1099
+ }
1100
+ const cond = s.cond;
1101
+ if (cond.kind !== "Binary" || !["<", "<=", ">", ">="].includes(cond.op) || cond.left.kind !== "Name" || cond.left.res?.kind !== "local" || (cond.left.res.sym as Sym).id !== sym.id) {
1102
+ return null;
1103
+ }
1104
+ if (!isInvariant(cond.right, s.body)) {
1105
+ return null;
1106
+ }
1107
+ let delta: number | null = null;
1108
+ const step = s.step;
1109
+ if (step.kind === "Expr" && step.expr.kind === "Update" && isSym(step.expr.target, sym)) {
1110
+ delta = step.expr.delta;
1111
+ } else if (step.kind === "Assign" && isSym(step.target, sym) && step.value.kind === "Number" && !step.value.isFloat && (step.op === "+=" || step.op === "-=")) {
1112
+ delta = step.op === "+=" ? step.value.value : -step.value.value;
1113
+ } else if (step.kind === "Assign" && step.op === "=" && isSym(step.target, sym) && step.value.kind === "Binary" && (step.value.op === "+" || step.value.op === "-") && isSym(step.value.left, sym) && step.value.right.kind === "Number" && !step.value.right.isFloat) {
1114
+ delta = step.value.op === "+" ? step.value.right.value : -step.value.right.value;
1115
+ }
1116
+ if (!delta || assigns(s.body, sym)) {
1117
+ return null;
1118
+ }
1119
+ const up = cond.op === "<" || cond.op === "<=";
1120
+ if ((up && delta < 0) || (!up && delta > 0)) {
1121
+ return null;
1122
+ }
1123
+ const bound = this.expr(cond.right);
1124
+ let to: string;
1125
+ if (cond.op === "<=" || cond.op === ">=") {
1126
+ to = bound.code;
1127
+ } else if (cond.right.kind === "Number") {
1128
+ to = luauNumber(cond.right.value + (up ? -1 : 1));
1129
+ } else {
1130
+ to = `${wrap(bound, P.ADD)} ${up ? "-" : "+"} 1`;
1131
+ }
1132
+ return { name: this.nameOf(sym), from: this.expr(init.init).code, to, step: String(delta) };
1133
+ }
1134
+
1135
+ private forIn(s: Extract<Stmt, { kind: "ForIn" }>): void {
1136
+ const sym = s.name.sym as Sym | undefined;
1137
+ const name = sym ? this.nameOf(sym) : sanitizeIdentifier(s.name.name);
1138
+ const iterable = this.expr(s.iterable);
1139
+ switch (s.iterKind) {
1140
+ case "range": {
1141
+ const to = s.iterable.kind === "Number" ? luauNumber(s.iterable.value - 1) : `${wrap(iterable, P.ADD)} - 1`;
1142
+ this.line(`for ${name} = 0, ${to} do`);
1143
+ this.loopBody(s.body, {});
1144
+ this.line("end");
1145
+ return;
1146
+ }
1147
+ case "string": {
1148
+ const cp = this.temp("cp");
1149
+ this.line(`for _, ${cp} in utf8.codes(${iterable.code}) do`);
1150
+ this.block(() => this.line(`local ${name} = utf8.char(${cp})`));
1151
+ this.loopBody(s.body, {});
1152
+ this.line("end");
1153
+ return;
1154
+ }
1155
+ case "dict":
1156
+ this.line(`for ${name} in ${iterable.code} do`);
1157
+ this.loopBody(s.body, {});
1158
+ this.line("end");
1159
+ return;
1160
+ default: {
1161
+ if (s.downcast && s.declaredType) {
1162
+ const raw = this.temp("item");
1163
+ this.line(`for _, ${raw} in ${iterable.code} do`);
1164
+ const t = this.typeOf(sym?.type);
1165
+ this.block(() => this.line(`local ${name} = ${raw}${t ? ` :: ${t}` : ""}`));
1166
+ } else {
1167
+ this.line(`for _, ${name} in ${iterable.code} do`);
1168
+ }
1169
+ this.loopBody(s.body, {});
1170
+ this.line("end");
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ private breakStmt(): void {
1176
+ for (let i = this.loops.length - 1; i >= 0; i--) {
1177
+ const f = this.loops[i];
1178
+ if (f.kind === "try") {
1179
+ f.usedBreak = true;
1180
+ this.line("return 2");
1181
+ return;
1182
+ }
1183
+ if (f.kind === "switch") {
1184
+ if (f.wrapped) {
1185
+ this.line("break");
1186
+ }
1187
+ return;
1188
+ }
1189
+ this.line("break");
1190
+ return;
1191
+ }
1192
+ this.line("break");
1193
+ }
1194
+
1195
+ private continueStmt(): void {
1196
+ for (let i = this.loops.length - 1; i >= 0; i--) {
1197
+ const f = this.loops[i];
1198
+ if (f.kind === "try") {
1199
+ f.usedContinue = true;
1200
+ this.line("return 3");
1201
+ return;
1202
+ }
1203
+ if (f.kind === "switch") {
1204
+ if (f.wrapped) {
1205
+ f.continueFlag ||= this.temp("continue");
1206
+ this.line(`${f.continueFlag} = true`);
1207
+ this.line("break");
1208
+ return;
1209
+ }
1210
+ continue;
1211
+ }
1212
+ if (f.step) {
1213
+ f.step();
1214
+ }
1215
+ this.line("continue");
1216
+ return;
1217
+ }
1218
+ this.line("continue");
1219
+ }
1220
+
1221
+ private returnStmt(s: Extract<Stmt, { kind: "Return" }>): void {
1222
+ const value = s.value ? this.valueOf(s.value).code : null;
1223
+ const tryFrame = [...this.loops].reverse().find((f) => f.kind === "try");
1224
+ if (tryFrame && this.fnDepth > 0) {
1225
+ tryFrame.usedReturn = true;
1226
+ this.line(value ? `return 1, ${value}` : "return 1");
1227
+ return;
1228
+ }
1229
+ this.line(value ? `return ${value}` : "return");
1230
+ }
1231
+
1232
+ private subject(e: Expr): string {
1233
+ if (e.kind === "Name" && e.res?.kind === "local") {
1234
+ return this.nameOf(e.res.sym as Sym);
1235
+ }
1236
+ const t = this.temp("m");
1237
+ this.line(`local ${t} = ${this.expr(e).code}`);
1238
+ return t;
1239
+ }
1240
+
1241
+ private switchStmt(s: Extract<Stmt, { kind: "Switch" }>): void {
1242
+ const needsWrap = s.cases.some((k) => hasInnerBreak(k.body));
1243
+ const frame: LoopFrame = { kind: "switch", wrapped: needsWrap };
1244
+ const repeatAt = this.lines.length;
1245
+ if (needsWrap) {
1246
+ this.line("repeat");
1247
+ this.indent++;
1248
+ }
1249
+ const subj = this.subject(s.subject);
1250
+ const subjType = prune(s.subject.ty ?? T.any);
1251
+ const cases = s.cases.filter((k) => k.test !== null);
1252
+ const def = s.cases.find((k) => k.test === null);
1253
+ this.loops.push(frame);
1254
+ const body = (k: SwitchCase) => this.block(() => this.stmts(trimTrailingBreak(k.body)));
1255
+ cases.forEach((k, i) => {
1256
+ const test = this.equality({ code: subj, prec: P.ATOM }, k.test!, subjType);
1257
+ this.line(`${i === 0 ? "if" : "elseif"} ${test} then`);
1258
+ body(k);
1259
+ });
1260
+ if (def) {
1261
+ if (cases.length) {
1262
+ this.line("else");
1263
+ body(def);
1264
+ } else {
1265
+ this.line("do");
1266
+ body(def);
1267
+ }
1268
+ }
1269
+ if (cases.length || def) {
1270
+ this.line("end");
1271
+ }
1272
+ this.loops.pop();
1273
+ if (needsWrap) {
1274
+ this.indent--;
1275
+ this.line("until true");
1276
+ if (frame.continueFlag) {
1277
+ this.lines.splice(repeatAt, 0, `${"\t".repeat(this.indent)}local ${frame.continueFlag} = false`);
1278
+ this.line(`if ${frame.continueFlag} then`);
1279
+ this.block(() => this.continueStmt());
1280
+ this.line("end");
1281
+ }
1282
+ }
1283
+ }
1284
+
1285
+ private equality(left: Code, right: Expr, t: Type): string {
1286
+ const r = this.expr(right);
1287
+ if (isValueTable(t)) {
1288
+ return `${this.use("__cl_eq")}(${left.code}, ${r.code})`;
1289
+ }
1290
+ return `${wrap(left, P.CONCAT)} == ${wrap(r, P.CONCAT)}`;
1291
+ }
1292
+
1293
+ private matchStmt(s: Extract<Stmt, { kind: "Match" }>): void {
1294
+ const subj = this.subject(s.subject);
1295
+ const subjType = prune(s.subject.ty ?? T.any);
1296
+ let first = true;
1297
+ let opened = false;
1298
+ for (let index = 0; index < s.arms.length; index++) {
1299
+ const arm = s.arms[index];
1300
+ const lastExhaustive = s.exhaustive === true && index === s.arms.length - 1 && !first && !arm.guard;
1301
+ const computed = this.armTest(arm, subj, subjType);
1302
+ const test = lastExhaustive ? "true" : computed.test;
1303
+ const bind = computed.bind;
1304
+ if (test === "true" && first) {
1305
+ // a lone wildcard
1306
+ this.line("do");
1307
+ opened = true;
1308
+ this.block(() => {
1309
+ bind();
1310
+ this.stmts(arm.body);
1311
+ });
1312
+ break;
1313
+ }
1314
+ if (test === "true") {
1315
+ this.line("else");
1316
+ this.block(() => {
1317
+ bind();
1318
+ this.stmts(arm.body);
1319
+ });
1320
+ break;
1321
+ }
1322
+ this.line(`${first ? "if" : "elseif"} ${test} then`);
1323
+ opened = true;
1324
+ first = false;
1325
+ this.block(() => {
1326
+ bind();
1327
+ this.stmts(arm.body);
1328
+ });
1329
+ }
1330
+ if (opened) {
1331
+ this.line("end");
1332
+ }
1333
+ }
1334
+
1335
+ private armTest(arm: MatchArm, subj: string, subjType: Type): { test: string; bind: () => void } {
1336
+ const pattern = arm.pattern;
1337
+ let test = "true";
1338
+ let bindName: string | null = null;
1339
+ let bindSource: string | null = null;
1340
+ const caseTest = (p: Extract<Pattern, { kind: "Case" }>) => {
1341
+ const res = p.res;
1342
+ if (res?.kind === "enumCase") {
1343
+ if (res.anyPayload) {
1344
+ test = `${subj}.tag == ${res.index}`;
1345
+ if (p.binding) {
1346
+ bindSource = `${subj}.value`;
1347
+ }
1348
+ } else {
1349
+ test = `${subj} == ${res.index}`;
1350
+ }
1351
+ } else if (res?.kind === "builtin") {
1352
+ switch (res.name) {
1353
+ case "Some":
1354
+ test = `${subj} ~= nil`;
1355
+ bindSource = subj;
1356
+ break;
1357
+ case "None":
1358
+ test = `${subj} == nil`;
1359
+ break;
1360
+ case "Ok":
1361
+ test = `${subj}.ok`;
1362
+ bindSource = `${subj}.value`;
1363
+ break;
1364
+ case "Err":
1365
+ test = `not ${subj}.ok`;
1366
+ bindSource = `${subj}.error`;
1367
+ break;
1368
+ default:
1369
+ break;
1370
+ }
1371
+ } else if (res?.kind === "rbxEnumItem") {
1372
+ test = `${subj} == Enum.${res.enumName}.${res.item}`;
1373
+ } else if (p.valueExpr) {
1374
+ test = this.equality({ code: subj, prec: P.ATOM }, p.valueExpr, subjType);
1375
+ }
1376
+ if (p.binding && bindSource) {
1377
+ const sym = p.binding.sym as Sym | undefined;
1378
+ bindName = sym ? this.nameOf(sym) : sanitizeIdentifier(p.binding.name);
1379
+ if (sym) {
1380
+ this.substitutions.set(sym.id, bindSource);
1381
+ }
1382
+ }
1383
+ };
1384
+ if (pattern.kind === "Value") {
1385
+ test = this.equality({ code: subj, prec: P.ATOM }, pattern.expr, subjType);
1386
+ } else if (pattern.kind === "Case") {
1387
+ caseTest(pattern);
1388
+ }
1389
+ if (arm.guard) {
1390
+ const guard = this.cond(arm.guard);
1391
+ test = test === "true" ? guard : `${test} and ${parenIfLow(guard)}`;
1392
+ }
1393
+ const sym = pattern.kind === "Case" ? (pattern.binding?.sym as Sym | undefined) : undefined;
1394
+ if (sym) {
1395
+ this.substitutions.delete(sym.id);
1396
+ }
1397
+ return {
1398
+ test,
1399
+ bind: () => {
1400
+ if (bindName && bindSource) {
1401
+ const t = sym ? this.typeOf(sym.type) : null;
1402
+ this.line(`local ${bindName}${t && t !== "any" ? `: ${t}` : ""} = ${bindSource}${t && t !== "any" && pattern.kind === "Case" && ((pattern.res?.kind === "builtin" && pattern.res.name !== "Some") || pattern.res?.kind === "enumCase") ? ` :: ${t}` : ""}`);
1403
+ }
1404
+ },
1405
+ };
1406
+ }
1407
+
1408
+ private tryStmt(s: Extract<Stmt, { kind: "Try" }>): void {
1409
+ const frame: LoopFrame = { kind: "try" };
1410
+ const ok = this.temp("ok");
1411
+ const err = this.temp("err");
1412
+ const val = this.temp("value");
1413
+ this.loops.push(frame);
1414
+ const body = this.capture(() => this.block(() => this.block(() => this.stmts(s.body))));
1415
+ this.loops.pop();
1416
+ const flow = frame.usedBreak || frame.usedContinue || frame.usedReturn;
1417
+ this.line(`local ${ok}, ${err}${flow ? `, ${val}` : ""} = pcall(function()`);
1418
+ this.lines.push(...body.map((l) => (l.startsWith("\t") ? l.slice(1) : l)));
1419
+ this.line("end)");
1420
+ this.line(`if not ${ok} then`);
1421
+ this.block(() => {
1422
+ if (s.catchName) {
1423
+ const sym = s.catchName.sym as Sym | undefined;
1424
+ const name = sym ? this.nameOf(sym) : sanitizeIdentifier(s.catchName.name);
1425
+ this.line(`local ${name} = ${this.use("__cl_errmsg")}(${err})`);
1426
+ }
1427
+ this.stmts(s.handler);
1428
+ });
1429
+ if (frame.usedReturn) {
1430
+ this.line(`elseif ${err} == 1 then`);
1431
+ this.block(() => this.line(`return ${val}`));
1432
+ }
1433
+ if (frame.usedBreak) {
1434
+ this.line(`elseif ${err} == 2 then`);
1435
+ this.block(() => this.breakStmt());
1436
+ }
1437
+ if (frame.usedContinue) {
1438
+ this.line(`elseif ${err} == 3 then`);
1439
+ this.block(() => this.continueStmt());
1440
+ }
1441
+ this.line("end");
1442
+ }
1443
+
1444
+ // ---------- expressions ----------
1445
+
1446
+ /** Expression whose value flows into a new owner (copy for value semantics). */
1447
+ private valueOf(e: Expr): Code {
1448
+ const c = this.expr(e);
1449
+ if (e.copy) {
1450
+ return atom(`${this.use("__cl_copy")}(${c.code})`);
1451
+ }
1452
+ return c;
1453
+ }
1454
+
1455
+ /** Condition with CL++ truthiness (0 and "" are false). */
1456
+ private cond(e: Expr): string {
1457
+ return this.truthy(this.expr(e), prune(e.ty ?? T.any));
1458
+ }
1459
+
1460
+ private truthy(c: Code, t: Type): string {
1461
+ switch (t.k) {
1462
+ case "int":
1463
+ case "float":
1464
+ return `${wrap(c, P.CMP + 1)} ~= 0`;
1465
+ case "string":
1466
+ return `${wrap(c, P.CMP + 1)} ~= ""`;
1467
+ case "lit":
1468
+ return typeof t.value === "boolean" ? c.code : typeof t.value === "string" ? `${wrap(c, P.CMP + 1)} ~= ""` : `${wrap(c, P.CMP + 1)} ~= 0`;
1469
+ default:
1470
+ return c.code;
1471
+ }
1472
+ }
1473
+
1474
+ private truthyCode(e: Expr): Code {
1475
+ const t = prune(e.ty ?? T.any);
1476
+ const c = this.expr(e);
1477
+ if (t.k === "int" || t.k === "float" || t.k === "string" || (t.k === "lit" && typeof t.value !== "boolean")) {
1478
+ return { code: this.truthy(c, t), prec: P.CMP };
1479
+ }
1480
+ return c;
1481
+ }
1482
+
1483
+ expr(e: Expr): Code {
1484
+ switch (e.kind) {
1485
+ case "Number":
1486
+ return { code: luauNumber(e.value), prec: e.value < 0 ? P.UNARY : P.ATOM };
1487
+ case "String":
1488
+ return atom(luauString(e.value));
1489
+ case "Template":
1490
+ return this.template(e);
1491
+ case "Bool":
1492
+ return atom(e.value ? "true" : "false");
1493
+ case "Null":
1494
+ return atom("nil");
1495
+ case "Name":
1496
+ return this.name(e);
1497
+ case "SelfField":
1498
+ if (e.res?.kind === "sharedField") {
1499
+ return atom(this.shared(e.name));
1500
+ }
1501
+ if (e.res?.kind === "dataModel") {
1502
+ return atom("game");
1503
+ }
1504
+ if (e.res?.kind === "global") {
1505
+ return atom(e.res.luau);
1506
+ }
1507
+ return atom(`${this.selfName}.${e.name}`);
1508
+ case "Member":
1509
+ return this.member(e);
1510
+ case "Scope":
1511
+ return this.scopeAccess(e);
1512
+ case "Index":
1513
+ return this.index(e);
1514
+ case "Call":
1515
+ case "New":
1516
+ return this.call(e);
1517
+ case "Unary":
1518
+ return this.unary(e);
1519
+ case "Binary":
1520
+ return this.binary(e);
1521
+ case "Ternary": {
1522
+ const cond = this.cond(e.cond);
1523
+ const a = this.valueOf(e.then);
1524
+ const b = this.valueOf(e.else);
1525
+ return { code: `if ${cond} then ${a.code} else ${b.code}`, prec: P.IF };
1526
+ }
1527
+ case "Function":
1528
+ return this.functionExpr(e);
1529
+ case "Spawn":
1530
+ case "Thread":
1531
+ return this.spawn(e.kind === "Spawn" ? e.call : e.call);
1532
+ case "Await":
1533
+ return this.await(e);
1534
+ case "Join":
1535
+ this.use("__cl_task");
1536
+ return atom(`__cl_await(${this.expr(e.operand).code})`);
1537
+ case "Move":
1538
+ return this.move(e);
1539
+ case "PCall": {
1540
+ const inner = this.expr(e.operand).code;
1541
+ const body = prune(e.operand.ty ?? T.any).k === "void" ? inner : `return ${inner}`;
1542
+ return { code: `if pcall(function() ${body} end) then 1 else 0`, prec: P.IF };
1543
+ }
1544
+ case "Parallel": {
1545
+ this.use("__cl_parallel");
1546
+ const jobs = e.calls.map((c) => this.spawn(c).code);
1547
+ return atom(`__cl_parallel({ ${jobs.join(", ")} })`);
1548
+ }
1549
+ case "Update":
1550
+ return this.updateExpr(e);
1551
+ case "Cast": {
1552
+ const inner = this.expr(e.operand);
1553
+ const t = this.typeOf(e.ty);
1554
+ return t ? atom(`(${wrap(inner, P.UNARY).toString()} :: ${t})`) : inner;
1555
+ }
1556
+ default:
1557
+ return atom("nil");
1558
+ }
1559
+ }
1560
+
1561
+ private name(e: Extract<Expr, { kind: "Name" }>): Code {
1562
+ const res = e.res;
1563
+ let code: string;
1564
+ switch (res?.kind) {
1565
+ case "local":
1566
+ code = this.nameOf(res.sym as Sym);
1567
+ break;
1568
+ case "func": {
1569
+ const sym = res.sym as Sym;
1570
+ code = this.nameOf(sym);
1571
+ if (sym.topLevel && !this.definedFuncs.has(sym.id) && this.fnDepth > 0) {
1572
+ this.referencedFuncs.add(sym.id);
1573
+ }
1574
+ if (sym.topLevel && !this.definedFuncs.has(sym.id)) {
1575
+ this.referencedFuncs.add(sym.id);
1576
+ }
1577
+ break;
1578
+ }
1579
+ case "const":
1580
+ return literalCode(res.value);
1581
+ case "struct":
1582
+ code = this.structRef((res.sym as Sym).typeId ?? "");
1583
+ break;
1584
+ case "enum": {
1585
+ const id = (res.sym as Sym).typeId ?? "";
1586
+ this.enumTablesUsed.add(id);
1587
+ const info = this.file.enumRegistry.get(id);
1588
+ code = info ? info.luauName : sanitizeIdentifier(e.name);
1589
+ break;
1590
+ }
1591
+ case "module": {
1592
+ const link = this.file.links.find((l) => l.decl.alias === e.name);
1593
+ if (link) {
1594
+ this.usedLinks.add(link);
1595
+ code = this.linkName(link);
1596
+ } else {
1597
+ code = sanitizeIdentifier(e.name);
1598
+ }
1599
+ break;
1600
+ }
1601
+ case "signal":
1602
+ code = this.nameOf(res.sym as Sym);
1603
+ break;
1604
+ case "builtin":
1605
+ code = res.name === "None" ? "nil" : e.name;
1606
+ break;
1607
+ case "global":
1608
+ code = res.luau;
1609
+ break;
1610
+ case "service":
1611
+ code = this.service(res.service);
1612
+ break;
1613
+ case "dataModel":
1614
+ code = "game";
1615
+ break;
1616
+ default:
1617
+ code = sanitizeIdentifier(e.name);
1618
+ }
1619
+ return atom(code);
1620
+ }
1621
+
1622
+ private member(e: Extract<Expr, { kind: "Member" }>): Code {
1623
+ const res = e.res;
1624
+ switch (res?.kind) {
1625
+ case "const":
1626
+ return literalCode(res.value);
1627
+ case "service":
1628
+ return atom(this.service(res.service));
1629
+ case "enumCase":
1630
+ if (res.anyPayload) {
1631
+ return atom(`{ tag = ${res.index} }`);
1632
+ }
1633
+ return atom(String(res.index));
1634
+ case "rbxEnum":
1635
+ return atom(`Enum.${res.enumName}`);
1636
+ case "rbxEnumItem":
1637
+ return atom(`Enum.${res.enumName}.${res.item}`);
1638
+ case "stdlib":
1639
+ return atom(this.stdValue(res.module, res.member));
1640
+ case "rbxChild": {
1641
+ const obj = this.expr(e.object);
1642
+ const t = this.typeOf(e.ty);
1643
+ if (res.safe) {
1644
+ return atom(`(${wrap(obj, P.ATOM)}:WaitForChild(${luauString(res.childName)})${t && t !== "Instance" ? ` :: ${t}` : ""})`);
1645
+ }
1646
+ return atom(`${wrap(obj, P.ATOM)}${memberAccess(e.name)}`);
1647
+ }
1648
+ case "moduleMember":
1649
+ if (res.moduleId === "namespace") {
1650
+ return atom(`${sanitizeIdentifier(res.alias)}${memberAccess(res.member)}`);
1651
+ }
1652
+ break;
1653
+ case "vectorComponent": {
1654
+ const obj = this.expr(e.object);
1655
+ return atom(`${wrap(obj, P.ATOM)}.${res.component}`);
1656
+ }
1657
+ default:
1658
+ break;
1659
+ }
1660
+ const obj = this.expr(e.object);
1661
+ return atom(`${wrap(obj, P.ATOM)}${memberAccess(e.name)}`);
1662
+ }
1663
+
1664
+ private stdValue(module: string, member: string): string {
1665
+ const fn = stdModule(module as "axiom").get(member);
1666
+ if (!fn) {
1667
+ return "nil";
1668
+ }
1669
+ if (module === "axiom" && !fn.inline) {
1670
+ this.axiomRuntime = true;
1671
+ const link = this.file.links.find((l) => l.target.kind === "std" && l.target.module === "axiom");
1672
+ return `${link ? this.linkName(link) : "Axiom"}.${member}`;
1673
+ }
1674
+ if (fn.helper) {
1675
+ return this.use(fn.helper);
1676
+ }
1677
+ const params = fn.params.map((p) => p.name);
1678
+ const body = this.std(fn.inline!, params);
1679
+ return `function(${params.join(", ")}) return ${body} end`;
1680
+ }
1681
+
1682
+ private scopeAccess(e: Extract<Expr, { kind: "Scope" }>): Code {
1683
+ const obj = this.expr(e.object);
1684
+ if (e.res?.kind === "luauMethod") {
1685
+ return atom(`${wrap(obj, P.ATOM)}:${e.name}`);
1686
+ }
1687
+ if (e.res?.kind === "stdlib") {
1688
+ return atom(this.stdValue(e.res.module, e.res.member));
1689
+ }
1690
+ if (e.res?.kind === "library") {
1691
+ return atom(`${e.res.lib}.${e.res.member}`);
1692
+ }
1693
+ return atom(`${wrap(obj, P.ATOM)}.${e.name}`);
1694
+ }
1695
+
1696
+ private index(e: Extract<Expr, { kind: "Index" }>): Code {
1697
+ const objType = prune(e.object.ty ?? T.any);
1698
+ const obj = this.expr(e.object);
1699
+ if (e.index.kind === "Binary" && e.index.op === "..") {
1700
+ return atom(`${this.use("__cl_slice")}(${obj.code}, ${this.expr(e.index.left).code}, ${this.expr(e.index.right).code})`);
1701
+ }
1702
+ switch (objType.k) {
1703
+ case "list":
1704
+ return atom(`${wrap(obj, P.ATOM)}[${this.plusOne(e.index)}]`);
1705
+ case "string":
1706
+ case "lit":
1707
+ return atom(`${this.use("__cl_char")}(${obj.code}, ${this.expr(e.index).code})`);
1708
+ case "instance":
1709
+ return atom(`${wrap(obj, P.ATOM)}:FindFirstChild(${this.expr(e.index).code})`);
1710
+ case "tuple":
1711
+ return atom(`(select(${this.plusOne(e.index)}, ${obj.code}))`);
1712
+ default:
1713
+ return atom(`${wrap(obj, P.ATOM)}[${this.expr(e.index).code}]`);
1714
+ }
1715
+ }
1716
+
1717
+ private plusOne(e: Expr): string {
1718
+ if (e.kind === "Number") {
1719
+ return luauNumber(e.value + 1);
1720
+ }
1721
+ return `${wrap(this.expr(e), P.ADD)} + 1`;
1722
+ }
1723
+
1724
+ private unary(e: Extract<Expr, { kind: "Unary" }>): Code {
1725
+ switch (e.op) {
1726
+ case "-": {
1727
+ if (e.operand.kind === "Number") {
1728
+ return { code: luauNumber(-e.operand.value), prec: P.UNARY };
1729
+ }
1730
+ return { code: `-${wrap(this.expr(e.operand), P.UNARY)}`, prec: P.UNARY };
1731
+ }
1732
+ case "not": {
1733
+ const t = prune(e.operand.ty ?? T.any);
1734
+ const inner = this.expr(e.operand);
1735
+ if (t.k === "int" || t.k === "float") {
1736
+ return { code: `${wrap(inner, P.CMP + 1)} == 0`, prec: P.CMP };
1737
+ }
1738
+ if (t.k === "string") {
1739
+ return { code: `${wrap(inner, P.CMP + 1)} == ""`, prec: P.CMP };
1740
+ }
1741
+ return { code: `not ${wrap(inner, P.UNARY)}`, prec: P.UNARY };
1742
+ }
1743
+ case "~":
1744
+ return atom(`bit32.bnot(${this.expr(e.operand).code})`);
1745
+ default:
1746
+ return atom("nil");
1747
+ }
1748
+ }
1749
+
1750
+ private binary(e: Extract<Expr, { kind: "Binary" }>): Code {
1751
+ const lt = prune(e.left.ty ?? T.any);
1752
+ const rt = prune(e.right.ty ?? T.any);
1753
+ switch (e.op) {
1754
+ case "and":
1755
+ case "or": {
1756
+ if (e.res?.kind === "builtin" && e.res.name === "valueOr") {
1757
+ return { code: `${wrap(this.expr(e.left), P.OR)} or ${wrap(this.expr(e.right), P.OR + 1)}`, prec: P.OR };
1758
+ }
1759
+ const prec = e.op === "and" ? P.AND : P.OR;
1760
+ const l = this.truthyCode(e.left);
1761
+ const r = this.truthyCode(e.right);
1762
+ return { code: `${wrap(l, prec)} ${e.op} ${wrap(r, prec + 1)}`, prec };
1763
+ }
1764
+ case ".:": {
1765
+ const l = this.concatPart(e.left);
1766
+ const r = this.concatPart(e.right);
1767
+ return { code: `${wrap(l, P.CONCAT)} .. ${wrap(r, P.CONCAT)}`, prec: P.CONCAT };
1768
+ }
1769
+ case "+":
1770
+ case "-":
1771
+ case "*": {
1772
+ if (e.res?.kind === "func") {
1773
+ return atom(`${this.nameOf(e.res.sym as Sym)}(${this.expr(e.left).code}, ${this.expr(e.right).code})`);
1774
+ }
1775
+ const prec = e.op === "*" ? P.MUL : P.ADD;
1776
+ return { code: `${wrap(this.expr(e.left), prec)} ${e.op} ${wrap(this.expr(e.right), prec + 1)}`, prec };
1777
+ }
1778
+ case "/":
1779
+ if (isIntPair(lt, rt)) {
1780
+ return atom(`${this.use("__cl_idiv")}(${this.expr(e.left).code}, ${this.expr(e.right).code})`);
1781
+ }
1782
+ return { code: `${wrap(this.expr(e.left), P.MUL)} / ${wrap(this.expr(e.right), P.MUL + 1)}`, prec: P.MUL };
1783
+ case "%":
1784
+ if (lt.k === "datatype") {
1785
+ return { code: `${wrap(this.expr(e.left), P.MUL)} % ${wrap(this.expr(e.right), P.MUL + 1)}`, prec: P.MUL };
1786
+ }
1787
+ return atom(`math.fmod(${this.expr(e.left).code}, ${this.expr(e.right).code})`);
1788
+ case "==":
1789
+ case "!=": {
1790
+ if ((isValueTable(lt) || isValueTable(rt)) && e.left.kind !== "Null" && e.right.kind !== "Null") {
1791
+ const call = `${this.use("__cl_eq")}(${this.expr(e.left).code}, ${this.expr(e.right).code})`;
1792
+ return e.op === "==" ? atom(call) : { code: `not ${call}`, prec: P.UNARY };
1793
+ }
1794
+ return { code: `${wrap(this.expr(e.left), P.CMP + 1)} ${e.op === "==" ? "==" : "~="} ${wrap(this.expr(e.right), P.CMP + 1)}`, prec: P.CMP };
1795
+ }
1796
+ case "<":
1797
+ case "<=":
1798
+ case ">":
1799
+ case ">=": {
1800
+ // generic operands: Luau cannot prove T supports ordering, CL++ checked the call sites
1801
+ const side = (x: Expr) => (prune(x.ty ?? T.any).k === "param" ? `(${this.expr(x).code} :: any)` : wrap(this.expr(x), P.CMP + 1));
1802
+ return { code: `${side(e.left)} ${e.op} ${side(e.right)}`, prec: P.CMP };
1803
+ }
1804
+ case "&":
1805
+ return atom(`bit32.band(${this.expr(e.left).code}, ${this.expr(e.right).code})`);
1806
+ case "|":
1807
+ return atom(`bit32.bor(${this.expr(e.left).code}, ${this.expr(e.right).code})`);
1808
+ case "^":
1809
+ return atom(`bit32.bxor(${this.expr(e.left).code}, ${this.expr(e.right).code})`);
1810
+ case "shl":
1811
+ return atom(`bit32.lshift(${this.expr(e.left).code}, ${this.expr(e.right).code})`);
1812
+ case "shr":
1813
+ return atom(`bit32.arshift(${this.expr(e.left).code}, ${this.expr(e.right).code})`);
1814
+ default:
1815
+ return atom("nil");
1816
+ }
1817
+ }
1818
+
1819
+ private concatPart(e: Expr): Code {
1820
+ const t = prune(e.ty ?? T.any);
1821
+ const c = this.expr(e);
1822
+ if (t.k === "string" || t.k === "int" || t.k === "float" || (t.k === "lit" && typeof t.value !== "boolean")) {
1823
+ return c;
1824
+ }
1825
+ if (t.k === "bool" || t.k === "lit" || t.k === "instance" || t.k === "rbxEnumItem" || (t.k === "datatype" && t.name !== "Vector3" && t.name !== "Vector2")) {
1826
+ return atom(`tostring(${c.code})`);
1827
+ }
1828
+ return atom(`${this.use("__cl_str")}(${c.code})`);
1829
+ }
1830
+
1831
+ private template(e: Extract<Expr, { kind: "Template" }>): Code {
1832
+ if (e.parts.every((p) => typeof p === "string")) {
1833
+ return atom(luauString((e.parts as string[]).join("")));
1834
+ }
1835
+ let out = "`";
1836
+ for (const part of e.parts) {
1837
+ if (typeof part === "string") {
1838
+ out += interpolationText(part);
1839
+ continue;
1840
+ }
1841
+ const constant = part.res?.kind === "const" ? part.res.value : undefined;
1842
+ if (typeof constant === "string" || typeof constant === "number") {
1843
+ out += interpolationText(String(constant));
1844
+ continue;
1845
+ }
1846
+ const t = prune(part.ty ?? T.any);
1847
+ let code = this.expr(part).code;
1848
+ if (isValueTable(t) || (t.k === "datatype" && (t.name === "Vector3" || t.name === "Vector2")) || t.k === "null" || t.k === "option") {
1849
+ code = `${this.use("__cl_str")}(${code})`;
1850
+ }
1851
+ if (code.includes("{") || code.includes("`")) {
1852
+ const tmp = this.temp("s");
1853
+ this.line(`local ${tmp} = ${code}`);
1854
+ code = tmp;
1855
+ }
1856
+ out += `{${code}}`;
1857
+ }
1858
+ return atom(`${out}\``);
1859
+ }
1860
+
1861
+ private functionExpr(e: Extract<Expr, { kind: "Function" }>): Code {
1862
+ const { list, prologue } = this.paramList(e.params, null);
1863
+ const ret = prune(e.ty ?? T.any);
1864
+ const retType = ret.k === "func" ? this.returnType(ret.ret, false) : null;
1865
+ const savedLoops = this.loops.splice(0);
1866
+ this.fnDepth++;
1867
+ const body = this.capture(() =>
1868
+ this.block(() => {
1869
+ for (const p of prologue) {
1870
+ p();
1871
+ }
1872
+ if (Array.isArray(e.body)) {
1873
+ this.stmts(e.body);
1874
+ } else {
1875
+ const v = this.valueOf(e.body);
1876
+ if (prune(e.body.ty ?? T.any).k === "void" && isCallCode(v.code)) {
1877
+ this.line(v.code);
1878
+ } else {
1879
+ this.line(`return ${v.code}`);
1880
+ }
1881
+ }
1882
+ }),
1883
+ );
1884
+ this.fnDepth--;
1885
+ this.loops.push(...savedLoops);
1886
+ const head = `function(${list.join(", ")})${retType && retType !== "()" ? `: ${retType}` : ""}`;
1887
+ if (body.length === 1 && !Array.isArray(e.body) && body[0].trim().length < 60) {
1888
+ return atom(`${head} ${body[0].trim()} end`);
1889
+ }
1890
+ return atom(`${head}\n${body.join("\n")}\n${"\t".repeat(this.indent)}end`);
1891
+ }
1892
+
1893
+ private spawn(call: Expr): Code {
1894
+ this.use("__cl_task");
1895
+ const t = prune(call.ty ?? T.any);
1896
+ const inner = this.callRaw(call);
1897
+ const body = t.k === "void" ? inner : `return ${inner}`;
1898
+ return atom(`__cl_spawn(function() ${body} end)`);
1899
+ }
1900
+
1901
+ /** A call to an async function emitted without the implicit spawn. */
1902
+ private callRaw(call: Expr): string {
1903
+ if (call.kind === "Call") {
1904
+ const saved = (call as { __raw?: boolean }).__raw;
1905
+ (call as { __raw?: boolean }).__raw = true;
1906
+ const code = this.expr(call).code;
1907
+ (call as { __raw?: boolean }).__raw = saved;
1908
+ return code;
1909
+ }
1910
+ return this.expr(call).code;
1911
+ }
1912
+
1913
+ private await(e: Extract<Expr, { kind: "Await" }>): Code {
1914
+ const t = prune(e.operand.ty ?? T.any);
1915
+ if (t.k === "rbxSignal") {
1916
+ return atom(`${wrap(this.expr(e.operand), P.ATOM)}:Wait()`);
1917
+ }
1918
+ if (e.operand.kind === "Call" && this.isAsyncCall(e.operand)) {
1919
+ return atom(this.callRaw(e.operand));
1920
+ }
1921
+ if (t.k === "task") {
1922
+ this.use("__cl_task");
1923
+ return atom(`__cl_await(${this.expr(e.operand).code})`);
1924
+ }
1925
+ return this.expr(e.operand);
1926
+ }
1927
+
1928
+ private isAsyncCall(e: Expr): boolean {
1929
+ if (e.kind !== "Call") {
1930
+ return false;
1931
+ }
1932
+ const res = e.callee.res;
1933
+ if (res && (res.kind === "func" || res.kind === "local")) {
1934
+ return Boolean((res.sym as Sym).decl?.isAsync);
1935
+ }
1936
+ if (res && res.kind === "method") {
1937
+ return Boolean(this.file.structRegistry.get(res.struct)?.methods.get(res.name)?.decl.isAsync);
1938
+ }
1939
+ return false;
1940
+ }
1941
+
1942
+ private move(e: Extract<Expr, { kind: "Move" }>): Code {
1943
+ const src = this.place(e.operand);
1944
+ const tmp = this.temp("moved");
1945
+ this.line(`local ${tmp} = ${src}`);
1946
+ const t = prune(e.operand.ty ?? T.any);
1947
+ this.line(`${src} = ${t.k === "string" ? '""' : t.k === "list" || t.k === "dict" ? "{}" : "nil"}`);
1948
+ return atom(tmp);
1949
+ }
1950
+
1951
+ private updateExpr(e: Extract<Expr, { kind: "Update" }>): Code {
1952
+ const target = this.place(e.target);
1953
+ const op = e.delta > 0 ? "+=" : "-=";
1954
+ if (e.prefix) {
1955
+ this.line(`${target} ${op} 1`);
1956
+ return atom(target);
1957
+ }
1958
+ const tmp = this.temp("old");
1959
+ this.line(`local ${tmp} = ${target}`);
1960
+ this.line(`${target} ${op} 1`);
1961
+ return atom(tmp);
1962
+ }
1963
+
1964
+ // ---------- calls ----------
1965
+
1966
+ private args(args: Array<Expr | null>, rest?: Expr[]): string {
1967
+ const out = args.map((a) => (a ? this.valueOf(a).code : "nil"));
1968
+ for (const r of rest || []) {
1969
+ out.push(this.valueOf(r).code);
1970
+ }
1971
+ return out.join(", ");
1972
+ }
1973
+
1974
+ private call(e: Expr): Code {
1975
+ const res = e.res;
1976
+ if (!res || res.kind !== "call") {
1977
+ if (e.kind === "Call") {
1978
+ return atom(`${wrap(this.expr(e.callee), P.ATOM)}(${e.args.map((a) => this.valueOf(a.value).code).join(", ")})`);
1979
+ }
1980
+ return atom("nil");
1981
+ }
1982
+ const call = res.call;
1983
+ const args = res.args;
1984
+ switch (call.form) {
1985
+ case "struct":
1986
+ return atom(`${this.structRef(call.id)}.new(${this.args(args)})`);
1987
+ case "instance": {
1988
+ if (call.props.length === 0 && !call.parent) {
1989
+ return atom(`Instance.new(${luauString(call.cls)})`);
1990
+ }
1991
+ const tmp = this.temp("i");
1992
+ this.instanceInto(e, `local ${tmp}`, tmp);
1993
+ return atom(tmp);
1994
+ }
1995
+ case "datatype": {
1996
+ if (call.props.length === 0) {
1997
+ return atom(`${call.name}.new(${this.args(args, res.variadicArgs)})`);
1998
+ }
1999
+ const tmp = this.temp("d");
2000
+ this.datatypeInto(e, `local ${tmp}`, tmp);
2001
+ return atom(tmp);
2002
+ }
2003
+ case "enumCase":
2004
+ return atom(`{ tag = ${call.index}, value = ${args[0] ? this.valueOf(args[0]).code : "nil"} }`);
2005
+ case "some":
2006
+ case "variant":
2007
+ return args[0] ? this.valueOf(args[0]) : atom("nil");
2008
+ case "ok":
2009
+ return atom(`{ ok = true, value = ${args[0] ? this.valueOf(args[0]).code : "nil"}, error = nil }`);
2010
+ case "err":
2011
+ return atom(`{ ok = false, value = nil, error = ${args[0] ? this.valueOf(args[0]).code : "nil"} }`);
2012
+ case "builtin":
2013
+ return this.builtin(call.name, args, call.elemType);
2014
+ case "std":
2015
+ return this.stdCall(call.module, call.fn, args);
2016
+ case "signalFire":
2017
+ this.fireSignal(e, call.sym as Sym, args);
2018
+ return atom("nil");
2019
+ case "classArg": {
2020
+ const callee = (e as Extract<Expr, { kind: "Call" }>).callee as Extract<Expr, { kind: "Member" }>;
2021
+ const obj = this.expr(callee.object);
2022
+ const rest = this.args(args, res.variadicArgs);
2023
+ const code = `${wrap(obj, P.ATOM)}:${callee.name}(${luauString(call.cls)}${rest ? `, ${rest}` : ""})`;
2024
+ if (call.cast) {
2025
+ const t = this.typeOf(e.ty);
2026
+ return t ? atom(`(${code} :: ${t})`) : atom(code);
2027
+ }
2028
+ return atom(code);
2029
+ }
2030
+ case "cast": {
2031
+ const inner = this.plainCall(e as Extract<Expr, { kind: "Call" }>, args, res.variadicArgs);
2032
+ const t = this.typeOf(e.ty);
2033
+ return t ? atom(`(${inner} :: ${t})`) : atom(inner);
2034
+ }
2035
+ case "service":
2036
+ return atom(this.service(call.service));
2037
+ case "convert":
2038
+ return this.convert(call.to, args[0]);
2039
+ case "stringMethod": {
2040
+ const callee = (e as Extract<Expr, { kind: "Call" }>).callee as Extract<Expr, { kind: "Member" }>;
2041
+ return atom(`${wrap(this.expr(callee.object), P.ATOM)}:${call.name}(${this.args(args, res.variadicArgs)})`);
2042
+ }
2043
+ case "observe": {
2044
+ const sym = call.sym as Sym;
2045
+ return atom(`table.insert(${this.nameOf(sym)}_changed, ${args[0] ? this.expr(args[0]).code : "nil"})`);
2046
+ }
2047
+ case "table": {
2048
+ const parts = call.keys.map((k, i) => `${fieldKey(k)} = ${args[i] ? this.valueOf(args[i]!).code : "nil"}`);
2049
+ return atom(parts.length ? `{ ${parts.join(", ")} }` : "{}");
2050
+ }
2051
+ case "luauMethod":
2052
+ case "call":
2053
+ default: {
2054
+ const ce = e as Extract<Expr, { kind: "Call" }>;
2055
+ const code = this.plainCall(ce, args, res.variadicArgs);
2056
+ if (this.isAsyncCall(ce) && !(ce as { __raw?: boolean }).__raw) {
2057
+ this.use("__cl_task");
2058
+ return atom(`__cl_spawn(function() return ${code} end)`);
2059
+ }
2060
+ return atom(code);
2061
+ }
2062
+ }
2063
+ }
2064
+
2065
+ private plainCall(e: Extract<Expr, { kind: "Call" }>, args: Array<Expr | null>, rest?: Expr[]): string {
2066
+ const callee = e.callee;
2067
+ const argText = this.args(args, rest);
2068
+ if (callee.kind === "Member") {
2069
+ const r = callee.res;
2070
+ const isMethod =
2071
+ (r?.kind === "rbxMethod") ||
2072
+ (r?.kind === "method" && !r.isStatic) ||
2073
+ (r?.kind === "datatypeMember" && r.isMethod) ||
2074
+ (r?.kind === "library" && r.stringMethod);
2075
+ if (isMethod) {
2076
+ return `${wrap(this.expr(callee.object), P.ATOM)}:${callee.name}(${argText})`;
2077
+ }
2078
+ if (r?.kind === "method" && r.isStatic) {
2079
+ return `${this.structRef(r.struct)}.${callee.name}(${argText})`;
2080
+ }
2081
+ }
2082
+ if (callee.kind === "SelfField" && callee.res?.kind === "method" && !callee.res.isStatic) {
2083
+ return `${this.selfName}:${callee.name}(${argText})`;
2084
+ }
2085
+ if (callee.kind === "Scope" && callee.res?.kind === "luauMethod") {
2086
+ return `${wrap(this.expr(callee.object), P.ATOM)}:${callee.name}(${argText})`;
2087
+ }
2088
+ return `${wrap(this.expr(callee), P.ATOM)}(${argText})`;
2089
+ }
2090
+
2091
+ private convert(to: "int" | "float" | "string" | "bool", arg: Expr | null): Code {
2092
+ if (!arg) {
2093
+ return atom("nil");
2094
+ }
2095
+ const t = prune(arg.ty ?? T.any);
2096
+ const c = this.expr(arg);
2097
+ switch (to) {
2098
+ case "int":
2099
+ if (t.k === "int" || t.k === "enum") {
2100
+ return c;
2101
+ }
2102
+ if (t.k === "rbxEnumItem") {
2103
+ return atom(`${wrap(c, P.ATOM)}.Value`);
2104
+ }
2105
+ if (t.k === "string" || t.k === "lit") {
2106
+ return atom(`${this.use("__cl_toint")}(${c.code})`);
2107
+ }
2108
+ return atom(`${this.use("__cl_trunc")}(${c.code})`);
2109
+ case "float":
2110
+ if (t.k === "string" || (t.k === "lit" && typeof t.value === "string")) {
2111
+ return atom(`${this.use("__cl_tonumber")}(${c.code})`);
2112
+ }
2113
+ return c;
2114
+ case "string":
2115
+ if (t.k === "string") {
2116
+ return c;
2117
+ }
2118
+ if (t.k === "int" || t.k === "float" || t.k === "bool") {
2119
+ return atom(`tostring(${c.code})`);
2120
+ }
2121
+ return atom(`${this.use("__cl_str")}(${c.code})`);
2122
+ case "bool":
2123
+ return { code: this.truthy(c, t), prec: P.CMP };
2124
+ default:
2125
+ return c;
2126
+ }
2127
+ }
2128
+
2129
+ private builtin(name: string, args: Array<Expr | null>, elemType?: Type): Code {
2130
+ const a = (i: number) => (args[i] ? this.expr(args[i]!) : atom("nil"));
2131
+ const v = (i: number) => (args[i] ? this.valueOf(args[i]!) : atom("nil"));
2132
+ switch (name) {
2133
+ case "len": {
2134
+ const t = prune(args[0]?.ty ?? T.any);
2135
+ if (t.k === "string" || t.k === "lit") {
2136
+ return atom(`${this.use("__cl_strlen")}(${a(0).code})`);
2137
+ }
2138
+ if (t.k === "dict" || t.k === "table") {
2139
+ return atom(`${this.use("__cl_count")}(${a(0).code})`);
2140
+ }
2141
+ if (t.k === "buffer") {
2142
+ return atom(`buffer.len(${a(0).code})`);
2143
+ }
2144
+ return { code: `#${wrap(a(0), P.UNARY)}`, prec: P.UNARY };
2145
+ }
2146
+ case "list":
2147
+ case "array": {
2148
+ const items = args.map((x) => (x ? this.valueOf(x).code : "nil"));
2149
+ return atom(items.length ? `{ ${items.join(", ")} }` : "{}");
2150
+ }
2151
+ case "dictionary": {
2152
+ const parts: string[] = [];
2153
+ for (let i = 0; i + 1 < args.length; i += 2) {
2154
+ const k = args[i]!;
2155
+ const key = k.kind === "String" && isIdentifier(k.value) ? k.value : `[${this.expr(k).code}]`;
2156
+ parts.push(`${key} = ${this.valueOf(args[i + 1]!).code}`);
2157
+ }
2158
+ return atom(parts.length ? `{ ${parts.join(", ")} }` : "{}");
2159
+ }
2160
+ case "push":
2161
+ return atom(`table.insert(${a(0).code}, ${v(1).code})`);
2162
+ case "pop":
2163
+ return atom(`table.remove(${a(0).code})`);
2164
+ case "insert":
2165
+ return atom(`table.insert(${a(0).code}, ${args[1] ? this.plusOne(args[1]) : "1"}, ${v(2).code})`);
2166
+ case "remove": {
2167
+ const t = prune(args[0]?.ty ?? T.any);
2168
+ if (t.k === "dict" || t.k === "table") {
2169
+ return atom(`${this.use("__cl_take")}(${a(0).code}, ${a(1).code})`);
2170
+ }
2171
+ return atom(`table.remove(${a(0).code}, ${args[1] ? this.plusOne(args[1]) : "1"})`);
2172
+ }
2173
+ case "find": {
2174
+ if (elemType && isValueTable(elemType)) {
2175
+ return atom(`${this.use("__cl_findeq")}(${a(0).code}, ${a(1).code})`);
2176
+ }
2177
+ return { code: `(table.find(${a(0).code}, ${a(1).code}) or 0) - 1`, prec: P.ADD };
2178
+ }
2179
+ case "sort":
2180
+ return atom(`${this.use("__cl_sorted")}(${a(0).code})`);
2181
+ case "mutex":
2182
+ this.use("__cl_mutex");
2183
+ return atom("__cl_mutex()");
2184
+ case "lock":
2185
+ this.use("__cl_mutex");
2186
+ return atom(`__cl_lock(${a(0).code})`);
2187
+ case "unlock":
2188
+ this.use("__cl_mutex");
2189
+ return atom(`__cl_unlock(${a(0).code})`);
2190
+ case "fetch_add": {
2191
+ const target = this.place(args[0]!);
2192
+ const tmp = this.temp("old");
2193
+ this.line(`local ${tmp} = ${target}`);
2194
+ this.line(`${target} += ${a(1).code}`);
2195
+ return atom(tmp);
2196
+ }
2197
+ case "atomic_load":
2198
+ return a(0);
2199
+ default:
2200
+ return atom("nil");
2201
+ }
2202
+ }
2203
+
2204
+ private stdCall(module: string, member: string, args: Array<Expr | null>): Code {
2205
+ const fn = stdModule(module as "axiom").get(member);
2206
+ if (!fn) {
2207
+ return atom("nil");
2208
+ }
2209
+ const codes = args.map((x) => (x ? this.valueOf(x) : atom("nil")));
2210
+ while (codes.length < fn.params.length && fn.params[codes.length]?.optional) {
2211
+ break;
2212
+ }
2213
+ if (fn.inline) {
2214
+ const multi = /@\d/.test(fn.inline);
2215
+ const simple = args.every((x) => !x || isSimple(x));
2216
+ if (!multi || simple) {
2217
+ return { code: this.std(fn.inline, codes.map((c) => c.code), codes), prec: inlinePrec(fn.inline) };
2218
+ }
2219
+ }
2220
+ if (fn.helper) {
2221
+ return atom(`${this.use(fn.helper)}(${codes.map((c) => c.code).join(", ")})`);
2222
+ }
2223
+ this.axiomRuntime = true;
2224
+ const link = this.file.links.find((l) => l.target.kind === "std" && l.target.module === module);
2225
+ const alias = link ? this.linkName(link) : "Axiom";
2226
+ return atom(`${alias}.${member}(${codes.map((c) => c.code).join(", ")})`);
2227
+ }
2228
+
2229
+ private std(template: string, args: string[], codes?: Code[]): string {
2230
+ return template.replace(/[$@](\d)/g, (_m, n: string) => {
2231
+ const i = Number(n) - 1;
2232
+ const code = codes?.[i];
2233
+ if (code && code.prec < P.ATOM) {
2234
+ return `(${code.code})`;
2235
+ }
2236
+ return args[i] ?? "nil";
2237
+ });
2238
+ }
2239
+ }
2240
+
2241
+ // ---------- helpers ----------
2242
+
2243
+ /** Luau infers the type of calls and member reads on its own; annotations there are noise. */
2244
+ function isInferredByLuau(e: Expr): boolean {
2245
+ return e.kind === "Call" || e.kind === "Member" || e.kind === "Index" || e.kind === "Await";
2246
+ }
2247
+
2248
+ function wrap(c: Code, min: number): string {
2249
+ return c.prec < min ? `(${c.code})` : c.code;
2250
+ }
2251
+
2252
+ function wrapCode(code: string, min: number): string {
2253
+ return /^[\w.:"()[\]]+$/.test(code) || min <= P.ADD ? code : `(${code})`;
2254
+ }
2255
+
2256
+ function parenIfLow(code: string): string {
2257
+ return /\s(and|or)\s/.test(code) ? `(${code})` : code;
2258
+ }
2259
+
2260
+ function optionalType(t: string): string {
2261
+ if (t.endsWith("?") || t === "any") {
2262
+ return t;
2263
+ }
2264
+ return /->|\|/.test(t) ? `(${t})?` : `${t}?`;
2265
+ }
2266
+
2267
+ function safeName(name: string): string {
2268
+ const s = sanitizeIdentifier(name);
2269
+ return LUAU_KEYWORDS.has(s) ? `${s}_` : s;
2270
+ }
2271
+
2272
+ function fieldKey(name: string): string {
2273
+ return isIdentifier(name) ? name : `[${luauString(name)}]`;
2274
+ }
2275
+
2276
+ function memberAccess(name: string): string {
2277
+ return isIdentifier(name) ? `.${name}` : `[${luauString(name)}]`;
2278
+ }
2279
+
2280
+ function literal(value: number | string | boolean | null): string {
2281
+ if (value === null) {
2282
+ return "nil";
2283
+ }
2284
+ if (typeof value === "string") {
2285
+ return luauString(value);
2286
+ }
2287
+ if (typeof value === "number") {
2288
+ return luauNumber(value);
2289
+ }
2290
+ return value ? "true" : "false";
2291
+ }
2292
+
2293
+ function literalCode(value: number | string | boolean | null): Code {
2294
+ return { code: literal(value), prec: typeof value === "number" && value < 0 ? P.UNARY : P.ATOM };
2295
+ }
2296
+
2297
+ function inlinePrec(template: string): number {
2298
+ if (/^\w[\w.]*\([^()]*(\([^()]*\))*[^()]*\)$/.test(template) || /^[\w.]+$/.test(template)) {
2299
+ return P.ATOM;
2300
+ }
2301
+ if (/ ~= | == | <= | >= | < | > /.test(template)) {
2302
+ return / and | or /.test(template) ? P.AND : P.CMP;
2303
+ }
2304
+ if (/ [+-] /.test(template)) {
2305
+ return P.ADD;
2306
+ }
2307
+ if (/ [*/%] /.test(template)) {
2308
+ return P.MUL;
2309
+ }
2310
+ if (/\^/.test(template)) {
2311
+ return P.POW;
2312
+ }
2313
+ return P.UNARY;
2314
+ }
2315
+
2316
+ function isCallCode(code: string): boolean {
2317
+ return /\)$/.test(code.trim()) && !/^\(/.test(code.trim()) && !/^if /.test(code.trim());
2318
+ }
2319
+
2320
+ function isSimple(e: Expr): boolean {
2321
+ return e.kind === "Name" || e.kind === "Number" || e.kind === "String" || e.kind === "Bool" || e.kind === "SelfField" || (e.kind === "Member" && isSimple(e.object));
2322
+ }
2323
+
2324
+ function isIntLikeType(t: Type): boolean {
2325
+ const p = prune(t);
2326
+ return p.k === "int";
2327
+ }
2328
+
2329
+ function isIntPair(a: Type, b: Type): boolean {
2330
+ const isInt = (t: Type) => t.k === "int" || (t.k === "lit" && typeof t.value === "number" && Number.isInteger(t.value));
2331
+ return isInt(a) && isInt(b);
2332
+ }
2333
+
2334
+ function isSym(e: Expr, sym: Sym): boolean {
2335
+ return e.kind === "Name" && e.res?.kind === "local" && (e.res.sym as Sym).id === sym.id;
2336
+ }
2337
+
2338
+ function assigns(body: Stmt[], sym: Sym): boolean {
2339
+ let found = false;
2340
+ const visit = (n: unknown) => {
2341
+ if (found || !n || typeof n !== "object") {
2342
+ return;
2343
+ }
2344
+ if (Array.isArray(n)) {
2345
+ n.forEach(visit);
2346
+ return;
2347
+ }
2348
+ const rec = n as { kind?: string; target?: Expr };
2349
+ if ((rec.kind === "Assign" || rec.kind === "Update") && rec.target && isSym(rec.target, sym)) {
2350
+ found = true;
2351
+ return;
2352
+ }
2353
+ if (rec.kind === "Function") {
2354
+ found = found || JSON.stringify(Object.keys(rec)).length < 0;
2355
+ }
2356
+ for (const [k, v] of Object.entries(n)) {
2357
+ if (k !== "ty" && k !== "res" && k !== "span" && k !== "sym" && k !== "narrowedFrom") {
2358
+ visit(v);
2359
+ }
2360
+ }
2361
+ };
2362
+ visit(body);
2363
+ return found;
2364
+ }
2365
+
2366
+ function isInvariant(e: Expr, body: Stmt[]): boolean {
2367
+ if (e.kind === "Number") {
2368
+ return true;
2369
+ }
2370
+ if (e.kind === "Name") {
2371
+ if (e.res?.kind === "const") {
2372
+ return true;
2373
+ }
2374
+ if (e.res?.kind === "local") {
2375
+ const sym = e.res.sym as Sym;
2376
+ return !sym.mutable || !assigns(body, sym);
2377
+ }
2378
+ }
2379
+ if (e.kind === "Binary" && (e.op === "+" || e.op === "-" || e.op === "*")) {
2380
+ return isInvariant(e.left, body) && isInvariant(e.right, body);
2381
+ }
2382
+ if (e.kind === "Member" && e.res?.kind === "const") {
2383
+ return true;
2384
+ }
2385
+ return false;
2386
+ }
2387
+
2388
+ function trimTrailingBreak(body: Stmt[]): Stmt[] {
2389
+ if (body.length && body[body.length - 1].kind === "Break") {
2390
+ return body.slice(0, -1);
2391
+ }
2392
+ return body;
2393
+ }
2394
+
2395
+ /** A `break` before the end of a switch case (needs a `repeat … until true` wrapper). */
2396
+ function hasInnerBreak(body: Stmt[]): boolean {
2397
+ const trimmed = trimTrailingBreak(body);
2398
+ let found = false;
2399
+ const visit = (s: Stmt) => {
2400
+ if (found) {
2401
+ return;
2402
+ }
2403
+ switch (s.kind) {
2404
+ case "Break":
2405
+ found = true;
2406
+ return;
2407
+ case "If":
2408
+ s.then.forEach(visit);
2409
+ s.else?.forEach(visit);
2410
+ return;
2411
+ case "Block":
2412
+ s.body.forEach(visit);
2413
+ return;
2414
+ case "Match":
2415
+ s.arms.forEach((a) => a.body.forEach(visit));
2416
+ return;
2417
+ case "Try":
2418
+ s.body.forEach(visit);
2419
+ s.handler.forEach(visit);
2420
+ return;
2421
+ default:
2422
+ return;
2423
+ }
2424
+ };
2425
+ trimmed.forEach(visit);
2426
+ return found;
2427
+ }
2428
+
2429
+ function walkStmt(s: Stmt, fn: (s: Stmt, depth: number) => void, depth: number): void {
2430
+ fn(s, depth);
2431
+ const inner = (list: Stmt[] | null | undefined, d = depth) => list?.forEach((x) => walkStmt(x, fn, d));
2432
+ switch (s.kind) {
2433
+ case "If":
2434
+ inner(s.then);
2435
+ inner(s.else);
2436
+ break;
2437
+ case "While":
2438
+ case "ForIn":
2439
+ inner(s.body);
2440
+ break;
2441
+ case "For":
2442
+ inner(s.body);
2443
+ break;
2444
+ case "Block":
2445
+ inner(s.body);
2446
+ break;
2447
+ case "Try":
2448
+ inner(s.body);
2449
+ inner(s.handler);
2450
+ break;
2451
+ case "Match":
2452
+ s.arms.forEach((a) => inner(a.body));
2453
+ break;
2454
+ case "Switch":
2455
+ s.cases.forEach((k) => inner(k.body));
2456
+ break;
2457
+ case "Decl":
2458
+ if (s.decl.kind === "Func") {
2459
+ inner(s.decl.body, depth + 1);
2460
+ } else if (s.decl.kind === "Struct") {
2461
+ s.decl.methods.forEach((m) => inner(m.body, depth + 1));
2462
+ } else if (s.decl.kind === "Namespace") {
2463
+ s.decl.funcs.forEach((f) => inner(f.body, depth + 1));
2464
+ }
2465
+ break;
2466
+ default:
2467
+ break;
2468
+ }
2469
+ }
2470
+
2471
+ function collapseBlankLines(lines: string[]): string[] {
2472
+ const out: string[] = [];
2473
+ for (const l of lines) {
2474
+ if (l.trim() === "" && (out.length === 0 || out[out.length - 1].trim() === "")) {
2475
+ continue;
2476
+ }
2477
+ out.push(l);
2478
+ }
2479
+ return out;
2480
+ }
2481
+
2482
+ export function emitLuau(program: Program, file: FileInfo, api: RobloxApi, options: EmitOptions): string {
2483
+ return new LuauEmitter(program, file, api, options).emit();
2484
+ }
2485
+
2486
+ export type { Arg, EnumInfo, isDynamic };