cluaupp 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +5 -8
  3. package/docs/README.md +13 -10
  4. package/docs/architecture.md +18 -11
  5. package/docs/cli.md +2 -2
  6. package/docs/config.md +1 -1
  7. package/docs/cpp-advanced.md +10 -6
  8. package/docs/cpp-organization.md +3 -1
  9. package/docs/cpp-safety.md +11 -7
  10. package/docs/cpp-types.md +1 -1
  11. package/docs/examples/_category_.json +5 -0
  12. package/docs/examples/combat.md +239 -0
  13. package/docs/examples/data-boot.md +98 -0
  14. package/docs/examples/hud.md +96 -0
  15. package/docs/examples/index.md +43 -0
  16. package/docs/examples/leaderstats.md +140 -0
  17. package/docs/examples/shop.md +122 -0
  18. package/docs/examples/sword.md +108 -0
  19. package/docs/getting-started.md +12 -6
  20. package/docs/intellisense.md +31 -0
  21. package/docs/intro.md +2 -2
  22. package/docs/libraries/_category_.json +5 -0
  23. package/docs/libraries/dataservice.md +104 -0
  24. package/docs/libraries/index.md +22 -0
  25. package/docs/libraries/janitor.md +65 -0
  26. package/docs/libraries/more.md +79 -0
  27. package/docs/libraries/net.md +35 -0
  28. package/docs/libraries/promise.md +27 -0
  29. package/docs/oop/_category_.json +5 -0
  30. package/docs/oop/file-tags.md +49 -0
  31. package/docs/oop/index.md +22 -0
  32. package/docs/oop/modules.md +86 -0
  33. package/docs/oop/services.md +83 -0
  34. package/docs/print-cout.md +91 -0
  35. package/docs/syntax.md +59 -5
  36. package/editors/vscode/extension.js +146 -0
  37. package/editors/vscode/package.json +25 -0
  38. package/include/cluaupp/libs/janitor.hpp +7 -3
  39. package/include/cluaupp/roblox.hpp +19 -0
  40. package/package.json +66 -65
  41. package/runtime/Janitor/init.luau +4 -34
  42. package/src/architecture.js +108 -48
  43. package/src/cli.js +85 -10
  44. package/src/client/init.client.cpp +5 -0
  45. package/src/compile.js +20 -4
  46. package/src/editor-install.js +218 -0
  47. package/src/emit.js +320 -8
  48. package/src/intellisense.js +1368 -0
  49. package/src/layout.js +129 -0
  50. package/src/lex.js +17 -9
  51. package/src/libs.js +95 -16
  52. package/src/lsp.js +227 -0
  53. package/src/parse.js +187 -6
  54. package/src/preprocess.js +71 -3
  55. package/src/server/leaderstats.server.cpp +28 -0
  56. package/src/shared/config.cpp +5 -0
  57. package/src/shared/config.h +3 -0
  58. package/src/understand.js +64 -6
  59. package/templates/game/.clangd +11 -0
  60. package/templates/game/.vscode/c_cpp_properties.json +6 -2
  61. package/templates/game/.vscode/extensions.json +6 -0
  62. package/templates/game/.vscode/settings.json +16 -2
  63. package/templates/game/compile_flags.txt +2 -0
  64. package/docs/libraries.md +0 -80
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const https = require("https");
7
+ const { spawnSync } = require("child_process");
8
+
9
+ const CPPTOOLS_ID = "ms-vscode.cpptools";
10
+ const GITHUB_LATEST = "https://api.github.com/repos/microsoft/vscode-cpptools/releases/latest";
11
+
12
+ function which(command) {
13
+ const finder = process.platform === "win32" ? "where" : "which";
14
+ const result = spawnSync(finder, [command], { encoding: "utf8", windowsHide: true });
15
+ if (result.status !== 0) {
16
+ return null;
17
+ }
18
+ return String(result.stdout || "")
19
+ .split(/\r?\n/)
20
+ .map((line) => line.trim())
21
+ .find((line) => line && !line.toLowerCase().includes("info:"));
22
+ }
23
+
24
+ function editorBin() {
25
+ const found = which("cursor") || which("code");
26
+ if (found) {
27
+ return found;
28
+ }
29
+ const local = process.env.LOCALAPPDATA || "";
30
+ const fallbacks = [
31
+ path.join(local, "Programs", "cursor", "resources", "app", "bin", "cursor.cmd"),
32
+ path.join(local, "Programs", "Microsoft VS Code", "bin", "code.cmd"),
33
+ "/usr/bin/cursor",
34
+ "/usr/local/bin/cursor",
35
+ "/usr/bin/code",
36
+ "/usr/local/bin/code",
37
+ ];
38
+ return fallbacks.find((file) => fs.existsSync(file)) || null;
39
+ }
40
+
41
+ function extensionHomes() {
42
+ return [
43
+ path.join(os.homedir(), ".cursor", "extensions"),
44
+ path.join(os.homedir(), ".vscode", "extensions"),
45
+ ];
46
+ }
47
+
48
+ function isCppToolsInstalled() {
49
+ for (const home of extensionHomes()) {
50
+ if (!fs.existsSync(home)) {
51
+ continue;
52
+ }
53
+ try {
54
+ if (fs.readdirSync(home).some((name) => name.startsWith(CPPTOOLS_ID + "-"))) {
55
+ return true;
56
+ }
57
+ } catch {
58
+ // ignore
59
+ }
60
+ }
61
+ const bin = editorBin();
62
+ if (!bin) {
63
+ return false;
64
+ }
65
+ const listed = spawnSync(bin, ["--list-extensions"], { encoding: "utf8", windowsHide: true, timeout: 20000 });
66
+ return String(listed.stdout || "").split(/\r?\n/).includes(CPPTOOLS_ID);
67
+ }
68
+
69
+ function vsixAssetName(platform = process.platform, arch = process.arch) {
70
+ const cpu = arch === "arm64" ? "arm64" : "x64";
71
+ if (platform === "win32") {
72
+ return `cpptools-windows-${cpu}.vsix`;
73
+ }
74
+ if (platform === "darwin") {
75
+ return `cpptools-macOS-${cpu}.vsix`;
76
+ }
77
+ return `cpptools-linux-${cpu}.vsix`;
78
+ }
79
+
80
+ function httpsJson(url) {
81
+ return new Promise((resolve, reject) => {
82
+ const request = https.get(
83
+ url,
84
+ { headers: { "User-Agent": "cluaupp", Accept: "application/vnd.github+json" } },
85
+ (response) => {
86
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
87
+ response.resume();
88
+ httpsJson(response.headers.location).then(resolve, reject);
89
+ return;
90
+ }
91
+ const chunks = [];
92
+ response.on("data", (chunk) => chunks.push(chunk));
93
+ response.on("end", () => {
94
+ if (response.statusCode !== 200) {
95
+ reject(new Error("GitHub " + response.statusCode));
96
+ return;
97
+ }
98
+ try {
99
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
100
+ } catch (err) {
101
+ reject(err);
102
+ }
103
+ });
104
+ },
105
+ );
106
+ request.on("error", reject);
107
+ });
108
+ }
109
+
110
+ function downloadFile(url, dest) {
111
+ return new Promise((resolve, reject) => {
112
+ const follow = (current) => {
113
+ https
114
+ .get(current, { headers: { "User-Agent": "cluaupp" } }, (response) => {
115
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
116
+ response.resume();
117
+ follow(response.headers.location);
118
+ return;
119
+ }
120
+ if (response.statusCode !== 200) {
121
+ reject(new Error("download failed " + response.statusCode));
122
+ return;
123
+ }
124
+ const out = fs.createWriteStream(dest);
125
+ response.pipe(out);
126
+ out.on("finish", () => out.close(() => resolve(dest)));
127
+ out.on("error", reject);
128
+ })
129
+ .on("error", reject);
130
+ };
131
+ follow(url);
132
+ });
133
+ }
134
+
135
+ async function downloadCppToolsVsix() {
136
+ const name = vsixAssetName();
137
+ const dest = path.join(os.tmpdir(), name);
138
+ if (fs.existsSync(dest) && fs.statSync(dest).size > 1_000_000) {
139
+ return dest;
140
+ }
141
+ const release = await httpsJson(GITHUB_LATEST);
142
+ const asset = (release.assets || []).find((item) => item.name === name);
143
+ if (!asset || !asset.browser_download_url) {
144
+ throw new Error("no " + name + " in vscode-cpptools " + (release.tag_name || "latest"));
145
+ }
146
+ console.log("cluaupp: downloading", name, release.tag_name || "");
147
+ return downloadFile(asset.browser_download_url, dest);
148
+ }
149
+
150
+ function spawnInstall(bin, target) {
151
+ const result = spawnSync(bin, ["--install-extension", target, "--force"], {
152
+ encoding: "utf8",
153
+ windowsHide: true,
154
+ timeout: 180000,
155
+ });
156
+ return {
157
+ ok: result.status === 0 && !/not found/i.test(String(result.stderr || "") + String(result.stdout || "")),
158
+ stdout: String(result.stdout || ""),
159
+ stderr: String(result.stderr || ""),
160
+ status: result.status,
161
+ };
162
+ }
163
+
164
+ async function installCppTools() {
165
+ if (isCppToolsInstalled()) {
166
+ return { status: "already", id: CPPTOOLS_ID };
167
+ }
168
+ const bin = editorBin();
169
+ if (!bin) {
170
+ return { status: "missing-editor", id: CPPTOOLS_ID };
171
+ }
172
+ console.log("cluaupp: installing", CPPTOOLS_ID);
173
+ const marketplace = spawnInstall(bin, CPPTOOLS_ID);
174
+ if (marketplace.ok) {
175
+ return { status: "installed", id: CPPTOOLS_ID, how: "marketplace" };
176
+ }
177
+ try {
178
+ const vsix = await downloadCppToolsVsix();
179
+ const fromFile = spawnInstall(bin, vsix);
180
+ if (fromFile.ok) {
181
+ return { status: "installed", id: CPPTOOLS_ID, how: "vsix", file: vsix };
182
+ }
183
+ return { status: "failed", id: CPPTOOLS_ID, detail: fromFile.stderr || fromFile.stdout || marketplace.stderr };
184
+ } catch (err) {
185
+ return { status: "failed", id: CPPTOOLS_ID, detail: err.message };
186
+ }
187
+ }
188
+
189
+ function reportCppTools(result) {
190
+ if (!result) {
191
+ return;
192
+ }
193
+ if (result.status === "already") {
194
+ console.log("cluaupp: Microsoft C/C++ already installed");
195
+ return;
196
+ }
197
+ if (result.status === "installed") {
198
+ console.log("cluaupp: installed", CPPTOOLS_ID, result.how === "vsix" ? "(VSIX — Cursor marketplace does not list it)" : "");
199
+ console.log("cluaupp: reload the editor (Ctrl+Shift+P → Developer: Reload Window)");
200
+ return;
201
+ }
202
+ if (result.status === "missing-editor") {
203
+ console.error("cluaupp: Cursor/VS Code CLI not found. Install ms-vscode.cpptools from the VSIX at");
204
+ console.error(" https://github.com/microsoft/vscode-cpptools/releases");
205
+ return;
206
+ }
207
+ console.error("cluaupp: could not install", CPPTOOLS_ID + ":", result.detail || "unknown error");
208
+ console.error(" download https://github.com/microsoft/vscode-cpptools/releases and: cursor --install-extension cpptools-windows-x64.vsix");
209
+ }
210
+
211
+ module.exports = {
212
+ CPPTOOLS_ID,
213
+ editorBin,
214
+ isCppToolsInstalled,
215
+ vsixAssetName,
216
+ installCppTools,
217
+ reportCppTools,
218
+ };
package/src/emit.js CHANGED
@@ -15,6 +15,133 @@ function emit(ast, options = {}) {
15
15
  }
