cluaupp 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2
4
+
5
+ - Watch does not write `out/` while any file fails to compile (Studio keeps the last good scripts).
6
+ - Watch/build skip rewriting Luau whose contents did not change (a space in C++ no longer floods Rojo).
7
+ - `libs/` is fill-only: missing files are restored, existing files are never overwritten or deleted. Fixes Rojo 7 crashing on `libs/ArrayIndexer` when watch/build raced a live serve.
8
+ - Dropped `$optional` from `default.project.json` (Rojo 7.7 failed to deserialize it).
9
+
10
+ ## 0.1.1
11
+
12
+ - DataService: `DataServiceOptions<T>.Template` is the required player-data table (`T` matches the save struct). Also documents `Exclude`.
13
+ - Compiler: C++ designated initializers (`Type { .Field = value }`) emit Luau tables, so `DataService.Server:Init({ Template = ... })` type-checks and compiles.
14
+ - Watch/build: delete orphaned `out/` files when a source is removed. Never wipe `out/` or `libs/` as a whole — Rojo 7 crashes if `libs/ArrayIndexer` disappears mid-serve. Vendor files are only created if missing; existing `libs/` is never overwritten on watch/build. Watch skips writing `out/` when compile fails, and skips rewriting Luau whose contents did not change.
15
+
3
16
  ## 0.1.0
4
17
 
5
18
  - Product name: **Cluaupp** (C++ × Luau)
package/docs/cli.md CHANGED
@@ -31,6 +31,8 @@ Transpiles `src/**/*.{cpp,h,hpp}` into `out/`. Script files become PascalCase **
31
31
  - `.h` / `.hpp` with a sibling `.cpp` are not emitted twice
32
32
  - `new Folder(parent)` in a stats system becomes `CacheController.Ensure`
33
33
  - A parse error exits with code 1 and `file:line:column`
34
+ - After emit, **orphans in `out/` are removed** (source deleted → matching Luau deleted). `out/` itself is never wiped, so a running Rojo serve keeps the live tree.
35
+ - `libs/` and `include/cluaupp` are synced by writing missing/changed files only. They are **never** deleted as a folder — Rojo 7 unwrap-crashes if `libs/ArrayIndexer` vanishes while serving.
34
36
 
35
37
  ```bash
36
38
  cluaupp build
@@ -39,7 +41,7 @@ cluaupp build ./my-game
39
41
 
40
42
  ## `cluaupp watch [folder]`
41
43
 
42
- Runs a `build` and rebuilds when a `.cpp`, `.h`, or `.hpp` under `src/` changes. Parse errors are printed; the watcher stays alive.
44
+ Runs a `build` and rebuilds when anything under `src/` changes, including deletes. Parse errors are printed; the watcher stays alive and **does not write `out/`** until the project compiles cleanly (Studio keeps the last good scripts). Rebuilds are debounced and do **not** recopy `libs/`. Deleted `.cpp` files prune their `out/` artifacts on the next successful compile.
43
45
 
44
46
  ## `cluaupp --version` / `cluaupp -v`
45
47
 
package/docs/libraries.md CHANGED
@@ -67,7 +67,10 @@ void init() {
67
67
  auto* coins = Net::Event("Coins");
68
68
  coins->On(OnCoins);
69
69
  print(FormatNumber::Abbreviate(1500));
70
- DataService::Set(GetService<Players>()->GetPlayers()[0], "Currencies.Coins", 10);
70
+ DataService::Server.Init(DataServiceOptions {
71
+ .Template = { .Coins = 0 },
72
+ .StoreName = "PlayerData",
73
+ });
71
74
  janitor->Add(coins);
72
75
  }
73
76
  ```
package/docs/syntax.md CHANGED
@@ -108,6 +108,25 @@ The only `for` accepted today is **range-for**: `for (auto* x : list)`. C-style
108
108
  | `obj->Method(a)` | `obj:Method(a)` if the method is a Roblox API |
109
109
  | `fn(a)` | `fn(a)` |
110
110
  | `signal.Connect(fn)` | `signal:Connect(fn)` |
