@tilua/compiler 9.1.1 → 9.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -546,12 +546,12 @@ a script that requires any of them knows what it got.
546
546
  <!-- compatibility:start -->
547
547
  ## Compatibility
548
548
 
549
- `@tilua/compiler@9.1.1` works with these tilua packages. *Released with* is the
549
+ `@tilua/compiler@9.1.3` works with these tilua packages. *Released with* is the
550
550
  version it was built and tested against.
551
551
 
552
552
  | Package | Range | Released with | |
553
553
  | --- | --- | --- | --- |
554
- | `@tilua/parser` | `^9.1.1` | 9.1.1 | dependency |
554
+ | `@tilua/parser` | `^9.1.3` | 9.1.3 | dependency |
555
555
  <!-- compatibility:end -->
556
556
 
557
557
  ## Development
@@ -710,6 +710,8 @@ var Lowerer = class {
710
710
  receiverGlobal,
711
711
  argumentCount: args.length,
712
712
  at: this.callSite(name),
713
+ perFile: this.options.perFile ?? false,
714
+ report: (message) => this.diagnostics.push({ message, line: node.line.start, column: node.column.start }),
713
715
  get arguments() {
714
716
  return info();
715
717
  },
@@ -739,6 +741,8 @@ var Lowerer = class {
739
741
  options: lowerings[i].options ?? {},
740
742
  name,
741
743
  at: this.callSite(node.callee),
744
+ perFile: this.options.perFile ?? false,
745
+ report: (message) => this.diagnostics.push({ message, line: node.line.start, column: node.column.start }),
742
746
  get arguments() {
743
747
  return info();
744
748
  },
@@ -754,11 +758,16 @@ var Lowerer = class {
754
758
  if (!lowerings?.some((l) => l.plugin.globalValue)) return void 0;
755
759
  const name = this.globalName(node);
756
760
  if (name === void 0) return void 0;
761
+ const called = this.readingCallee;
757
762
  for (let i = lowerings.length - 1; i >= 0; i--) {
758
763
  const answer = lowerings[i].plugin.globalValue?.({
759
764
  options: lowerings[i].options ?? {},
760
765
  name,
761
766
  at: this.callSite(node),
767
+ perFile: this.options.perFile ?? false,
768
+ report: (message) => {
769
+ if (!called) this.diagnostics.push({ message, line: node.line.start, column: node.column.start });
770
+ },
762
771
  use: (runtime2) => this.loweringRuntime(i, runtime2)
763
772
  });
764
773
  if (answer !== void 0) return this.libraryExpression(answer, i, node);
@@ -2122,8 +2131,14 @@ end
2122
2131
  if (node.type === "CallExpression" && object2.type === "Identifier") {
2123
2132
  return this.link(node, this.reference(object2));
2124
2133
  }
2125
- return this.link(node, this.expression(object2));
2134
+ const callee = node.type === "CallExpression" && node.callee === object2;
2135
+ const reading = this.readingCallee;
2136
+ this.readingCallee = callee;
2137
+ const lowered = this.expression(object2);
2138
+ this.readingCallee = reading;
2139
+ return this.link(node, lowered);
2126
2140
  }
2141
+ readingCallee = false;
2127
2142
  /** A call is one value in tilua, and Luau would hand on every value one
2128
2143
  * returns in the last place of an argument list or a `return`: it is
2129
2144
  * kept to one there, `f((g()))`. Every value is taken only where the
@@ -2802,6 +2817,8 @@ import {
2802
2817
  analyzeScopes,
2803
2818
  analyzeTypes,
2804
2819
  moduleExports,
2820
+ classesOf,
2821
+ exportsFingerprint,
2805
2822
  resolveModulePath,
2806
2823
  resolveTypeLibraries,
2807
2824
  importExpressions,
@@ -3272,7 +3289,7 @@ async function bundle(options) {
3272
3289
  for (let n = 2; taken.has(pathKey(runtimeFile)); n++) runtimeFile = join(outDir, `_tilua${n}.luau`);
3273
3290
  for (let i = lowerings.length - 1; i >= 0 && !host; i--) {
3274
3291
  const { plugin: plugin3, options: own } = lowerings[i];
3275
- host = plugin3.files?.({ options: own ?? {}, directory: config?.directory ?? root, outDir, files: outputs, runtime: runtimeFile });
3292
+ host = plugin3.files?.({ options: own ?? {}, directory: config?.directory ?? root, rootDir, outDir, files: outputs, runtime: runtimeFile });
3276
3293
  }
3277
3294
  for (const problem of host?.problems ?? []) {
3278
3295
  diagnostics.push({
@@ -3315,6 +3332,7 @@ async function bundle(options) {
3315
3332
  const dynamicImports = /* @__PURE__ */ new Set();
3316
3333
  const lowered2 = lower(source.program, source.scopes, {
3317
3334
  names,
3335
+ perFile,
3318
3336
  types: analysis.types.get(file),
3319
3337
  lowerings,
3320
3338
  noLib: config?.noLib,
@@ -3493,6 +3511,27 @@ ${G}.arrays[${G}.scriptArgs] = true`).body.statements);
3493
3511
  files2.push({ path: runtimePath, code: `${runtimeCode}
3494
3512
  ${WRITTEN_BY_TILUA}: the runtime every file of the project requires.
3495
3513
  `, modules: [] });
3514
+ try {
3515
+ const more = await host?.output?.();
3516
+ for (const { file, code } of more?.files ?? []) {
3517
+ if (isOutside(outDir, resolve2(file))) return stop(`A library's file has to go inside outDir, not at ${file}`);
3518
+ files2.push({ path: relative(outDir, file).replace(/\\/g, "/"), code: `${code}
3519
+ ${WRITTEN_BY_TILUA}: a library's own file.
3520
+ `, modules: [] });
3521
+ }
3522
+ for (const problem of more?.problems ?? []) {
3523
+ diagnostics.push({
3524
+ file: problem.file ?? config?.path ?? anchor,
3525
+ message: problem.message,
3526
+ line: 1,
3527
+ column: 1,
3528
+ category: "config",
3529
+ ...problem.severity === "warning" && { severity: "warning" }
3530
+ });
3531
+ }
3532
+ } catch (error) {
3533
+ return stop(`A library could not write its files: ${error.message}`);
3534
+ }
3496
3535
  return { code: files2[0].code, files: files2, modules: moduleNames, diagnostics };
3497
3536
  }
3498
3537
  const several = mode !== "single";
@@ -4049,7 +4088,11 @@ ${several ? `
4049
4088
  })
4050
4089
  record = { loading = true, exports = exports }
4051
4090
  ${G}.records[name] = record
4052
- module.load(exports)
4091
+ -- A module runs once, so nothing keeps its body: anything can reach
4092
+ -- this table, and the body holds what the file kept to itself.
4093
+ local run = module.load
4094
+ module.load = nil
4095
+ run(exports)
4053
4096
  record.loading = false
4054
4097
  return exports
4055
4098
  end,
@@ -4142,6 +4185,7 @@ function parsedLibrary(file) {
4142
4185
  return program2;
4143
4186
  }
4144
4187
  var moduleAnalyses = /* @__PURE__ */ new Map();
4188
+ var MAX_CYCLE_PASSES = 6;
4145
4189
  function analyzeModules(modules, config) {
4146
4190
  const out = [];
4147
4191
  const analyses = /* @__PURE__ */ new Map();
@@ -4160,12 +4204,14 @@ function analyzeModules(modules, config) {
4160
4204
  const inProgress = /* @__PURE__ */ new Set();
4161
4205
  const analyzed = /* @__PURE__ */ new Map();
4162
4206
  let partials = 0;
4207
+ const cycles = /* @__PURE__ */ new Set();
4208
+ const provisional = /* @__PURE__ */ new Map();
4163
4209
  const readWhile = /* @__PURE__ */ new Map();
4164
4210
  const settingFor = `${libraries.files.join("|")}#${config?.directory ?? ""}#${JSON.stringify(config?.paths ?? {})}`;
4165
4211
  const analyzeModule = (file, program2) => {
4166
4212
  const done = analyzed.get(file);
4167
4213
  if (done) return done;
4168
- const remembered = moduleAnalyses.get(file);
4214
+ const remembered = cycles.size ? void 0 : moduleAnalyses.get(file);
4169
4215
  const script = scriptOf(file);
4170
4216
  const ownLibs = script ? [...libs, script] : libs;
4171
4217
  if (remembered && remembered.setting === settingFor && ownLibs.length === remembered.libs.length && remembered.libs.every((lib, i) => lib === ownLibs[i]) && remembered.program === program2 && [...remembered.read].every(([read3, stamp]) => stampOf(read3) === stamp)) {
@@ -4193,6 +4239,8 @@ function analyzeModules(modules, config) {
4193
4239
  const result2 = { types, diagnostics: said, scopes };
4194
4240
  if (partials === before) {
4195
4241
  analyzed.set(file, result2);
4242
+ }
4243
+ if (partials === before && !cycles.size) {
4196
4244
  const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
4197
4245
  const own = stampOf(file);
4198
4246
  if (own !== void 0) read3.set(file, own);
@@ -4215,7 +4263,8 @@ function analyzeModules(modules, config) {
4215
4263
  };
4216
4264
  if (inProgress.has(target)) {
4217
4265
  partials++;
4218
- return { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
4266
+ cycles.add(target);
4267
+ return provisional.get(target) ?? { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
4219
4268
  }
4220
4269
  const cached = exportsCache.get(target);
4221
4270
  if (cached) {
@@ -4242,11 +4291,6 @@ function analyzeModules(modules, config) {
4242
4291
  inProgress.delete(target);
4243
4292
  }
4244
4293
  };
4245
- for (const module of modules) {
4246
- const { types, diagnostics } = analyzeModule(module.file, module.program);
4247
- analyses.set(module.file, types);
4248
- out.push(...diagnostics);
4249
- }
4250
4294
  const exportsOf = (file) => {
4251
4295
  const cached = exportsCache.get(file);
4252
4296
  if (cached) return cached;
@@ -4256,6 +4300,30 @@ function analyzeModules(modules, config) {
4256
4300
  exportsCache.set(file, exports);
4257
4301
  return exports;
4258
4302
  };
4303
+ const reported = out.length;
4304
+ const libraryClasses = classesOf(libs);
4305
+ const fingerprints = /* @__PURE__ */ new Map();
4306
+ for (let pass = 1; ; pass++) {
4307
+ out.length = reported;
4308
+ for (const module of modules) {
4309
+ const { types, diagnostics } = analyzeModule(module.file, module.program);
4310
+ analyses.set(module.file, types);
4311
+ out.push(...diagnostics);
4312
+ }
4313
+ if (!cycles.size || pass === MAX_CYCLE_PASSES) break;
4314
+ let settled = pass > 1;
4315
+ for (const file of cycles) {
4316
+ const exports = byFile.has(file) ? exportsOf(file) : exportsCache.get(file);
4317
+ if (exports && !exports.partial) provisional.set(file, exports);
4318
+ const fingerprint = exports && !exports.partial ? exportsFingerprint(exports, libraryClasses) : void 0;
4319
+ if (fingerprint === void 0 || fingerprint !== fingerprints.get(file)) settled = false;
4320
+ fingerprints.set(file, fingerprint);
4321
+ }
4322
+ if (settled) break;
4323
+ analyzed.clear();
4324
+ exportsCache.clear();
4325
+ readWhile.clear();
4326
+ }
4259
4327
  return { types: analyses, diagnostics: out, lowerings: [...libraries.lowerings], exportsOf };
4260
4328
  }
4261
4329
  function allStatements(statements) {
package/dist/cli.cjs CHANGED
@@ -727,6 +727,8 @@ var Lowerer = class {
727
727
  receiverGlobal,
728
728
  argumentCount: args2.length,
729
729
  at: this.callSite(name),
730
+ perFile: this.options.perFile ?? false,
731
+ report: (message) => this.diagnostics.push({ message, line: node.line.start, column: node.column.start }),
730
732
  get arguments() {
731
733
  return info();
732
734
  },
@@ -756,6 +758,8 @@ var Lowerer = class {
756
758
  options: lowerings[i].options ?? {},
757
759
  name,
758
760
  at: this.callSite(node.callee),
761
+ perFile: this.options.perFile ?? false,
762
+ report: (message) => this.diagnostics.push({ message, line: node.line.start, column: node.column.start }),
759
763
  get arguments() {
760
764
  return info();
761
765
  },
@@ -771,11 +775,16 @@ var Lowerer = class {
771
775
  if (!lowerings?.some((l) => l.plugin.globalValue)) return void 0;
772
776
  const name = this.globalName(node);
773
777
  if (name === void 0) return void 0;
778
+ const called = this.readingCallee;
774
779
  for (let i = lowerings.length - 1; i >= 0; i--) {
775
780
  const answer = lowerings[i].plugin.globalValue?.({
776
781
  options: lowerings[i].options ?? {},
777
782
  name,
778
783
  at: this.callSite(node),
784
+ perFile: this.options.perFile ?? false,
785
+ report: (message) => {
786
+ if (!called) this.diagnostics.push({ message, line: node.line.start, column: node.column.start });
787
+ },
779
788
  use: (runtime2) => this.loweringRuntime(i, runtime2)
780
789
  });
781
790
  if (answer !== void 0) return this.libraryExpression(answer, i, node);
@@ -2139,8 +2148,14 @@ end
2139
2148
  if (node.type === "CallExpression" && object2.type === "Identifier") {
2140
2149
  return this.link(node, this.reference(object2));
2141
2150
  }
2142
- return this.link(node, this.expression(object2));
2151
+ const callee = node.type === "CallExpression" && node.callee === object2;
2152
+ const reading = this.readingCallee;
2153
+ this.readingCallee = callee;
2154
+ const lowered = this.expression(object2);
2155
+ this.readingCallee = reading;
2156
+ return this.link(node, lowered);
2143
2157
  }
2158
+ readingCallee = false;
2144
2159
  /** A call is one value in tilua, and Luau would hand on every value one
2145
2160
  * returns in the last place of an argument list or a `return`: it is
2146
2161
  * kept to one there, `f((g()))`. Every value is taken only where the
@@ -3265,7 +3280,7 @@ async function bundle(options) {
3265
3280
  for (let n = 2; taken.has(pathKey(runtimeFile)); n++) runtimeFile = (0, import_node_path2.join)(outDir, `_tilua${n}.luau`);
3266
3281
  for (let i = lowerings.length - 1; i >= 0 && !host; i--) {
3267
3282
  const { plugin: plugin3, options: own } = lowerings[i];
3268
- host = plugin3.files?.({ options: own ?? {}, directory: config2?.directory ?? root, outDir, files: outputs, runtime: runtimeFile });
3283
+ host = plugin3.files?.({ options: own ?? {}, directory: config2?.directory ?? root, rootDir, outDir, files: outputs, runtime: runtimeFile });
3269
3284
  }
3270
3285
  for (const problem of host?.problems ?? []) {
3271
3286
  diagnostics.push({
@@ -3308,6 +3323,7 @@ async function bundle(options) {
3308
3323
  const dynamicImports = /* @__PURE__ */ new Set();
3309
3324
  const lowered2 = lower(source.program, source.scopes, {
3310
3325
  names,
3326
+ perFile,
3311
3327
  types: analysis.types.get(file),
3312
3328
  lowerings,
3313
3329
  noLib: config2?.noLib,
@@ -3486,6 +3502,27 @@ ${G}.arrays[${G}.scriptArgs] = true`).body.statements);
3486
3502
  files2.push({ path: runtimePath, code: `${runtimeCode}
3487
3503
  ${WRITTEN_BY_TILUA}: the runtime every file of the project requires.
3488
3504
  `, modules: [] });
3505
+ try {
3506
+ const more = await host?.output?.();
3507
+ for (const { file, code } of more?.files ?? []) {
3508
+ if (isOutside(outDir, (0, import_node_path2.resolve)(file))) return stop(`A library's file has to go inside outDir, not at ${file}`);
3509
+ files2.push({ path: (0, import_node_path2.relative)(outDir, file).replace(/\\/g, "/"), code: `${code}
3510
+ ${WRITTEN_BY_TILUA}: a library's own file.
3511
+ `, modules: [] });
3512
+ }
3513
+ for (const problem of more?.problems ?? []) {
3514
+ diagnostics.push({
3515
+ file: problem.file ?? config2?.path ?? anchor,
3516
+ message: problem.message,
3517
+ line: 1,
3518
+ column: 1,
3519
+ category: "config",
3520
+ ...problem.severity === "warning" && { severity: "warning" }
3521
+ });
3522
+ }
3523
+ } catch (error) {
3524
+ return stop(`A library could not write its files: ${error.message}`);
3525
+ }
3489
3526
  return { code: files2[0].code, files: files2, modules: moduleNames, diagnostics };
3490
3527
  }
3491
3528
  const several = mode !== "single";
@@ -4042,7 +4079,11 @@ ${several ? `
4042
4079
  })
4043
4080
  record = { loading = true, exports = exports }
4044
4081
  ${G}.records[name] = record
4045
- module.load(exports)
4082
+ -- A module runs once, so nothing keeps its body: anything can reach
4083
+ -- this table, and the body holds what the file kept to itself.
4084
+ local run = module.load
4085
+ module.load = nil
4086
+ run(exports)
4046
4087
  record.loading = false
4047
4088
  return exports
4048
4089
  end,
@@ -4135,6 +4176,7 @@ function parsedLibrary(file) {
4135
4176
  return program2;
4136
4177
  }
4137
4178
  var moduleAnalyses = /* @__PURE__ */ new Map();
4179
+ var MAX_CYCLE_PASSES = 6;
4138
4180
  function analyzeModules(modules, config2) {
4139
4181
  const out2 = [];
4140
4182
  const analyses = /* @__PURE__ */ new Map();
@@ -4153,12 +4195,14 @@ function analyzeModules(modules, config2) {
4153
4195
  const inProgress = /* @__PURE__ */ new Set();
4154
4196
  const analyzed = /* @__PURE__ */ new Map();
4155
4197
  let partials = 0;
4198
+ const cycles = /* @__PURE__ */ new Set();
4199
+ const provisional = /* @__PURE__ */ new Map();
4156
4200
  const readWhile = /* @__PURE__ */ new Map();
4157
4201
  const settingFor = `${libraries.files.join("|")}#${config2?.directory ?? ""}#${JSON.stringify(config2?.paths ?? {})}`;
4158
4202
  const analyzeModule = (file, program2) => {
4159
4203
  const done = analyzed.get(file);
4160
4204
  if (done) return done;
4161
- const remembered = moduleAnalyses.get(file);
4205
+ const remembered = cycles.size ? void 0 : moduleAnalyses.get(file);
4162
4206
  const script = scriptOf(file);
4163
4207
  const ownLibs = script ? [...libs, script] : libs;
4164
4208
  if (remembered && remembered.setting === settingFor && ownLibs.length === remembered.libs.length && remembered.libs.every((lib, i) => lib === ownLibs[i]) && remembered.program === program2 && [...remembered.read].every(([read3, stamp]) => stampOf(read3) === stamp)) {
@@ -4186,6 +4230,8 @@ function analyzeModules(modules, config2) {
4186
4230
  const result2 = { types, diagnostics: said, scopes };
4187
4231
  if (partials === before) {
4188
4232
  analyzed.set(file, result2);
4233
+ }
4234
+ if (partials === before && !cycles.size) {
4189
4235
  const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
4190
4236
  const own = stampOf(file);
4191
4237
  if (own !== void 0) read3.set(file, own);
@@ -4208,7 +4254,8 @@ function analyzeModules(modules, config2) {
4208
4254
  };
4209
4255
  if (inProgress.has(target)) {
4210
4256
  partials++;
4211
- return { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
4257
+ cycles.add(target);
4258
+ return provisional.get(target) ?? { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
4212
4259
  }
4213
4260
  const cached = exportsCache.get(target);
4214
4261
  if (cached) {
@@ -4235,11 +4282,6 @@ function analyzeModules(modules, config2) {
4235
4282
  inProgress.delete(target);
4236
4283
  }
4237
4284
  };
4238
- for (const module2 of modules) {
4239
- const { types, diagnostics } = analyzeModule(module2.file, module2.program);
4240
- analyses.set(module2.file, types);
4241
- out2.push(...diagnostics);
4242
- }
4243
4285
  const exportsOf = (file) => {
4244
4286
  const cached = exportsCache.get(file);
4245
4287
  if (cached) return cached;
@@ -4249,6 +4291,30 @@ function analyzeModules(modules, config2) {
4249
4291
  exportsCache.set(file, exports2);
4250
4292
  return exports2;
4251
4293
  };
4294
+ const reported = out2.length;
4295
+ const libraryClasses = (0, import_parser7.classesOf)(libs);
4296
+ const fingerprints = /* @__PURE__ */ new Map();
4297
+ for (let pass = 1; ; pass++) {
4298
+ out2.length = reported;
4299
+ for (const module2 of modules) {
4300
+ const { types, diagnostics } = analyzeModule(module2.file, module2.program);
4301
+ analyses.set(module2.file, types);
4302
+ out2.push(...diagnostics);
4303
+ }
4304
+ if (!cycles.size || pass === MAX_CYCLE_PASSES) break;
4305
+ let settled = pass > 1;
4306
+ for (const file of cycles) {
4307
+ const exports2 = byFile.has(file) ? exportsOf(file) : exportsCache.get(file);
4308
+ if (exports2 && !exports2.partial) provisional.set(file, exports2);
4309
+ const fingerprint = exports2 && !exports2.partial ? (0, import_parser7.exportsFingerprint)(exports2, libraryClasses) : void 0;
4310
+ if (fingerprint === void 0 || fingerprint !== fingerprints.get(file)) settled = false;
4311
+ fingerprints.set(file, fingerprint);
4312
+ }
4313
+ if (settled) break;
4314
+ analyzed.clear();
4315
+ exportsCache.clear();
4316
+ readWhile.clear();
4317
+ }
4252
4318
  return { types: analyses, diagnostics: out2, lowerings: [...libraries.lowerings], exportsOf };
4253
4319
  }
4254
4320
  function allStatements(statements) {
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  WRITTEN_BY_TILUA,
4
4
  bundle,
5
5
  resolveConfig
6
- } from "./chunk-3RJVN4KC.js";
6
+ } from "./chunk-YWRP7SXR.js";
7
7
 
8
8
  // src/cli.ts
9
9
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, watch, writeFileSync } from "fs";
package/dist/index.cjs CHANGED
@@ -751,6 +751,8 @@ var Lowerer = class {
751
751
  receiverGlobal,
752
752
  argumentCount: args.length,
753
753
  at: this.callSite(name),
754
+ perFile: this.options.perFile ?? false,
755
+ report: (message) => this.diagnostics.push({ message, line: node.line.start, column: node.column.start }),
754
756
  get arguments() {
755
757
  return info();
756
758
  },
@@ -780,6 +782,8 @@ var Lowerer = class {
780
782
  options: lowerings[i].options ?? {},
781
783
  name,
782
784
  at: this.callSite(node.callee),
785
+ perFile: this.options.perFile ?? false,
786
+ report: (message) => this.diagnostics.push({ message, line: node.line.start, column: node.column.start }),
783
787
  get arguments() {
784
788
  return info();
785
789
  },
@@ -795,11 +799,16 @@ var Lowerer = class {
795
799
  if (!lowerings?.some((l) => l.plugin.globalValue)) return void 0;
796
800
  const name = this.globalName(node);
797
801
  if (name === void 0) return void 0;
802
+ const called = this.readingCallee;
798
803
  for (let i = lowerings.length - 1; i >= 0; i--) {
799
804
  const answer = lowerings[i].plugin.globalValue?.({
800
805
  options: lowerings[i].options ?? {},
801
806
  name,
802
807
  at: this.callSite(node),
808
+ perFile: this.options.perFile ?? false,
809
+ report: (message) => {
810
+ if (!called) this.diagnostics.push({ message, line: node.line.start, column: node.column.start });
811
+ },
803
812
  use: (runtime2) => this.loweringRuntime(i, runtime2)
804
813
  });
805
814
  if (answer !== void 0) return this.libraryExpression(answer, i, node);
@@ -2163,8 +2172,14 @@ end
2163
2172
  if (node.type === "CallExpression" && object2.type === "Identifier") {
2164
2173
  return this.link(node, this.reference(object2));
2165
2174
  }
2166
- return this.link(node, this.expression(object2));
2175
+ const callee = node.type === "CallExpression" && node.callee === object2;
2176
+ const reading = this.readingCallee;
2177
+ this.readingCallee = callee;
2178
+ const lowered = this.expression(object2);
2179
+ this.readingCallee = reading;
2180
+ return this.link(node, lowered);
2167
2181
  }
2182
+ readingCallee = false;
2168
2183
  /** A call is one value in tilua, and Luau would hand on every value one
2169
2184
  * returns in the last place of an argument list or a `return`: it is
2170
2185
  * kept to one there, `f((g()))`. Every value is taken only where the
@@ -3289,7 +3304,7 @@ async function bundle(options) {
3289
3304
  for (let n = 2; taken.has(pathKey(runtimeFile)); n++) runtimeFile = (0, import_node_path2.join)(outDir, `_tilua${n}.luau`);
3290
3305
  for (let i = lowerings.length - 1; i >= 0 && !host; i--) {
3291
3306
  const { plugin: plugin3, options: own } = lowerings[i];
3292
- host = plugin3.files?.({ options: own ?? {}, directory: config?.directory ?? root, outDir, files: outputs, runtime: runtimeFile });
3307
+ host = plugin3.files?.({ options: own ?? {}, directory: config?.directory ?? root, rootDir, outDir, files: outputs, runtime: runtimeFile });
3293
3308
  }
3294
3309
  for (const problem of host?.problems ?? []) {
3295
3310
  diagnostics.push({
@@ -3332,6 +3347,7 @@ async function bundle(options) {
3332
3347
  const dynamicImports = /* @__PURE__ */ new Set();
3333
3348
  const lowered2 = lower(source.program, source.scopes, {
3334
3349
  names,
3350
+ perFile,
3335
3351
  types: analysis.types.get(file),
3336
3352
  lowerings,
3337
3353
  noLib: config?.noLib,
@@ -3510,6 +3526,27 @@ ${G}.arrays[${G}.scriptArgs] = true`).body.statements);
3510
3526
  files2.push({ path: runtimePath, code: `${runtimeCode}
3511
3527
  ${WRITTEN_BY_TILUA}: the runtime every file of the project requires.
3512
3528
  `, modules: [] });
3529
+ try {
3530
+ const more = await host?.output?.();
3531
+ for (const { file, code } of more?.files ?? []) {
3532
+ if (isOutside(outDir, (0, import_node_path2.resolve)(file))) return stop(`A library's file has to go inside outDir, not at ${file}`);
3533
+ files2.push({ path: (0, import_node_path2.relative)(outDir, file).replace(/\\/g, "/"), code: `${code}
3534
+ ${WRITTEN_BY_TILUA}: a library's own file.
3535
+ `, modules: [] });
3536
+ }
3537
+ for (const problem of more?.problems ?? []) {
3538
+ diagnostics.push({
3539
+ file: problem.file ?? config?.path ?? anchor,
3540
+ message: problem.message,
3541
+ line: 1,
3542
+ column: 1,
3543
+ category: "config",
3544
+ ...problem.severity === "warning" && { severity: "warning" }
3545
+ });
3546
+ }
3547
+ } catch (error) {
3548
+ return stop(`A library could not write its files: ${error.message}`);
3549
+ }
3513
3550
  return { code: files2[0].code, files: files2, modules: moduleNames, diagnostics };
3514
3551
  }
3515
3552
  const several = mode !== "single";
@@ -4066,7 +4103,11 @@ ${several ? `
4066
4103
  })
4067
4104
  record = { loading = true, exports = exports }
4068
4105
  ${G}.records[name] = record
4069
- module.load(exports)
4106
+ -- A module runs once, so nothing keeps its body: anything can reach
4107
+ -- this table, and the body holds what the file kept to itself.
4108
+ local run = module.load
4109
+ module.load = nil
4110
+ run(exports)
4070
4111
  record.loading = false
4071
4112
  return exports
4072
4113
  end,
@@ -4159,6 +4200,7 @@ function parsedLibrary(file) {
4159
4200
  return program2;
4160
4201
  }
4161
4202
  var moduleAnalyses = /* @__PURE__ */ new Map();
4203
+ var MAX_CYCLE_PASSES = 6;
4162
4204
  function analyzeModules(modules, config) {
4163
4205
  const out = [];
4164
4206
  const analyses = /* @__PURE__ */ new Map();
@@ -4177,12 +4219,14 @@ function analyzeModules(modules, config) {
4177
4219
  const inProgress = /* @__PURE__ */ new Set();
4178
4220
  const analyzed = /* @__PURE__ */ new Map();
4179
4221
  let partials = 0;
4222
+ const cycles = /* @__PURE__ */ new Set();
4223
+ const provisional = /* @__PURE__ */ new Map();
4180
4224
  const readWhile = /* @__PURE__ */ new Map();
4181
4225
  const settingFor = `${libraries.files.join("|")}#${config?.directory ?? ""}#${JSON.stringify(config?.paths ?? {})}`;
4182
4226
  const analyzeModule = (file, program2) => {
4183
4227
  const done = analyzed.get(file);
4184
4228
  if (done) return done;
4185
- const remembered = moduleAnalyses.get(file);
4229
+ const remembered = cycles.size ? void 0 : moduleAnalyses.get(file);
4186
4230
  const script = scriptOf(file);
4187
4231
  const ownLibs = script ? [...libs, script] : libs;
4188
4232
  if (remembered && remembered.setting === settingFor && ownLibs.length === remembered.libs.length && remembered.libs.every((lib, i) => lib === ownLibs[i]) && remembered.program === program2 && [...remembered.read].every(([read3, stamp]) => stampOf(read3) === stamp)) {
@@ -4210,6 +4254,8 @@ function analyzeModules(modules, config) {
4210
4254
  const result2 = { types, diagnostics: said, scopes };
4211
4255
  if (partials === before) {
4212
4256
  analyzed.set(file, result2);
4257
+ }
4258
+ if (partials === before && !cycles.size) {
4213
4259
  const read3 = readWhile.get(file) ?? /* @__PURE__ */ new Map();
4214
4260
  const own = stampOf(file);
4215
4261
  if (own !== void 0) read3.set(file, own);
@@ -4232,7 +4278,8 @@ function analyzeModules(modules, config) {
4232
4278
  };
4233
4279
  if (inProgress.has(target)) {
4234
4280
  partials++;
4235
- return { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
4281
+ cycles.add(target);
4282
+ return provisional.get(target) ?? { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
4236
4283
  }
4237
4284
  const cached = exportsCache.get(target);
4238
4285
  if (cached) {
@@ -4259,11 +4306,6 @@ function analyzeModules(modules, config) {
4259
4306
  inProgress.delete(target);
4260
4307
  }
4261
4308
  };
4262
- for (const module2 of modules) {
4263
- const { types, diagnostics } = analyzeModule(module2.file, module2.program);
4264
- analyses.set(module2.file, types);
4265
- out.push(...diagnostics);
4266
- }
4267
4309
  const exportsOf = (file) => {
4268
4310
  const cached = exportsCache.get(file);
4269
4311
  if (cached) return cached;
@@ -4273,6 +4315,30 @@ function analyzeModules(modules, config) {
4273
4315
  exportsCache.set(file, exports2);
4274
4316
  return exports2;
4275
4317
  };
4318
+ const reported = out.length;
4319
+ const libraryClasses = (0, import_parser7.classesOf)(libs);
4320
+ const fingerprints = /* @__PURE__ */ new Map();
4321
+ for (let pass = 1; ; pass++) {
4322
+ out.length = reported;
4323
+ for (const module2 of modules) {
4324
+ const { types, diagnostics } = analyzeModule(module2.file, module2.program);
4325
+ analyses.set(module2.file, types);
4326
+ out.push(...diagnostics);
4327
+ }
4328
+ if (!cycles.size || pass === MAX_CYCLE_PASSES) break;
4329
+ let settled = pass > 1;
4330
+ for (const file of cycles) {
4331
+ const exports2 = byFile.has(file) ? exportsOf(file) : exportsCache.get(file);
4332
+ if (exports2 && !exports2.partial) provisional.set(file, exports2);
4333
+ const fingerprint = exports2 && !exports2.partial ? (0, import_parser7.exportsFingerprint)(exports2, libraryClasses) : void 0;
4334
+ if (fingerprint === void 0 || fingerprint !== fingerprints.get(file)) settled = false;
4335
+ fingerprints.set(file, fingerprint);
4336
+ }
4337
+ if (settled) break;
4338
+ analyzed.clear();
4339
+ exportsCache.clear();
4340
+ readWhile.clear();
4341
+ }
4276
4342
  return { types: analyses, diagnostics: out, lowerings: [...libraries.lowerings], exportsOf };
4277
4343
  }
4278
4344
  function allStatements(statements) {
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as T from '@tilua/parser';
2
2
  import { OutputMode, TiluaConfig, ConfigProblem, LoweringPlugin, TypeAnalysis, ScopeAnalysis } from '@tilua/parser';
3
- export { EntryExports, EntryExportsLowering, FilesAnswer, FilesLayout, LoweringPlugin, LuaSyntax, MethodCall, MethodLowering, PluginContext } from '@tilua/parser';
3
+ export { EntryExports, EntryExportsLowering, FilesAnswer, FilesLayout, FilesOutput, FilesProblem, LoweringPlugin, LuaSyntax, MethodCall, MethodLowering, PluginContext } from '@tilua/parser';
4
4
  import * as L from 'luau-parser';
5
5
 
6
6
  /** What `tilua.config.json` holds. */
@@ -264,6 +264,8 @@ interface ModuleContext {
264
264
  readonly arrays?: string;
265
265
  }
266
266
  interface LowerOptions {
267
+ /** The build is `"output": "files"`, as a library is told. */
268
+ readonly perFile?: boolean;
267
269
  /** Lower the file as a module of a bundle. Without it, `import` and
268
270
  * `export` are errors. */
269
271
  readonly module?: ModuleContext;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as T from '@tilua/parser';
2
2
  import { OutputMode, TiluaConfig, ConfigProblem, LoweringPlugin, TypeAnalysis, ScopeAnalysis } from '@tilua/parser';
3
- export { EntryExports, EntryExportsLowering, FilesAnswer, FilesLayout, LoweringPlugin, LuaSyntax, MethodCall, MethodLowering, PluginContext } from '@tilua/parser';
3
+ export { EntryExports, EntryExportsLowering, FilesAnswer, FilesLayout, FilesOutput, FilesProblem, LoweringPlugin, LuaSyntax, MethodCall, MethodLowering, PluginContext } from '@tilua/parser';
4
4
  import * as L from 'luau-parser';
5
5
 
6
6
  /** What `tilua.config.json` holds. */
@@ -264,6 +264,8 @@ interface ModuleContext {
264
264
  readonly arrays?: string;
265
265
  }
266
266
  interface LowerOptions {
267
+ /** The build is `"output": "files"`, as a library is told. */
268
+ readonly perFile?: boolean;
267
269
  /** Lower the file as a module of a bundle. Without it, `import` and
268
270
  * `export` are errors. */
269
271
  readonly module?: ModuleContext;
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  program,
9
9
  resolveConfig,
10
10
  syntaxOf
11
- } from "./chunk-3RJVN4KC.js";
11
+ } from "./chunk-YWRP7SXR.js";
12
12
 
13
13
  // src/compile.ts
14
14
  import { readFileSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tilua/compiler",
3
- "version": "9.1.1",
3
+ "version": "9.1.3",
4
4
  "description": "Compiles tilua to Luau: lowers the @tilua/parser AST to a luau-parser AST and prints it",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -37,7 +37,7 @@
37
37
  "license": "ISC",
38
38
  "type": "module",
39
39
  "dependencies": {
40
- "@tilua/parser": "^9.1.1",
40
+ "@tilua/parser": "^9.1.3",
41
41
  "luau-parser": "^1.1.0"
42
42
  },
43
43
  "devDependencies": {