16
16
 
17
17
  const indentOf = (n) => "\t".repeat(n);
18
+ let switchId = 0;
19
+
20
+ const classOwners = new Set();
21
+ const classFields = new Set();
22
+ const classMethods = new Set();
23
+ for (const decl of ast.body || []) {
24
+ if (decl.owner) {
25
+ classOwners.add(decl.owner);
26
+ }
27
+ if (decl.type === "decl" && decl.owner) {
28
+ classFields.add(decl.name);
29
+ }
30
+ if ((decl.type === "function" || decl.type === "proto") && decl.owner && decl.name) {
31
+ classMethods.add(decl.name);
32
+ }
33
+ }
34
+ const ownedFns = (ast.body || []).filter((decl) => decl.type === "function");
35
+ const classModule = ownedFns.length > 0 && ownedFns.every((decl) => Boolean(decl.owner));
36
+ const nestedTypes = new Set();
37
+ for (const decl of ast.body || []) {
38
+ if (decl.type === "decl" && decl.owner && decl.valueType && classOwners.has(decl.valueType) && decl.valueType !== decl.owner) {
39
+ nestedTypes.add(decl.valueType);
40
+ }
41
+ }
42
+ const structRoots = [...classOwners].filter((name) => !nestedTypes.has(name));
43
+ const structModule = !classModule && ownedFns.length === 0 && structRoots.length > 0;
44
+
45
+ const localNames = new Set();
46
+ let selfOwner = null;
47
+
48
+ const isBareGlobal = (name) => {
49
+ return (
50
+ name === "print" ||
51
+ name === "warn" ||
52
+ name === "error" ||
53
+ name === "game" ||
54
+ name === "workspace" ||
55
+ name === "script" ||
56
+ name === "cout" ||
57
+ name === "cerr" ||
58
+ name === "endl" ||
59
+ isLibraryType(name) ||
60
+ isInstanceType(name) ||
61
+ isDatatype(name)
62
+ );
63
+ };
64
+
65
+ const emitSelfIdent = (name) => {
66
+ if (name === "this") {
67
+ return "self";
68
+ }
69
+ if (!selfOwner || localNames.has(name) || isBareGlobal(name)) {
70
+ return name;
71
+ }
72
+ if (classFields.has(name)) {
73
+ return `self.${name}`;
74
+ }
75
+ return name;
76
+ };
77
+
78
+ const emitMethodArg = (arg) => {
79
+ if (arg && arg.type === "ident" && selfOwner && classMethods.has(arg.name) && !localNames.has(arg.name)) {
80
+ return `function(...) self:${arg.name}(...) end`;
81
+ }
82
+ return emitExpr(arg);
83
+ };
84
+
85
+ const flattenShift = (node) => {
86
+ const parts = [];
87
+ let current = node;
88
+ while (current && current.type === "binary" && current.op === "<<") {
89
+ parts.unshift(current.right);
90
+ current = current.left;
91
+ }
92
+ return { stream: current, args: parts };
93
+ };
94
+
95
+ const isEndl = (node) => node && node.type === "ident" && node.name === "endl";
96
+
97
+ const streamPrint = (stream) => {
98
+ if (!stream) {
99
+ return null;
100
+ }
101
+ if (stream.type === "ident") {
102
+ if (stream.name === "cout") {
103
+ return "print";
104
+ }
105
+ if (stream.name === "cerr") {
106
+ return "warn";
107
+ }
108
+ return null;
109
+ }
110
+ if (stream.type === "member" && stream.object && stream.object.type === "ident" && stream.object.name === "cout") {
111
+ const mapped = { print: "print", warn: "warn", error: "error", ping: "print", endl: "print" };
112
+ return mapped[stream.name] || "print";
113
+ }
114
+ return null;
115
+ };
116
+
117
+ const emitCout = (node) => {
118
+ const { stream, args } = flattenShift(node);
119
+ const fn = streamPrint(stream);
120
+ if (!fn) {
121
+ return null;
122
+ }
123
+ const linesOut = [];
124
+ let current = [];
125
+ const flush = () => {
126
+ if (current.length === 0) {
127
+ return;
128
+ }
129
+ linesOut.push(`${fn}(${current.map(emitExpr).join(", ")})`);
130
+ current = [];
131
+ };
132
+ for (const arg of args) {
133
+ if (isEndl(arg)) {
134
+ flush();
135
+ continue;
136
+ }
137
+ current.push(arg);
138
+ }
139
+ flush();
140
+ if (linesOut.length === 0) {
141
+ linesOut.push(`${fn}()`);
142
+ }
143
+ return linesOut;
144
+ };
18
145
 