111
+ | `Type { .Field = value }` | `{ Field = value }` |
112
+
113
+ Designated initializers become Luau tables. Nested braces work the same way:
114
+
115
+ ```cpp
116
+ DataService::Server.Init(DataServiceOptions {
117
+ .Template = playerData,
118
+ .StoreName = "PlayerData",
119
+ .UseMock = true,
120
+ });
121
+ ```
122
+
123
+ ```luau
124
+ DataService.Server:Init({
125
+ Template = playerData,
126
+ StoreName = "PlayerData",
127
+ UseMock = true,
128
+ })
129
+ ```
111
130
 
112
131
  ## `new` and services
113
132
 
@@ -34,10 +34,13 @@ struct DataServiceEnum {
34
34
  DataOrderList OrderList;
35
35
  };
36
36
 
37
+ template <typename T>
37
38
  struct DataServiceOptions {
39
+ T Template;
38
40
  string StoreName;
39
41
  bool UseMock;
40
42
  string KeyPrefix;
43
+ LuaArray<DataPath> Exclude;
41
44
  bool StrictPaths;
42
45
  bool AutoCreateMissingTables;
43
46
  };
@@ -112,7 +115,8 @@ public:
112
115
  class DataServiceServer {
113
116
  public:
114
117
  DataPath Paths;
115
- DataServiceServer* Init(DataServiceOptions options);
118
+ template <typename T>
119
+ DataServiceServer* Init(DataServiceOptions<T> options);
116
120
  Data* WaitFor(Player* player);
117
121
  Data* Get(Player* player);
118
122
  bool HasData(Player* player);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cluaupp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Cluaupp — the definitive merge of C++ and modern Luau. Source-to-source transpiler with first-class Roblox APIs.",
5
5
  "author": "KartzDev",
6
6
  "license": "MIT",
@@ -29,7 +29,7 @@
29
29
  "cluau": "node bin/cluaupp.js",
30
30
  "build": "node bin/cluaupp.js build",
31
31
  "watch": "node bin/cluaupp.js watch",
32
- "test": "node test/leaderstats.test.js && node test/understand.test.js && node test/datatypes.test.js && node test/headers.test.js && node test/libs.test.js && node test/runtime-libs.test.js && node test/intellisense.test.js",
32
+ "test": "node test/leaderstats.test.js && node test/understand.test.js && node test/datatypes.test.js && node test/headers.test.js && node test/libs.test.js && node test/runtime-libs.test.js && node test/intellisense.test.js && node test/dataservice.test.js && node test/prune.test.js && node test/hold.test.js",
33
33
  "generate-api": "node scripts/generate-api.js",
34
34
  "check-highlight": "node scripts/check-highlight.js",
35
35
  "vendor-libs": "node scripts/vendor-libs.js",
package/src/cli.js CHANGED
@@ -34,27 +34,72 @@ function copyDir(from, to) {
34
34
  }
35
35
  }
36
36
 