19
146
  const emitExpr = (node) => {
20
147
  if (!node) {
@@ -30,7 +157,7 @@ function emit(ast, options = {}) {
30
157
  case "string":
31
158
  return `"${node.value}"`;
32
159
  case "ident":
33
- return node.name;
160
+ return emitSelfIdent(node.name);
34
161
  case "initlist": {
35
162
  const entries = (node.fields || []).map((field) => `${field.name} = ${emitExpr(field.value)}`);
36
163
  if (entries.length === 0) {
@@ -57,19 +184,39 @@ function emit(ast, options = {}) {
57
184
  case "member":
58
185
  return `${emitExpr(node.object)}.${node.name}`;
59
186
  case "call": {
60
- const args = node.args.map(emitExpr).join(", ");
187
+ const args = node.args.map(emitMethodArg).join(", ");
61
188
  if (!node.object) {
62
189
  if (isDatatype(node.name)) {
63
190
  return `${node.name}.new(${args})`;
64
191
  }
192
+ if (selfOwner && classMethods.has(node.name) && !localNames.has(node.name)) {
193
+ return `self:${node.name}(${args})`;
194
+ }
65
195
  return `${node.name}(${args})`;
66
196
  }
67
197
  const obj = emitExpr(node.object);
198
+ if (node.access === "::" && node.object.type === "ident" && node.object.name === "cout") {
199
+ if (node.name === "endl") {
200
+ return "print()";
201
+ }
202
+ const mapped = { print: "print", warn: "warn", error: "error", ping: "print" };
203
+ return `${mapped[node.name] || "print"}(${args})`;
204
+ }
68
205
  if (node.access === "::") {
69
206
  if (MODULE_COLON.has(node.name)) {
70
207
  return `${obj}:${node.name}(${args})`;
71
208
  }
72
- return `${obj}.${node.name}(${args})`;
209
+ const staticNs =
210
+ node.object.type === "ident" &&
211
+ (isLibraryType(node.object.name) ||
212
+ isDatatype(node.object.name) ||
213
+ node.object.name === "DataService" ||
214
+ node.object.name === "FormatNumber" ||
215
+ node.object.name === "Enum");
216
+ if (staticNs) {
217
+ return `${obj}.${node.name}(${args})`;
218
+ }
219
+ return `${obj}:${node.name}(${args})`;
73
220
  }
74
221
  if (isDatatype(obj) || (node.object.type === "ident" && isDatatype(node.object.name))) {
75
222
  return `${obj}.${node.name}(${args})`;
@@ -80,6 +227,12 @@ function emit(ast, options = {}) {
80
227
  case "assign":
81
228
  return `${emitExpr(node.left)} = ${emitExpr(node.right)}`;
82
229
  case "binary": {
230
+ if (node.op === "<<") {
231
+ const printed = emitCout(node);
232
+ if (printed) {
233
+ return printed.join("; ");
234
+ }
235
+ }
83
236
  const ops = { "!=": "~=", "&&": "and", "||": "or" };
84
237
  const op = ops[node.op] || node.op;
85
238
  return `${emitExpr(node.left)} ${op} ${emitExpr(node.right)}`;
@@ -110,9 +263,24 @@ function emit(ast, options = {}) {
110
263
  const emitNewDecl = (node, indent) => {
111
264
  const prefix = indentOf(indent);
112
265
  const typeAnn = inferredType(node);
266
+ const created = node.value;
267
+ if (node.owner && indent === 0 && classModule) {
268
+ const value = node.value ? emitExpr(node.value) : "nil";
269
+ if (created && created.type === "new" && isInstanceType(created.className) && !isLibraryType(created.className)) {
270
+ const linesOut = [`${prefix}${node.owner}.${node.name} = Instance.new("${created.className}")`];
271
+ if (created.args[0]) {
272
+ linesOut.push(`${prefix}${node.owner}.${node.name}.Parent = ${emitExpr(created.args[0])}`);
273
+ }
274
+ return linesOut;
275
+ }
276
+ if (created && created.type === "new" && (isDatatype(created.className) || isLibraryType(created.className))) {
277
+ const args = created.args.map(emitExpr).join(", ");
278
+ return [`${prefix}${node.owner}.${node.name} = ${created.className}.new(${args})`];
279
+ }
280
+ return [`${prefix}${node.owner}.${node.name} = ${value}`];
281
+ }
113
282
  const kind = node.isConst ? "const" : "local";
114
283
  const typed = typeAnn ? `: ${typeAnn}` : "";
115
- const created = node.value;
116
284
  if (created && created.type === "new" && isInstanceType(created.className) && !isLibraryType(created.className)) {
117
285
  const linesOut = [`${prefix}${kind} ${node.name}${typed} = Instance.new("${created.className}")`];
118
286
  if (created.args[0]) {
@@ -132,9 +300,35 @@ function emit(ast, options = {}) {
132
300
  const prefix = indentOf(indent);
133
301
  switch (node.type) {
134
302
  case "decl":
303
+ if (node.name) {
304
+ localNames.add(node.name);
305
+ }
135
306
  return emitNewDecl(node, indent);
136
- case "expr":
137
- return [`${prefix}${emitExpr(node.expr)}`];
307
+ case "expr": {
308
+ const expr = node.expr;
309
+ if (expr && expr.type === "binary" && expr.op === "<<") {
310
+ const printed = emitCout(expr);
311
+ if (printed) {
312
+ return printed.map((line) => `${prefix}${line}`);
313
+ }
314
+ }
315
+ if (
316
+ expr &&
317
+ expr.type === "assign" &&
318
+ expr.right &&
319
+ expr.right.type === "new" &&
320
+ isInstanceType(expr.right.className) &&
321
+ !isLibraryType(expr.right.className)
322
+ ) {
323
+ const left = emitExpr(expr.left);
324
+ const linesOut = [`${prefix}${left} = Instance.new("${expr.right.className}")`];
325
+ if (expr.right.args[0]) {
326
+ linesOut.push(`${prefix}${left}.Parent = ${emitExpr(expr.right.args[0])}`);
327
+ }
328
+ return linesOut;
329
+ }
330
+ return [`${prefix}${emitExpr(expr)}`];
331
+ }
138
332
  case "return":
139
333
  return node.value ? [`${prefix}return ${emitExpr(node.value)}`] : [`${prefix}return`];
140
334
  case "if": {
@@ -167,6 +361,52 @@ function emit(ast, options = {}) {
167
361
  out.push(`${prefix}end`);
168
362
  return out;
169
363
  }
364
+ case "break":
365
+ return [`${prefix}break`];
366
+ case "switch": {
367
+ switchId += 1;
368
+ const id = `__switch${switchId}`;
369
+ const disc = node.discriminant;
370
+ const simple =
371
+ disc &&
372
+ (disc.type === "ident" || disc.type === "number" || disc.type === "string" || disc.type === "bool");
373
+ const subject = simple ? emitExpr(disc) : id;
374
+ const out = [`${prefix}repeat`];
375
+ const inner = indent + 1;
376
+ const innerP = indentOf(inner);
377
+ if (!simple) {
378
+ out.push(`${innerP}local ${id} = ${emitExpr(disc)}`);
379
+ }
380
+ const regular = (node.cases || []).filter((item) => !item.isDefault);
381
+ const fallback = (node.cases || []).find((item) => item.isDefault);
382
+ const testOf = (item) =>
383
+ (item.values || []).map((value) => `${subject} == ${emitExpr(value)}`).join(" or ");
384
+ for (let index = 0; index < regular.length; index += 1) {
385
+ const item = regular[index];
386
+ const keyword = index === 0 ? "if" : "elseif";
387
+ out.push(`${innerP}${keyword} ${testOf(item)} then`);
388
+ for (const stmt of item.body || []) {
389
+ out.push(...emitStmt(stmt, inner + 1));
390
+ }
391
+ }
392
+ if (fallback) {
393
+ if (regular.length === 0) {
394
+ for (const stmt of fallback.body || []) {
395
+ out.push(...emitStmt(stmt, inner));
396
+ }
397
+ } else {
398
+ out.push(`${innerP}else`);
399
+ for (const stmt of fallback.body || []) {
400
+ out.push(...emitStmt(stmt, inner + 1));
401
+ }
402
+ }
403
+ }
404
+ if (regular.length > 0) {
405
+ out.push(`${innerP}end`);
406
+ }
407
+ out.push(`${prefix}until true`);
408
+ return out;
409
+ }
170
410
  default:
171
411
  return [];
172
412
  }
@@ -191,25 +431,97 @@ function emit(ast, options = {}) {
191
431
  return `: ${typeAnn}`;
192
432
  };
193
433
 
434
+ const emitFieldLiteral = (decl) => {
435
+ if (decl.value && decl.value.type === "initlist") {
436
+ const inner = (decl.value.fields || [])
437
+ .map((field) => `${field.name} = ${emitExpr(field.value)}`)
438
+ .join(", ");
439
+ return `{ ${inner} }`;
440
+ }
441
+ if (decl.valueType && nestedTypes.has(decl.valueType)) {
442
+ const nested = (ast.body || []).filter((item) => item.type === "decl" && item.owner === decl.valueType);
443
+ const inner = nested.map((item) => `${item.name} = ${emitFieldLiteral(item)}`).join(", ");
444
+ return `{ ${inner} }`;
445
+ }
446
+ return decl.value ? emitExpr(decl.value) : "nil";
447
+ };
448
+
449
+ const emitStructConstructor = (root) => {
450
+ const fields = (ast.body || []).filter((decl) => decl.type === "decl" && decl.owner === root);
451
+ const inner = fields.map((field) => `\t\t${field.name} = ${emitFieldLiteral(field)},`).join("\n");
452
+ lines.push(`const function ${root}()`);
453
+ lines.push(" return {");
454
+ if (inner) {
455
+ lines.push(inner);
456
+ }
457
+ lines.push(" }");
458
+ lines.push("end");
459
+ lines.push("");
460
+ lines.push(`return ${root}`);
461
+ lines.push("");
462
+ };
463
+
464
+ if (classModule) {
465
+ for (const owner of classOwners) {
466
+ lines.push(`local ${owner} = {}`);
467
+ lines.push("");
468
+ }
469
+ }
470
+
471
+ if (structModule) {
472
+ for (const root of structRoots) {
473
+ emitStructConstructor(root);
474
+ }
475
+ return lines.join("\n");
476
+ }
477
+
194
478
  for (const decl of ast.body) {
195
479
  if (decl.type === "decl") {
196
480
  lines.push(...emitNewDecl(decl, 0));
197
481
  lines.push("");
198
482
  continue;
199
483
  }
484
+ if (decl.type === "expr") {
485
+ lines.push(emitExpr(decl.expr));
486
+ lines.push("");
487
+ continue;
488
+ }
200
489
  if (decl.type !== "function") {
201
490
  continue;
202
491
  }
203
- lines.push(`local function ${decl.name}(${paramList(decl)})${returnAnn(decl)}`);
492
+ selfOwner = decl.owner || null;
493
+ localNames.clear();
494
+ for (const param of decl.params || []) {
495
+ const paramName = typeof param === "string" ? param : param.name;
496
+ if (paramName) {
497
+ localNames.add(paramName);
498
+ }
499
+ }
500
+ if (decl.owner) {
501
+ lines.push(`function ${decl.owner}:${decl.name}(${paramList(decl)})${returnAnn(decl)}`);
502
+ } else {
503
+ lines.push(`const function ${decl.name}(${paramList(decl)})${returnAnn(decl)}`);
504
+ }
204
505
  for (const stmt of decl.body) {
205
506
  lines.push(...emitStmt(stmt, 1));
206
507
  }
207
508
  lines.push("end");
208
509
  lines.push("");
510
+ selfOwner = null;
511
+ localNames.clear();
209
512
  }
210
513
 
211
514
  const hasInit = ast.body.some((decl) => decl.type === "function" && decl.name === "init");
212
- if (hasInit && !options.skipInit) {
515
+ if (classModule) {
516
+ for (const owner of classOwners) {
517
+ if (hasInit) {
518
+ lines.push(`${owner}.Start = ${owner}.init`);
519
+ lines.push(`${owner}.Init = ${owner}.init`);
520
+ }
521
+ lines.push(`return ${owner}`);
522
+ lines.push("");
523
+ }
524
+ } else if (hasInit && !options.skipInit) {
213
525
  lines.push("init()");
214
526
  lines.push("");
215
527
  }