37
+ function tryRm(target) {
38
+ try {
39
+ fs.rmSync(target, { recursive: true, force: true });
40
+ return true;
41
+ } catch (err) {
42
+ if (err.code === "EPERM" || err.code === "EBUSY" || err.code === "ENOTEMPTY") {
43
+ console.error("cluaupp: skip remove", target, `(${err.code})`);
44
+ return false;
45
+ }
46
+ throw err;
47
+ }
48
+ }
49
+
50
+ function copyFileIfChanged(src, dest, mode = "fill") {
51
+ if (mode === "fill" && fs.existsSync(dest)) {
52
+ return;
53
+ }
54
+ if (mode === "update" && fs.existsSync(dest)) {
55
+ const from = fs.statSync(src);
56
+ const to = fs.statSync(dest);
57
+ if (from.size === to.size && from.mtimeMs <= to.mtimeMs) {
58
+ return;
59
+ }
60
+ }
61
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
62
+ try {
63
+ fs.copyFileSync(src, dest);
64
+ } catch (err) {
65
+ if (err.code === "EPERM" || err.code === "EBUSY") {
66
+ console.error("cluaupp: skip copy", dest, `(${err.code})`);
67
+ return;
68
+ }
69
+ throw err;
70
+ }
71
+ }
72
+
73
+ function syncDir(from, to, mode = "fill") {
74
+ if (!fs.existsSync(from)) {
75
+ return;
76
+ }
77
+ fs.mkdirSync(to, { recursive: true });
78
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
79
+ const src = path.join(from, entry.name);
80
+ const dest = path.join(to, entry.name);
81
+ if (entry.isDirectory()) {
82
+ syncDir(src, dest, mode);
83
+ } else {
84
+ copyFileIfChanged(src, dest, mode);
85
+ }
86
+ }
87
+ }
88
+
37
89
  function copyRuntime(dest) {
38
90
  const runtime = path.join(__dirname, "..", "runtime");
39
- const libs = path.join(dest, "libs");
40
- if (fs.existsSync(libs)) {
41
- fs.rmSync(libs, { recursive: true, force: true });
42
- }
43
- if (fs.existsSync(runtime)) {
44
- copyDir(runtime, libs);
91
+ if (!fs.existsSync(runtime)) {
92
+ return;
45
93
  }
94
+ syncDir(runtime, path.join(dest, "libs"));
46
95
  }
47
96
 
48
97
  function copyHeaders(dest) {
49
98
  const from = path.join(__dirname, "..", "include", "cluaupp");
50
- const to = path.join(dest, "include", "cluaupp");
51
99
  if (!fs.existsSync(from)) {
52
100
  return;
53
101
  }
54
- if (fs.existsSync(to)) {
55
- fs.rmSync(to, { recursive: true, force: true });
56
- }
57
- copyDir(from, to);
102
+ syncDir(from, path.join(dest, "include", "cluaupp"));
58
103
  }
59
104
 
60
105
  function loadConfig(root) {
@@ -83,6 +128,21 @@ function collectCpp(dir, files = []) {
83
128
  return files;
84
129
  }
85
130
 
131
+ function collectFiles(dir, files = []) {
132
+ if (!fs.existsSync(dir)) {
133
+ return files;
134
+ }
135
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
136
+ const full = path.join(dir, entry.name);
137
+ if (entry.isDirectory()) {
138
+ collectFiles(full, files);
139
+ } else {
140
+ files.push(full);
141
+ }
142
+ }
143
+ return files;
144
+ }
145
+
86
146
  function siblingCpp(file) {
87
147
  return file.replace(/\.(h|hpp|hh)$/i, ".cpp");
88
148
  }
@@ -92,29 +152,84 @@ function resolveKey(file) {
92
152
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
93
153
  }
94
154
 
95
- function build(root, options = {}) {
96
- const exitOnError = options.exitOnError !== false;
97
- const config = loadConfig(root);
98
- copyRuntime(root);
99
- copyHeaders(root);
100
- const files = collectCpp(path.join(root, config.rootDir));
101
- if (files.length === 0) {
102
- console.error("no .cpp/.h/.hpp files in", config.rootDir);
103
- if (exitOnError) {
104
- process.exit(1);
155
+ function posixRel(from, file) {
156
+ return path.relative(from, file).replace(/\\/g, "/");
157
+ }
158
+
159
+ function sourcePrefixes(rel) {
160
+ const noExt = rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, "");
161
+ const noTag = noExt.replace(/\.(server|client)$/i, "");
162
+ return [...new Set([noExt, noTag, toLuauPath(rel).replace(/\\/g, "/")])];
163
+ }
164
+
165
+ function matchesPrefix(relOut, prefixes) {
166
+ const n = relOut.replace(/\\/g, "/").toLowerCase();
167
+ return prefixes.some((prefix) => {
168
+ const k = prefix.replace(/\\/g, "/").toLowerCase();
169
+ return n === k || n === `${k}.luau` || n.startsWith(`${k}/`) || n.startsWith(`${k}.`);
170
+ });
171
+ }
172
+
173
+ function removeEmptyOutDirs(outDir, projectRoot) {
174
+ if (!fs.existsSync(outDir)) {
175
+ return;
176
+ }
177
+ const walk = (dir) => {
178
+ if (resolveKey(dir) === resolveKey(outDir)) {
179
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
180
+ if (entry.isDirectory()) {
181
+ walk(path.join(dir, entry.name));
182
+ }
183
+ }
184
+ return;
185
+ }
186
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
187
+ if (entry.isDirectory()) {
188
+ walk(path.join(dir, entry.name));
189
+ }
105
190
  }
191
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
192
+ if (tryRm(dir)) {
193
+ console.log("cluaupp: removed", path.relative(projectRoot, dir));
194
+ }
195
+ }
196
+ };
197
+ walk(outDir);
198
+ }
199
+
200
+ function pruneOut(root, config, written, failedPrefixes) {
201
+ const outDir = path.join(root, config.outDir);
202
+ if (!fs.existsSync(outDir)) {
106
203
  return;
107
204
  }
205
+ for (const file of collectFiles(outDir)) {
206
+ if (written.has(resolveKey(file))) {
207
+ continue;
208
+ }
209
+ const rel = posixRel(outDir, file);
210
+ if (matchesPrefix(rel, failedPrefixes)) {
211
+ continue;
212
+ }
213
+ if (tryRm(file)) {
214
+ console.log("cluaupp: removed", path.relative(root, file));
215
+ }
216
+ }
217
+ removeEmptyOutDirs(outDir, root);
218
+ }
108
219
 
220
+ function compileProject(root, config) {
221
+ const srcDir = path.join(root, config.rootDir);
222
+ const files = collectCpp(srcDir);
109
223
  const fileSet = new Set(files.map((file) => path.resolve(file)));
110
- let failed = 0;
111
- const written = new Set();
224
+ const jobs = [];
225
+ const errors = [];
226
+
112
227
  for (const file of files) {
113
228
  if (isHeaderFile(file) && fileSet.has(path.resolve(siblingCpp(file)))) {
114
229
  continue;
115
230
  }
116
231
  const source = fs.readFileSync(file, "utf8");
117
- const rel = path.relative(path.join(root, config.rootDir), file).replace(/\\/g, "/");
232
+ const rel = posixRel(srcDir, file);
118
233
  try {
119
234
  const result = compileService(source, rel, {
120
235
  ...config,
@@ -122,30 +237,100 @@ function build(root, options = {}) {
122
237
  relativeName: rel,
123
238
  outName: toLuauPath(rel),
124
239
  architecture: config.architecture !== false,
125
- includeDirs: [path.dirname(file), path.join(root, config.rootDir), path.join(root, "include")],
240
+ includeDirs: [path.dirname(file), srcDir, path.join(root, "include")],
126
241
  });
127
- for (const artifact of result.files) {
128
- const dest = path.join(root, config.outDir, artifact.name);
129
- fs.mkdirSync(path.dirname(dest), { recursive: true });
130
- fs.writeFileSync(dest, artifact.contents, "utf8");
131
- written.add(resolveKey(dest));
132
- console.log("cluaupp:", rel, "→", path.relative(root, dest));
242
+ jobs.push({ rel, files: result.files, stale: result.stale || [] });
243
+ } catch (err) {
244
+ errors.push({ rel, prefixes: sourcePrefixes(rel), message: err.message });
245
+ }
246
+ }
247
+
248
+ return { files, jobs, errors };
249
+ }
250
+
251
+ function writeTextIfChanged(dest, contents) {
252
+ if (fs.existsSync(dest) && fs.readFileSync(dest, "utf8") === contents) {
253
+ return false;
254
+ }
255
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
256
+ fs.writeFileSync(dest, contents, "utf8");
257
+ return true;
258
+ }
259
+
260
+ function writeJobs(root, config, jobs) {
261
+ const written = new Set();
262
+ for (const job of jobs) {
263
+ for (const artifact of job.files) {
264
+ const dest = path.join(root, config.outDir, artifact.name);
265
+ const changed = writeTextIfChanged(dest, artifact.contents);
266
+ written.add(resolveKey(dest));
267
+ if (changed) {
268
+ console.log("cluaupp:", job.rel, "→", path.relative(root, dest));
133
269
  }
134
- for (const stale of result.stale || []) {
135
- const dest = path.join(root, config.outDir, stale);
136
- if (fs.existsSync(dest) && !written.has(resolveKey(dest))) {
137
- fs.unlinkSync(dest);
270
+ }
271
+ for (const stale of job.stale) {
272
+ const dest = path.join(root, config.outDir, stale);
273
+ if (fs.existsSync(dest) && !written.has(resolveKey(dest))) {
274
+ if (tryRm(dest)) {
138
275
  console.log("cluaupp: removed", path.relative(root, dest));
139
276
  }
140
277
  }
141
- } catch (err) {
142
- failed += 1;
278
+ }
279
+ }
280
+ return written;
281
+ }
282
+
283
+ function libsPresent(root) {
284
+ const libs = path.join(root, "libs");
285
+ return fs.existsSync(libs) && fs.readdirSync(libs).length > 0;
286
+ }
287
+
288
+ function ensureVendor(root) {
289
+ copyRuntime(root);
290
+ copyHeaders(root);
291
+ }
292
+
293
+ function build(root, options = {}) {
294
+ const exitOnError = options.exitOnError !== false;
295
+ const holdOnError = options.holdOnError === true;
296
+ const syncVendor = options.syncVendor === true;
297
+ const config = loadConfig(root);
298
+ if (syncVendor) {
299
+ copyRuntime(root);
300
+ copyHeaders(root);
301
+ } else {
302
+ ensureVendor(root);
303
+ }
304
+ const srcDir = path.join(root, config.rootDir);
305
+ if (!fs.existsSync(srcDir) || collectCpp(srcDir).length === 0) {
306
+ console.error("no .cpp/.h/.hpp files in", config.rootDir);
307
+ if (!holdOnError) {
308
+ pruneOut(root, config, new Set(), []);
309
+ }
310
+ if (exitOnError) {
311
+ process.exit(1);
312
+ }
313
+ return { failed: 0, written: new Set() };
314
+ }
315
+
316
+ const compiled = compileProject(root, config);
317
+ if (compiled.errors.length > 0) {
318
+ for (const err of compiled.errors) {
143
319
  console.error(err.message);
144
320
  }
321
+ if (holdOnError) {
322
+ console.error(`cluaupp: out not updated (${compiled.errors.length} compile error${compiled.errors.length === 1 ? "" : "s"})`);
323
+ return { failed: compiled.errors.length, written: new Set() };
324
+ }
145
325
  }
146
- if (failed > 0 && exitOnError) {
326
+
327
+ const written = writeJobs(root, config, compiled.jobs);
328
+ const failedPrefixes = compiled.errors.flatMap((err) => err.prefixes);
329
+ pruneOut(root, config, written, failedPrefixes);
330
+ if (compiled.errors.length > 0 && exitOnError) {
147
331
  process.exit(1);
148
332
  }
333
+ return { failed: compiled.errors.length, written };
149
334
  }
150
335
 
151
336
  function init(dest) {
@@ -160,33 +345,62 @@ function init(dest) {
160
345
  }
161
346
 
162
347
  function watch(root) {
163
- build(root, { exitOnError: false });
348
+ build(root, { exitOnError: false, syncVendor: false, holdOnError: true });
164
349
  const config = loadConfig(root);
165
350
  const dir = path.join(root, config.rootDir);
166
- console.log("watching", dir);
167
- fs.watch(dir, { recursive: true }, (_event, filename) => {
168
- if (filename && isSourceFile(filename)) {
169
- try {
170
- build(root, { exitOnError: false });
171
- } catch (err) {
172
- console.error(err.message);
351
+ let timer = null;
352
+ let running = false;
353
+ let queued = false;
354
+
355
+ const run = () => {
356
+ if (running) {
357
+ queued = true;
358
+ return;
359
+ }
360
+ running = true;
361
+ try {
362
+ build(root, { exitOnError: false, syncVendor: false, holdOnError: true });
363
+ } catch (err) {
364
+ console.error(err.message);
365
+ } finally {
366
+ running = false;
367
+ if (queued) {
368
+ queued = false;
369
+ run();
173
370
  }
174
371
  }
372
+ };
373
+
374
+ console.log("watching", dir);
375
+ if (!fs.existsSync(dir)) {
376
+ fs.mkdirSync(dir, { recursive: true });
377
+ }
378
+ fs.watch(dir, { recursive: true }, () => {
379
+ clearTimeout(timer);
380
+ timer = setTimeout(run, 400);
175
381
  });
176
382
  }
177
383
 
178
- const args = process.argv.slice(2);
179
- const cmd = args[0] || "help";
180
- const cwd = process.cwd();
384
+ function dispatch(args) {
385
+ const cmd = args[0] || "help";
386
+ const cwd = process.cwd();
387
+ if (cmd === "init") {
388
+ init(path.resolve(cwd, args[1] || "."));
389
+ } else if (cmd === "build") {
390
+ build(path.resolve(cwd, args[1] || "."));
391
+ } else if (cmd === "watch") {
392
+ watch(path.resolve(cwd, args[1] || "."));
393
+ } else if (cmd === "--version" || cmd === "-v" || cmd === "version") {
394
+ console.log(pkg.version);
395
+ } else {
396
+ printHelp();
397
+ }
398
+ }
181
399
 
182
- if (cmd === "init") {
183
- init(path.resolve(cwd, args[1] || "."));
184
- } else if (cmd === "build") {
185
- build(path.resolve(cwd, args[1] || "."));
186
- } else if (cmd === "watch") {
187
- watch(path.resolve(cwd, args[1] || "."));
188
- } else if (cmd === "--version" || cmd === "-v" || cmd === "version") {
189
- console.log(pkg.version);
190
- } else {
191
- printHelp();
400
+ const launchedAsCli =
401
+ require.main && ["cli.js", "cluaupp.js", "cluau.js"].includes(path.basename(require.main.filename));
402
+ if (launchedAsCli) {
403
+ dispatch(process.argv.slice(2));
192
404
  }
405
+
406
+ module.exports = { build, watch, dispatch };
package/src/emit.js CHANGED
@@ -31,6 +31,13 @@ function emit(ast, options = {}) {
31
31
  return `"${node.value}"`;
32
32
  case "ident":
33
33
  return node.name;
34
+ case "initlist": {
35
+ const entries = (node.fields || []).map((field) => `${field.name} = ${emitExpr(field.value)}`);
36
+ if (entries.length === 0) {
37
+ return "{}";
38
+ }
39
+ return `{ ${entries.join(", ")} }`;
40
+ }
34
41
  case "unary": {
35
42
  const inner = emitExpr(node.argument);
36
43
  if (node.op === "!") {
package/src/parse.js CHANGED
@@ -84,6 +84,28 @@ function parse(source, fileName) {
84
84
  return args;
85
85
  }
86
86
 
87
+ function parseInitList() {
88
+ eat("op", "{");
89
+ const fields = [];
90
+ while (!at("op", "}") && !at("eof")) {
91
+ if (at("op", ".")) {
92
+ i += 1;
93
+ const name = eat("ident").value;
94
+ eat("op", "=");
95
+ fields.push({ name, value: parseExpr() });
96
+ } else {
97
+ throw error("expected designated initializer .Field = value");
98
+ }
99
+ if (at("op", ",")) {
100
+ i += 1;
101
+ } else {
102
+ break;
103
+ }
104
+ }
105
+ eat("op", "}");
106
+ return { type: "initlist", fields };
107
+ }
108
+
87
109
  function parsePrimary() {
88
110
  if (at("kw", "true") || at("kw", "false")) {
89
111
  return { type: "bool", value: eat("kw").value === "true" };
@@ -120,6 +142,9 @@ function parse(source, fileName) {
120
142
  eat("op", ")");
121
143
  return expr;
122
144
  }
145
+ if (at("op", "{")) {
146
+ return parseInitList();
147
+ }
123
148
  throw error("invalid expression");
124
149
  }
125
150
 
@@ -155,6 +180,10 @@ function parse(source, fileName) {
155
180
  node = { type: "call", object: null, name: node.name, args: parseArgs(), access: "." };
156
181
  continue;
157
182
  }
183
+ if (at("op", "{") && (node.type === "ident" || node.type === "member")) {
184
+ node = parseInitList();
185
+ continue;
186
+ }
158
187
  break;
159
188
  }
160
189
  return node;