@thigasdevelopment/luam 0.19.1 → 0.19.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/luam.mjs CHANGED
@@ -1644,8 +1644,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
1644
1644
  * @param {boolean} [combine] - if `true` or omitted, an optional value can be specified directly after the flag.
1645
1645
  * @return {Command} `this` command for chaining
1646
1646
  */
1647
- combineFlagAndOptionalValue(combine = true) {
1648
- this._combineFlagAndOptionalValue = !!combine;
1647
+ combineFlagAndOptionalValue(combine2 = true) {
1648
+ this._combineFlagAndOptionalValue = !!combine2;
1649
1649
  return this;
1650
1650
  }
1651
1651
  /**
@@ -3143,6 +3143,7 @@ var DEFAULT_ENVIRONMENT_FILE = ".env";
3143
3143
  var DEFAULT_LOCAL_ENVIRONMENT_FILE = ".env.local";
3144
3144
  var DEFAULT_ASSET_DESTINATION = ".";
3145
3145
  var DEFAULT_OUT_DIR = "build";
3146
+ var DEFAULT_CONTRACTS_DIR = ".luam/contracts";
3146
3147
  var DEFAULT_RESOURCES_DIR = "mods/deathmatch/resources";
3147
3148
  var DEFAULT_COMPILER_OPTIONS = {
3148
3149
  strict: true,
@@ -3252,7 +3253,7 @@ var EXIT_USAGE = 2;
3252
3253
  // src/config/manifest-context.ts
3253
3254
  var DEVELOPMENT_MODE = "development";
3254
3255
  var PRODUCTION_MODE = "production";
3255
- var DEVELOPMENT_COMMANDS = ["dev", "ensure", "server"];
3256
+ var DEVELOPMENT_COMMANDS = ["dev", "ensure", "server", "test"];
3256
3257
  var PRODUCTION_COMMANDS = ["build"];
3257
3258
  function manifestMode(command) {
3258
3259
  if (DEVELOPMENT_COMMANDS.includes(command)) {
@@ -3492,14 +3493,14 @@ function readEscape(cursor) {
3492
3493
  }
3493
3494
  function scanQuotedString(cursor, diagnostics) {
3494
3495
  const position2 = cursor.position();
3495
- const quote2 = cursor.peek();
3496
+ const quote3 = cursor.peek();
3496
3497
  cursor.advance();
3497
3498
  let value = "";
3498
- while (!cursor.isEof() && cursor.peek() !== quote2 && !isLineBreak(cursor.peek())) {
3499
+ while (!cursor.isEof() && cursor.peek() !== quote3 && !isLineBreak(cursor.peek())) {
3499
3500
  value += cursor.peek() === "\\" ? readEscape(cursor) : cursor.advance();
3500
3501
  }
3501
- if (cursor.peek() !== quote2) {
3502
- const message = `Unterminated string. Close it with ${quote2}.`;
3502
+ if (cursor.peek() !== quote3) {
3503
+ const message = `Unterminated string. Close it with ${quote3}.`;
3503
3504
  diagnostics.push(createDiagnostic("lexer", "lex-unterminated-string", message, position2));
3504
3505
  return value;
3505
3506
  }
@@ -3827,19 +3828,21 @@ function parseTypeName(stream) {
3827
3828
  return { kind: "type-name", name: token.value, typeArguments, position: token.position };
3828
3829
  }
3829
3830
  function parseParameterType(stream) {
3831
+ let name = null;
3830
3832
  if (stream.check("identifier") && stream.checkAhead(1, "punctuation", ":")) {
3831
- stream.next();
3833
+ name = stream.next().value;
3832
3834
  stream.next();
3833
3835
  }
3834
- return parseTypeAnnotation(stream);
3836
+ return { name, annotation: parseTypeAnnotation(stream) };
3835
3837
  }
3836
3838
  function parseFunctionType(stream) {
3837
3839
  const position2 = stream.expect("identifier", FUNCTION_TYPE).position;
3838
3840
  if (!stream.check("punctuation", "(")) {
3839
- return { kind: "type-function", parameters: [], isVariadic: true, returnType: null, position: position2 };
3841
+ return { kind: "type-function", parameters: [], parameterNames: [], isVariadic: true, returnType: null, position: position2 };
3840
3842
  }
3841
3843
  stream.next();
3842
3844
  const parameters = [];
3845
+ const parameterNames = [];
3843
3846
  let isVariadic = false;
3844
3847
  while (!stream.check("punctuation", ")") && !stream.isEof()) {
3845
3848
  if (stream.match("operator", "...")) {
@@ -3848,7 +3851,9 @@ function parseFunctionType(stream) {
3848
3851
  parseTypeAnnotation(stream);
3849
3852
  }
3850
3853
  } else {
3851
- parameters.push(parseParameterType(stream));
3854
+ const parameter = parseParameterType(stream);
3855
+ parameters.push(parameter.annotation);
3856
+ parameterNames.push(parameter.name);
3852
3857
  }
3853
3858
  if (!stream.match("punctuation", ",")) {
3854
3859
  break;
@@ -3856,7 +3861,7 @@ function parseFunctionType(stream) {
3856
3861
  }
3857
3862
  stream.expect("punctuation", ")");
3858
3863
  const returnType = stream.match("punctuation", ":") ? parseTypeAnnotation(stream) : null;
3859
- return { kind: "type-function", parameters, isVariadic, returnType, position: position2 };
3864
+ return { kind: "type-function", parameters, parameterNames, isVariadic, returnType, position: position2 };
3860
3865
  }
3861
3866
  function parseGroupedType(stream) {
3862
3867
  const position2 = stream.expect("punctuation", "(").position;
@@ -3989,10 +3994,18 @@ function parseNamedAnnotation(stream, declaration) {
3989
3994
  if (optional === null && annotation?.kind === "type-optional") {
3990
3995
  stream.report("parse-optional-position", `Write optional ${label2} as "name?: Type", not "name: Type?".`, annotation.position);
3991
3996
  }
3997
+ if (optional !== null && annotation?.kind === "type-optional") {
3998
+ const message = `The marker on the name already allows "nil". Remove the "?" after the type.`;
3999
+ stream.report("parse-redundant-optional", message, annotation.position);
4000
+ }
3992
4001
  if (optional !== null && annotation !== null) {
3993
4002
  stream.eraseFrom(checkpoint);
3994
4003
  }
3995
- return optional === null || annotation === null ? annotation : { kind: "type-optional", element: annotation, position: optional.position };
4004
+ if (optional === null || annotation === null) {
4005
+ return annotation;
4006
+ }
4007
+ const element = annotation.kind === "type-optional" ? annotation.element : annotation;
4008
+ return { kind: "type-optional", element, position: optional.position };
3996
4009
  }
3997
4010
 
3998
4011
  // ../compiler/src/parser/function-expression.ts
@@ -4146,7 +4159,8 @@ function parsePrimary(stream) {
4146
4159
  if (token.kind === "keyword" && token.value === "new" && stream.checkAhead(1, "identifier")) {
4147
4160
  stream.next();
4148
4161
  const className = stream.next().value;
4149
- return { kind: "new-expression", className, args: parseArguments(stream), position: token.position };
4162
+ const typeArguments = parseNewTypeArguments(stream);
4163
+ return { kind: "new-expression", className, typeArguments, args: parseArguments(stream), position: token.position };
4150
4164
  }
4151
4165
  if (token.kind === "keyword" && CALLABLE_KEYWORDS.has(token.value) && stream.checkAhead(1, "punctuation", "(")) {
4152
4166
  stream.next();
@@ -4178,6 +4192,20 @@ function parsePrimary(stream) {
4178
4192
  }
4179
4193
  throw stream.error(`Unexpected "${stream.describeCurrent()}" in expression.`, "parse-unexpected-token");
4180
4194
  }
4195
+ function parseNewTypeArguments(stream) {
4196
+ const args = [];
4197
+ if (!stream.check("operator", "<")) {
4198
+ return args;
4199
+ }
4200
+ const checkpoint = stream.checkpoint();
4201
+ stream.next();
4202
+ do {
4203
+ args.push(parseTypeAnnotation(stream));
4204
+ } while (stream.match("punctuation", ","));
4205
+ stream.expect("operator", ">");
4206
+ stream.eraseFrom(checkpoint);
4207
+ return args;
4208
+ }
4181
4209
  function parseArguments(stream) {
4182
4210
  if (stream.check("string") || stream.check("template") || stream.check("punctuation", "{")) {
4183
4211
  return [parsePrimary(stream)];
@@ -4464,8 +4492,23 @@ var TokenStream = class {
4464
4492
  // ../compiler/src/parser/declarations.ts
4465
4493
  var DECLARATION_NAMES = /* @__PURE__ */ new Set(["class", "interface", "enum"]);
4466
4494
  var CLASS_MODIFIERS = /* @__PURE__ */ new Set(["extends", "implements"]);
4495
+ function skipTypeParameters(stream, offset) {
4496
+ if (!stream.checkAhead(offset, "operator", "<")) {
4497
+ return offset;
4498
+ }
4499
+ let depth = 0;
4500
+ let index = offset;
4501
+ do {
4502
+ const token = stream.peek(index);
4503
+ depth += token.kind === "operator" && token.value === "<" ? 1 : 0;
4504
+ depth -= token.kind === "operator" && token.value === ">" ? 1 : 0;
4505
+ index += 1;
4506
+ } while (depth > 0 && stream.peek(index).kind !== "eof");
4507
+ return index;
4508
+ }
4467
4509
  function isClassHeader(stream, offset = 0) {
4468
- return stream.checkAhead(offset + 2, "punctuation", "{") || stream.checkAhead(offset + 2, "keyword", "extends") || stream.checkAhead(offset + 2, "keyword", "implements");
4510
+ const after = skipTypeParameters(stream, offset + 2);
4511
+ return stream.checkAhead(after, "punctuation", "{") || stream.checkAhead(after, "keyword", "extends") || stream.checkAhead(after, "keyword", "implements");
4469
4512
  }
4470
4513
  function isDeclarationStart(stream) {
4471
4514
  let offset = 0;
@@ -4595,10 +4638,40 @@ function parseClassMember(stream) {
4595
4638
  expectClassFieldBoundary(stream, token);
4596
4639
  return { kind: "class-field", name: token.value, annotation, value, decorators, isStatic, position: token.position };
4597
4640
  }
4641
+ function parseTypeParameters(stream) {
4642
+ const list = { names: [], constraints: [] };
4643
+ if (!stream.check("operator", "<")) {
4644
+ return list;
4645
+ }
4646
+ const checkpoint = stream.checkpoint();
4647
+ stream.next();
4648
+ do {
4649
+ list.names.push(stream.expect("identifier").value);
4650
+ list.constraints.push(stream.match("keyword", "extends") ? parseTypeAnnotation(stream) : null);
4651
+ } while (stream.match("punctuation", ","));
4652
+ stream.expect("operator", ">");
4653
+ stream.eraseFrom(checkpoint);
4654
+ return list;
4655
+ }
4656
+ function parseTypeArguments(stream) {
4657
+ const args = [];
4658
+ if (!stream.check("operator", "<")) {
4659
+ return args;
4660
+ }
4661
+ const checkpoint = stream.checkpoint();
4662
+ stream.next();
4663
+ do {
4664
+ args.push(parseTypeAnnotation(stream));
4665
+ } while (stream.match("punctuation", ","));
4666
+ stream.expect("operator", ">");
4667
+ stream.eraseFrom(checkpoint);
4668
+ return args;
4669
+ }
4598
4670
  function parseClassModifiers(stream, declaration) {
4599
4671
  while (stream.check("keyword") && CLASS_MODIFIERS.has(stream.current().value)) {
4600
4672
  if (stream.next().value === "extends") {
4601
4673
  declaration.superClass = stream.expect("identifier").value;
4674
+ declaration.superClassArguments = parseTypeArguments(stream);
4602
4675
  continue;
4603
4676
  }
4604
4677
  do {
@@ -4609,7 +4682,19 @@ function parseClassModifiers(stream, declaration) {
4609
4682
  function parseClassDeclaration(stream, decorators) {
4610
4683
  const position2 = stream.next().position;
4611
4684
  const name = stream.expect("identifier").value;
4612
- const declaration = { kind: "class-declaration", name, superClass: null, interfaces: [], members: [], decorators, position: position2 };
4685
+ const typeParameters = parseTypeParameters(stream);
4686
+ const declaration = {
4687
+ kind: "class-declaration",
4688
+ name,
4689
+ typeParameters: typeParameters.names,
4690
+ typeConstraints: typeParameters.constraints,
4691
+ superClass: null,
4692
+ superClassArguments: [],
4693
+ interfaces: [],
4694
+ members: [],
4695
+ decorators,
4696
+ position: position2
4697
+ };
4613
4698
  parseClassModifiers(stream, declaration);
4614
4699
  stream.expect("punctuation", "{");
4615
4700
  while (!stream.check("punctuation", "}") && !stream.isEof()) {
@@ -5069,8 +5154,8 @@ var TABLE_TYPE = { kind: "table" };
5069
5154
  function createArray(element) {
5070
5155
  return { kind: "array", element };
5071
5156
  }
5072
- function createNamed(name) {
5073
- return { kind: "named", name };
5157
+ function createNamed(name, typeArguments = []) {
5158
+ return typeArguments.length === 0 ? { kind: "named", name } : { kind: "named", name, typeArguments };
5074
5159
  }
5075
5160
  function createMap(key, value) {
5076
5161
  return { kind: "map", key, value };
@@ -5183,6 +5268,12 @@ function createUnion(options) {
5183
5268
  }
5184
5269
  return { kind: "union", options: flattened };
5185
5270
  }
5271
+ function namedNesting(type) {
5272
+ if (type.kind !== "named") {
5273
+ return 0;
5274
+ }
5275
+ return 1 + Math.max(0, ...(type.typeArguments ?? []).map(namedNesting));
5276
+ }
5186
5277
  function typeToString(type) {
5187
5278
  if (type.kind === "array") {
5188
5279
  return `${typeToString(type.element)}[]`;
@@ -5202,7 +5293,11 @@ function typeToString(type) {
5202
5293
  if (type.kind === "boolean-literal" || type.kind === "number-literal") {
5203
5294
  return String(type.value);
5204
5295
  }
5205
- if (type.kind === "named" || type.kind === "record") {
5296
+ if (type.kind === "named") {
5297
+ const args = type.typeArguments ?? [];
5298
+ return args.length === 0 ? type.name : `${type.name}<${args.map(typeToString).join(", ")}>`;
5299
+ }
5300
+ if (type.kind === "record") {
5206
5301
  return type.name;
5207
5302
  }
5208
5303
  if (type.kind === "tuple") {
@@ -5292,6 +5387,59 @@ function isRecordAssignable(source, target, options) {
5292
5387
  }
5293
5388
  return true;
5294
5389
  }
5390
+ function isNamedAssignable(source, target, options) {
5391
+ const sourceArguments = source.typeArguments ?? [];
5392
+ const targetArguments = target.typeArguments ?? [];
5393
+ if (source.name !== target.name || sourceArguments.length === 0 || targetArguments.length !== sourceArguments.length) {
5394
+ return true;
5395
+ }
5396
+ return sourceArguments.every((argument, index) => isAssignable(argument, targetArguments[index] ?? ANY_TYPE, options));
5397
+ }
5398
+ function asRecord(name, members) {
5399
+ return { kind: "record", name, origin: null, members };
5400
+ }
5401
+ function shapeOf(name, options) {
5402
+ return options.resolveNominal?.(name) ?? null;
5403
+ }
5404
+ function withVisit(options, pair) {
5405
+ return { ...options, visited: /* @__PURE__ */ new Set([...options.visited ?? [], pair]) };
5406
+ }
5407
+ function isNominalAssignable(source, target, options) {
5408
+ const pair = `${source.name}>${target.name}`;
5409
+ if (options.visited?.has(pair) === true) {
5410
+ return true;
5411
+ }
5412
+ const declared = shapeOf(target.name, options);
5413
+ const actual = shapeOf(source.name, options);
5414
+ if (declared === null || actual === null) {
5415
+ return true;
5416
+ }
5417
+ if (declared.kind === "class" || actual.kind === "class") {
5418
+ return actual.kind === "class" && actual.ancestors.has(target.name);
5419
+ }
5420
+ return isRecordAssignable(asRecord(source.name, actual.members), asRecord(target.name, declared.members), withVisit(options, pair));
5421
+ }
5422
+ var SCALAR_KINDS = /* @__PURE__ */ new Set(["string", "number", "boolean", "thread", "userdata", "void", "nil", "function"]);
5423
+ function isNamedSourceAssignable(source, target, options) {
5424
+ const actual = shapeOf(source.name, options);
5425
+ if (actual === null) {
5426
+ return true;
5427
+ }
5428
+ if (actual.kind === "class") {
5429
+ return !SCALAR_KINDS.has(target.kind) && !isLiteralType(target);
5430
+ }
5431
+ return isAssignable(asRecord(source.name, actual.members), target, options);
5432
+ }
5433
+ function isNamedTargetAssignable(source, target, options) {
5434
+ const declared = shapeOf(target.name, options);
5435
+ if (declared === null) {
5436
+ return true;
5437
+ }
5438
+ if (declared.kind === "class") {
5439
+ return false;
5440
+ }
5441
+ return isAssignable(source, asRecord(target.name, declared.members), options);
5442
+ }
5295
5443
  function isAssignable(source, target, options = { allowNil: false }) {
5296
5444
  if (source.kind === "any" || target.kind === "any" || target.kind === "unknown") {
5297
5445
  return true;
@@ -5299,9 +5447,6 @@ function isAssignable(source, target, options = { allowNil: false }) {
5299
5447
  if (source.kind === "tuple" || target.kind === "tuple") {
5300
5448
  return isAssignable(firstValueOf(source), firstValueOf(target), options);
5301
5449
  }
5302
- if (source.kind === "named" || target.kind === "named") {
5303
- return true;
5304
- }
5305
5450
  if (source.kind === "nil" && (options.allowNil || target.kind === "optional" || target.kind === "nil")) {
5306
5451
  return true;
5307
5452
  }
@@ -5320,6 +5465,15 @@ function isAssignable(source, target, options = { allowNil: false }) {
5320
5465
  if (target.kind === "optional") {
5321
5466
  return isAssignable(source, target.element, options);
5322
5467
  }
5468
+ if (source.kind === "named" && target.kind === "named") {
5469
+ return source.name === target.name ? isNamedAssignable(source, target, options) : isNominalAssignable(source, target, options);
5470
+ }
5471
+ if (source.kind === "named") {
5472
+ return isNamedSourceAssignable(source, target, options);
5473
+ }
5474
+ if (target.kind === "named") {
5475
+ return isNamedTargetAssignable(source, target, options);
5476
+ }
5323
5477
  if (target.kind === "table") {
5324
5478
  return isTableLike(source) || source.kind === "record";
5325
5479
  }
@@ -5458,7 +5612,8 @@ var RUNTIME_HELPERS = {
5458
5612
  math: { name: "math", file: "math.lua", injection: "automatic", features: ["number-extension"] },
5459
5613
  string: { name: "string", file: "string.lua", injection: "automatic", features: ["string-extension", "template-string"] },
5460
5614
  table: { name: "table", file: "table.lua", injection: "automatic", features: ["table-extension"] },
5461
- threads: { name: "threads", file: "threads.lua", injection: "reference", features: ["thread"], globals: ["sleep", "Threads"] }
5615
+ threads: { name: "threads", file: "threads.lua", injection: "reference", features: ["thread"], globals: ["sleep", "Threads"] },
5616
+ validate: { name: "validate", file: "validate.lua", injection: "automatic", features: ["boundary-validation"] }
5462
5617
  };
5463
5618
  var DEVELOPMENT_RUNTIME_HELPERS = {
5464
5619
  "development-logs-client": {
@@ -5599,6 +5754,11 @@ var MANIFEST_FIELDS = [
5599
5754
  owner: "dependencies",
5600
5755
  allowEmpty: true
5601
5756
  }),
5757
+ field("contracts", STRING_TYPE, "Directory the export contract is written to and dependency contracts are read from.", {
5758
+ defaultValue: DEFAULT_CONTRACTS_DIR,
5759
+ rule: "contained-path",
5760
+ owner: "dependencies"
5761
+ }),
5602
5762
  table("engine", "Requirements the MTA server and client must meet.", ENGINE_FIELDS, { defaultValue: {}, owner: "engine" }),
5603
5763
  table("environment", "Files that declare and override the project environment.", ENVIRONMENT_FIELDS, { defaultValue: {}, owner: "environment" }),
5604
5764
  field("outDir", STRING_TYPE, "Directory the built resource is written to.", { defaultValue: DEFAULT_OUT_DIR, rule: "static-path", owner: "output" }),
@@ -5758,8 +5918,8 @@ function isNumber(type) {
5758
5918
  function isText(type) {
5759
5919
  return type.kind === "string" || type.kind === "string-literal" || type.kind === "any";
5760
5920
  }
5761
- function fail(operator, expected, left, right, describe) {
5762
- const culprit = describe(isNumber(left) || isText(left) ? right : left);
5921
+ function fail(operator, expected, left, right, describe2) {
5922
+ const culprit = describe2(isNumber(left) || isText(left) ? right : left);
5763
5923
  return { evaluated: null, error: `"${operator}" expects ${expected} on both sides but received ${culprit}.` };
5764
5924
  }
5765
5925
  function arithmetic(operator, left, right) {
@@ -5798,22 +5958,22 @@ function applyOr(left, right) {
5798
5958
  const value = isTruthy(left.value) ? left.value : right.value;
5799
5959
  return { value, type: unionOf([left.truthy, right.type]), truthy: unionOf([left.truthy, right.truthy]), falsy: right.falsy };
5800
5960
  }
5801
- function applyOrdering(operator, left, right, describe) {
5961
+ function applyOrdering(operator, left, right, describe2) {
5802
5962
  const numeric = isNumber(left.type) && isNumber(right.type);
5803
5963
  const textual = isText(left.type) && isText(right.type);
5804
5964
  if (!numeric && !textual) {
5805
- return fail(operator, "two numbers or two strings", left.type, right.type, describe);
5965
+ return fail(operator, "two numbers or two strings", left.type, right.type, describe2);
5806
5966
  }
5807
5967
  return { evaluated: booleanValue(compare(operator, left.value, right.value)), error: null };
5808
5968
  }
5809
- function applyConcat(left, right, describe) {
5969
+ function applyConcat(left, right, describe2) {
5810
5970
  const usable = (type) => isText(type) || isNumber(type);
5811
5971
  if (!usable(left.type) || !usable(right.type)) {
5812
- return fail("..", "a string or a number", left.type, right.type, describe);
5972
+ return fail("..", "a string or a number", left.type, right.type, describe2);
5813
5973
  }
5814
5974
  return { evaluated: stringValue(`${left.value}${right.value}`), error: null };
5815
5975
  }
5816
- function applyBinary(operator, left, right, describe) {
5976
+ function applyBinary(operator, left, right, describe2) {
5817
5977
  if (operator === "and") {
5818
5978
  return { evaluated: applyAnd(left, right), error: null };
5819
5979
  }
@@ -5825,23 +5985,23 @@ function applyBinary(operator, left, right, describe) {
5825
5985
  return { evaluated: booleanValue(operator === "==" ? same : !same), error: null };
5826
5986
  }
5827
5987
  if (ORDERING.includes(operator)) {
5828
- return applyOrdering(operator, left, right, describe);
5988
+ return applyOrdering(operator, left, right, describe2);
5829
5989
  }
5830
5990
  if (operator === "..") {
5831
- return applyConcat(left, right, describe);
5991
+ return applyConcat(left, right, describe2);
5832
5992
  }
5833
5993
  if (!isNumber(left.type) || !isNumber(right.type)) {
5834
- return fail(operator, "a number", left.type, right.type, describe);
5994
+ return fail(operator, "a number", left.type, right.type, describe2);
5835
5995
  }
5836
5996
  return { evaluated: numberValue(arithmetic(operator, left.value, right.value)), error: null };
5837
5997
  }
5838
- function applyUnary(operator, operand, describe) {
5998
+ function applyUnary(operator, operand, describe2) {
5839
5999
  if (operator === "not") {
5840
6000
  return { evaluated: booleanValue(!isTruthy(operand.value)), error: null };
5841
6001
  }
5842
6002
  if (operator === "-") {
5843
6003
  if (!isNumber(operand.type)) {
5844
- return { evaluated: null, error: `"-" expects a number but received ${describe(operand.type)}.` };
6004
+ return { evaluated: null, error: `"-" expects a number but received ${describe2(operand.type)}.` };
5845
6005
  }
5846
6006
  return { evaluated: numberValue(-operand.value), error: null };
5847
6007
  }
@@ -6300,8 +6460,8 @@ var RuleWalk = class {
6300
6460
  }
6301
6461
  };
6302
6462
  function normalizeManifest(value, positions) {
6303
- const walk3 = new RuleWalk(positions);
6304
- return { value: walk3.fields(MANIFEST_FIELDS, value, "", ""), diagnostics: walk3.diagnostics };
6463
+ const walk4 = new RuleWalk(positions);
6464
+ return { value: walk4.fields(MANIFEST_FIELDS, value, "", ""), diagnostics: walk4.diagnostics };
6305
6465
  }
6306
6466
 
6307
6467
  // ../compiler/src/manifest/manifest-analysis.ts
@@ -6537,6 +6697,7 @@ function validateConfig(value, positions) {
6537
6697
  sources: readSourceMapping(value),
6538
6698
  assets: readAssetMappings(value),
6539
6699
  dependencies,
6700
+ contracts: readString(value, "contracts") ?? "",
6540
6701
  engine: readEngine(value),
6541
6702
  environment: readEnvironmentFiles(value),
6542
6703
  outDir: readString(value, "outDir") ?? "",
@@ -6601,7 +6762,7 @@ import { tmpdir } from "node:os";
6601
6762
  import { join as join2 } from "node:path";
6602
6763
 
6603
6764
  // src/cli/version.ts
6604
- var VERSION = true ? "0.19.1" : "0.0.0-dev";
6765
+ var VERSION = true ? "0.19.3" : "0.0.0-dev";
6605
6766
  var PROGRAM_NAME = "luam";
6606
6767
  var PROGRAM_DESCRIPTION = "luam \u2014 the Luam compiler for Multi Theft Auto resources.";
6607
6768
 
@@ -6786,9 +6947,9 @@ function pluralize(count, singular, plural = `${singular}s`) {
6786
6947
  var MAX_ENTRIES_PER_FILE = 10;
6787
6948
  var MAX_FILES = 10;
6788
6949
  var GUTTER_WIDTH = 6;
6789
- function groupByFile(entries2) {
6950
+ function groupByFile(entries3) {
6790
6951
  const groups = /* @__PURE__ */ new Map();
6791
- for (const entry of entries2) {
6952
+ for (const entry of entries3) {
6792
6953
  const group = groups.get(entry.path);
6793
6954
  if (group === void 0) {
6794
6955
  groups.set(entry.path, [entry]);
@@ -6835,19 +6996,19 @@ function emit(reporter, entry, block) {
6835
6996
  }
6836
6997
  reporter.rawWarn(block);
6837
6998
  }
6838
- function reportGroup(reporter, path, entries2, options) {
6999
+ function reportGroup(reporter, path, entries3, options) {
6839
7000
  const limit = options.maxPerFile ?? MAX_ENTRIES_PER_FILE;
6840
7001
  const source = options.sources.get(path);
6841
7002
  reporter.raw(reporter.style.paint("strong", path));
6842
- for (const entry of entries2.slice(0, limit)) {
7003
+ for (const entry of entries3.slice(0, limit)) {
6843
7004
  emit(reporter, entry, entryBlock(reporter, entry, source));
6844
7005
  }
6845
- if (entries2.length > limit) {
6846
- reporter.detail(` ... and ${pluralize(entries2.length - limit, "more diagnostic")} in this file.`);
7006
+ if (entries3.length > limit) {
7007
+ reporter.detail(` ... and ${pluralize(entries3.length - limit, "more diagnostic")} in this file.`);
6847
7008
  }
6848
7009
  }
6849
- function reportGroupedDiagnostics(reporter, entries2, options) {
6850
- const groups = [...groupByFile(entries2)];
7010
+ function reportGroupedDiagnostics(reporter, entries3, options) {
7011
+ const groups = [...groupByFile(entries3)];
6851
7012
  const limit = options.maxFiles ?? MAX_FILES;
6852
7013
  for (const [path, group] of groups.slice(0, limit)) {
6853
7014
  reportGroup(reporter, path, group, options);
@@ -6863,9 +7024,9 @@ function formatFileDiagnostic(entry) {
6863
7024
  const location = `${path}:${diagnostic.position.line}:${diagnostic.position.column}`;
6864
7025
  return `${location} ${diagnostic.severity} ${diagnostic.code}: ${diagnostic.message}`;
6865
7026
  }
6866
- function countFileDiagnostics(entries2) {
6867
- const errors = entries2.filter((entry) => entry.diagnostic.severity === "error").length;
6868
- return { errors, warnings: entries2.length - errors };
7027
+ function countFileDiagnostics(entries3) {
7028
+ const errors = entries3.filter((entry) => entry.diagnostic.severity === "error").length;
7029
+ return { errors, warnings: entries3.length - errors };
6869
7030
  }
6870
7031
  function countCliDiagnostics(diagnostics) {
6871
7032
  const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
@@ -6875,8 +7036,8 @@ function reportManifestDiagnostics(reporter, path, source, diagnostics) {
6875
7036
  if (diagnostics.length === 0) {
6876
7037
  return;
6877
7038
  }
6878
- const entries2 = diagnostics.map((diagnostic) => ({ path, diagnostic }));
6879
- reportFileDiagnostics(reporter, entries2, /* @__PURE__ */ new Map([[path, source]]));
7039
+ const entries3 = diagnostics.map((diagnostic) => ({ path, diagnostic }));
7040
+ reportFileDiagnostics(reporter, entries3, /* @__PURE__ */ new Map([[path, source]]));
6880
7041
  }
6881
7042
  function reportCliDiagnostics(reporter, diagnostics) {
6882
7043
  for (const diagnostic of diagnostics) {
@@ -6888,8 +7049,8 @@ function reportCliDiagnostics(reporter, diagnostics) {
6888
7049
  reporter.warn(line2);
6889
7050
  }
6890
7051
  }
6891
- function reportPlainFileDiagnostics(reporter, entries2) {
6892
- for (const entry of entries2) {
7052
+ function reportPlainFileDiagnostics(reporter, entries3) {
7053
+ for (const entry of entries3) {
6893
7054
  const line2 = formatFileDiagnostic(entry);
6894
7055
  if (entry.diagnostic.severity === "error") {
6895
7056
  reporter.rawError(line2);
@@ -6898,12 +7059,12 @@ function reportPlainFileDiagnostics(reporter, entries2) {
6898
7059
  reporter.rawWarn(line2);
6899
7060
  }
6900
7061
  }
6901
- function reportFileDiagnostics(reporter, entries2, sources) {
7062
+ function reportFileDiagnostics(reporter, entries3, sources) {
6902
7063
  if (!reporter.capability.interactive) {
6903
- reportPlainFileDiagnostics(reporter, entries2);
7064
+ reportPlainFileDiagnostics(reporter, entries3);
6904
7065
  return;
6905
7066
  }
6906
- reportGroupedDiagnostics(reporter, entries2, { sources });
7067
+ reportGroupedDiagnostics(reporter, entries3, { sources });
6907
7068
  }
6908
7069
  function formatCounts(counts) {
6909
7070
  return `${pluralize(counts.errors, "error")}, ${pluralize(counts.warnings, "warning")}`;
@@ -7154,8 +7315,153 @@ function addProjectOptions(command) {
7154
7315
  return command.addOption(cwdOption()).addOption(manifestOption()).addOption(colorOption());
7155
7316
  }
7156
7317
 
7318
+ // src/build/contract-files.ts
7319
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
7320
+ import { dirname as dirname2, resolve as resolve4 } from "node:path";
7321
+
7322
+ // ../compiler/src/project/export-abi.ts
7323
+ var ABI_VERSION = 1;
7324
+ var ABI_EXTENSION = ".abi.json";
7325
+ var MAX_ABI_BYTES = 262144;
7326
+ var MAX_EXPORTS = 512;
7327
+ var MAX_PARAMETERS = 32;
7328
+ var MAX_TYPE_LENGTH = 512;
7329
+ var NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
7330
+ var RESOURCE_PATTERN = /^[A-Za-z0-9_-]+$/;
7331
+ var SIDES = /* @__PURE__ */ new Set(["server", "client", "shared"]);
7332
+ function serializeAbi(abi) {
7333
+ const exports = [...abi.exports].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
7334
+ return `${JSON.stringify({ abi: abi.abi, resource: abi.resource, exports }, null, 4)}
7335
+ `;
7336
+ }
7337
+ function readString2(value, limit) {
7338
+ return typeof value === "string" && value.length > 0 && value.length <= limit ? value : null;
7339
+ }
7340
+ function readParameter(value) {
7341
+ if (typeof value !== "object" || value === null) {
7342
+ return null;
7343
+ }
7344
+ const entry = value;
7345
+ const name = readString2(entry.name, 64);
7346
+ const type = readString2(entry.type, MAX_TYPE_LENGTH);
7347
+ return name === null || type === null || !NAME_PATTERN.test(name) ? null : { name, type };
7348
+ }
7349
+ function readExport(value) {
7350
+ if (typeof value !== "object" || value === null) {
7351
+ return null;
7352
+ }
7353
+ const entry = value;
7354
+ const name = readString2(entry.name, 128);
7355
+ const side2 = readString2(entry.side, 16);
7356
+ const returns = readString2(entry.returns, MAX_TYPE_LENGTH);
7357
+ const parameters = Array.isArray(entry.parameters) ? entry.parameters : null;
7358
+ if (name === null || !NAME_PATTERN.test(name) || side2 === null || !SIDES.has(side2) || returns === null || parameters === null) {
7359
+ return null;
7360
+ }
7361
+ if (parameters.length > MAX_PARAMETERS) {
7362
+ return null;
7363
+ }
7364
+ const read = parameters.map(readParameter);
7365
+ if (read.some((parameter) => parameter === null)) {
7366
+ return null;
7367
+ }
7368
+ return {
7369
+ name,
7370
+ side: side2,
7371
+ http: entry.http === true,
7372
+ parameters: read,
7373
+ minimumArguments: typeof entry.minimumArguments === "number" && Number.isInteger(entry.minimumArguments) ? entry.minimumArguments : 0,
7374
+ variadic: entry.variadic === true,
7375
+ returns
7376
+ };
7377
+ }
7378
+ function parseResourceAbi(text) {
7379
+ if (text.length > MAX_ABI_BYTES) {
7380
+ return null;
7381
+ }
7382
+ let parsed;
7383
+ try {
7384
+ parsed = JSON.parse(text);
7385
+ } catch {
7386
+ return null;
7387
+ }
7388
+ if (typeof parsed !== "object" || parsed === null) {
7389
+ return null;
7390
+ }
7391
+ const document = parsed;
7392
+ const resource = readString2(document.resource, 64);
7393
+ const entries3 = Array.isArray(document.exports) ? document.exports : null;
7394
+ if (document.abi !== ABI_VERSION || resource === null || !RESOURCE_PATTERN.test(resource) || entries3 === null || entries3.length > MAX_EXPORTS) {
7395
+ return null;
7396
+ }
7397
+ const read = entries3.map(readExport);
7398
+ if (read.some((entry) => entry === null)) {
7399
+ return null;
7400
+ }
7401
+ return { abi: ABI_VERSION, resource, exports: read };
7402
+ }
7403
+ function abiFileName(resource) {
7404
+ return `${resource}${ABI_EXTENSION}`;
7405
+ }
7406
+ function lookupAbiExport(contracts, resource, name) {
7407
+ return contracts.find((entry) => entry.resource === resource)?.exports.find((entry) => entry.name === name) ?? null;
7408
+ }
7409
+ function knowsResource(contracts, resource) {
7410
+ return contracts.some((entry) => entry.resource === resource);
7411
+ }
7412
+ function buildResourceAbi(resource, contributions) {
7413
+ const exports = contributions.map((contribution) => {
7414
+ const signature = contribution.signature;
7415
+ return {
7416
+ name: contribution.name,
7417
+ side: contribution.side,
7418
+ http: contribution.http,
7419
+ parameters: signature?.parameters ?? [],
7420
+ minimumArguments: signature?.minimumArguments ?? 0,
7421
+ variadic: signature?.variadic ?? true,
7422
+ returns: signature?.returns ?? "any"
7423
+ };
7424
+ });
7425
+ return { abi: ABI_VERSION, resource, exports };
7426
+ }
7427
+
7428
+ // src/build/contract-files.ts
7429
+ function contractPath(root, config, resource) {
7430
+ return resolve4(root, config.contracts, abiFileName(resource));
7431
+ }
7432
+ function readDependencyContracts(root, config) {
7433
+ const contracts = [];
7434
+ const diagnostics = [];
7435
+ for (const dependency of config.dependencies) {
7436
+ const path = contractPath(root, config, dependency);
7437
+ let text;
7438
+ try {
7439
+ text = readFileSync3(path, "utf8");
7440
+ } catch {
7441
+ continue;
7442
+ }
7443
+ const parsed = parseResourceAbi(text);
7444
+ if (parsed === null) {
7445
+ diagnostics.push(cliWarning("build-invalid-contract", `The export contract for "${dependency}" at "${path}" could not be read and was ignored.`));
7446
+ continue;
7447
+ }
7448
+ if (parsed.resource !== dependency) {
7449
+ diagnostics.push(cliWarning("build-invalid-contract", `The export contract at "${path}" names resource "${parsed.resource}" and was ignored.`));
7450
+ continue;
7451
+ }
7452
+ contracts.push(parsed);
7453
+ }
7454
+ return { contracts, diagnostics };
7455
+ }
7456
+ function writeResourceContract(root, config, contract) {
7457
+ const path = contractPath(root, config, config.name);
7458
+ mkdirSync2(dirname2(path), { recursive: true });
7459
+ writeFileSync3(path, serializeAbi(contract), "utf8");
7460
+ return path;
7461
+ }
7462
+
7157
7463
  // src/build/helper-files.ts
7158
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
7464
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
7159
7465
  import { fileURLToPath } from "node:url";
7160
7466
  function bundledHelperPath(file) {
7161
7467
  return fileURLToPath(new URL(`./lua/${file}`, import.meta.url));
@@ -7167,7 +7473,7 @@ function resolveHelperPath(helper, file) {
7167
7473
  return existsSync3(packaged) ? packaged : bundledHelperPath(file);
7168
7474
  }
7169
7475
  function readHelperSource(helper, file) {
7170
- return readFileSync3(resolveHelperPath(helper, file), "utf8");
7476
+ return readFileSync4(resolveHelperPath(helper, file), "utf8");
7171
7477
  }
7172
7478
 
7173
7479
  // src/build/build-phase.ts
@@ -7236,16 +7542,16 @@ function createPhaseTracker(listener = null) {
7236
7542
  }
7237
7543
 
7238
7544
  // src/build/project-inputs.ts
7239
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
7240
- import { resolve as resolve6 } from "node:path";
7545
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
7546
+ import { resolve as resolve7 } from "node:path";
7241
7547
 
7242
7548
  // src/build/asset-resolution.ts
7243
7549
  import { statSync as statSync3 } from "node:fs";
7244
- import { resolve as resolve5 } from "node:path";
7550
+ import { resolve as resolve6 } from "node:path";
7245
7551
 
7246
7552
  // src/build/project-files.ts
7247
7553
  import { readdirSync, statSync as statSync2 } from "node:fs";
7248
- import { join as join3, relative, resolve as resolve4 } from "node:path";
7554
+ import { join as join3, relative, resolve as resolve5 } from "node:path";
7249
7555
  function isDirectory(path) {
7250
7556
  try {
7251
7557
  return statSync2(path).isDirectory();
@@ -7257,14 +7563,14 @@ function relativePath(root, absolute) {
7257
7563
  return normalizePattern(relative(root, absolute));
7258
7564
  }
7259
7565
  function walk(root, directory, excluded, tree) {
7260
- let entries2;
7566
+ let entries3;
7261
7567
  try {
7262
- entries2 = readdirSync(directory, { withFileTypes: true });
7568
+ entries3 = readdirSync(directory, { withFileTypes: true });
7263
7569
  } catch (error) {
7264
7570
  tree.errors.push(`"${relativePath(root, directory)}" could not be read: ${error instanceof Error ? error.message : String(error)}`);
7265
7571
  return;
7266
7572
  }
7267
- for (const entry of entries2) {
7573
+ for (const entry of entries3) {
7268
7574
  const absolute = join3(directory, entry.name);
7269
7575
  const path = relativePath(root, absolute);
7270
7576
  if (path.length === 0 || isExcludedPath(path, excluded)) {
@@ -7281,7 +7587,7 @@ function listProjectFiles(root, roots, excluded = []) {
7281
7587
  const tree = { files: [], errors: [] };
7282
7588
  const seen = /* @__PURE__ */ new Set();
7283
7589
  for (const start of [...new Set(roots)].sort()) {
7284
- const absolute = resolve4(root, start);
7590
+ const absolute = resolve5(root, start);
7285
7591
  if (!isDirectory(absolute) || seen.has(absolute)) {
7286
7592
  continue;
7287
7593
  }
@@ -7425,9 +7731,23 @@ var OCCUPIED_SIDES = {
7425
7731
  function occupiedSides(contribution) {
7426
7732
  return OCCUPIED_SIDES[contribution.side];
7427
7733
  }
7428
- function toContributions(directives, environment) {
7734
+ function exportSignature(type) {
7735
+ if (type === void 0 || type.kind !== "function") {
7736
+ return null;
7737
+ }
7738
+ const parameters = type.parameters.map((parameter, index) => ({ name: type.parameterNames?.[index] ?? `arg${index + 1}`, type: typeToString(parameter) }));
7739
+ return { parameters, minimumArguments: type.minimumArguments, variadic: type.isVariadic, returns: typeToString(type.returnType) };
7740
+ }
7741
+ function toContributions(directives, environment, globals) {
7429
7742
  return directives.exports.map(
7430
- (entry) => ({ kind: "export", name: entry.name, http: entry.http, side: environment, position: entry.position })
7743
+ (entry) => ({
7744
+ kind: "export",
7745
+ name: entry.name,
7746
+ http: entry.http,
7747
+ side: environment,
7748
+ signature: exportSignature(globals.get(entry.name)),
7749
+ position: entry.position
7750
+ })
7431
7751
  );
7432
7752
  }
7433
7753
  var INDENT = " ";
@@ -7461,8 +7781,8 @@ function attribute(name, value) {
7461
7781
  function textElement(name, value) {
7462
7782
  return `${INDENT}<${name}>${escapeXml(value)}</${name}>`;
7463
7783
  }
7464
- function section(text, entries2) {
7465
- return entries2.length === 0 ? [] : [`${INDENT}<!-- ${text} -->`, ...entries2];
7784
+ function section(text, entries3) {
7785
+ return entries3.length === 0 ? [] : [`${INDENT}<!-- ${text} -->`, ...entries3];
7466
7786
  }
7467
7787
  function infoElement(info) {
7468
7788
  const attributes = info.author === void 0 ? [] : [attribute("author", info.author)];
@@ -7590,12 +7910,12 @@ var MISSING_LOAD_ORDER = 'is listed in "loadOrder" but no source file or asset m
7590
7910
  function missingEntry(entry) {
7591
7911
  return { path: entry, diagnostic: createDiagnostic("project", "project-load-order-missing", `"${entry}" ${MISSING_LOAD_ORDER}`, FILE_START) };
7592
7912
  }
7593
- function resolveLoadOrder(entries2, scripts, assets) {
7913
+ function resolveLoadOrder(entries3, scripts, assets) {
7594
7914
  const byScript = new Map(scripts.map((script) => [normalizePath(script.source), script]));
7595
7915
  const byAsset = new Map(assets.map((asset) => [normalizePath(asset.source), asset]));
7596
7916
  const resolved2 = { scripts: [], assets: [], diagnostics: [] };
7597
7917
  const seen = /* @__PURE__ */ new Set();
7598
- for (const entry of entries2) {
7918
+ for (const entry of entries3) {
7599
7919
  const path = normalizePath(entry).replace(/^\.\//, "");
7600
7920
  const script = byScript.get(path);
7601
7921
  const asset = byAsset.get(path);
@@ -7671,11 +7991,11 @@ var ENTRY = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
7671
7991
  var SENSITIVE = /(password|secret|token|key|credential|dsn|private)/i;
7672
7992
  var EMPTY_ENV_FILE = { entries: [], errors: [] };
7673
7993
  function unquote(raw) {
7674
- const quote2 = raw.slice(0, 1);
7675
- if (quote2 !== '"' && quote2 !== "'") {
7994
+ const quote3 = raw.slice(0, 1);
7995
+ if (quote3 !== '"' && quote3 !== "'") {
7676
7996
  return null;
7677
7997
  }
7678
- const closing = raw.lastIndexOf(quote2);
7998
+ const closing = raw.lastIndexOf(quote3);
7679
7999
  return closing > 0 ? raw.slice(1, closing) : null;
7680
8000
  }
7681
8001
  function stripComment(raw) {
@@ -7693,7 +8013,7 @@ function classify(raw) {
7693
8013
  return { value, kind: value.length > 0 && Number.isFinite(Number(value)) ? "number" : "string" };
7694
8014
  }
7695
8015
  function parseEnvFile(source) {
7696
- const entries2 = [];
8016
+ const entries3 = [];
7697
8017
  const errors = [];
7698
8018
  source.split(/\r?\n/).forEach((raw, index) => {
7699
8019
  const trimmed = raw.trim();
@@ -7707,9 +8027,9 @@ function parseEnvFile(source) {
7707
8027
  return;
7708
8028
  }
7709
8029
  const { value, kind } = classify(match[2]);
7710
- entries2.push({ key: match[1], value, kind, line: line2 });
8030
+ entries3.push({ key: match[1], value, kind, line: line2 });
7711
8031
  });
7712
- return { entries: entries2, errors };
8032
+ return { entries: entries3, errors };
7713
8033
  }
7714
8034
  function isSensitiveKey(key) {
7715
8035
  return SENSITIVE.test(key);
@@ -7875,14 +8195,14 @@ var MANIFEST_OUTPUT = "meta.xml";
7875
8195
  var HERE = ".";
7876
8196
  function isDirectory2(root, path) {
7877
8197
  try {
7878
- return statSync3(resolve5(root, path)).isDirectory();
8198
+ return statSync3(resolve6(root, path)).isDirectory();
7879
8199
  } catch {
7880
8200
  return false;
7881
8201
  }
7882
8202
  }
7883
8203
  function exists(root, path) {
7884
8204
  try {
7885
- statSync3(resolve5(root, path));
8205
+ statSync3(resolve6(root, path));
7886
8206
  return true;
7887
8207
  } catch {
7888
8208
  return false;
@@ -7950,10 +8270,10 @@ var CONFIGURATION_FILE = "config.lua";
7950
8270
  var MALFORMED_ENV = "build-env-malformed";
7951
8271
  var MISSING_ENV = "config-missing-env-file";
7952
8272
  function readTextFile(path) {
7953
- return existsSync4(path) ? readFileSync4(path, "utf8") : null;
8273
+ return existsSync4(path) ? readFileSync5(path, "utf8") : null;
7954
8274
  }
7955
8275
  function readEnvironment(root, file, required, diagnostics) {
7956
- const source = readTextFile(resolve6(root, file));
8276
+ const source = readTextFile(resolve7(root, file));
7957
8277
  if (source === null) {
7958
8278
  if (required) {
7959
8279
  diagnostics.push(cliError(MISSING_ENV, `"${file}" is configured under "environment" but does not exist. Create it or remove the setting.`));
@@ -7967,7 +8287,7 @@ function readEnvironment(root, file, required, diagnostics) {
7967
8287
  return parsed;
7968
8288
  }
7969
8289
  function readConfiguration(root, diagnostics) {
7970
- const content = readTextFile(resolve6(root, CONFIGURATION_FILE));
8290
+ const content = readTextFile(resolve7(root, CONFIGURATION_FILE));
7971
8291
  if (content === null) {
7972
8292
  return null;
7973
8293
  }
@@ -7986,14 +8306,14 @@ function readProjectInputs(root, options) {
7986
8306
  }
7987
8307
 
7988
8308
  // src/build/source-discovery.ts
7989
- import { readFileSync as readFileSync5 } from "node:fs";
7990
- import { resolve as resolve7 } from "node:path";
8309
+ import { readFileSync as readFileSync6 } from "node:fs";
8310
+ import { resolve as resolve8 } from "node:path";
7991
8311
 
7992
8312
  // ../compiler/src/project/source-mapping.ts
7993
8313
  function createSourceResolver(mapping) {
7994
8314
  const matchers = new Map(SOURCE_SIDES.map((environment) => [environment, createPatternMatcher(mapping[environment])]));
7995
8315
  const patterns = SOURCE_SIDES.flatMap((environment) => [...matchers.get(environment)?.patterns ?? []]);
7996
- function resolve17(path) {
8316
+ function resolve21(path) {
7997
8317
  const normalized = normalizePattern(path);
7998
8318
  const matches = [];
7999
8319
  for (const environment of SOURCE_SIDES) {
@@ -8007,8 +8327,8 @@ function createSourceResolver(mapping) {
8007
8327
  return {
8008
8328
  patterns,
8009
8329
  roots: watchRoots(patterns),
8010
- resolve: resolve17,
8011
- side: (path) => resolve17(path).environment
8330
+ resolve: resolve21,
8331
+ side: (path) => resolve21(path).environment
8012
8332
  };
8013
8333
  }
8014
8334
  function describeMatches(matches) {
@@ -8018,17 +8338,28 @@ function describeMatches(matches) {
8018
8338
  // ../compiler/src/project/source-kind.ts
8019
8339
  var SOURCE_EXTENSION = ".luam";
8020
8340
  var DECLARATION_EXTENSION = ".d.luam";
8341
+ var TEST_EXTENSION = ".test.luam";
8021
8342
  function isDeclarationPath(path) {
8022
8343
  return normalizePath(path).endsWith(DECLARATION_EXTENSION);
8023
8344
  }
8345
+ function isTestPath(path) {
8346
+ return normalizePath(path).endsWith(TEST_EXTENSION);
8347
+ }
8024
8348
 
8025
8349
  // src/build/source-discovery.ts
8026
8350
  var MISSING_SOURCE = "config-missing-source";
8027
8351
  var NO_SOURCES = "config-no-sources";
8028
8352
  var SIDE_CONFLICT = "config-source-side-conflict";
8353
+ var TEST_SOURCE = "config-test-source";
8029
8354
  var UNREADABLE_SOURCE = "build-source-unreadable";
8030
8355
  function checkLiterals(root, resolver, found, diagnostics) {
8031
8356
  for (const pattern of resolver.patterns.filter(isLiteralPattern)) {
8357
+ if (isTestPath(pattern)) {
8358
+ diagnostics.push(
8359
+ cliError(TEST_SOURCE, `"${pattern}" is listed in "sources" but "${TEST_EXTENSION}" files are run by "luam test" and never built into the resource.`)
8360
+ );
8361
+ continue;
8362
+ }
8032
8363
  if (!pattern.endsWith(SOURCE_EXTENSION)) {
8033
8364
  diagnostics.push(cliError(MISSING_SOURCE, `"${pattern}" is listed in "sources" but does not end in "${SOURCE_EXTENSION}".`));
8034
8365
  continue;
@@ -8040,7 +8371,7 @@ function checkLiterals(root, resolver, found, diagnostics) {
8040
8371
  }
8041
8372
  function readSource(root, path, diagnostics) {
8042
8373
  try {
8043
- return readFileSync5(resolve7(root, path), "utf8");
8374
+ return readFileSync6(resolve8(root, path), "utf8");
8044
8375
  } catch (error) {
8045
8376
  diagnostics.push(cliError(UNREADABLE_SOURCE, `The source file "${path}" could not be read: ${error instanceof Error ? error.message : String(error)}`));
8046
8377
  return null;
@@ -8052,7 +8383,7 @@ function discoverSources(root, sources, excluded = []) {
8052
8383
  const diagnostics = tree.errors.map((message) => cliError(UNREADABLE_SOURCE, message));
8053
8384
  const files = [];
8054
8385
  const matched = /* @__PURE__ */ new Set();
8055
- for (const path of tree.files.filter((entry) => entry.endsWith(SOURCE_EXTENSION))) {
8386
+ for (const path of tree.files.filter((entry) => entry.endsWith(SOURCE_EXTENSION) && !isTestPath(entry))) {
8056
8387
  const resolution = resolver.resolve(path);
8057
8388
  if (resolution.matches.length === 0) {
8058
8389
  continue;
@@ -8120,25 +8451,57 @@ function fn(parameters, returnType, minimumArguments2, isVariadic = false, param
8120
8451
  var EMPTY_PROJECT_DECLARATIONS = { globals: [] };
8121
8452
  var ENV_GLOBAL = "env";
8122
8453
  var VALUE_TYPES = { boolean: BOOLEAN, number: NUMBER, string: STRING };
8123
- function envMembers(entries2) {
8454
+ function envMembers(entries3) {
8124
8455
  const members = /* @__PURE__ */ new Map();
8125
- for (const entry of entries2) {
8456
+ for (const entry of entries3) {
8126
8457
  members.set(entry.key, { name: entry.key, type: VALUE_TYPES[entry.kind] });
8127
8458
  }
8128
8459
  return [...members.values()].sort((left, right) => left.name.localeCompare(right.name));
8129
8460
  }
8130
- function envDeclaration(entries2, origin) {
8131
- const env = record(ENV_GLOBAL, envMembers(entries2), origin);
8461
+ function envDeclaration(entries3, origin) {
8462
+ const env = record(ENV_GLOBAL, envMembers(entries3), origin);
8132
8463
  return { name: ENV_GLOBAL, environment: "server", source: "project", type: env };
8133
8464
  }
8134
- function projectDeclarations(entries2, origin) {
8135
- if (entries2 === null) {
8465
+ function projectDeclarations(entries3, origin) {
8466
+ if (entries3 === null) {
8136
8467
  return EMPTY_PROJECT_DECLARATIONS;
8137
8468
  }
8138
- return { globals: [envDeclaration(entries2, origin)] };
8139
- }
8469
+ return { globals: [envDeclaration(entries3, origin)] };
8470
+ }
8471
+
8472
+ // ../compiler/src/checker/test-declarations.ts
8473
+ var TEST_ORIGIN = "luam:test";
8474
+ var CALLBACK = fn([], VOID);
8475
+ var EXPECTATION_MEMBERS = [
8476
+ { name: "toBe", type: fn([ANY], VOID, 1, false, ["expected"]) },
8477
+ { name: "toBeFalsy", type: fn([], VOID) },
8478
+ { name: "toBeNil", type: fn([], VOID) },
8479
+ { name: "toBeTruthy", type: fn([], VOID) },
8480
+ { name: "toContain", type: fn([ANY], VOID, 1, false, ["value"]) },
8481
+ { name: "toEqual", type: fn([ANY], VOID, 1, false, ["expected"]) },
8482
+ { name: "toNotBe", type: fn([ANY], VOID, 1, false, ["expected"]) },
8483
+ { name: "toNotEqual", type: fn([ANY], VOID, 1, false, ["expected"]) },
8484
+ { name: "toThrow", type: fn([optionalOf(STRING)], VOID, 0, false, ["message"]) }
8485
+ ];
8486
+ var STUB_MEMBERS = [
8487
+ { name: "calls", type: fn([STRING], arrayOf(TABLE), 1, false, ["name"]) },
8488
+ { name: "reset", type: fn([], VOID) },
8489
+ { name: "returns", type: fn([STRING, ANY], VOID, 2, false, ["name", "value"]) },
8490
+ { name: "stub", type: fn([STRING, ANY], VOID, 2, false, ["name", "implementation"]) }
8491
+ ];
8492
+ var EXPECTATION_TYPE = record("Expectation", EXPECTATION_MEMBERS, TEST_ORIGIN);
8493
+ var MTA_STUBS_TYPE = record("MtaStubs", STUB_MEMBERS, TEST_ORIGIN);
8494
+ var TEST_DECLARATIONS = [
8495
+ { name: "afterEach", environment: "shared", source: "extension", type: fn([CALLBACK], VOID, 1, false, ["body"]) },
8496
+ { name: "beforeEach", environment: "shared", source: "extension", type: fn([CALLBACK], VOID, 1, false, ["body"]) },
8497
+ { name: "describe", environment: "shared", source: "extension", type: fn([STRING, CALLBACK], VOID, 2, false, ["name", "body"]) },
8498
+ { name: "expect", environment: "shared", source: "extension", type: fn([ANY], EXPECTATION_TYPE, 1, false, ["value"]) },
8499
+ { name: "mta", environment: "shared", source: "extension", type: MTA_STUBS_TYPE },
8500
+ { name: "test", environment: "shared", source: "extension", type: fn([STRING, CALLBACK], VOID, 2, false, ["name", "body"]) }
8501
+ ];
8140
8502
 
8141
8503
  // ../compiler/src/checker/ambient.ts
8504
+ var EVENT_NAME_PREFIX = "event:";
8142
8505
  var EMPTY_AMBIENT = { classes: [], interfaces: [], enums: [], globals: [], events: [] };
8143
8506
  function ambientFromRegistry(registry) {
8144
8507
  return {
@@ -8149,9 +8512,9 @@ function ambientFromRegistry(registry) {
8149
8512
  events: registry.allEvents()
8150
8513
  };
8151
8514
  }
8152
- function withoutNames(entries2, excluded) {
8515
+ function withoutNames(entries3, excluded) {
8153
8516
  const names = new Set(excluded.map((entry) => entry.name));
8154
- return entries2.filter((entry) => !names.has(entry.name));
8517
+ return entries3.filter((entry) => !names.has(entry.name));
8155
8518
  }
8156
8519
  function ownDeclarations(registry, ambient) {
8157
8520
  const all = ambientFromRegistry(registry);
@@ -8328,6 +8691,9 @@ var Binder = class {
8328
8691
  declaredSymbols() {
8329
8692
  return [...this.discarded, ...this.scopes.flatMap((scope) => [...scope.values()])];
8330
8693
  }
8694
+ currentScopeNames() {
8695
+ return [...this.scopes[this.scopes.length - 1]?.keys() ?? []];
8696
+ }
8331
8697
  declare(symbol) {
8332
8698
  const scope = this.scopes[this.scopes.length - 1];
8333
8699
  scope?.set(symbol.name, symbol);
@@ -8366,6 +8732,211 @@ var Binder = class {
8366
8732
  }
8367
8733
  };
8368
8734
 
8735
+ // ../compiler/src/checker/flow.ts
8736
+ function createFlow() {
8737
+ return { facts: /* @__PURE__ */ new Map(), reachable: true };
8738
+ }
8739
+ function cloneFlow(state) {
8740
+ return { facts: new Map(state.facts), reachable: state.reachable };
8741
+ }
8742
+ function applyFacts(state, facts) {
8743
+ for (const [path, type] of facts) {
8744
+ state.facts.set(path, type);
8745
+ }
8746
+ }
8747
+ function forgetPath(state, path) {
8748
+ for (const key of [...state.facts.keys()]) {
8749
+ if (key === path || key.startsWith(`${path}.`) || path.startsWith(`${key}.`)) {
8750
+ state.facts.delete(key);
8751
+ }
8752
+ }
8753
+ }
8754
+ function joinFlows(states) {
8755
+ const live = states.filter((state) => state.reachable);
8756
+ const [first, ...rest] = live;
8757
+ if (first === void 0) {
8758
+ return { facts: /* @__PURE__ */ new Map(), reachable: false };
8759
+ }
8760
+ const facts = /* @__PURE__ */ new Map();
8761
+ for (const [path, type] of first.facts) {
8762
+ const others = rest.map((state) => state.facts.get(path));
8763
+ if (others.some((other) => other === void 0)) {
8764
+ continue;
8765
+ }
8766
+ facts.set(path, createUnion([type, ...others]));
8767
+ }
8768
+ return { facts, reachable: true };
8769
+ }
8770
+ function optionsOf(declared) {
8771
+ if (declared.kind === "union") {
8772
+ return declared.options;
8773
+ }
8774
+ return declared.kind === "optional" ? [declared.element, NIL_TYPE] : [];
8775
+ }
8776
+ function assignmentFact(narrowed, declared) {
8777
+ const matched = optionsOf(declared).filter((option) => isAssignable(narrowed, option));
8778
+ const [only] = matched;
8779
+ return matched.length === 1 && only !== void 0 ? only : null;
8780
+ }
8781
+
8782
+ // ../compiler/src/checker/type-substitution.ts
8783
+ function substituteType(type, substitutions) {
8784
+ if (type.kind === "named") {
8785
+ const args = type.typeArguments ?? [];
8786
+ if (args.length > 0) {
8787
+ return createNamed(type.name, args.map((argument) => substituteType(argument, substitutions)));
8788
+ }
8789
+ return substitutions.get(type.name) ?? type;
8790
+ }
8791
+ if (type.kind === "array") {
8792
+ return createArray(substituteType(type.element, substitutions));
8793
+ }
8794
+ if (type.kind === "map") {
8795
+ return createMap(substituteType(type.key, substitutions), substituteType(type.value, substitutions));
8796
+ }
8797
+ if (type.kind === "optional") {
8798
+ return createOptional(substituteType(type.element, substitutions));
8799
+ }
8800
+ if (type.kind === "union") {
8801
+ return createUnion(type.options.map((option) => substituteType(option, substitutions)));
8802
+ }
8803
+ if (type.kind === "function") {
8804
+ const parameters = type.parameters.map((parameter) => substituteType(parameter, substitutions));
8805
+ const returnType = substituteType(type.returnType, substitutions);
8806
+ const variadicType = type.variadicType === void 0 ? void 0 : substituteType(type.variadicType, substitutions);
8807
+ return createFunction(parameters, returnType, type.minimumArguments, type.isVariadic, type.parameterNames, variadicType);
8808
+ }
8809
+ if (type.kind === "tuple") {
8810
+ return createTuple(type.elements.map((element) => substituteType(element, substitutions)));
8811
+ }
8812
+ if (type.kind === "record") {
8813
+ const members = new Map([...type.members].map(([name, member]) => [name, substituteType(member, substitutions)]));
8814
+ return createRecord(type.name, members, type.origin);
8815
+ }
8816
+ return type;
8817
+ }
8818
+
8819
+ // ../compiler/src/checker/generic-class.ts
8820
+ var MAX_DEPTH = 32;
8821
+ function classSubstitutions(info, typeArguments) {
8822
+ return new Map(info.typeParameters.map((parameter, index) => [parameter, typeArguments[index] ?? ANY_TYPE]));
8823
+ }
8824
+ function parentSubstitutions(registry, info, current) {
8825
+ const parent = info.superClass === null ? null : registry.lookupClass(info.superClass);
8826
+ if (parent === null) {
8827
+ return null;
8828
+ }
8829
+ return classSubstitutions(parent, info.superArguments.map((argument) => substituteType(argument, current)));
8830
+ }
8831
+ function substitutionsFor(registry, receiver, owner) {
8832
+ const info = registry.lookupClass(receiver.name);
8833
+ if (info === null) {
8834
+ return /* @__PURE__ */ new Map();
8835
+ }
8836
+ let current = info;
8837
+ let substitutions = classSubstitutions(info, receiver.typeArguments ?? []);
8838
+ for (let depth = 0; current !== null && depth < MAX_DEPTH; depth += 1) {
8839
+ if (current.name === owner) {
8840
+ return substitutions;
8841
+ }
8842
+ const next = parentSubstitutions(registry, current, substitutions);
8843
+ if (next === null) {
8844
+ return /* @__PURE__ */ new Map();
8845
+ }
8846
+ substitutions = next;
8847
+ current = current.superClass === null ? null : registry.lookupClass(current.superClass);
8848
+ }
8849
+ return /* @__PURE__ */ new Map();
8850
+ }
8851
+ function specializeMember(context, receiver, member, property) {
8852
+ const owner = context.declarations.lookupMemberOwner(receiver.name, property);
8853
+ if (owner === null || owner === receiver.name) {
8854
+ const own = context.declarations.lookupClass(receiver.name);
8855
+ if (own === null || own.typeParameters.length === 0 || (receiver.typeArguments ?? []).length === 0) {
8856
+ return member;
8857
+ }
8858
+ return { ...member, type: substituteType(member.type, classSubstitutions(own, receiver.typeArguments ?? [])) };
8859
+ }
8860
+ return { ...member, type: substituteType(member.type, substitutionsFor(context.declarations, receiver, owner)) };
8861
+ }
8862
+ function memberOf(context, receiver, property) {
8863
+ const member = context.declarations.lookupMember(receiver.name, property);
8864
+ return member === null ? null : specializeMember(context, receiver, member, property);
8865
+ }
8866
+ function unify(parameter, argument, names, bindings) {
8867
+ if (parameter.kind === "named" && names.has(parameter.name)) {
8868
+ if (!bindings.has(parameter.name) && argument.kind !== "nil") {
8869
+ bindings.set(parameter.name, argument);
8870
+ }
8871
+ return;
8872
+ }
8873
+ if (parameter.kind === "array" && argument.kind === "array") {
8874
+ unify(parameter.element, argument.element, names, bindings);
8875
+ return;
8876
+ }
8877
+ if (parameter.kind === "optional") {
8878
+ unify(parameter.element, argument.kind === "optional" ? argument.element : argument, names, bindings);
8879
+ return;
8880
+ }
8881
+ if (parameter.kind === "map" && argument.kind === "map") {
8882
+ unify(parameter.key, argument.key, names, bindings);
8883
+ unify(parameter.value, argument.value, names, bindings);
8884
+ return;
8885
+ }
8886
+ if (parameter.kind === "named" && argument.kind === "named" && parameter.name === argument.name) {
8887
+ const parameterArguments = parameter.typeArguments ?? [];
8888
+ const argumentArguments = argument.typeArguments ?? [];
8889
+ parameterArguments.forEach((entry, index) => unify(entry, argumentArguments[index] ?? ANY_TYPE, names, bindings));
8890
+ return;
8891
+ }
8892
+ if (parameter.kind === "function" && argument.kind === "function") {
8893
+ parameter.parameters.forEach((entry, index) => unify(entry, argument.parameters[index] ?? ANY_TYPE, names, bindings));
8894
+ unify(parameter.returnType, argument.returnType, names, bindings);
8895
+ }
8896
+ }
8897
+ function inferTypeArguments(typeParameters, parameters, args) {
8898
+ const names = new Set(typeParameters);
8899
+ const bindings = /* @__PURE__ */ new Map();
8900
+ parameters.forEach((parameter, index) => {
8901
+ const argument = args[index];
8902
+ if (argument !== void 0) {
8903
+ unify(parameter, widenInferred(argument), names, bindings);
8904
+ }
8905
+ });
8906
+ return typeParameters.map((parameter) => bindings.get(parameter) ?? ANY_TYPE);
8907
+ }
8908
+ function inheritsFrom(context, name, target) {
8909
+ let current = context.declarations.lookupClass(name);
8910
+ for (let depth = 0; current !== null && depth < MAX_DEPTH; depth += 1) {
8911
+ if (current.name === target || current.interfaces.includes(target)) {
8912
+ return true;
8913
+ }
8914
+ current = current.superClass === null ? null : context.declarations.lookupClass(current.superClass);
8915
+ }
8916
+ return context.declarations.interfaceExtends(name, target);
8917
+ }
8918
+ function satisfies(context, argument, constraint) {
8919
+ if (argument.kind === "named" && constraint.kind === "named") {
8920
+ return argument.name === constraint.name || inheritsFrom(context, argument.name, constraint.name);
8921
+ }
8922
+ return isAssignable(argument, constraint, { allowNil: false });
8923
+ }
8924
+ function checkTypeConstraints(context, name, typeArguments, position2) {
8925
+ const info = context.declarations.lookupClass(name);
8926
+ if (info === null) {
8927
+ return;
8928
+ }
8929
+ info.typeConstraints.forEach((constraint, index) => {
8930
+ const argument = typeArguments[index];
8931
+ if (constraint === null || argument === void 0 || argument.kind === "any" || satisfies(context, argument, constraint)) {
8932
+ return;
8933
+ }
8934
+ const parameter = info.typeParameters[index] ?? "T";
8935
+ const message = `Type argument "${typeToString(argument)}" does not satisfy "${parameter} extends ${typeToString(constraint)}" on class "${name}".`;
8936
+ context.report("check-generic-constraint", message, position2);
8937
+ });
8938
+ }
8939
+
8369
8940
  // ../mta-types/src/lua-standard.ts
8370
8941
  var LUA_GLOBALS = {
8371
8942
  _G: TABLE,
@@ -13203,6 +13774,18 @@ var DeclarationRegistry = class {
13203
13774
  }
13204
13775
  return null;
13205
13776
  }
13777
+ lookupMemberOwner(name, member) {
13778
+ const visited = /* @__PURE__ */ new Set();
13779
+ let current = this.lookupClass(name);
13780
+ while (current !== null && !visited.has(current.name)) {
13781
+ if (current.members.has(member)) {
13782
+ return current.name;
13783
+ }
13784
+ visited.add(current.name);
13785
+ current = current.superClass === null ? null : this.lookupClass(current.superClass);
13786
+ }
13787
+ return null;
13788
+ }
13206
13789
  lookupStaticMember(name, member) {
13207
13790
  const visited = /* @__PURE__ */ new Set();
13208
13791
  let current = this.lookupClass(name);
@@ -13262,7 +13845,17 @@ function buildRegistry() {
13262
13845
  procedural: member.procedural
13263
13846
  });
13264
13847
  }
13265
- registry.declareClass({ name: declaration.name, superClass: declaration.parent, interfaces: [], members, statics: /* @__PURE__ */ new Map(), position: MTA_POSITION });
13848
+ registry.declareClass({
13849
+ name: declaration.name,
13850
+ typeParameters: [],
13851
+ typeConstraints: [],
13852
+ superClass: declaration.parent,
13853
+ superArguments: [],
13854
+ interfaces: [],
13855
+ members,
13856
+ statics: /* @__PURE__ */ new Map(),
13857
+ position: MTA_POSITION
13858
+ });
13266
13859
  }
13267
13860
  return registry;
13268
13861
  }
@@ -13349,12 +13942,22 @@ function missingKeys(source, target) {
13349
13942
  }
13350
13943
  return missing2;
13351
13944
  }
13352
- function missingKeyHint(source, target) {
13945
+ function expandCandidate(option, resolveNominal) {
13946
+ if (option.kind === "record") {
13947
+ return option;
13948
+ }
13949
+ if (option.kind !== "named") {
13950
+ return null;
13951
+ }
13952
+ const shape = resolveNominal?.(option.name) ?? null;
13953
+ return shape === null || shape.kind !== "interface" ? null : { kind: "record", name: option.name, origin: null, members: shape.members };
13954
+ }
13955
+ function missingKeyHint(source, target, resolveNominal) {
13353
13956
  if (source.kind !== "record" || source.isLiteral !== true) {
13354
13957
  return "";
13355
13958
  }
13356
13959
  const options = target.kind === "union" ? target.options : [target];
13357
- const candidates3 = options.filter((option) => option.kind === "record");
13960
+ const candidates3 = options.map((option) => expandCandidate(option, resolveNominal)).filter((option) => option !== null);
13358
13961
  const candidate = chooseCandidate(source, candidates3);
13359
13962
  if (candidate === null) {
13360
13963
  return "";
@@ -13402,39 +14005,6 @@ function mergeIntersection(context, parts, position2) {
13402
14005
  return createObjectType(members);
13403
14006
  }
13404
14007
 
13405
- // ../compiler/src/checker/type-substitution.ts
13406
- function substituteType(type, substitutions) {
13407
- if (type.kind === "named") {
13408
- return substitutions.get(type.name) ?? type;
13409
- }
13410
- if (type.kind === "array") {
13411
- return createArray(substituteType(type.element, substitutions));
13412
- }
13413
- if (type.kind === "map") {
13414
- return createMap(substituteType(type.key, substitutions), substituteType(type.value, substitutions));
13415
- }
13416
- if (type.kind === "optional") {
13417
- return createOptional(substituteType(type.element, substitutions));
13418
- }
13419
- if (type.kind === "union") {
13420
- return createUnion(type.options.map((option) => substituteType(option, substitutions)));
13421
- }
13422
- if (type.kind === "function") {
13423
- const parameters = type.parameters.map((parameter) => substituteType(parameter, substitutions));
13424
- const returnType = substituteType(type.returnType, substitutions);
13425
- const variadicType = type.variadicType === void 0 ? void 0 : substituteType(type.variadicType, substitutions);
13426
- return createFunction(parameters, returnType, type.minimumArguments, type.isVariadic, type.parameterNames, variadicType);
13427
- }
13428
- if (type.kind === "tuple") {
13429
- return createTuple(type.elements.map((element) => substituteType(element, substitutions)));
13430
- }
13431
- if (type.kind === "record") {
13432
- const members = new Map([...type.members].map(([name, member]) => [name, substituteType(member, substitutions)]));
13433
- return createRecord(type.name, members, type.origin);
13434
- }
13435
- return type;
13436
- }
13437
-
13438
14008
  // ../compiler/src/checker/context.ts
13439
14009
  var BUILTIN_TYPES = {
13440
14010
  any: ANY_TYPE,
@@ -13450,6 +14020,7 @@ var BUILTIN_TYPES = {
13450
14020
  };
13451
14021
  var TABLE_ANNOTATION = "table";
13452
14022
  var MAP_ARGUMENTS = 2;
14023
+ var MAX_GENERIC_DEPTH = 8;
13453
14024
  var PROJECT_POSITION = { line: 0, column: 0, offset: 0 };
13454
14025
  function nilHint(source, target, mode) {
13455
14026
  if (mode !== "strict" || source.kind !== "nil" || target.kind === "nil" || target.kind === "optional") {
@@ -13462,12 +14033,15 @@ var CheckContext = class {
13462
14033
  declarations = new DeclarationRegistry();
13463
14034
  diagnostics = [];
13464
14035
  reported = /* @__PURE__ */ new Set();
13465
- narrowings = [];
14036
+ flow = createFlow();
13466
14037
  types = /* @__PURE__ */ new Map();
13467
14038
  references = /* @__PURE__ */ new Set();
13468
14039
  declaredGlobals = /* @__PURE__ */ new Map();
14040
+ moduleGlobals = /* @__PURE__ */ new Map();
13469
14041
  externalReferences = /* @__PURE__ */ new Map();
13470
14042
  unknownTypes = /* @__PURE__ */ new Map();
14043
+ eventReferences = /* @__PURE__ */ new Set();
14044
+ globalReferences = /* @__PURE__ */ new Set();
13471
14045
  calledMembers = /* @__PURE__ */ new Set();
13472
14046
  staticAccess = /* @__PURE__ */ new Set();
13473
14047
  typeParameters = /* @__PURE__ */ new Set();
@@ -13475,6 +14049,7 @@ var CheckContext = class {
13475
14049
  mode;
13476
14050
  environment;
13477
14051
  mtaClasses;
14052
+ contracts = [];
13478
14053
  isDeclarationFile = false;
13479
14054
  returnStack = [];
13480
14055
  methodStack = [];
@@ -13500,33 +14075,28 @@ var CheckContext = class {
13500
14075
  declareModuleGlobal(symbol) {
13501
14076
  this.binder.declareGlobal(symbol);
13502
14077
  this.declaredGlobals.set(symbol.name, symbol.position);
14078
+ this.moduleGlobals.set(symbol.name, symbol.type);
13503
14079
  }
13504
- pushNarrowing(facts) {
13505
- this.narrowings.push(new Map(facts));
14080
+ get flowState() {
14081
+ return this.flow;
13506
14082
  }
13507
- get isNarrowed() {
13508
- return this.narrowings.length > 0;
14083
+ setFlow(state) {
14084
+ this.flow = state;
14085
+ }
14086
+ applyFlowFacts(facts) {
14087
+ applyFacts(this.flow, facts);
14088
+ }
14089
+ markUnreachable() {
14090
+ this.flow.reachable = false;
13509
14091
  }
13510
- popNarrowing() {
13511
- this.narrowings.pop();
14092
+ get isNarrowed() {
14093
+ return this.flow.facts.size > 0;
13512
14094
  }
13513
14095
  forgetNarrowing(path) {
13514
- for (const frame of this.narrowings) {
13515
- for (const key of [...frame.keys()]) {
13516
- if (key === path || key.startsWith(`${path}.`) || path.startsWith(`${key}.`)) {
13517
- frame.delete(key);
13518
- }
13519
- }
13520
- }
14096
+ forgetPath(this.flow, path);
13521
14097
  }
13522
14098
  narrowedType(path) {
13523
- for (let index = this.narrowings.length - 1; index >= 0; index -= 1) {
13524
- const found = this.narrowings[index]?.get(path);
13525
- if (found !== void 0) {
13526
- return found;
13527
- }
13528
- }
13529
- return null;
14099
+ return this.flow.facts.get(path) ?? null;
13530
14100
  }
13531
14101
  predeclareClass(info) {
13532
14102
  this.predeclared.set(info.name, info);
@@ -13593,6 +14163,12 @@ var CheckContext = class {
13593
14163
  isTypeParameter(name) {
13594
14164
  return this.typeParameters.has(name);
13595
14165
  }
14166
+ noteGlobalReference(name) {
14167
+ this.globalReferences.add(name);
14168
+ }
14169
+ noteEventReference(name) {
14170
+ this.eventReferences.add(name);
14171
+ }
13596
14172
  noteUnknownType(name, position2) {
13597
14173
  if (!this.unknownTypes.has(name)) {
13598
14174
  this.unknownTypes.set(name, position2);
@@ -13682,17 +14258,57 @@ var CheckContext = class {
13682
14258
  const parameters = annotation.parameters.map((parameter) => this.resolveAnnotation(parameter));
13683
14259
  const optional = parameters.findIndex((parameter) => parameter.kind === "optional");
13684
14260
  const minimum = optional === -1 ? parameters.length : optional;
13685
- return createFunction(parameters, this.resolveAnnotation(annotation.returnType), minimum, annotation.isVariadic);
14261
+ return createFunction(parameters, this.resolveAnnotation(annotation.returnType), minimum, annotation.isVariadic, [...annotation.parameterNames]);
13686
14262
  }
13687
14263
  return this.resolveNamedAnnotation(annotation.name, annotation.typeArguments, annotation.position);
13688
14264
  }
14265
+ nominalShape(name) {
14266
+ const declaredInterface = this.declarations.lookupInterface(name);
14267
+ if (declaredInterface !== null) {
14268
+ const ancestors = this.declarations.allInterfaces().filter((other) => this.declarations.interfaceExtends(name, other.name));
14269
+ return {
14270
+ kind: "interface",
14271
+ members: new Map(this.declarations.collectInterfaceContract(name).map((member) => [member.name, member.type])),
14272
+ ancestors: new Set(ancestors.map((other) => other.name))
14273
+ };
14274
+ }
14275
+ const declaredClass = this.declarations.lookupClass(name);
14276
+ if (declaredClass === null) {
14277
+ return null;
14278
+ }
14279
+ return {
14280
+ kind: "class",
14281
+ members: new Map(this.declarations.collectMembers(name).map((member) => [member.name, member.type])),
14282
+ ancestors: this.classAncestors(name)
14283
+ };
14284
+ }
14285
+ classAncestors(name) {
14286
+ const ancestors = /* @__PURE__ */ new Set();
14287
+ let current = this.declarations.lookupClass(name);
14288
+ while (current !== null && !ancestors.has(current.name)) {
14289
+ ancestors.add(current.name);
14290
+ for (const contract of current.interfaces) {
14291
+ ancestors.add(contract);
14292
+ for (const other of this.declarations.allInterfaces()) {
14293
+ if (this.declarations.interfaceExtends(contract, other.name)) {
14294
+ ancestors.add(other.name);
14295
+ }
14296
+ }
14297
+ }
14298
+ current = current.superClass === null ? null : this.declarations.lookupClass(current.superClass);
14299
+ }
14300
+ return ancestors;
14301
+ }
14302
+ assignability() {
14303
+ return { allowNil: this.allowNil, resolveNominal: (name) => this.nominalShape(name) };
14304
+ }
13689
14305
  expectAssignable(source, target, position2, subject) {
13690
- if (isAssignable(source, target, { allowNil: this.allowNil })) {
14306
+ if (isAssignable(source, target, this.assignability())) {
13691
14307
  return;
13692
14308
  }
13693
14309
  const received = isLiteralType(target) || target.kind === "union" ? source : widenLiteral(source);
13694
14310
  const message = `${subject} expects "${typeToString(target)}" but received "${typeToString(received)}".`;
13695
- const hint = `${nilHint(source, target, this.mode)}${missingKeyHint(source, target)}`;
14311
+ const hint = `${nilHint(source, target, this.mode)}${missingKeyHint(source, target, (name) => this.nominalShape(name))}`;
13696
14312
  this.report("check-type-mismatch", `${message}${hint}`, position2);
13697
14313
  }
13698
14314
  declareProject(project) {
@@ -13738,6 +14354,25 @@ var CheckContext = class {
13738
14354
  }
13739
14355
  return createMap(this.resolveAnnotation(key), this.resolveAnnotation(value));
13740
14356
  }
14357
+ resolveClassAnnotation(name, typeArguments, position2) {
14358
+ const declared = this.declarations.lookupClass(name)?.typeParameters ?? [];
14359
+ const resolved2 = typeArguments.map((argument) => this.resolveAnnotation(argument));
14360
+ if (declared.length === 0) {
14361
+ return createNamed(name);
14362
+ }
14363
+ if (resolved2.length !== declared.length) {
14364
+ const expected = declared.length === 1 ? "1 type argument" : `${declared.length} type arguments`;
14365
+ this.report("check-generic-arity", `Class "${name}" expects ${expected} but received ${resolved2.length}.`, position2);
14366
+ }
14367
+ const specialized = createNamed(name, declared.map((unused, index) => resolved2[index] ?? ANY_TYPE));
14368
+ if (namedNesting(specialized) > MAX_GENERIC_DEPTH) {
14369
+ const message = `Class "${name}" is nested more than ${MAX_GENERIC_DEPTH} levels deep here. Name the inner type with a "type" alias.`;
14370
+ this.report("check-generic-depth", message, position2);
14371
+ return createNamed(name);
14372
+ }
14373
+ checkTypeConstraints(this, name, specialized.typeArguments ?? [], position2);
14374
+ return specialized;
14375
+ }
13741
14376
  resolveNamedAnnotation(name, typeArguments, position2) {
13742
14377
  const builtin = BUILTIN_TYPES[name];
13743
14378
  if (builtin !== void 0) {
@@ -13752,7 +14387,7 @@ var CheckContext = class {
13752
14387
  const alias = this.binder.lookupAlias(name);
13753
14388
  if (alias === null) {
13754
14389
  this.noteUnknownType(name, position2);
13755
- return createNamed(name);
14390
+ return this.resolveClassAnnotation(name, typeArguments, position2);
13756
14391
  }
13757
14392
  if (typeArguments.length !== alias.parameters.length) {
13758
14393
  const expected = alias.parameters.length === 1 ? "1 type argument" : `${alias.parameters.length} type arguments`;
@@ -14009,6 +14644,73 @@ function checkOverrides(context, info, statement) {
14009
14644
  }
14010
14645
  }
14011
14646
 
14647
+ // ../compiler/src/checker/metamethods.ts
14648
+ var CONTRACTS = [
14649
+ { name: "__tostring", parameters: 0, returns: "string", documentation: 'Renders the instance for "tostring" and string coercion.' },
14650
+ { name: "__eq", parameters: 1, returns: "boolean", documentation: 'Compares two instances of the same class with "==".' },
14651
+ { name: "__lt", parameters: 1, returns: "boolean", documentation: 'Orders two instances with "<" and ">".' },
14652
+ { name: "__le", parameters: 1, returns: "boolean", documentation: 'Orders two instances with "<=" and ">=".' },
14653
+ { name: "__len", parameters: 0, returns: "number", documentation: 'Answers the length operator "#".' },
14654
+ { name: "__concat", parameters: 1, returns: "any", documentation: 'Answers the concatenation operator "..".' },
14655
+ { name: "__unm", parameters: 0, returns: "any", documentation: "Answers unary minus." },
14656
+ { name: "__add", parameters: 1, returns: "any", documentation: 'Answers "+".' },
14657
+ { name: "__sub", parameters: 1, returns: "any", documentation: 'Answers "-".' },
14658
+ { name: "__mul", parameters: 1, returns: "any", documentation: 'Answers "*".' },
14659
+ { name: "__div", parameters: 1, returns: "any", documentation: 'Answers "/".' },
14660
+ { name: "__mod", parameters: 1, returns: "any", documentation: 'Answers "%".' },
14661
+ { name: "__pow", parameters: 1, returns: "any", documentation: 'Answers "^".' }
14662
+ ];
14663
+ var BLOCKED = {
14664
+ __index: "it replaces member lookup, which the class helper owns",
14665
+ __newindex: "it swallows a field write, which the class helper owns",
14666
+ __call: "it makes an instance callable, which hides construction",
14667
+ __gc: "Lua 5.1 does not run it for a table",
14668
+ __metatable: "it hides the metatable the class helper needs",
14669
+ __mode: "it turns instances into weak references the class helper cannot track"
14670
+ };
14671
+ var METAMETHOD_CONTRACTS = new Map(CONTRACTS.map((contract) => [contract.name, contract]));
14672
+ function isMetamethodName(name) {
14673
+ return name.startsWith("__");
14674
+ }
14675
+ function isAllowedMetamethod(name) {
14676
+ return METAMETHOD_CONTRACTS.has(name);
14677
+ }
14678
+ function isMetamethodMember(member) {
14679
+ return member.kind === "class-method" && !member.isStatic && isAllowedMetamethod(member.name);
14680
+ }
14681
+ function reportReturn(context, className, contract, type, member) {
14682
+ if (contract.returns === "any" || type.kind === "any" || type.kind === contract.returns) {
14683
+ return;
14684
+ }
14685
+ const message = `Metamethod "${contract.name}" of class "${className}" must return "${contract.returns}" but returns "${typeToString(type)}".`;
14686
+ context.report("check-invalid-metamethod", message, member.position);
14687
+ }
14688
+ function checkMetamethod(context, className, member, type) {
14689
+ const contract = METAMETHOD_CONTRACTS.get(member.name);
14690
+ if (contract === void 0 || type.kind !== "function") {
14691
+ return;
14692
+ }
14693
+ if (type.parameters.length !== contract.parameters) {
14694
+ const expected = contract.parameters === 1 ? "1 parameter" : `${contract.parameters} parameters`;
14695
+ const message = `Metamethod "${contract.name}" of class "${className}" takes ${expected} beside "self" but declares ${type.parameters.length}.`;
14696
+ context.report("check-invalid-metamethod", message, member.position);
14697
+ }
14698
+ reportReturn(context, className, contract, type.returnType, member);
14699
+ }
14700
+ function reportRejectedMetamethod(context, className, member) {
14701
+ if (member.kind !== "class-method" || !isMetamethodName(member.name) || isAllowedMetamethod(member.name)) {
14702
+ return;
14703
+ }
14704
+ const blocked = BLOCKED[member.name];
14705
+ if (blocked !== void 0) {
14706
+ context.report("check-blocked-metamethod", `Metamethod "${member.name}" cannot be declared on class "${className}" because ${blocked}.`, member.position);
14707
+ return;
14708
+ }
14709
+ const known = [...METAMETHOD_CONTRACTS.keys()].map((name) => `"${name}"`).join(", ");
14710
+ const message = `"${member.name}" is not a metamethod Luam exposes on class "${className}". Declared metamethods are ${known}.`;
14711
+ context.report("check-blocked-metamethod", message, member.position);
14712
+ }
14713
+
14012
14714
  // ../compiler/src/checker/accessor-names.ts
14013
14715
  function capitalize(name) {
14014
14716
  return `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
@@ -14093,6 +14795,25 @@ var KNOWN_DECORATORS = /* @__PURE__ */ new Map([
14093
14795
  diagnostics: [CONFLICT]
14094
14796
  }
14095
14797
  ],
14798
+ [
14799
+ "Validated",
14800
+ {
14801
+ name: "Validated",
14802
+ documentation: "Generates runtime checks for the declared field types.",
14803
+ targets: ["class"],
14804
+ generates: [
14805
+ "`ClassName.validate(value: table): ClassName`, returning the value or raising a Lua error naming the path and the expected type.",
14806
+ "`ClassName.matches(value: table): boolean`, the same check without raising."
14807
+ ],
14808
+ rules: [
14809
+ "Both are static, so they are called on the class and never on an instance.",
14810
+ "The check walks the declared field types with a depth, entry-count and string-length limit, and a failure names the path and the expected type but never the value.",
14811
+ "A field whose type has no runtime shape is `check-unreifiable-type`, reported before anything is emitted.",
14812
+ "It is the only decorator that adds runtime code beyond the class helper; a class without it emits nothing extra."
14813
+ ],
14814
+ diagnostics: [CONFLICT, "`check-unreifiable-type` when a field type cannot be checked at runtime."]
14815
+ }
14816
+ ],
14096
14817
  [
14097
14818
  "Serializable",
14098
14819
  {
@@ -14183,6 +14904,102 @@ var KNOWN_DECORATORS = /* @__PURE__ */ new Map([
14183
14904
  ]
14184
14905
  ]);
14185
14906
 
14907
+ // ../compiler/src/checker/validation-descriptor.ts
14908
+ var MAX_EXPANSION = 8;
14909
+ var PRIMITIVE_KINDS = {
14910
+ nil: "nil",
14911
+ boolean: "boolean",
14912
+ number: "number",
14913
+ string: "string",
14914
+ table: "table",
14915
+ thread: "thread",
14916
+ userdata: "userdata",
14917
+ function: "function"
14918
+ };
14919
+ function quote2(value) {
14920
+ return `'${value.replace(/([\\'])/g, "\\$1").replace(/\n/g, "\\n")}'`;
14921
+ }
14922
+ function failed(type) {
14923
+ return { lua: null, unreifiable: typeToString(type) };
14924
+ }
14925
+ function entries2(parts) {
14926
+ return `{ ${parts.join(", ")} }`;
14927
+ }
14928
+ function combine(parts, render2) {
14929
+ const broken = parts.find((part) => part.lua === null);
14930
+ if (broken !== void 0) {
14931
+ return broken;
14932
+ }
14933
+ return { lua: render2(parts.map((part) => part.lua)), unreifiable: null };
14934
+ }
14935
+ function namedDescriptor(context, type, name, seen) {
14936
+ if (context.declarations.lookupClass(name) !== null) {
14937
+ return { lua: entries2([`kind = 'instance'`, `name = ${quote2(name)}`]), unreifiable: null };
14938
+ }
14939
+ if (context.declarations.lookupEnum(name) !== null) {
14940
+ return { lua: entries2([`kind = 'number'`]), unreifiable: null };
14941
+ }
14942
+ if (context.declarations.lookupInterface(name) === null || seen.has(name) || seen.size >= MAX_EXPANSION) {
14943
+ return failed(type);
14944
+ }
14945
+ const members = context.declarations.collectMembers(name);
14946
+ const nested = /* @__PURE__ */ new Set([...seen, name]);
14947
+ const parts = members.map((member) => describe(context, member.type, nested));
14948
+ return combine(parts, (values) => {
14949
+ const rendered = members.map((member, index) => entries2([`key = ${quote2(member.name)}`, `value = ${values[index] ?? ""}`]));
14950
+ return entries2([`kind = 'record'`, `name = ${quote2(name)}`, `members = ${entries2(rendered)}`]);
14951
+ });
14952
+ }
14953
+ function describe(context, type, seen = /* @__PURE__ */ new Set()) {
14954
+ const primitive = PRIMITIVE_KINDS[type.kind];
14955
+ if (primitive !== void 0) {
14956
+ return { lua: entries2([`kind = ${quote2(primitive)}`]), unreifiable: null };
14957
+ }
14958
+ if (type.kind === "string-literal") {
14959
+ return { lua: entries2([`kind = 'literal'`, `value = ${quote2(type.value)}`]), unreifiable: null };
14960
+ }
14961
+ if (type.kind === "boolean-literal" || type.kind === "number-literal") {
14962
+ return { lua: entries2([`kind = 'literal'`, `value = ${String(type.value)}`]), unreifiable: null };
14963
+ }
14964
+ if (type.kind === "optional") {
14965
+ return combine([describe(context, type.element, seen)], (values) => entries2([`kind = 'optional'`, `element = ${values[0] ?? ""}`]));
14966
+ }
14967
+ if (type.kind === "array") {
14968
+ return combine([describe(context, type.element, seen)], (values) => entries2([`kind = 'array'`, `element = ${values[0] ?? ""}`]));
14969
+ }
14970
+ if (type.kind === "map") {
14971
+ return combine(
14972
+ [describe(context, type.key, seen), describe(context, type.value, seen)],
14973
+ (values) => entries2([`kind = 'map'`, `key = ${values[0] ?? ""}`, `value = ${values[1] ?? ""}`])
14974
+ );
14975
+ }
14976
+ if (type.kind === "union") {
14977
+ return combine(
14978
+ type.options.map((option) => describe(context, option, seen)),
14979
+ (values) => entries2([`kind = 'union'`, `options = ${entries2(values)}`])
14980
+ );
14981
+ }
14982
+ if (type.kind === "record") {
14983
+ const members = [...type.members];
14984
+ const parts = members.map(([, member]) => describe(context, member, seen));
14985
+ return combine(parts, (values) => {
14986
+ const rendered = members.map(([key], index) => entries2([`key = ${quote2(key)}`, `value = ${values[index] ?? ""}`]));
14987
+ return entries2([`kind = 'record'`, `name = ${quote2(type.name)}`, `members = ${entries2(rendered)}`]);
14988
+ });
14989
+ }
14990
+ if (type.kind === "named") {
14991
+ return namedDescriptor(context, type, type.name, seen);
14992
+ }
14993
+ return failed(type);
14994
+ }
14995
+ function classDescriptor(context, className, fields) {
14996
+ const parts = fields.map((field2) => describe(context, field2.type));
14997
+ return combine(parts, (values) => {
14998
+ const rendered = fields.map((field2, index) => entries2([`key = ${quote2(field2.name)}`, `value = ${values[index] ?? ""}`]));
14999
+ return entries2([`kind = 'record'`, `name = ${quote2(className)}`, `members = ${entries2(rendered)}`]);
15000
+ });
15001
+ }
15002
+
14186
15003
  // ../compiler/src/checker/decorators.ts
14187
15004
  function memberExpression(field2) {
14188
15005
  return {
@@ -14248,6 +15065,46 @@ function validateDecorators(context, decorators, target) {
14248
15065
  function decoratorFor(name, classDecorators, fieldDecorators) {
14249
15066
  return fieldDecorators.find((decorator) => decorator.name === name) ?? classDecorators.find((decorator) => decorator.name === name) ?? null;
14250
15067
  }
15068
+ function staticMethod(statement, name, returnAnnotation, kind, descriptor) {
15069
+ const value = { name: "value", annotation: typeName("table", statement), isVararg: false, position: statement.position };
15070
+ return {
15071
+ kind: "class-method",
15072
+ name,
15073
+ isConstructor: false,
15074
+ isSynthetic: true,
15075
+ isStatic: true,
15076
+ parameters: [value],
15077
+ returnAnnotation,
15078
+ body: [],
15079
+ decorators: [],
15080
+ generated: { kind, fields: [], descriptor },
15081
+ position: statement.position
15082
+ };
15083
+ }
15084
+ function expandValidated(context, statement, classDecorators, fieldTypes, occupied, generated) {
15085
+ const found = classDecorators.find((decorator) => decorator.name === "Validated");
15086
+ if (found === void 0) {
15087
+ return;
15088
+ }
15089
+ const fields = [...fieldTypes].map(([field2, type]) => ({ name: field2.name, type, position: field2.position }));
15090
+ const described = classDescriptor(context, statement.name, fields);
15091
+ if (described.lua === null) {
15092
+ const message = `Decorator "@Validated" cannot check type "${described.unreifiable}", which has no runtime shape. Remove the field, or narrow it to a checkable type.`;
15093
+ context.report("check-unreifiable-type", message, found.position);
15094
+ return;
15095
+ }
15096
+ for (const [name, kind, annotation] of [
15097
+ ["validate", "validate", typeName(statement.name, statement)],
15098
+ ["matches", "matches", typeName("boolean", statement)]
15099
+ ]) {
15100
+ if (occupied.has(name)) {
15101
+ context.report("check-decorator-conflict", `Decorator "@Validated" would generate "${name}", which is already declared by "${occupied.get(name)}".`, found.position);
15102
+ continue;
15103
+ }
15104
+ occupied.set(name, statement.name);
15105
+ generated.push(staticMethod(statement, name, annotation, kind, described.lua));
15106
+ }
15107
+ }
14251
15108
  function expandClassDecorators(context, statement, fieldTypes) {
14252
15109
  const generated = [];
14253
15110
  const occupied = new Map(statement.members.filter((member) => member.kind === "class-method").map((member) => [member.name, member.name]));
@@ -14309,6 +15166,7 @@ function expandClassDecorators(context, statement, fieldTypes) {
14309
15166
  Serializable: ["toTable", typeName("table", statement), "serializable"],
14310
15167
  Deserialize: ["fromTable", typeName("void", statement), "deserialize"]
14311
15168
  };
15169
+ expandValidated(context, statement, classDecorators, fieldTypes, occupied, generated);
14312
15170
  for (const [decorator, [name, returnAnnotation, kind]] of Object.entries(classMethods)) {
14313
15171
  const found = classDecorators.find((value) => value.name === decorator);
14314
15172
  if (found !== void 0 && !occupied.has(name)) {
@@ -17567,6 +18425,9 @@ function checkEventUsage(context, expression) {
17567
18425
  return;
17568
18426
  }
17569
18427
  const name = eventName(expression);
18428
+ if (name !== null) {
18429
+ context.noteEventReference(name);
18430
+ }
17570
18431
  const custom = name === null ? null : context.declarations.lookupEvent(name);
17571
18432
  const declared = name === null ? null : eventEnvironment(name);
17572
18433
  if (name === null || custom?.environment === "shared" || custom?.environment === context.environment || declared === null || declared === context.environment) {
@@ -17709,6 +18570,105 @@ function specializeEventCall(context, expression, signature) {
17709
18570
  return candidate === null ? null : specialize(signature, candidate);
17710
18571
  }
17711
18572
 
18573
+ // ../compiler/src/checker/method-receiver.ts
18574
+ var SELF_PARAMETER = "self";
18575
+ function resolveNonNominalMethod(receiver, method) {
18576
+ if (receiver.kind !== "record") {
18577
+ return null;
18578
+ }
18579
+ const member = receiver.members.get(method);
18580
+ return member !== void 0 && member.kind === "function" ? member : null;
18581
+ }
18582
+ function withoutSelfParameter(signature) {
18583
+ const names = signature.parameterNames;
18584
+ if (names === void 0 || names[0] !== SELF_PARAMETER) {
18585
+ return signature;
18586
+ }
18587
+ const minimum = Math.max(0, signature.minimumArguments - 1);
18588
+ return createFunction(signature.parameters.slice(1), signature.returnType, minimum, signature.isVariadic, names.slice(1), signature.variadicType);
18589
+ }
18590
+
18591
+ // ../compiler/src/checker/resource-exports.ts
18592
+ var RESOURCE_LOOKUP = "getResourceFromName";
18593
+ var EXPORTS_TABLE = "exports";
18594
+ var CALL_FUNCTION = "call";
18595
+ function literalString(expression) {
18596
+ return expression?.kind === "string-literal" ? expression.value : null;
18597
+ }
18598
+ function resourceOfLookup(expression) {
18599
+ if (expression?.kind !== "call-expression" || expression.callee.kind !== "identifier" || expression.callee.name !== RESOURCE_LOOKUP) {
18600
+ return null;
18601
+ }
18602
+ return literalString(expression.args[0]);
18603
+ }
18604
+ function resourceOfExports(expression) {
18605
+ if (expression.kind === "member-expression" && expression.object.kind === "identifier" && expression.object.name === EXPORTS_TABLE) {
18606
+ return expression.property;
18607
+ }
18608
+ if (expression.kind !== "index-expression" || expression.object.kind !== "identifier" || expression.object.name !== EXPORTS_TABLE) {
18609
+ return null;
18610
+ }
18611
+ return literalString(expression.index);
18612
+ }
18613
+ function abiType(context, text) {
18614
+ const scanned = scan(text);
18615
+ if (scanned.diagnostics.length > 0) {
18616
+ return ANY_TYPE;
18617
+ }
18618
+ try {
18619
+ return context.resolveAnnotation(parseTypeAnnotation(new TokenStream(scanned.tokens)));
18620
+ } catch {
18621
+ return ANY_TYPE;
18622
+ }
18623
+ }
18624
+ function signatureOf(context, entry) {
18625
+ const parameters = entry.parameters.map((parameter) => abiType(context, parameter.type));
18626
+ const names = entry.parameters.map((parameter) => parameter.name);
18627
+ const minimum = Math.min(Math.max(entry.minimumArguments, 0), parameters.length);
18628
+ return createFunction(parameters, abiType(context, entry.returns), minimum, entry.variadic, names);
18629
+ }
18630
+ function checkSide(context, entry, resource, expression) {
18631
+ if (canReference(context.environment, entry.side)) {
18632
+ return;
18633
+ }
18634
+ const message = `Export "${entry.name}" of resource "${resource}" is ${entry.side}-only and cannot be called from a "${context.environment}" file.`;
18635
+ context.report("check-resource-export-side", message, expression.position);
18636
+ }
18637
+ function checkKnownExport(context, resource, name, args, expression) {
18638
+ const entry = lookupAbiExport(context.contracts, resource, name);
18639
+ if (entry === null) {
18640
+ const known = context.contracts.find((contract) => contract.resource === resource)?.exports.map((declared) => `"${declared.name}"`).join(", ");
18641
+ const hint = known === void 0 || known.length === 0 ? " It exports nothing." : ` It exports ${known}.`;
18642
+ checkValueList(context, args);
18643
+ context.report("check-unknown-resource-export", `Resource "${resource}" has no export "${name}".${hint}`, expression.position);
18644
+ return ANY_TYPE;
18645
+ }
18646
+ checkSide(context, entry, resource, expression);
18647
+ return checkSignature(context, args, signatureOf(context, entry), expression.position);
18648
+ }
18649
+ function checkResourceCall(context, expression) {
18650
+ if (context.contracts.length === 0) {
18651
+ return null;
18652
+ }
18653
+ if (expression.method !== null) {
18654
+ const resource2 = resourceOfExports(expression.callee);
18655
+ if (resource2 === null || !knowsResource(context.contracts, resource2)) {
18656
+ return null;
18657
+ }
18658
+ return checkKnownExport(context, resource2, expression.method, expression.args, expression);
18659
+ }
18660
+ if (expression.callee.kind !== "identifier" || expression.callee.name !== CALL_FUNCTION) {
18661
+ return null;
18662
+ }
18663
+ const resource = resourceOfLookup(expression.args[0]);
18664
+ const name = literalString(expression.args[1]);
18665
+ if (resource === null || name === null || !knowsResource(context.contracts, resource)) {
18666
+ return null;
18667
+ }
18668
+ checkValueList(context, expression.args.slice(0, 2));
18669
+ return checkKnownExport(context, resource, name, expression.args.slice(2), expression);
18670
+ }
18671
+
17712
18672
  // ../mta-types/src/library-members.ts
17713
18673
  var MATH_MEMBERS = {
17714
18674
  abs: fn([NUMBER], NUMBER, 1),
@@ -17983,12 +18943,13 @@ function resolveStaticMember(context, className, expression) {
17983
18943
  context.report("check-unknown-member", `Class "${className}" has no static member "${expression.property}".${hint}`, expression.position);
17984
18944
  return ANY_TYPE;
17985
18945
  }
17986
- function resolveNamedMember(context, name, expression) {
18946
+ function resolveNamedMember(context, receiver, expression) {
18947
+ const name = receiver.name;
17987
18948
  const enumeration = context.declarations.lookupEnum(name);
17988
18949
  if (enumeration !== null) {
17989
18950
  return checkEnumMember(context, name, enumeration.members, expression);
17990
18951
  }
17991
- const member = context.declarations.lookupMember(name, expression.property);
18952
+ const member = memberOf(context, receiver, expression.property);
17992
18953
  if (member !== null) {
17993
18954
  if (member.deprecated === true) {
17994
18955
  context.warn("check-deprecated-use", `Member "${expression.property}" is deprecated.`, expression.position);
@@ -18023,6 +18984,29 @@ function nativeConstructor(context, name) {
18023
18984
  const constructor = symbol.type.members.get(NATIVE_CONSTRUCTOR);
18024
18985
  return constructor === void 0 || constructor.kind !== "function" ? null : constructor;
18025
18986
  }
18987
+ function constructorParameters(context, className) {
18988
+ const constructor = context.declarations.lookupMember(className, "constructor");
18989
+ return constructor?.type.kind === "function" ? constructor.type.parameters : [];
18990
+ }
18991
+ function resolveConstructorArguments(context, expression, typeParameters) {
18992
+ const explicit = expression.typeArguments.map((argument) => context.resolveAnnotation(argument));
18993
+ if (typeParameters.length === 0) {
18994
+ if (explicit.length > 0) {
18995
+ context.report("check-generic-arity", `Class "${expression.className}" does not accept type arguments.`, expression.position);
18996
+ }
18997
+ return [];
18998
+ }
18999
+ if (explicit.length === typeParameters.length) {
19000
+ return explicit;
19001
+ }
19002
+ if (explicit.length > 0) {
19003
+ const expected = typeParameters.length === 1 ? "1 type argument" : `${typeParameters.length} type arguments`;
19004
+ context.report("check-generic-arity", `Class "${expression.className}" expects ${expected} but received ${explicit.length}.`, expression.position);
19005
+ return typeParameters.map((unused, index) => explicit[index] ?? ANY_TYPE);
19006
+ }
19007
+ const argumentTypes = expression.args.map((argument) => checkExpression(context, argument));
19008
+ return inferTypeArguments(typeParameters, constructorParameters(context, expression.className), argumentTypes);
19009
+ }
18026
19010
  function checkNewExpression(context, expression) {
18027
19011
  if (context.declarations.lookupClass(expression.className) === null) {
18028
19012
  const constructor2 = nativeConstructor(context, expression.className);
@@ -18041,13 +19025,20 @@ function checkNewExpression(context, expression) {
18041
19025
  const subject = pending === expression.className ? `Class "${pending}"` : `Class "${expression.className}" extends "${pending}", which`;
18042
19026
  context.report("check-class-before-declaration", `${subject} is declared further down this file, so it does not exist yet at this point.`, expression.position);
18043
19027
  }
19028
+ const info = context.declarations.lookupClass(expression.className);
19029
+ const typeArguments = resolveConstructorArguments(context, expression, info?.typeParameters ?? []);
19030
+ checkTypeConstraints(context, expression.className, typeArguments, expression.position);
18044
19031
  const constructor = context.declarations.lookupMember(expression.className, "constructor");
18045
19032
  if (constructor !== null && constructor.type.kind === "function") {
18046
- checkSignature(context, expression.args, constructor.type, expression.position);
19033
+ const substitutions = info === null ? /* @__PURE__ */ new Map() : classSubstitutions(info, typeArguments);
19034
+ const signature = substituteType(constructor.type, substitutions);
19035
+ if (signature.kind === "function") {
19036
+ checkSignature(context, expression.args, signature, expression.position);
19037
+ }
18047
19038
  } else {
18048
19039
  expression.args.forEach((argument) => checkExpression(context, argument));
18049
19040
  }
18050
- return createNamed(expression.className);
19041
+ return createNamed(expression.className, typeArguments);
18051
19042
  }
18052
19043
  function resolveSuperMethod(context, expression) {
18053
19044
  const frame = context.currentClassMethod();
@@ -18136,14 +19127,14 @@ function isShapeOption(context, option) {
18136
19127
  }
18137
19128
  return context.declarations.lookupInterface(option.name) !== null || context.declarations.lookupClass(option.name) !== null;
18138
19129
  }
18139
- function memberOf(context, option, property) {
19130
+ function memberOf2(context, option, property) {
18140
19131
  if (option.kind === "record") {
18141
19132
  return option.members.get(property) ?? null;
18142
19133
  }
18143
19134
  return option.kind === "named" ? context.declarations.lookupMember(option.name, property)?.type ?? null : null;
18144
19135
  }
18145
19136
  function optionMemberType(context, option, property) {
18146
- return isShapeOption(context, option) ? memberOf(context, option, property) : null;
19137
+ return isShapeOption(context, option) ? memberOf2(context, option, property) : null;
18147
19138
  }
18148
19139
  function resolveUnionMember(context, type, expression) {
18149
19140
  if (!type.options.every((option) => isShapeOption(context, option))) {
@@ -18152,7 +19143,7 @@ function resolveUnionMember(context, type, expression) {
18152
19143
  const resolved2 = [];
18153
19144
  const missing2 = [];
18154
19145
  for (const option of type.options) {
18155
- const member = memberOf(context, option, expression.property);
19146
+ const member = memberOf2(context, option, expression.property);
18156
19147
  if (member === null) {
18157
19148
  missing2.push(`"${typeToString(option)}"`);
18158
19149
  } else {
@@ -18319,26 +19310,6 @@ function conditionFacts(context, condition) {
18319
19310
  collectFacts(context, condition, facts);
18320
19311
  return facts;
18321
19312
  }
18322
- function alwaysExits(body) {
18323
- const last = body[body.length - 1];
18324
- if (last === void 0) {
18325
- return false;
18326
- }
18327
- if (last.kind === "return-statement" || last.kind === "break-statement") {
18328
- return true;
18329
- }
18330
- if (last.kind !== "if-statement" || last.alternate === null) {
18331
- return false;
18332
- }
18333
- return last.clauses.every((clause) => alwaysExits(clause.body)) && alwaysExits(last.alternate);
18334
- }
18335
- function guardFacts(context, statement) {
18336
- const [clause] = statement.kind === "if-statement" ? statement.clauses : [];
18337
- if (statement.kind !== "if-statement" || statement.alternate !== null || statement.clauses.length !== 1 || clause === void 0) {
18338
- return /* @__PURE__ */ new Map();
18339
- }
18340
- return alwaysExits(clause.body) ? negatedFacts(context, clause.condition) : /* @__PURE__ */ new Map();
18341
- }
18342
19313
  function negatedFacts(context, condition) {
18343
19314
  const facts = /* @__PURE__ */ new Map();
18344
19315
  if (condition.kind === "binary-expression" && condition.operator === "or") {
@@ -18369,23 +19340,58 @@ var ITERATOR_FUNCTIONS = /* @__PURE__ */ new Set(["pairs", "ipairs"]);
18369
19340
  function checkBlock(context, body) {
18370
19341
  context.binder.pushScope();
18371
19342
  checkStatements(context, body);
19343
+ const declared = context.binder.currentScopeNames();
18372
19344
  context.binder.popScope();
19345
+ for (const name of declared) {
19346
+ context.forgetNarrowing(name);
19347
+ }
18373
19348
  }
18374
- function checkNarrowedBlock(context, facts, body) {
18375
- context.pushNarrowing(facts);
19349
+ function branch(context, entry, facts, body) {
19350
+ const state = cloneFlow(entry);
19351
+ applyFacts(state, facts);
19352
+ context.setFlow(state);
18376
19353
  checkBlock(context, body);
18377
- context.popNarrowing();
19354
+ return context.flowState;
18378
19355
  }
18379
19356
  function checkIf(context, clauses, alternate) {
19357
+ const exits = [];
19358
+ let current = context.flowState;
18380
19359
  for (const clause of clauses) {
19360
+ context.setFlow(current);
18381
19361
  checkExpression(context, clause.condition);
18382
- checkNarrowedBlock(context, conditionFacts(context, clause.condition), clause.body);
19362
+ const taken = conditionFacts(context, clause.condition);
19363
+ const skipped = cloneFlow(current);
19364
+ applyFacts(skipped, negatedFacts(context, clause.condition));
19365
+ exits.push(branch(context, current, taken, clause.body));
19366
+ current = skipped;
18383
19367
  }
18384
19368
  if (alternate === null) {
18385
- return;
19369
+ exits.push(current);
19370
+ } else {
19371
+ context.setFlow(current);
19372
+ checkBlock(context, alternate);
19373
+ exits.push(context.flowState);
18386
19374
  }
18387
- const [only] = clauses;
18388
- checkNarrowedBlock(context, clauses.length === 1 && only !== void 0 ? negatedFacts(context, only.condition) : /* @__PURE__ */ new Map(), alternate);
19375
+ context.setFlow(joinFlows(exits));
19376
+ }
19377
+ function checkLoopBody(context, body, facts) {
19378
+ forgetAssignedPaths(context, body);
19379
+ const entry = context.flowState;
19380
+ const state = cloneFlow(entry);
19381
+ applyFacts(state, facts);
19382
+ context.setFlow(state);
19383
+ checkBlock(context, body);
19384
+ context.setFlow(entry);
19385
+ }
19386
+ function checkWhile(context, statement) {
19387
+ forgetAssignedPaths(context, statement.body);
19388
+ checkExpression(context, statement.condition);
19389
+ checkLoopBody(context, statement.body, conditionFacts(context, statement.condition));
19390
+ context.applyFlowFacts(negatedFacts(context, statement.condition));
19391
+ }
19392
+ function checkRepeat(context, statement) {
19393
+ checkLoopBody(context, statement.body, /* @__PURE__ */ new Map());
19394
+ checkExpression(context, statement.condition);
18389
19395
  }
18390
19396
  function checkNumericFor(context, statement) {
18391
19397
  const bounds = [statement.start, statement.limit, statement.step].filter((value) => value !== null);
@@ -18396,10 +19402,13 @@ function checkNumericFor(context, statement) {
18396
19402
  }
18397
19403
  }
18398
19404
  forgetAssignedPaths(context, statement.body);
19405
+ const entry = context.flowState;
19406
+ context.setFlow(cloneFlow(entry));
18399
19407
  context.binder.pushScope();
18400
19408
  context.binder.declare({ name: statement.variable.name, type: NUMBER_TYPE, isLocal: true, position: statement.variable.position, origin: "local" });
18401
19409
  checkStatements(context, statement.body);
18402
19410
  context.binder.popScope();
19411
+ context.setFlow(entry);
18403
19412
  }
18404
19413
  function iteratedTypes(context, iterators) {
18405
19414
  const [iterator] = iterators;
@@ -18420,6 +19429,8 @@ function checkGenericFor(context, statement) {
18420
19429
  statement.iterators.forEach((iterator) => checkExpression(context, iterator));
18421
19430
  const iterated = iteratedTypes(context, statement.iterators);
18422
19431
  forgetAssignedPaths(context, statement.body);
19432
+ const entry = context.flowState;
19433
+ context.setFlow(cloneFlow(entry));
18423
19434
  context.binder.pushScope();
18424
19435
  statement.variables.forEach((variable, index) => {
18425
19436
  const inferred = iterated[index] ?? ANY_TYPE;
@@ -18428,6 +19439,7 @@ function checkGenericFor(context, statement) {
18428
19439
  });
18429
19440
  checkStatements(context, statement.body);
18430
19441
  context.binder.popScope();
19442
+ context.setFlow(entry);
18431
19443
  }
18432
19444
 
18433
19445
  // ../compiler/src/checker/events.ts
@@ -18488,15 +19500,19 @@ function minimumArguments(context, parameters) {
18488
19500
  }
18489
19501
  function buildFunctionType(context, parameters, returnAnnotation, contextual = null) {
18490
19502
  const isVariadic = parameters.some((parameter) => parameter.isVararg);
18491
- const types = parameters.filter((parameter) => !parameter.isVararg).map((parameter, index) => parameter.annotation === null ? contextual?.parameters[index] ?? ANY_TYPE : context.resolveAnnotation(parameter.annotation));
18492
- return createFunction(types, context.resolveAnnotation(returnAnnotation), minimumArguments(context, parameters), isVariadic);
19503
+ const named2 = parameters.filter((parameter) => !parameter.isVararg);
19504
+ const types = named2.map((parameter, index) => parameter.annotation === null ? contextual?.parameters[index] ?? ANY_TYPE : context.resolveAnnotation(parameter.annotation));
19505
+ const names = named2.map((parameter) => parameter.name);
19506
+ return createFunction(types, context.resolveAnnotation(returnAnnotation), minimumArguments(context, parameters), isVariadic, names);
18493
19507
  }
18494
- function checkFunctionBody(context, parameters, returnAnnotation, body, signature, selfType) {
19508
+ function checkFunctionBody(context, parameters, returnAnnotation, body, signature, selfType2) {
18495
19509
  forgetAssignedPaths(context, body);
19510
+ const entry = context.flowState;
19511
+ context.setFlow(cloneFlow(entry));
18496
19512
  context.binder.pushScope();
18497
19513
  context.pushReturnType(returnAnnotation === null ? null : context.resolveAnnotation(returnAnnotation));
18498
- if (selfType !== null) {
18499
- context.binder.declare({ name: "self", type: selfType, isLocal: true, position: ORIGIN });
19514
+ if (selfType2 !== null) {
19515
+ context.binder.declare({ name: "self", type: selfType2, isLocal: true, position: ORIGIN });
18500
19516
  }
18501
19517
  let parameterIndex = 0;
18502
19518
  for (const parameter of parameters) {
@@ -18512,6 +19528,7 @@ function checkFunctionBody(context, parameters, returnAnnotation, body, signatur
18512
19528
  signature.returnType = inferred;
18513
19529
  }
18514
19530
  context.binder.popScope();
19531
+ context.setFlow(entry);
18515
19532
  }
18516
19533
  function valueAt(values, valueTypes, index) {
18517
19534
  if (values[index] !== void 0) {
@@ -18535,6 +19552,9 @@ function checkLocal(context, statement) {
18535
19552
  context.expectAssignable(valueType, declared, value.position, `Variable "${declaration.name}"`);
18536
19553
  }
18537
19554
  context.binder.declare({ name: declaration.name, type: declared, isLocal: true, position: declaration.position, origin: "local" });
19555
+ if (value !== void 0) {
19556
+ recordAssignedFact(context, declaration.name, valueType, declared);
19557
+ }
18538
19558
  });
18539
19559
  }
18540
19560
  function checkCompoundOperand(context, operator, type, position2) {
@@ -18548,6 +19568,15 @@ function checkCompoundOperand(context, operator, type, position2) {
18548
19568
  context.report("check-invalid-operand", `Operator "${operator}" cannot be applied to "${typeToString(type)}".`, position2);
18549
19569
  }
18550
19570
  }
19571
+ function recordAssignedFact(context, path, valueType, declared) {
19572
+ if (path === null || valueType.kind === "any") {
19573
+ return;
19574
+ }
19575
+ const fact = assignmentFact(widenInferred(valueType), declared);
19576
+ if (fact !== null) {
19577
+ context.applyFlowFacts(/* @__PURE__ */ new Map([[path, fact]]));
19578
+ }
19579
+ }
18551
19580
  function checkAssignment(context, statement) {
18552
19581
  const valueTypes = checkValueList(context, statement.values);
18553
19582
  statement.targets.forEach((target, index) => {
@@ -18583,6 +19612,7 @@ function checkAssignment(context, statement) {
18583
19612
  if (value !== void 0) {
18584
19613
  context.expectAssignable(valueType, declared, value.position, "Assignment");
18585
19614
  }
19615
+ recordAssignedFact(context, pathOf2(target), valueType, declared);
18586
19616
  });
18587
19617
  }
18588
19618
  function checkFunctionDeclaration(context, statement) {
@@ -18661,18 +19691,19 @@ function checkStatement(context, statement) {
18661
19691
  case "function-declaration":
18662
19692
  return checkFunctionDeclaration(context, statement);
18663
19693
  case "return-statement":
18664
- return checkReturn(context, statement);
19694
+ checkReturn(context, statement);
19695
+ context.markUnreachable();
19696
+ return;
19697
+ case "break-statement":
19698
+ case "continue-statement":
19699
+ context.markUnreachable();
19700
+ return;
18665
19701
  case "do-statement":
18666
19702
  return checkBlock(context, statement.body);
18667
19703
  case "while-statement":
18668
- checkExpression(context, statement.condition);
18669
- forgetAssignedPaths(context, statement.body);
18670
- return checkBlock(context, statement.body);
19704
+ return checkWhile(context, statement);
18671
19705
  case "repeat-statement":
18672
- forgetAssignedPaths(context, statement.body);
18673
- checkBlock(context, statement.body);
18674
- checkExpression(context, statement.condition);
18675
- return;
19706
+ return checkRepeat(context, statement);
18676
19707
  case "if-statement":
18677
19708
  return checkIf(context, statement.clauses, statement.alternate);
18678
19709
  case "numeric-for-statement":
@@ -18702,18 +19733,8 @@ function checkStatement(context, statement) {
18702
19733
  }
18703
19734
  }
18704
19735
  function checkStatements(context, statements) {
18705
- let guards = 0;
18706
19736
  for (const statement of statements) {
18707
19737
  checkStatement(context, statement);
18708
- const facts = guardFacts(context, statement);
18709
- if (facts.size > 0) {
18710
- context.pushNarrowing(facts);
18711
- guards += 1;
18712
- }
18713
- }
18714
- while (guards > 0) {
18715
- context.popNarrowing();
18716
- guards -= 1;
18717
19738
  }
18718
19739
  }
18719
19740
 
@@ -18788,7 +19809,7 @@ function checkMember(context, expression) {
18788
19809
  if (objectType.kind === "map" && isAssignable(STRING_TYPE, objectType.key)) {
18789
19810
  return context.record(expression, objectType.value);
18790
19811
  }
18791
- const named2 = objectType.kind === "named" ? resolveNamedMember(context, objectType.name, expression) : null;
19812
+ const named2 = objectType.kind === "named" ? resolveNamedMember(context, objectType, expression) : null;
18792
19813
  if (named2 !== null) {
18793
19814
  return context.record(expression, named2);
18794
19815
  }
@@ -18845,10 +19866,14 @@ function isLegacySuperCall(expression) {
18845
19866
  }
18846
19867
  function checkMethodCall(context, expression, method, receiver) {
18847
19868
  if (receiver.kind !== "named") {
18848
- checkValueList(context, expression.args);
18849
- return ANY_TYPE;
19869
+ const signature = resolveNonNominalMethod(receiver, method);
19870
+ if (signature === null) {
19871
+ checkValueList(context, expression.args);
19872
+ return ANY_TYPE;
19873
+ }
19874
+ return checkSignature(context, expression.args, withoutSelfParameter(signature), expression.position);
18850
19875
  }
18851
- const declared = context.declarations.lookupMember(receiver.name, method);
19876
+ const declared = memberOf(context, receiver, method);
18852
19877
  if (declared?.type.kind === "function") {
18853
19878
  if (declared.deprecated === true) {
18854
19879
  context.warn("check-deprecated-use", `Member "${method}" is deprecated.`, expression.position);
@@ -18867,6 +19892,10 @@ function checkMethodCall(context, expression, method, receiver) {
18867
19892
  return checkSignature(context, expression.args, member.type, expression.position);
18868
19893
  }
18869
19894
  function checkCall(context, expression) {
19895
+ const contracted = checkResourceCall(context, expression);
19896
+ if (contracted !== null) {
19897
+ return context.record(expression, contracted);
19898
+ }
18870
19899
  if (isLegacySuperCall(expression)) {
18871
19900
  checkValueList(context, expression.args);
18872
19901
  context.report("check-invalid-super", 'Call "super(...)" directly instead of "self:super(...)".', expression.position);
@@ -18967,6 +19996,9 @@ function checkMultiValueExpression(context, expression, expected = null) {
18967
19996
  if (symbol === null) {
18968
19997
  checkGlobalReference(context, expression.name, expression.position);
18969
19998
  }
19999
+ if (symbol === null || symbol.isLocal !== true) {
20000
+ context.noteGlobalReference(expression.name);
20001
+ }
18970
20002
  return context.record(expression, context.narrowedType(expression.name) ?? symbol?.type ?? ANY_TYPE);
18971
20003
  }
18972
20004
  case "member-expression":
@@ -19079,6 +20111,11 @@ function registerMembers(context, info, statement) {
19079
20111
  for (const member of [...statement.members, ...generated]) {
19080
20112
  const type = member.kind === "class-field" ? fieldTypes.get(member) ?? ANY_TYPE : member.isSynthetic ? syntheticMethodType(context, member, fieldTypes) : buildFunctionType(context, member.parameters, member.returnAnnotation);
19081
20113
  const decorators = member.decorators.map((decorator) => decorator.name);
20114
+ if (member.kind === "class-method" && isMetamethodMember(member)) {
20115
+ checkMetamethod(context, info.name, member, type);
20116
+ continue;
20117
+ }
20118
+ reportRejectedMetamethod(context, info.name, member);
19082
20119
  const space = member.isStatic ? info.statics : info.members;
19083
20120
  space.set(member.name, {
19084
20121
  name: member.name,
@@ -19106,9 +20143,22 @@ function declareBuilder(context, info, statement) {
19106
20143
  }
19107
20144
  }
19108
20145
  members.set("build", { name: "build", type: { kind: "function", parameters: [], minimumArguments: 0, isVariadic: false, returnType: createNamed(statement.name) }, isMethod: true, position: statement.position });
19109
- context.declarations.declareClass({ name, superClass: null, interfaces: [], members, statics: /* @__PURE__ */ new Map(), position: statement.position });
20146
+ context.declarations.declareClass({
20147
+ name,
20148
+ typeParameters: [],
20149
+ typeConstraints: [],
20150
+ superClass: null,
20151
+ superArguments: [],
20152
+ interfaces: [],
20153
+ members,
20154
+ statics: /* @__PURE__ */ new Map(),
20155
+ position: statement.position
20156
+ });
19110
20157
  context.declareModuleGlobal({ name, type: createNamed(name), isLocal: false, position: statement.position });
19111
20158
  }
20159
+ function selfType(info) {
20160
+ return createNamed(info.name, info.typeParameters.map((parameter) => createNamed(parameter)));
20161
+ }
19112
20162
  function checkMethodBody(context, info, member) {
19113
20163
  const explicitSelf = member.parameters.find((parameter) => parameter.name === "self");
19114
20164
  if (explicitSelf !== void 0) {
@@ -19123,7 +20173,7 @@ function checkMethodBody(context, info, member) {
19123
20173
  return;
19124
20174
  }
19125
20175
  context.pushClassMethod({ className: info.name, methodName: member.name });
19126
- checkFunctionBody(context, member.parameters, member.returnAnnotation, member.body, signature, createNamed(info.name));
20176
+ checkFunctionBody(context, member.parameters, member.returnAnnotation, member.body, signature, selfType(info));
19127
20177
  context.popClassMethod();
19128
20178
  }
19129
20179
  function resolveSuperClass(context, statement) {
@@ -19146,6 +20196,10 @@ function resolveSuperClass(context, statement) {
19146
20196
  context.report("check-unknown-class", message, statement.position);
19147
20197
  return null;
19148
20198
  }
20199
+ function resolveClassHeader(context, info, statement) {
20200
+ info.superArguments = statement.superClassArguments.map((argument) => context.resolveAnnotation(argument));
20201
+ info.typeConstraints = statement.typeConstraints.map((constraint) => constraint === null ? null : context.resolveAnnotation(constraint));
20202
+ }
19149
20203
  function declareClassInfo(context, statement) {
19150
20204
  if (context.declarations.lookupClass(statement.name) !== null) {
19151
20205
  context.report("check-duplicate-class", `Class "${statement.name}" is already defined.`, statement.position);
@@ -19157,12 +20211,16 @@ function declareClassInfo(context, statement) {
19157
20211
  }
19158
20212
  const info = {
19159
20213
  name: statement.name,
20214
+ typeParameters: statement.typeParameters,
20215
+ typeConstraints: [],
19160
20216
  superClass: null,
20217
+ superArguments: [],
19161
20218
  interfaces: statement.interfaces,
19162
20219
  members: /* @__PURE__ */ new Map(),
19163
20220
  statics: /* @__PURE__ */ new Map(),
19164
20221
  position: statement.position
19165
20222
  };
20223
+ context.noteTypeParameters(statement.typeParameters);
19166
20224
  context.declarations.declareClass(info);
19167
20225
  context.declareModuleGlobal({ name: info.name, type: createNamed(info.name), isLocal: false, position: statement.position });
19168
20226
  return info;
@@ -19173,6 +20231,7 @@ function declareLocalClass(context, statement) {
19173
20231
  return null;
19174
20232
  }
19175
20233
  info.superClass = resolveSuperClass(context, statement);
20234
+ resolveClassHeader(context, info, statement);
19176
20235
  return info;
19177
20236
  }
19178
20237
  function checkClassDeclaration(context, statement) {
@@ -19286,6 +20345,7 @@ function predeclareModule(context, body) {
19286
20345
  const declared = declareHeaders(context, body);
19287
20346
  for (const entry of declared) {
19288
20347
  entry.info.superClass = resolveSuperClass(context, entry.statement);
20348
+ resolveClassHeader(context, entry.info, entry.statement);
19289
20349
  }
19290
20350
  for (const entry of declared) {
19291
20351
  breakCycle(context, entry.info);
@@ -19345,17 +20405,23 @@ function externalReferences(context) {
19345
20405
  }
19346
20406
  return external;
19347
20407
  }
20408
+ function referencedNames(context, external) {
20409
+ const events = [...context.eventReferences].map((name) => `${EVENT_NAME_PREFIX}${name}`);
20410
+ return /* @__PURE__ */ new Set([...external.keys(), ...context.globalReferences, ...context.unknownTypes.keys(), ...events]);
20411
+ }
19348
20412
  function check(program2, mode, environment = DEFAULT_ENVIRONMENT, options = {}) {
19349
20413
  const ambient = options.ambient ?? EMPTY_AMBIENT;
19350
20414
  const project = options.project ?? EMPTY_PROJECT_DECLARATIONS;
19351
20415
  const context = new CheckContext(mode, environment, ambient, project, options.oop === true);
19352
20416
  context.isDeclarationFile = options.isDeclarationFile === true;
20417
+ context.contracts = options.contracts ?? [];
19353
20418
  const structure = context.isDeclarationFile ? checkDeclarationFile(program2.body) : checkJumps(program2.body);
19354
20419
  const directives = collectDirectives(program2.body, { isDeclarationFile: context.isDeclarationFile });
19355
20420
  predeclareModule(context, program2.body);
19356
20421
  checkStatements(context, program2.body);
19357
20422
  reportUnknownTypes(context);
19358
20423
  reportUnusedSymbols(context, options.noUnusedLocals === true, options.noUnusedParameters === true);
20424
+ const external = externalReferences(context);
19359
20425
  return {
19360
20426
  diagnostics: sortDiagnostics([...structure, ...directives.diagnostics, ...context.diagnostics]),
19361
20427
  types: context.types,
@@ -19364,7 +20430,9 @@ function check(program2, mode, environment = DEFAULT_ENVIRONMENT, options = {})
19364
20430
  declarations: context.declarations,
19365
20431
  aliases: context.binder.resolvedAliases(),
19366
20432
  declaredGlobals: context.declaredGlobals,
19367
- externalReferences: externalReferences(context),
20433
+ moduleGlobals: context.moduleGlobals,
20434
+ externalReferences: external,
20435
+ referencedNames: referencedNames(context, external),
19368
20436
  directives: directives.directives,
19369
20437
  mode,
19370
20438
  environment,
@@ -19491,8 +20559,8 @@ function typeOf(state, expression) {
19491
20559
  function indentLine(state, text) {
19492
20560
  return `${INDENT2.repeat(state.indent)}${text}`;
19493
20561
  }
19494
- function markSource(state, sourceLine, symbol = state.symbol) {
19495
- const marker = { sourceLine };
20562
+ function markSource(state, sourceLine2, symbol = state.symbol) {
20563
+ const marker = { sourceLine: sourceLine2 };
19496
20564
  if (symbol !== void 0) {
19497
20565
  marker.symbol = symbol;
19498
20566
  }
@@ -19762,6 +20830,13 @@ function emitGeneratedMethod(state, className, member) {
19762
20830
  end
19763
20831
  end`;
19764
20832
  }
20833
+ if (member.generated?.kind === "validate" || member.generated?.kind === "matches") {
20834
+ const helper = member.generated.kind === "validate" ? "__luam_validate" : "__luam_matches";
20835
+ requireHelper(state, "validate");
20836
+ return `${member.name} = function(value)
20837
+ return ${helper}(value, ${member.generated.descriptor ?? "{ kind = 'table' }"})
20838
+ end`;
20839
+ }
19765
20840
  if (member.generated?.kind === "to-string") {
19766
20841
  const values = names.map((name) => `'${name}=' .. tostring(self.${name})`).join(" .. ', ' .. ");
19767
20842
  const body = values.length === 0 ? "''" : values;
@@ -19797,24 +20872,24 @@ ${names.map((name) => ` self.${name} = values.${name}`).join("\n")}
19797
20872
  end`;
19798
20873
  }
19799
20874
  function emitMembers(state, statement) {
19800
- const entries2 = [];
20875
+ const entries3 = [];
19801
20876
  state.indent += 1;
19802
20877
  for (const member of statement.members) {
19803
20878
  if (member.kind === "class-method") {
19804
- entries2.push(indentLine(state, `${markSource(state, member.position.line, `${statement.name}${member.isStatic ? "." : ":"}${member.name}`)}${emitMethod(state, statement.name, member)}`));
20879
+ entries3.push(indentLine(state, `${markSource(state, member.position.line, `${statement.name}${member.isStatic ? "." : ":"}${member.name}`)}${emitMethod(state, statement.name, member)}`));
19805
20880
  continue;
19806
20881
  }
19807
20882
  if (member.decorators.some((decorator) => decorator.name === "Lazy")) {
19808
20883
  continue;
19809
20884
  }
19810
20885
  const value = member.value === null ? "nil" : emitExpression(state, member.value);
19811
- entries2.push(indentLine(state, `${markSource(state, member.position.line, statement.name)}${member.name} = ${value}`));
20886
+ entries3.push(indentLine(state, `${markSource(state, member.position.line, statement.name)}${member.name} = ${value}`));
19812
20887
  }
19813
20888
  for (const generated of state.generatedMembers.get(statement) ?? []) {
19814
- entries2.push(indentLine(state, `${markSource(state, generated.position.line, `${statement.name}:${generated.name}`)}${emitMethod(state, statement.name, generated)}`));
20889
+ entries3.push(indentLine(state, `${markSource(state, generated.position.line, `${statement.name}:${generated.name}`)}${emitMethod(state, statement.name, generated)}`));
19815
20890
  }
19816
20891
  state.indent -= 1;
19817
- return entries2;
20892
+ return entries3;
19818
20893
  }
19819
20894
  function emitClassHeader(statement) {
19820
20895
  const name = emitString(statement.name);
@@ -19840,15 +20915,15 @@ ${assignments}
19840
20915
  return value
19841
20916
  end`);
19842
20917
  state.indent += 1;
19843
- const entries2 = methods.map((method) => indentLine(state, method));
20918
+ const entries3 = methods.map((method) => indentLine(state, method));
19844
20919
  state.indent -= 1;
19845
- return [`class '${name}' {`, entries2.join(",\n"), "}"].join("\n");
20920
+ return [`class '${name}' {`, entries3.join(",\n"), "}"].join("\n");
19846
20921
  }
19847
20922
  function emitClassDeclaration(state, statement) {
19848
20923
  requireHelper(state, "class");
19849
20924
  const header = emitClassHeader(statement);
19850
- const entries2 = emitMembers(state, statement);
19851
- const declaration = entries2.length === 0 ? `${header} {}` : [`${header} {`, entries2.join(",\n"), indentLine(state, "}")].join("\n");
20925
+ const entries3 = emitMembers(state, statement);
20926
+ const declaration = entries3.length === 0 ? `${header} {}` : [`${header} {`, entries3.join(",\n"), indentLine(state, "}")].join("\n");
19852
20927
  const builder = emitBuilder(state, statement);
19853
20928
  return builder === null ? declaration : `${declaration}
19854
20929
  ${builder}`;
@@ -20786,6 +21861,7 @@ function compile(source, options = {}) {
20786
21861
  const isDeclarationFile = options.filePath !== void 0 && isDeclarationPath(options.filePath);
20787
21862
  const checked = check(parsed.program, mode, environment, {
20788
21863
  ambient: options.ambient ?? EMPTY_AMBIENT,
21864
+ contracts: options.contracts ?? [],
20789
21865
  project: options.project ?? EMPTY_PROJECT_DECLARATIONS,
20790
21866
  isDeclarationFile,
20791
21867
  oop: settings.oop,
@@ -20799,7 +21875,9 @@ function compile(source, options = {}) {
20799
21875
  environment,
20800
21876
  declarations: ownDeclarations(checked.declarations, options.ambient),
20801
21877
  declaredGlobals: checked.declaredGlobals,
21878
+ moduleGlobals: checked.moduleGlobals,
20802
21879
  externalReferences: checked.externalReferences,
21880
+ referencedNames: checked.referencedNames,
20803
21881
  directives: checked.directives,
20804
21882
  topLevelReturn: findTopLevelReturn(parsed.program.body),
20805
21883
  events: checked.events
@@ -20890,6 +21968,199 @@ function fingerprintDeclarations(declarations) {
20890
21968
  const events = declarations.events.map(eventText);
20891
21969
  return hashString([...classes, ...interfaces, ...enums, ...globals, ...events].sort(compareText).join(";"));
20892
21970
  }
21971
+ function appendText(entries3, name, text) {
21972
+ const existing = entries3.get(name);
21973
+ entries3.set(name, existing === void 0 ? text : `${existing}|${text}`);
21974
+ }
21975
+ function fingerprintByName(declarations) {
21976
+ const entries3 = /* @__PURE__ */ new Map();
21977
+ for (const info of declarations.classes) {
21978
+ appendText(entries3, info.name, classText(info));
21979
+ }
21980
+ for (const info of declarations.interfaces) {
21981
+ appendText(entries3, info.name, interfaceText(info));
21982
+ }
21983
+ for (const info of declarations.enums) {
21984
+ appendText(entries3, info.name, enumText(info));
21985
+ }
21986
+ for (const info of declarations.globals) {
21987
+ appendText(entries3, info.name, globalText(info));
21988
+ }
21989
+ for (const info of declarations.events) {
21990
+ appendText(entries3, `${EVENT_NAME_PREFIX}${info.name}`, eventText(info));
21991
+ }
21992
+ return new Map([...entries3].map(([name, text]) => [name, hashString(text)]));
21993
+ }
21994
+
21995
+ // ../compiler/src/project/dependency-graph.ts
21996
+ var MAX_CLOSURE_SIZE = 512;
21997
+ function indexOwners(nodes, environment) {
21998
+ const owners = /* @__PURE__ */ new Map();
21999
+ for (const node of nodes) {
22000
+ if (!canReference(environment, node.environment)) {
22001
+ continue;
22002
+ }
22003
+ for (const name of node.provides.keys()) {
22004
+ const existing = owners.get(name);
22005
+ if (existing === void 0) {
22006
+ owners.set(name, [node]);
22007
+ continue;
22008
+ }
22009
+ existing.push(node);
22010
+ }
22011
+ }
22012
+ return owners;
22013
+ }
22014
+ function walk3(node, owners) {
22015
+ const visited = /* @__PURE__ */ new Set();
22016
+ const pending = [...node.requires, ...node.provides.keys()];
22017
+ while (pending.length > 0) {
22018
+ const name = pending.pop();
22019
+ if (name === void 0 || visited.has(name)) {
22020
+ continue;
22021
+ }
22022
+ visited.add(name);
22023
+ if (visited.size > MAX_CLOSURE_SIZE) {
22024
+ return null;
22025
+ }
22026
+ for (const owner of owners.get(name) ?? []) {
22027
+ for (const required of owner.requires) {
22028
+ if (!visited.has(required)) {
22029
+ pending.push(required);
22030
+ }
22031
+ }
22032
+ }
22033
+ }
22034
+ return visited;
22035
+ }
22036
+ function ownerText(owners, name) {
22037
+ const list = owners.get(name) ?? [];
22038
+ return `${name}=${list.map((owner) => `${owner.path}:${owner.provides.get(name) ?? ""}`).sort().join("+")}`;
22039
+ }
22040
+ function createDependencyGraph(nodes) {
22041
+ const byPath = new Map(nodes.map((node) => [node.path, node]));
22042
+ const indexes = /* @__PURE__ */ new Map();
22043
+ const closures = /* @__PURE__ */ new Map();
22044
+ const keys = /* @__PURE__ */ new Map();
22045
+ function ownersFor(environment) {
22046
+ const cached = indexes.get(environment);
22047
+ if (cached !== void 0) {
22048
+ return cached;
22049
+ }
22050
+ const owners = indexOwners(nodes, environment);
22051
+ indexes.set(environment, owners);
22052
+ return owners;
22053
+ }
22054
+ function closureOf(path) {
22055
+ if (closures.has(path)) {
22056
+ return closures.get(path) ?? null;
22057
+ }
22058
+ const node = byPath.get(path);
22059
+ const closure = node === void 0 ? null : walk3(node, ownersFor(node.environment));
22060
+ closures.set(path, closure);
22061
+ return closure;
22062
+ }
22063
+ function keyOf(path) {
22064
+ if (keys.has(path)) {
22065
+ return keys.get(path) ?? null;
22066
+ }
22067
+ const node = byPath.get(path);
22068
+ const closure = closureOf(path);
22069
+ if (node === void 0 || closure === null) {
22070
+ keys.set(path, null);
22071
+ return null;
22072
+ }
22073
+ const owners = ownersFor(node.environment);
22074
+ const key = hashString([...closure].sort().map((name) => ownerText(owners, name)).join(";"));
22075
+ keys.set(path, key);
22076
+ return key;
22077
+ }
22078
+ return {
22079
+ closureOf,
22080
+ keyOf,
22081
+ dependentsOf: (names) => {
22082
+ const dependents = /* @__PURE__ */ new Set();
22083
+ for (const node of nodes) {
22084
+ const closure = closureOf(node.path);
22085
+ if (closure === null) {
22086
+ dependents.add(node.path);
22087
+ continue;
22088
+ }
22089
+ for (const name of names) {
22090
+ if (closure.has(name)) {
22091
+ dependents.add(node.path);
22092
+ break;
22093
+ }
22094
+ }
22095
+ }
22096
+ return dependents;
22097
+ },
22098
+ providedBy: (paths) => {
22099
+ const names = /* @__PURE__ */ new Set();
22100
+ for (const path of paths) {
22101
+ for (const name of byPath.get(path)?.provides.keys() ?? []) {
22102
+ names.add(name);
22103
+ }
22104
+ }
22105
+ return names;
22106
+ }
22107
+ };
22108
+ }
22109
+
22110
+ // ../compiler/src/project/ambient-scope.ts
22111
+ function optionsKey(options) {
22112
+ return `${options.strict ? "strict" : ""}|${options.oop ? "oop" : ""}|${options.noUnusedLocals ? "nul" : ""}|${options.noUnusedParameters ? "nup" : ""}`;
22113
+ }
22114
+ function declaresNothing(declarations) {
22115
+ const { classes, interfaces, enums, globals, events } = declarations;
22116
+ return classes.length === 0 && interfaces.length === 0 && enums.length === 0 && globals.length === 0 && events.length === 0;
22117
+ }
22118
+ function sharedEnums(collected) {
22119
+ const declared = new Set(collected.flatMap((entry) => entry.declarations.enums.map((enumeration) => enumeration.name)));
22120
+ if (declared.size === 0) {
22121
+ return declared;
22122
+ }
22123
+ return new Set(collected.flatMap((entry) => entry.externalReferences.filter((name) => declared.has(name))));
22124
+ }
22125
+ function baseKey(context) {
22126
+ const mode = context.development ? "development" : "release";
22127
+ const contracts = context.contracts.map((contract) => serializeAbi(contract)).join(";");
22128
+ return `${optionsKey(context.options)}|${mode}|${JSON.stringify(context.project.globals)}|${[...context.references].sort().join(",")}|${hashString(contracts)}`;
22129
+ }
22130
+ function environmentKeys(collected) {
22131
+ const keys = {};
22132
+ for (const environment of ALL_ENVIRONMENTS) {
22133
+ const visible = collected.filter((entry) => canReference(environment, entry.environment));
22134
+ keys[environment] = hashString(visible.map((entry) => `${entry.path}=${entry.fingerprint}`).join(";"));
22135
+ }
22136
+ return keys;
22137
+ }
22138
+ function createResolver(collected) {
22139
+ const visible = /* @__PURE__ */ new Map();
22140
+ const merged = /* @__PURE__ */ new Map();
22141
+ for (const environment of ALL_ENVIRONMENTS) {
22142
+ const entries3 = collected.filter((entry) => canReference(environment, entry.environment) && !declaresNothing(entry.declarations));
22143
+ visible.set(environment, entries3);
22144
+ merged.set(environment, mergeAmbient(entries3.map((entry) => entry.declarations)));
22145
+ }
22146
+ return (entry) => {
22147
+ if (declaresNothing(entry.declarations)) {
22148
+ return merged.get(entry.environment) ?? EMPTY_AMBIENT;
22149
+ }
22150
+ const entries3 = visible.get(entry.environment) ?? [];
22151
+ return mergeAmbient(entries3.filter((candidate) => candidate.path !== entry.path).map((candidate) => candidate.declarations));
22152
+ };
22153
+ }
22154
+ function createAmbientScope(collected, context) {
22155
+ const graph = createDependencyGraph(collected);
22156
+ const base = baseKey(context);
22157
+ const fallback = environmentKeys(collected);
22158
+ return {
22159
+ graph,
22160
+ keyFor: (entry) => hashString(`${base}|${graph.keyOf(entry.path) ?? fallback[entry.environment]}`),
22161
+ resolve: createResolver(collected)
22162
+ };
22163
+ }
20893
22164
 
20894
22165
  // ../compiler/src/project/module-graph.ts
20895
22166
  function buildSymbolTable(modules) {
@@ -20950,53 +22221,13 @@ function validateModuleReferences(modules) {
20950
22221
  function flattenDiagnostics(modules) {
20951
22222
  return modules.flatMap((module) => module.diagnostics.map((diagnostic) => ({ path: module.path, diagnostic })));
20952
22223
  }
20953
- function optionsKey(options) {
20954
- return `${options.strict ? "strict" : ""}|${options.oop ? "oop" : ""}|${options.noUnusedLocals ? "nul" : ""}|${options.noUnusedParameters ? "nup" : ""}`;
20955
- }
20956
- function sharedEnums(collected) {
20957
- const declared = new Set(collected.flatMap((entry) => entry.declarations.enums.map((enumeration) => enumeration.name)));
20958
- if (declared.size === 0) {
20959
- return declared;
20960
- }
20961
- return new Set(collected.flatMap((entry) => entry.externalReferences.filter((name) => declared.has(name))));
20962
- }
20963
- function ambientKeys(collected, context) {
20964
- const keys = {};
20965
- const { project, references, options } = context;
20966
- const mode = context.development ? "development" : "release";
20967
- const projectKey = `${optionsKey(options)}|${mode}|${JSON.stringify(project.globals)}|${[...references].sort().join(",")}`;
20968
- for (const environment of ALL_ENVIRONMENTS) {
20969
- const visible = collected.filter((entry) => canReference(environment, entry.environment));
20970
- keys[environment] = hashString(`${projectKey}|${visible.map((entry) => `${entry.path}=${entry.fingerprint}`).join(";")}`);
20971
- }
20972
- return keys;
20973
- }
20974
- function declaresNothing(declarations) {
20975
- const { classes, interfaces, enums, globals, events } = declarations;
20976
- return classes.length === 0 && interfaces.length === 0 && enums.length === 0 && globals.length === 0 && events.length === 0;
20977
- }
20978
- function createAmbientResolver(collected) {
20979
- const visible = /* @__PURE__ */ new Map();
20980
- const merged = /* @__PURE__ */ new Map();
20981
- for (const environment of ALL_ENVIRONMENTS) {
20982
- const entries2 = collected.filter((entry) => canReference(environment, entry.environment) && !declaresNothing(entry.declarations));
20983
- visible.set(environment, entries2);
20984
- merged.set(environment, mergeAmbient(entries2.map((entry) => entry.declarations)));
20985
- }
20986
- return (entry) => {
20987
- if (declaresNothing(entry.declarations)) {
20988
- return merged.get(entry.environment) ?? EMPTY_AMBIENT;
20989
- }
20990
- const entries2 = visible.get(entry.environment) ?? [];
20991
- return mergeAmbient(entries2.filter((candidate) => candidate.path !== entry.path).map((candidate) => candidate.declarations));
20992
- };
20993
- }
20994
22224
  function compileModule(file, ambient, context) {
20995
22225
  const result = compile(file.source, {
20996
22226
  filePath: file.path,
20997
22227
  ...file.environment === void 0 ? {} : { environment: file.environment },
20998
22228
  ambient,
20999
- project: context.project,
22229
+ contracts: context.contracts,
22230
+ project: isTestPath(file.path) ? context.testProject : context.project,
21000
22231
  projectReferences: context.projectReferences,
21001
22232
  compilerOptions: context.compilerOptions,
21002
22233
  development: context.development
@@ -21009,7 +22240,7 @@ function compileModule(file, ambient, context) {
21009
22240
  requiredHelpers: result.requiredHelpers,
21010
22241
  declaredGlobals: result.declaredGlobals,
21011
22242
  externalReferences: result.externalReferences,
21012
- contributions: toContributions(result.directives, result.environment),
22243
+ contributions: toContributions(result.directives, result.environment, result.moduleGlobals),
21013
22244
  diagnostics: result.diagnostics,
21014
22245
  lines: result.lines,
21015
22246
  topLevelReturn: result.topLevelReturn,
@@ -21050,20 +22281,22 @@ function createProjectCache() {
21050
22281
  environment: result.environment,
21051
22282
  declarations: result.declarations,
21052
22283
  externalReferences: [...result.externalReferences.keys()],
21053
- fingerprint: fingerprintDeclarations(result.declarations)
22284
+ fingerprint: fingerprintDeclarations(result.declarations),
22285
+ provides: fingerprintByName(result.declarations),
22286
+ requires: result.referencedNames
21054
22287
  };
21055
22288
  declarationCache.set(file.path, entry);
21056
22289
  return entry;
21057
22290
  }
21058
22291
  function moduleFor(pair, context, reused) {
21059
22292
  const { file, entry } = pair;
21060
- const ambientKey = context.keys[entry.environment];
22293
+ const ambientKey = context.scope.keyFor(entry);
21061
22294
  const cached = moduleCache.get(file.path);
21062
22295
  if (cached !== void 0 && cached.hash === entry.hash && cached.ambientKey === ambientKey) {
21063
22296
  reused.count += 1;
21064
22297
  return cached.module;
21065
22298
  }
21066
- const module = compileModule(file, context.resolve(entry), context);
22299
+ const module = compileModule(file, context.scope.resolve(entry), context);
21067
22300
  moduleCache.set(file.path, { hash: entry.hash, ambientKey, module });
21068
22301
  return module;
21069
22302
  }
@@ -21077,10 +22310,12 @@ function createProjectCache() {
21077
22310
  const collected = pairs.map((pair) => pair.entry);
21078
22311
  const projectReferences = sharedEnums(collected);
21079
22312
  const development = options.development === true;
22313
+ const contracts = options.contracts ?? [];
21080
22314
  const context = {
21081
- keys: ambientKeys(collected, { project, references: projectReferences, options: compilerOptions, development }),
21082
- resolve: createAmbientResolver(collected),
22315
+ scope: createAmbientScope(collected, { project, references: projectReferences, options: compilerOptions, development, contracts }),
22316
+ contracts,
21083
22317
  project,
22318
+ testProject: { globals: [...project.globals, ...TEST_DECLARATIONS] },
21084
22319
  projectReferences,
21085
22320
  compilerOptions,
21086
22321
  development
@@ -21159,8 +22394,8 @@ function materialize(build, resource, includeMap) {
21159
22394
  const bundled = materializeBundles(resource, build.bundles, helperContent);
21160
22395
  return { ...build, scripts: bundled.scripts, map: includeMap ? bundled.map : null };
21161
22396
  }
21162
- function diagnosticSources(files, entries2) {
21163
- const paths = new Set(entries2.map((entry) => entry.path));
22397
+ function diagnosticSources(files, entries3) {
22398
+ const paths = new Set(entries3.map((entry) => entry.path));
21164
22399
  return new Map(files.filter((file) => paths.has(file.path)).map((file) => [file.path, file.source]));
21165
22400
  }
21166
22401
  function runCompile(root, config, options = {}) {
@@ -21168,10 +22403,12 @@ function runCompile(root, config, options = {}) {
21168
22403
  const tracker = options.tracker ?? createPhaseTracker();
21169
22404
  const started = performance.now();
21170
22405
  tracker.begin("discovery");
21171
- const excluded = [config.outDir];
22406
+ const excluded = [config.outDir, config.contracts];
21172
22407
  const sources = discoverSources(root, config.sources, excluded);
21173
22408
  const inputs = readProjectInputs(root, { assets: config.assets, environment: config.environment, excluded });
21174
- const diagnostics = [...sources.diagnostics, ...inputs.diagnostics];
22409
+ const files = [...sources.files, ...options.additionalFiles ?? []].sort((left, right) => left.path.localeCompare(right.path));
22410
+ const contracts = readDependencyContracts(root, config);
22411
+ const diagnostics = [...sources.diagnostics, ...inputs.diagnostics, ...contracts.diagnostics];
21175
22412
  if (hasCliErrors(diagnostics)) {
21176
22413
  tracker.end("failed");
21177
22414
  return {
@@ -21184,13 +22421,15 @@ function runCompile(root, config, options = {}) {
21184
22421
  environmentTemplate: null,
21185
22422
  phases: tracker.durations(),
21186
22423
  sources: /* @__PURE__ */ new Map(),
21187
- map: null
22424
+ map: null,
22425
+ contract: null
21188
22426
  };
21189
22427
  }
21190
- tracker.begin("compile", sources.files.length);
22428
+ tracker.begin("compile", files.length);
21191
22429
  const declarations = projectDeclarations(inputs.declared?.entries ?? null, config.environment.file);
21192
- const project = cache3.compile(sources.files, {
22430
+ const project = cache3.compile(files, {
21193
22431
  project: declarations,
22432
+ contracts: contracts.contracts,
21194
22433
  compilerOptions: config.compilerOptions,
21195
22434
  development: options.development === true,
21196
22435
  onProgress: (event) => tracker.advance(event.item, event.index, event.total)
@@ -21211,19 +22450,20 @@ function runCompile(root, config, options = {}) {
21211
22450
  build,
21212
22451
  diagnostics,
21213
22452
  fileDiagnostics: assembly.diagnostics,
21214
- fileCount: sources.files.length,
22453
+ fileCount: files.length,
21215
22454
  durationMs: performance.now() - started,
21216
22455
  stats: project.stats,
21217
22456
  environmentTemplate: inputs.deployed === null ? null : renderEnvironmentTemplate(inputs.deployed),
21218
22457
  phases: tracker.durations(),
21219
- sources: diagnosticSources(sources.files, assembly.diagnostics),
21220
- map: build?.map ?? null
22458
+ sources: diagnosticSources(files, assembly.diagnostics),
22459
+ map: build?.map ?? null,
22460
+ contract: build === null ? null : buildResourceAbi(config.name, project.modules.flatMap((module) => module.contributions))
21221
22461
  };
21222
22462
  }
21223
22463
 
21224
22464
  // src/build/resource-map-file.ts
21225
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync6, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
21226
- import { dirname as dirname2, isAbsolute as isAbsolute3, join as join4, posix, relative as relative2, resolve as resolve8 } from "node:path";
22465
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
22466
+ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join4, posix, relative as relative2, resolve as resolve9 } from "node:path";
21227
22467
  var RESOURCE_MAP_SUFFIX = ".luam-map.json";
21228
22468
  function isObject(value) {
21229
22469
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -21288,10 +22528,10 @@ function reason(error) {
21288
22528
  return error instanceof Error ? error.message : String(error);
21289
22529
  }
21290
22530
  function resourceMapPath(root, config) {
21291
- return resolve8(root, config.outDir, `${config.name}${RESOURCE_MAP_SUFFIX}`);
22531
+ return resolve9(root, config.outDir, `${config.name}${RESOURCE_MAP_SUFFIX}`);
21292
22532
  }
21293
22533
  function explicitResourceMapPath(root, path) {
21294
- return isAbsolute3(path) ? path : resolve8(root, path);
22534
+ return isAbsolute3(path) ? path : resolve9(root, path);
21295
22535
  }
21296
22536
  function writeResourceMap(root, config, map) {
21297
22537
  const path = resourceMapPath(root, config);
@@ -21301,15 +22541,15 @@ function writeResourceMap(root, config, map) {
21301
22541
  }
21302
22542
  return;
21303
22543
  }
21304
- mkdirSync2(dirname2(path), { recursive: true });
22544
+ mkdirSync3(dirname3(path), { recursive: true });
21305
22545
  const content = serializeResourceMap(map);
21306
- if (!existsSync5(path) || readFileSync6(path, "utf8") !== content) {
21307
- writeFileSync3(path, content, "utf8");
22546
+ if (!existsSync5(path) || readFileSync7(path, "utf8") !== content) {
22547
+ writeFileSync4(path, content, "utf8");
21308
22548
  }
21309
22549
  }
21310
22550
  function readResourceMap(path) {
21311
22551
  try {
21312
- const value = JSON.parse(readFileSync6(path, "utf8"));
22552
+ const value = JSON.parse(readFileSync7(path, "utf8"));
21313
22553
  if (isObject(value) && Number.isInteger(value.version) && value.version !== RESOURCE_MAP_VERSION) {
21314
22554
  return { map: null, error: `Resource map version ${String(value.version)} is not supported.` };
21315
22555
  }
@@ -21323,13 +22563,13 @@ function isSearchable(name) {
21323
22563
  return !name.startsWith(".") && !SKIPPED_DIRECTORIES.has(name);
21324
22564
  }
21325
22565
  function collectResourceMaps(directory, found) {
21326
- let entries2;
22566
+ let entries3;
21327
22567
  try {
21328
- entries2 = readdirSync2(directory, { withFileTypes: true });
22568
+ entries3 = readdirSync2(directory, { withFileTypes: true });
21329
22569
  } catch {
21330
22570
  return;
21331
22571
  }
21332
- for (const entry of entries2) {
22572
+ for (const entry of entries3) {
21333
22573
  const absolute = join4(directory, entry.name);
21334
22574
  if (entry.isDirectory()) {
21335
22575
  if (isSearchable(entry.name)) {
@@ -21353,8 +22593,8 @@ function discoverResourceMap(root) {
21353
22593
  }
21354
22594
 
21355
22595
  // src/build/resource-writer.ts
21356
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
21357
- import { dirname as dirname3, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve10 } from "node:path";
22596
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
22597
+ import { dirname as dirname4, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve11 } from "node:path";
21358
22598
 
21359
22599
  // src/build/lua-tokens.ts
21360
22600
  var WHITESPACE = /* @__PURE__ */ new Set([" ", " ", "\n", "\r", "\v", "\f"]);
@@ -21411,7 +22651,7 @@ function readLongBracket2(source, start, level, file) {
21411
22651
  return end + closing.length;
21412
22652
  }
21413
22653
  function readShortString(source, start, file) {
21414
- const quote2 = source[start];
22654
+ const quote3 = source[start];
21415
22655
  let index = start + 1;
21416
22656
  while (index < source.length) {
21417
22657
  const char = source[index];
@@ -21419,7 +22659,7 @@ function readShortString(source, start, file) {
21419
22659
  index += 2;
21420
22660
  continue;
21421
22661
  }
21422
- if (char === quote2) {
22662
+ if (char === quote3) {
21423
22663
  return index + 1;
21424
22664
  }
21425
22665
  if (char === "\n") {
@@ -21558,7 +22798,7 @@ function minifyLuaFiles(files) {
21558
22798
 
21559
22799
  // src/build/resource-prune.ts
21560
22800
  import { existsSync as existsSync6, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2 } from "node:fs";
21561
- import { join as join5, relative as relative3, resolve as resolve9 } from "node:path";
22801
+ import { join as join5, relative as relative3, resolve as resolve10 } from "node:path";
21562
22802
  var GENERATED_MANIFEST = "meta.xml";
21563
22803
  var GENERATED_EXTENSION = ".lua";
21564
22804
  var PROTECTED = /* @__PURE__ */ new Set([ENVIRONMENT_FILE, ".env.local"]);
@@ -21575,14 +22815,14 @@ function isGenerated(relativePath2, options) {
21575
22815
  return options.generatedRoots.some((root) => isUnderRoot(relativePath2, root));
21576
22816
  }
21577
22817
  function removeEmptyDirectories(targetDir, directory) {
21578
- if (resolve9(directory) === resolve9(targetDir)) {
22818
+ if (resolve10(directory) === resolve10(targetDir)) {
21579
22819
  return;
21580
22820
  }
21581
22821
  if (readdirSync3(directory).length > 0) {
21582
22822
  return;
21583
22823
  }
21584
22824
  rmdirSync(directory);
21585
- removeEmptyDirectories(targetDir, resolve9(directory, ".."));
22825
+ removeEmptyDirectories(targetDir, resolve10(directory, ".."));
21586
22826
  }
21587
22827
  function pruneResource(targetDir, keep, options) {
21588
22828
  if (!existsSync6(targetDir)) {
@@ -21635,7 +22875,7 @@ function resourceFiles(build) {
21635
22875
  return files;
21636
22876
  }
21637
22877
  function resolveInside(targetDir, path) {
21638
- const absolute = resolve10(targetDir, path);
22878
+ const absolute = resolve11(targetDir, path);
21639
22879
  const inside = relative4(targetDir, absolute);
21640
22880
  if (inside.startsWith("..") || isAbsolute4(inside)) {
21641
22881
  throw new Error(`The generated file "${path}" resolves outside the resource directory "${targetDir}".`);
@@ -21643,11 +22883,11 @@ function resolveInside(targetDir, path) {
21643
22883
  return absolute;
21644
22884
  }
21645
22885
  function writeIfChanged(absolute, content) {
21646
- if (existsSync7(absolute) && readFileSync7(absolute).equals(content)) {
22886
+ if (existsSync7(absolute) && readFileSync8(absolute).equals(content)) {
21647
22887
  return false;
21648
22888
  }
21649
- mkdirSync3(dirname3(absolute), { recursive: true });
21650
- writeFileSync4(absolute, content);
22889
+ mkdirSync4(dirname4(absolute), { recursive: true });
22890
+ writeFileSync5(absolute, content);
21651
22891
  return true;
21652
22892
  }
21653
22893
  function writeEnvironmentFile(targetDir, template) {
@@ -21658,8 +22898,8 @@ function writeEnvironmentFile(targetDir, template) {
21658
22898
  if (existsSync7(absolute)) {
21659
22899
  return false;
21660
22900
  }
21661
- mkdirSync3(dirname3(absolute), { recursive: true });
21662
- writeFileSync4(absolute, template, "utf8");
22901
+ mkdirSync4(dirname4(absolute), { recursive: true });
22902
+ writeFileSync5(absolute, template, "utf8");
21663
22903
  return true;
21664
22904
  }
21665
22905
  function writeResource(targetDir, build, options) {
@@ -21683,7 +22923,7 @@ function writeResource(targetDir, build, options) {
21683
22923
  advance(path);
21684
22924
  }
21685
22925
  for (const asset of build.assets) {
21686
- if (writeIfChanged(resolveInside(targetDir, asset.path), readFileSync7(resolve10(options.root, asset.source)))) {
22926
+ if (writeIfChanged(resolveInside(targetDir, asset.path), readFileSync8(resolve11(options.root, asset.source)))) {
21687
22927
  written.push(asset.path);
21688
22928
  advance(asset.path);
21689
22929
  continue;
@@ -21800,16 +23040,16 @@ function reportBuildOutcome(context, outcome, action) {
21800
23040
  }
21801
23041
 
21802
23042
  // src/commands/resource-targets.ts
21803
- import { isAbsolute as isAbsolute5, resolve as resolve11 } from "node:path";
23043
+ import { isAbsolute as isAbsolute5, resolve as resolve12 } from "node:path";
21804
23044
  function resolveBuildTarget(root, config) {
21805
- return resolve11(root, config.outDir, config.name);
23045
+ return resolve12(root, config.outDir, config.name);
21806
23046
  }
21807
23047
  function resolveServerTarget(root, config) {
21808
23048
  if (config.serverPath === null) {
21809
23049
  return null;
21810
23050
  }
21811
- const serverRoot = isAbsolute5(config.serverPath) ? config.serverPath : resolve11(root, config.serverPath);
21812
- return resolve11(serverRoot, config.resourcesDir, config.name);
23051
+ const serverRoot = isAbsolute5(config.serverPath) ? config.serverPath : resolve12(root, config.serverPath);
23052
+ return resolve12(serverRoot, config.resourcesDir, config.name);
21813
23053
  }
21814
23054
 
21815
23055
  // src/reporting/progress-renderer.ts
@@ -21854,7 +23094,7 @@ function createProgressRenderer(reporter, options = {}) {
21854
23094
  paint(reporter.style.eraseLine());
21855
23095
  dirty = false;
21856
23096
  }
21857
- function render(event) {
23097
+ function render2(event) {
21858
23098
  const columns = Math.max(20, reporter.capability.columns - 1);
21859
23099
  const line2 = activeLine(reporter, event);
21860
23100
  paint(`${reporter.style.eraseLine()}${line2.slice(0, columns)}`);
@@ -21874,7 +23114,7 @@ function createProgressRenderer(reporter, options = {}) {
21874
23114
  if (frames > 0 && now() - lastPaintAt < throttleMs) {
21875
23115
  return;
21876
23116
  }
21877
- render(event);
23117
+ render2(event);
21878
23118
  }
21879
23119
  return {
21880
23120
  listen: (event) => {
@@ -21932,6 +23172,9 @@ async function runBuildCommand(context, options = {}) {
21932
23172
  return EXIT_DIAGNOSTICS;
21933
23173
  }
21934
23174
  writeResourceMap(context.root, context.config, writtenMap(outcome.map, minify));
23175
+ if (outcome.contract !== null && outcome.contract.exports.length > 0) {
23176
+ writeResourceContract(context.root, context.config, outcome.contract);
23177
+ }
21935
23178
  tracker.end();
21936
23179
  renderer.clear();
21937
23180
  reportBuildOutcome(context, outcome, "Build");
@@ -21982,6 +23225,298 @@ function registerCheckCommand(program2, runtime) {
21982
23225
  });
21983
23226
  }
21984
23227
 
23228
+ // src/commands/config-command.ts
23229
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
23230
+ import { dirname as dirname5, relative as relative5, resolve as resolve13 } from "node:path";
23231
+
23232
+ // ../compiler/src/project/config-reader.ts
23233
+ var Reader = class {
23234
+ text;
23235
+ index = 0;
23236
+ problems = [];
23237
+ constructor(text) {
23238
+ this.text = text;
23239
+ }
23240
+ position() {
23241
+ const before = this.text.slice(0, this.index);
23242
+ const lines = before.split("\n");
23243
+ return { line: lines.length, column: (lines[lines.length - 1]?.length ?? 0) + 1 };
23244
+ }
23245
+ fail(message) {
23246
+ const { line: line2, column } = this.position();
23247
+ this.problems.push({ line: line2, column, message });
23248
+ return null;
23249
+ }
23250
+ skipComment() {
23251
+ if (!this.text.startsWith("--", this.index)) {
23252
+ return false;
23253
+ }
23254
+ if (this.text.startsWith("--[[", this.index)) {
23255
+ const end2 = this.text.indexOf("]]", this.index + 4);
23256
+ this.index = end2 === -1 ? this.text.length : end2 + 2;
23257
+ return true;
23258
+ }
23259
+ const end = this.text.indexOf("\n", this.index);
23260
+ this.index = end === -1 ? this.text.length : end + 1;
23261
+ return true;
23262
+ }
23263
+ skip() {
23264
+ for (; ; ) {
23265
+ const before = this.index;
23266
+ while (this.index < this.text.length && /\s/.test(this.text[this.index] ?? "")) {
23267
+ this.index += 1;
23268
+ }
23269
+ this.skipComment();
23270
+ if (this.index === before) {
23271
+ return;
23272
+ }
23273
+ }
23274
+ }
23275
+ done() {
23276
+ this.skip();
23277
+ return this.index >= this.text.length;
23278
+ }
23279
+ peek() {
23280
+ this.skip();
23281
+ return this.text[this.index] ?? "";
23282
+ }
23283
+ take(value) {
23284
+ this.skip();
23285
+ if (!this.text.startsWith(value, this.index)) {
23286
+ return false;
23287
+ }
23288
+ this.index += value.length;
23289
+ return true;
23290
+ }
23291
+ word() {
23292
+ this.skip();
23293
+ const match = /^[A-Za-z_][A-Za-z0-9_]*/.exec(this.text.slice(this.index));
23294
+ if (match === null) {
23295
+ return null;
23296
+ }
23297
+ this.index += match[0].length;
23298
+ return match[0];
23299
+ }
23300
+ literal() {
23301
+ this.skip();
23302
+ const rest = this.text.slice(this.index);
23303
+ const string = /^(\[\[[\s\S]*?\]\]|'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")/.exec(rest);
23304
+ if (string !== null) {
23305
+ this.index += string[0].length;
23306
+ return "string";
23307
+ }
23308
+ const number = /^-?(?:0[xX][0-9a-fA-F]+|\d+\.?\d*(?:[eE][-+]?\d+)?|\.\d+)/.exec(rest);
23309
+ if (number !== null) {
23310
+ this.index += number[0].length;
23311
+ return "number";
23312
+ }
23313
+ const word = /^(true|false|nil)\b/.exec(rest);
23314
+ if (word === null) {
23315
+ return null;
23316
+ }
23317
+ this.index += word[0].length;
23318
+ return word[0] === "nil" ? "nil" : "boolean";
23319
+ }
23320
+ key() {
23321
+ this.skip();
23322
+ if (this.take("[")) {
23323
+ const rest = this.text.slice(this.index);
23324
+ const quoted2 = /^'([A-Za-z_][A-Za-z0-9_]*)'|^"([A-Za-z_][A-Za-z0-9_]*)"/.exec(rest);
23325
+ if (quoted2 === null) {
23326
+ return null;
23327
+ }
23328
+ this.index += quoted2[0].length;
23329
+ return this.take("]") && this.take("=") ? quoted2[1] ?? quoted2[2] ?? null : null;
23330
+ }
23331
+ const start = this.index;
23332
+ const word = this.word();
23333
+ if (word === null || !this.take("=") || this.peek() === "=") {
23334
+ this.index = start;
23335
+ return null;
23336
+ }
23337
+ return word;
23338
+ }
23339
+ };
23340
+
23341
+ // ../compiler/src/project/config-extractor.ts
23342
+ var GENERATED_MARKER = '# Generated by "luam config" from';
23343
+ var MAX_SOURCE_BYTES = 262144;
23344
+ var MAX_DEPTH2 = 8;
23345
+ var MAX_ENTRIES = 512;
23346
+ function renderRecord(fields) {
23347
+ return `{ ${fields.map(([name, type]) => `${name}: ${type}`).join(", ")} }`;
23348
+ }
23349
+ function render(value) {
23350
+ if (value.kind === "literal") {
23351
+ return value.type === "nil" ? "any" : value.type;
23352
+ }
23353
+ if (value.kind === "array") {
23354
+ return `${value.element}[]`;
23355
+ }
23356
+ return value.kind === "record" && value.fields.length > 0 ? renderRecord(value.fields) : "table";
23357
+ }
23358
+ function unify2(types) {
23359
+ const unique = [...new Set(types)];
23360
+ return unique.length === 1 ? unique[0] ?? "any" : "any";
23361
+ }
23362
+ function readTable2(reader, depth) {
23363
+ if (depth > MAX_DEPTH2) {
23364
+ return reader.fail(`A table nested deeper than ${MAX_DEPTH2} levels is not extracted.`);
23365
+ }
23366
+ const fields = [];
23367
+ const elements = [];
23368
+ for (let count = 0; ; count += 1) {
23369
+ if (reader.take("}")) {
23370
+ break;
23371
+ }
23372
+ if (count > MAX_ENTRIES) {
23373
+ return reader.fail(`A table with more than ${MAX_ENTRIES} entries is not extracted.`);
23374
+ }
23375
+ const key = reader.key();
23376
+ const value = readValue(reader, depth + 1);
23377
+ if (value === null) {
23378
+ return null;
23379
+ }
23380
+ if (key === null) {
23381
+ elements.push(render(value));
23382
+ } else {
23383
+ fields.push([key, render(value)]);
23384
+ }
23385
+ if (!reader.take(",") && !reader.take(";") && reader.peek() !== "}") {
23386
+ return reader.fail('Expected "," or "}" in a table.');
23387
+ }
23388
+ }
23389
+ if (fields.length > 0 && elements.length > 0) {
23390
+ return { kind: "table" };
23391
+ }
23392
+ return elements.length > 0 ? { kind: "array", element: unify2(elements) } : { kind: "record", fields };
23393
+ }
23394
+ function readValue(reader, depth) {
23395
+ if (reader.take("{")) {
23396
+ return readTable2(reader, depth);
23397
+ }
23398
+ const literal2 = reader.literal();
23399
+ if (literal2 === null) {
23400
+ return reader.fail("Only literals and table constructors are extracted. Declare this value by hand.");
23401
+ }
23402
+ if (reader.peek() === "." || reader.peek() === "(" || reader.peek() === "+") {
23403
+ return reader.fail("An expression is not extracted. Declare this value by hand.");
23404
+ }
23405
+ return { kind: "literal", type: literal2 };
23406
+ }
23407
+ function extractConfigDeclarations(source) {
23408
+ if (source.length > MAX_SOURCE_BYTES) {
23409
+ return { declarations: [], problems: [{ line: 1, column: 1, message: `A file larger than ${MAX_SOURCE_BYTES} bytes is not extracted.` }] };
23410
+ }
23411
+ const reader = new Reader(source);
23412
+ const declarations = /* @__PURE__ */ new Map();
23413
+ while (!reader.done()) {
23414
+ reader.take("local");
23415
+ const name = reader.word();
23416
+ if (name === null || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || !reader.take("=")) {
23417
+ reader.fail("Only a top-level assignment of a literal or a table is extracted.");
23418
+ break;
23419
+ }
23420
+ const value = readValue(reader, 1);
23421
+ if (value === null) {
23422
+ break;
23423
+ }
23424
+ declarations.set(name, render(value));
23425
+ reader.take(";");
23426
+ }
23427
+ return { declarations: [...declarations].map(([name, type]) => ({ name, type })), problems: reader.problems };
23428
+ }
23429
+ function renderConfigDeclarations(source, declarations) {
23430
+ const lines = declarations.map((declaration) => `declare ${declaration.name}: ${declaration.type}`);
23431
+ return `${GENERATED_MARKER} "${source}". Do not edit.
23432
+
23433
+ ${lines.join("\n")}
23434
+ `;
23435
+ }
23436
+ function isGeneratedDeclaration(text) {
23437
+ return text.startsWith(GENERATED_MARKER);
23438
+ }
23439
+
23440
+ // src/commands/config-command.ts
23441
+ var DEFAULT_CONFIG_SOURCE = "config.lua";
23442
+ var DEFAULT_CONFIG_OUTPUT = "config.d.luam";
23443
+ function escapes(root, path) {
23444
+ const inside = relative5(root, path);
23445
+ return inside.startsWith("..") || resolve13(root, inside) !== path;
23446
+ }
23447
+ function readSource2(root, source, reporter) {
23448
+ const path = resolve13(root, source);
23449
+ if (escapes(root, path)) {
23450
+ reporter.error(`Config declarations read only inside the project, but "${source}" points outside it.`);
23451
+ return null;
23452
+ }
23453
+ if (!existsSync8(path)) {
23454
+ reporter.error(`Config declarations found no file at "${source}".`);
23455
+ return null;
23456
+ }
23457
+ try {
23458
+ return readFileSync9(path, "utf8");
23459
+ } catch {
23460
+ reporter.error(`Config declarations could not read "${source}".`);
23461
+ return null;
23462
+ }
23463
+ }
23464
+ function writable(root, out, reporter) {
23465
+ const path = resolve13(root, out);
23466
+ if (escapes(root, path)) {
23467
+ reporter.error(`Config declarations write only inside the project, but "${out}" points outside it.`);
23468
+ return null;
23469
+ }
23470
+ if (existsSync8(path) && !isGeneratedDeclaration(readFileSync9(path, "utf8"))) {
23471
+ reporter.error(`"${out}" was not generated by this command and was left alone. Choose another path with --out.`);
23472
+ return null;
23473
+ }
23474
+ return path;
23475
+ }
23476
+ function runConfigCommand(root, reporter, options = {}) {
23477
+ const source = options.source ?? DEFAULT_CONFIG_SOURCE;
23478
+ const out = options.out ?? DEFAULT_CONFIG_OUTPUT;
23479
+ const text = readSource2(root, source, reporter);
23480
+ if (text === null) {
23481
+ return EXIT_DIAGNOSTICS;
23482
+ }
23483
+ const extracted = extractConfigDeclarations(text);
23484
+ for (const problem of extracted.problems) {
23485
+ reporter.warn(`${source}:${problem.line}:${problem.column} ${problem.message}`);
23486
+ }
23487
+ if (extracted.declarations.length === 0) {
23488
+ reporter.error(`Config declarations found nothing to declare in "${source}".`);
23489
+ return EXIT_DIAGNOSTICS;
23490
+ }
23491
+ const rendered = renderConfigDeclarations(source, extracted.declarations);
23492
+ if (options.write !== true) {
23493
+ reporter.info(rendered.trimEnd());
23494
+ reporter.info(`Config declarations would write ${extracted.declarations.length} name(s) to "${out}". Pass --write to save it.`);
23495
+ return EXIT_OK;
23496
+ }
23497
+ const path = writable(root, out, reporter);
23498
+ if (path === null) {
23499
+ return EXIT_DIAGNOSTICS;
23500
+ }
23501
+ mkdirSync5(dirname5(path), { recursive: true });
23502
+ writeFileSync6(path, rendered, "utf8");
23503
+ reporter.info(`Config declarations wrote ${extracted.declarations.length} name(s) to "${out}".`);
23504
+ return EXIT_OK;
23505
+ }
23506
+
23507
+ // src/cli/registry/config-registration.ts
23508
+ function registerConfigCommand(program2, runtime) {
23509
+ const command = program2.command("config").description('Derive a declaration file from the literal data in a native "config.lua".');
23510
+ addProjectOptions(command).addOption(valueOption("--source <path>", 'Native Lua file to read. Defaults to "config.lua".')).addOption(valueOption("--out <path>", 'Declaration file to write. Defaults to "config.d.luam".')).addOption(new Option("--write", "Write the declaration file instead of printing it."));
23511
+ command.action((options) => {
23512
+ runtime.exitCode = runConfigCommand(commandRoot(runtime, options), runtime.reporter, {
23513
+ source: options.source ?? null,
23514
+ out: options.out ?? null,
23515
+ write: options.write === true
23516
+ });
23517
+ });
23518
+ }
23519
+
21985
23520
  // src/commands/ensure-runner.ts
21986
23521
  function hasChanges(result) {
21987
23522
  return result.written.length > 0 || result.removed.length > 0;
@@ -22006,7 +23541,7 @@ function restartResource(scope, serverConsole) {
22006
23541
  reporter.success(`Restarted "${scope.context.config.name}" through the owned server console.`);
22007
23542
  return true;
22008
23543
  }
22009
- function failed(outcome) {
23544
+ function failed2(outcome) {
22010
23545
  return { ok: false, outcome, sync: null, restarted: false };
22011
23546
  }
22012
23547
  async function runOnce(scope, target, cache3, options) {
@@ -22029,7 +23564,7 @@ async function runOnce(scope, target, cache3, options) {
22029
23564
  if (outcome.build === null) {
22030
23565
  reportBuildOutcome(context, outcome, "Build");
22031
23566
  reporter.warn("Skipping sync and restart because the build reported errors.");
22032
- return failed(outcome);
23567
+ return failed2(outcome);
22033
23568
  }
22034
23569
  const writeOptions = trackedWriteOptions(context.root, context.config, outcome.environmentTemplate, tracker);
22035
23570
  reportBuildOutcome(context, outcome, "Build");
@@ -22077,8 +23612,8 @@ function reportRebuildSeparator(reporter, at = /* @__PURE__ */ new Date()) {
22077
23612
  }
22078
23613
 
22079
23614
  // src/watch/source-watcher.ts
22080
- import { existsSync as existsSync8, watch } from "node:fs";
22081
- import { relative as relative5, resolve as resolve12 } from "node:path";
23615
+ import { existsSync as existsSync9, watch } from "node:fs";
23616
+ import { relative as relative6, resolve as resolve14 } from "node:path";
22082
23617
  var DEFAULT_DEBOUNCE_MS = 120;
22083
23618
  function watchSources(root, sources, onChange, debounceMs = DEFAULT_DEBOUNCE_MS) {
22084
23619
  const resolver = createSourceResolver(sources);
@@ -22094,12 +23629,12 @@ function watchSources(root, sources, onChange, debounceMs = DEFAULT_DEBOUNCE_MS)
22094
23629
  }, debounceMs);
22095
23630
  };
22096
23631
  const isWatched = (directory, filename) => {
22097
- const path = normalizePattern(relative5(root, resolve12(directory, filename)));
23632
+ const path = normalizePattern(relative6(root, resolve14(directory, filename)));
22098
23633
  return path.endsWith(SOURCE_EXTENSION) && resolver.resolve(path).matches.length > 0;
22099
23634
  };
22100
23635
  for (const entry of resolver.roots) {
22101
- const absolute = resolve12(root, entry);
22102
- if (!existsSync8(absolute)) {
23636
+ const absolute = resolve14(root, entry);
23637
+ if (!existsSync9(absolute)) {
22103
23638
  continue;
22104
23639
  }
22105
23640
  watchers.push(
@@ -22292,26 +23827,26 @@ function parseMtaLogLine(line2, activeResource, fallback = /* @__PURE__ */ new D
22292
23827
  const level = severity?.[1] === void 0 ? "info" : logLevel(severity[1]) ?? "info";
22293
23828
  const message = stamped.rest.replace(resource[0], "").replace(severity?.[0] ?? "", "").replace(/^\s*[:|-]?\s*/, "");
22294
23829
  const sourcePath = resource[2];
22295
- const sourceLine = resource[3];
23830
+ const sourceLine2 = resource[3];
22296
23831
  return {
22297
23832
  timestamp: stamped.value,
22298
23833
  environment: "server",
22299
23834
  level,
22300
23835
  message,
22301
23836
  resource: activeResource,
22302
- ...sourcePath !== void 0 && sourceLine !== void 0 ? { source: { path: sourcePath, line: Number(sourceLine) } } : {}
23837
+ ...sourcePath !== void 0 && sourceLine2 !== void 0 ? { source: { path: sourcePath, line: Number(sourceLine2) } } : {}
22303
23838
  };
22304
23839
  }
22305
23840
  return { timestamp: stamped.value, environment: "server", level: "info", message: stamped.rest, resource: activeResource };
22306
23841
  }
22307
23842
 
22308
23843
  // src/logging/server-log-follower.ts
22309
- import { closeSync, existsSync as existsSync9, openSync, readSync, statSync as statSync4 } from "node:fs";
22310
- import { isAbsolute as isAbsolute6, resolve as resolve13 } from "node:path";
23844
+ import { closeSync, existsSync as existsSync10, openSync, readSync, statSync as statSync4 } from "node:fs";
23845
+ import { isAbsolute as isAbsolute6, resolve as resolve15 } from "node:path";
22311
23846
  var DEFAULT_POLL_INTERVAL_MS = 100;
22312
23847
  function resolveServerLogPath(root, serverPath) {
22313
- const server = isAbsolute6(serverPath) ? serverPath : resolve13(root, serverPath);
22314
- return resolve13(server, "mods/deathmatch/logs/server.log");
23848
+ const server = isAbsolute6(serverPath) ? serverPath : resolve15(root, serverPath);
23849
+ return resolve15(server, "mods/deathmatch/logs/server.log");
22315
23850
  }
22316
23851
  function identity(path) {
22317
23852
  const stats = statSync4(path);
@@ -22336,7 +23871,7 @@ function followServerLog(path, onLine, options = {}) {
22336
23871
  let initialized = false;
22337
23872
  let closed = false;
22338
23873
  const poll = () => {
22339
- if (closed || !existsSync9(path)) {
23874
+ if (closed || !existsSync10(path)) {
22340
23875
  return;
22341
23876
  }
22342
23877
  try {
@@ -22452,9 +23987,9 @@ function connectServerConsoleInput(input, output, interrupt) {
22452
23987
 
22453
23988
  // src/server/server-executable-resolver.ts
22454
23989
  import { realpathSync, statSync as statSync5 } from "node:fs";
22455
- import { isAbsolute as isAbsolute7, relative as relative6, resolve as resolve14 } from "node:path";
23990
+ import { isAbsolute as isAbsolute7, relative as relative7, resolve as resolve16 } from "node:path";
22456
23991
  function isInside(root, path) {
22457
- const pathFromRoot = relative6(root, path);
23992
+ const pathFromRoot = relative7(root, path);
22458
23993
  return pathFromRoot.length === 0 || !pathFromRoot.startsWith("..") && !isAbsolute7(pathFromRoot);
22459
23994
  }
22460
23995
  function candidates2(platform) {
@@ -22467,14 +24002,14 @@ function candidates2(platform) {
22467
24002
  throw new Error(`Local MTA server startup is not supported on platform "${platform}". Use Windows or Linux.`);
22468
24003
  }
22469
24004
  function resolveServerExecutable(options) {
22470
- const serverRoot = resolve14(options.root, options.serverPath);
24005
+ const serverRoot = resolve16(options.root, options.serverPath);
22471
24006
  const names = options.configured === null ? candidates2(options.platform ?? process.platform) : [options.configured];
22472
24007
  const attempted = [];
22473
- if (options.configured !== null && (isAbsolute7(options.configured) || !isInside(serverRoot, resolve14(serverRoot, options.configured)))) {
24008
+ if (options.configured !== null && (isAbsolute7(options.configured) || !isInside(serverRoot, resolve16(serverRoot, options.configured)))) {
22474
24009
  throw new Error(`Configured development.server.executable must stay inside serverPath but received "${options.configured}".`);
22475
24010
  }
22476
24011
  for (const name of names) {
22477
- const path = resolve14(serverRoot, name);
24012
+ const path = resolve16(serverRoot, name);
22478
24013
  attempted.push(path);
22479
24014
  try {
22480
24015
  if (!statSync5(path).isFile()) {
@@ -22744,9 +24279,52 @@ function registerDevCommand(program2, runtime) {
22744
24279
  });
22745
24280
  }
22746
24281
 
24282
+ // src/testing/lua-interpreter.ts
24283
+ import { spawnSync as spawnSync2 } from "node:child_process";
24284
+ var REQUIRED_LUA_VERSION = "Lua 5.1";
24285
+ var LUA_CANDIDATES = ["lua5.1", "lua51", "lua", "luajit"];
24286
+ var LUA_ENV_VARIABLE = "LUAM_LUA";
24287
+ var INSTALL_HINT = 'Install a Lua 5.1 interpreter and put it on PATH, or point "--lua" or the LUAM_LUA variable at one. LuaJIT reports "Lua 5.1" and is accepted.';
24288
+ function probeInterpreter(executable) {
24289
+ const result = spawnSync2(executable, ["-e", "io.write(_VERSION)"], { encoding: "utf8", shell: false, windowsHide: true });
24290
+ if (result.error !== void 0 || result.status !== 0) {
24291
+ return null;
24292
+ }
24293
+ return result.stdout.trim();
24294
+ }
24295
+ function findLuaInterpreter(request = {}) {
24296
+ const probe = request.probe ?? probeInterpreter;
24297
+ const pinned = request.explicit ?? request.env ?? null;
24298
+ const candidates3 = pinned === null ? LUA_CANDIDATES : [pinned];
24299
+ for (const executable of candidates3) {
24300
+ const version = probe(executable);
24301
+ if (version === REQUIRED_LUA_VERSION) {
24302
+ return { executable, version };
24303
+ }
24304
+ }
24305
+ return null;
24306
+ }
24307
+ function describeMissingInterpreter(request = {}) {
24308
+ const pinned = request.explicit ?? request.env ?? null;
24309
+ if (pinned !== null) {
24310
+ return `"${pinned}" is not a ${REQUIRED_LUA_VERSION} interpreter.`;
24311
+ }
24312
+ return `No ${REQUIRED_LUA_VERSION} interpreter was found on PATH (tried ${LUA_CANDIDATES.map((name) => `"${name}"`).join(", ")}).`;
24313
+ }
24314
+
22747
24315
  // src/commands/doctor-command.ts
22748
- function runDoctorCommand(reporter, editorService) {
24316
+ function reportInterpreter(reporter, probe) {
24317
+ const interpreter = findLuaInterpreter(probe === void 0 ? {} : { probe });
24318
+ if (interpreter === null) {
24319
+ reporter.warn(`No ${REQUIRED_LUA_VERSION} interpreter was found on PATH, so "luam test" cannot run.`);
24320
+ reporter.detail(INSTALL_HINT);
24321
+ return;
24322
+ }
24323
+ reporter.success(`"${interpreter.executable}" reports ${interpreter.version} and can run "luam test".`);
24324
+ }
24325
+ function runDoctorCommand(reporter, editorService, probe) {
22749
24326
  reporter.success(`Luam CLI ${VERSION} is running on Node.js ${process.versions.node}.`);
24327
+ reportInterpreter(reporter, probe);
22750
24328
  const editors = editorService.detect();
22751
24329
  if (editors.length === 0) {
22752
24330
  reporter.warn("No supported editor command was found on PATH.");
@@ -22792,7 +24370,7 @@ function registerEnsureCommand(program2, runtime) {
22792
24370
  }
22793
24371
 
22794
24372
  // src/cli/registry/init-registration.ts
22795
- import { resolve as resolve16 } from "node:path";
24373
+ import { resolve as resolve18 } from "node:path";
22796
24374
 
22797
24375
  // src/commands/init-command.ts
22798
24376
  import { basename as basename2 } from "node:path";
@@ -22854,7 +24432,7 @@ function renderManifest(source, details) {
22854
24432
  }
22855
24433
 
22856
24434
  // src/scaffold/template-files.ts
22857
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
24435
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
22858
24436
  import { fileURLToPath as fileURLToPath2 } from "node:url";
22859
24437
 
22860
24438
  // ../template/src/template.ts
@@ -22870,10 +24448,10 @@ function bundledTemplatePath(source) {
22870
24448
  }
22871
24449
  function resolveTemplatePath(source) {
22872
24450
  const packaged = fileURLToPath2(resolveTemplateUrl(source));
22873
- return existsSync10(packaged) ? packaged : bundledTemplatePath(source);
24451
+ return existsSync11(packaged) ? packaged : bundledTemplatePath(source);
22874
24452
  }
22875
24453
  function readTemplateSource(source) {
22876
- return readFileSync8(resolveTemplatePath(source), "utf8");
24454
+ return readFileSync10(resolveTemplatePath(source), "utf8");
22877
24455
  }
22878
24456
 
22879
24457
  // src/scaffold/scaffold-plan.ts
@@ -22889,19 +24467,19 @@ function buildScaffoldPlan(details) {
22889
24467
  }
22890
24468
 
22891
24469
  // src/scaffold/scaffold-writer.ts
22892
- import { existsSync as existsSync11, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
22893
- import { dirname as dirname4, resolve as resolve15 } from "node:path";
24470
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "node:fs";
24471
+ import { dirname as dirname6, resolve as resolve17 } from "node:path";
22894
24472
  function writeScaffold(targetDir, plan, force) {
22895
24473
  const written = [];
22896
24474
  const skipped = [];
22897
24475
  for (const file of plan.files) {
22898
- const absolute = resolve15(targetDir, file.path);
22899
- if (!force && existsSync11(absolute)) {
24476
+ const absolute = resolve17(targetDir, file.path);
24477
+ if (!force && existsSync12(absolute)) {
22900
24478
  skipped.push(file.path);
22901
24479
  continue;
22902
24480
  }
22903
- mkdirSync4(dirname4(absolute), { recursive: true });
22904
- writeFileSync5(absolute, file.content, "utf8");
24481
+ mkdirSync6(dirname6(absolute), { recursive: true });
24482
+ writeFileSync7(absolute, file.content, "utf8");
22905
24483
  written.push(file.path);
22906
24484
  }
22907
24485
  return { written: written.sort((left, right) => left.localeCompare(right)), skipped: skipped.sort((left, right) => left.localeCompare(right)) };
@@ -22951,7 +24529,7 @@ function registerInitCommand(program2, runtime) {
22951
24529
  command.addOption(cwdOption()).addOption(valueOption("--name <name>", "Resource name. Defaults to the destination directory name.")).addOption(new Option("--force", "Overwrite files that already exist.")).addOption(new Option("-y, --yes", "Accept the defaults without prompting.")).addOption(colorOption());
22952
24530
  command.action(async (path, options) => {
22953
24531
  const root = commandRoot(runtime, options);
22954
- const target = path === void 0 ? root : resolve16(root, path);
24532
+ const target = path === void 0 ? root : resolve18(root, path);
22955
24533
  runtime.exitCode = await runInitCommand(target, runtime.logger, {
22956
24534
  name: options.name ?? null,
22957
24535
  force: options.force === true,
@@ -22969,7 +24547,7 @@ async function runSetupCommand(reporter, options) {
22969
24547
  reporter.detail("Supported commands: code, code-insiders, cursor, codium, windsurf.");
22970
24548
  return EXIT_DIAGNOSTICS;
22971
24549
  }
22972
- let failed2 = false;
24550
+ let failed3 = false;
22973
24551
  for (const editor of editors) {
22974
24552
  if (options.editorService.hasExtension(editor)) {
22975
24553
  reporter.success(`${editor.name}: Luam is already installed.`);
@@ -22988,12 +24566,12 @@ async function runSetupCommand(reporter, options) {
22988
24566
  const result = await options.editorService.install(editor);
22989
24567
  if (result.error !== null) {
22990
24568
  reporter.error(`${editor.name}: installation failed. ${result.error}`);
22991
- failed2 = true;
24569
+ failed3 = true;
22992
24570
  continue;
22993
24571
  }
22994
24572
  reporter.success(`${editor.name}: installed from the ${result.source}.`);
22995
24573
  }
22996
- return failed2 ? EXIT_DIAGNOSTICS : EXIT_OK;
24574
+ return failed3 ? EXIT_DIAGNOSTICS : EXIT_OK;
22997
24575
  }
22998
24576
 
22999
24577
  // src/editor/installation-prompt.ts
@@ -23074,13 +24652,640 @@ function registerServerCommand(program2, runtime) {
23074
24652
  });
23075
24653
  }
23076
24654
 
24655
+ // src/testing/test-report.ts
24656
+ import { readFileSync as readFileSync11 } from "node:fs";
24657
+ import { resolve as resolve19 } from "node:path";
24658
+ function sourceLine(root, path, line2, cache3) {
24659
+ let lines = cache3.get(path);
24660
+ if (lines === void 0) {
24661
+ try {
24662
+ lines = readFileSync11(resolve19(root, path), "utf8").split(/\r?\n/);
24663
+ } catch {
24664
+ lines = [];
24665
+ }
24666
+ cache3.set(path, lines);
24667
+ }
24668
+ return lines[line2 - 1] ?? null;
24669
+ }
24670
+ function columnOf(text, symbol) {
24671
+ if (text === null) {
24672
+ return 1;
24673
+ }
24674
+ const symbolIndex = symbol === void 0 ? -1 : text.indexOf(symbol);
24675
+ if (symbolIndex >= 0) {
24676
+ return symbolIndex + 1;
24677
+ }
24678
+ const indent = /^\s*/.exec(text)?.[0].length ?? 0;
24679
+ return indent + 1;
24680
+ }
24681
+ function resolveTestPosition(root, map, result, cache3) {
24682
+ if (map === null || result.file === null || result.line === null) {
24683
+ return null;
24684
+ }
24685
+ const resolution = resolveResourcePosition(map, result.file, result.line);
24686
+ if (resolution.status !== "resolved") {
24687
+ return null;
24688
+ }
24689
+ const { file, line: line2, symbol } = resolution.position;
24690
+ return { path: file, line: line2, column: columnOf(sourceLine(root, file, line2, cache3), symbol) };
24691
+ }
24692
+ function formatLocation(root, map, result, cache3) {
24693
+ const position2 = resolveTestPosition(root, map, result, cache3);
24694
+ if (position2 !== null) {
24695
+ return `${position2.path}:${position2.line}:${position2.column}`;
24696
+ }
24697
+ return result.file === null || result.line === null ? "unknown position" : `${result.file}:${result.line}`;
24698
+ }
24699
+ function countResults(runs) {
24700
+ const results = runs.flatMap((run) => run.results);
24701
+ const passed = results.filter((result) => result.passed).length;
24702
+ return { passed, failed: results.length - passed };
24703
+ }
24704
+ function reportTestResults(reporter, runs, root, map, durationMs) {
24705
+ const cache3 = /* @__PURE__ */ new Map();
24706
+ const marker = (name) => reporter.style.marker(name);
24707
+ for (const run of runs) {
24708
+ for (const line2 of run.output) {
24709
+ reporter.detail(` ${line2}`);
24710
+ }
24711
+ for (const result of run.results) {
24712
+ const tone = result.passed ? "success" : "error";
24713
+ const label2 = `${result.environment} \xB7 ${result.name}`;
24714
+ reporter.raw(` ${reporter.style.paint(tone, marker(result.passed ? "success" : "failure"))} ${label2}`);
24715
+ if (!result.passed) {
24716
+ reporter.rawError(` ${formatLocation(root, map, result, cache3)} ${result.message}`);
24717
+ }
24718
+ }
24719
+ if (run.failure !== null) {
24720
+ reporter.error(`The ${run.environment} test run did not complete: ${run.failure}`);
24721
+ }
24722
+ }
24723
+ const totals = countResults(runs);
24724
+ const summary = `${pluralize(totals.passed, "test")} passed, ${totals.failed} failed in ${formatDuration(durationMs)}.`;
24725
+ const crashed = runs.some((run) => run.failure !== null);
24726
+ if (totals.failed === 0 && !crashed) {
24727
+ reporter.success(`Tests passed: ${summary}`);
24728
+ return true;
24729
+ }
24730
+ reporter.error(`Tests failed: ${summary}`);
24731
+ return false;
24732
+ }
24733
+
24734
+ // src/testing/test-discovery.ts
24735
+ import { readFileSync as readFileSync12 } from "node:fs";
24736
+ import { resolve as resolve20 } from "node:path";
24737
+ var DEFAULT_TEST_ENVIRONMENT = "shared";
24738
+ var SIDE_CONFLICT2 = "test-source-side-conflict";
24739
+ var UNREADABLE_TEST = "test-source-unreadable";
24740
+ function readTest(root, path, diagnostics) {
24741
+ try {
24742
+ return readFileSync12(resolve20(root, path), "utf8");
24743
+ } catch (error) {
24744
+ diagnostics.push(cliError(UNREADABLE_TEST, `The test file "${path}" could not be read: ${error instanceof Error ? error.message : String(error)}`));
24745
+ return null;
24746
+ }
24747
+ }
24748
+ function discoverTests(root, sources, excluded = []) {
24749
+ const resolver = createSourceResolver(sources);
24750
+ const tree = listProjectFiles(root, ["."], excluded);
24751
+ const diagnostics = tree.errors.map((message) => cliError(UNREADABLE_TEST, message));
24752
+ const files = [];
24753
+ for (const path of tree.files.filter(isTestPath)) {
24754
+ const resolution = resolver.resolve(path);
24755
+ if (resolution.matches.length > 1) {
24756
+ diagnostics.push(cliError(SIDE_CONFLICT2, `"${path}" is matched by more than one side: ${describeMatches(resolution.matches)}. Narrow the patterns.`));
24757
+ continue;
24758
+ }
24759
+ const source = readTest(root, path, diagnostics);
24760
+ if (source !== null) {
24761
+ files.push({ path, source, environment: resolution.environment ?? DEFAULT_TEST_ENVIRONMENT });
24762
+ }
24763
+ }
24764
+ return { files: files.sort((left, right) => left.path.localeCompare(right.path)), diagnostics };
24765
+ }
24766
+
24767
+ // src/testing/test-runner.ts
24768
+ import { spawnSync as spawnSync3 } from "node:child_process";
24769
+ import { mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "node:fs";
24770
+ import { tmpdir as tmpdir2 } from "node:os";
24771
+ import { dirname as dirname7, join as join6 } from "node:path";
24772
+
24773
+ // src/testing/harness-assertions.ts
24774
+ var ASSERTIONS_SOURCE = String.raw`local SENTINEL = '##luam:test'
24775
+ local HARNESS = debug.getinfo(1, 'S').short_src
24776
+
24777
+ local tests = {}
24778
+ local groups = {}
24779
+ local beforeHooks = {}
24780
+ local afterHooks = {}
24781
+ local callLog = {}
24782
+ local behaviour = {}
24783
+ local stubNames = rawget(_G, '__luamTestNames') or {}
24784
+
24785
+ local function escape(value)
24786
+ local text = tostring(value)
24787
+ text = text:gsub('\\', '\\\\')
24788
+ text = text:gsub('\t', '\\t')
24789
+ text = text:gsub('\r', '\\r')
24790
+ text = text:gsub('\n', '\\n')
24791
+ return text
24792
+ end
24793
+
24794
+ local function emit(kind, ...)
24795
+ local parts = { SENTINEL, kind }
24796
+ for index = 1, select('#', ...) do
24797
+ parts[#parts + 1] = escape(select(index, ...))
24798
+ end
24799
+ io.write(table.concat(parts, '\t'))
24800
+ io.write('\n')
24801
+ io.stdout:flush()
24802
+ end
24803
+
24804
+ local function origin()
24805
+ local index = 1
24806
+ while true do
24807
+ local info = debug.getinfo(index, 'Sl')
24808
+ if info == nil then
24809
+ return '', ''
24810
+ end
24811
+ if info.what ~= 'C' and info.short_src ~= HARNESS then
24812
+ return info.short_src, info.currentline
24813
+ end
24814
+ index = index + 1
24815
+ end
24816
+ end
24817
+
24818
+ local function failure(message)
24819
+ local file, line = origin()
24820
+ return { luamTestFailure = true, message = message, file = file, line = line }
24821
+ end
24822
+
24823
+ local function fail(message)
24824
+ error(failure(message), 0)
24825
+ end
24826
+
24827
+ local function handler(err)
24828
+ if type(err) == 'table' and err.luamTestFailure then
24829
+ return err
24830
+ end
24831
+ return failure(tostring(err))
24832
+ end
24833
+
24834
+ local function attempt(body)
24835
+ local ok, err = xpcall(body, handler)
24836
+ if ok then
24837
+ return nil
24838
+ end
24839
+ return err
24840
+ end
24841
+
24842
+ local function describeValue(value)
24843
+ if type(value) == 'string' then
24844
+ return string.format('%q', value)
24845
+ end
24846
+ return tostring(value)
24847
+ end
24848
+
24849
+ local function deepEqual(left, right)
24850
+ if left == right then
24851
+ return true
24852
+ end
24853
+ if type(left) ~= 'table' or type(right) ~= 'table' then
24854
+ return false
24855
+ end
24856
+ for key, value in pairs(left) do
24857
+ if not deepEqual(value, right[key]) then
24858
+ return false
24859
+ end
24860
+ end
24861
+ for key in pairs(right) do
24862
+ if left[key] == nil then
24863
+ return false
24864
+ end
24865
+ end
24866
+ return true
24867
+ end
24868
+
24869
+ local function expect(value)
24870
+ local matchers = {}
24871
+
24872
+ function matchers.toBe(expected)
24873
+ if value ~= expected then
24874
+ fail('expected ' .. describeValue(expected) .. ', got ' .. describeValue(value))
24875
+ end
24876
+ end
24877
+
24878
+ function matchers.toNotBe(expected)
24879
+ if value == expected then
24880
+ fail('expected a value other than ' .. describeValue(expected))
24881
+ end
24882
+ end
24883
+
24884
+ function matchers.toEqual(expected)
24885
+ if not deepEqual(value, expected) then
24886
+ fail('expected ' .. describeValue(expected) .. ', got ' .. describeValue(value))
24887
+ end
24888
+ end
24889
+
24890
+ function matchers.toNotEqual(expected)
24891
+ if deepEqual(value, expected) then
24892
+ fail('expected a value other than ' .. describeValue(expected))
24893
+ end
24894
+ end
24895
+
24896
+ function matchers.toBeNil()
24897
+ if value ~= nil then
24898
+ fail('expected nil, got ' .. describeValue(value))
24899
+ end
24900
+ end
24901
+
24902
+ function matchers.toBeTruthy()
24903
+ if not value then
24904
+ fail('expected a truthy value, got ' .. describeValue(value))
24905
+ end
24906
+ end
24907
+
24908
+ function matchers.toBeFalsy()
24909
+ if value then
24910
+ fail('expected a falsy value, got ' .. describeValue(value))
24911
+ end
24912
+ end
24913
+
24914
+ function matchers.toContain(entry)
24915
+ if type(value) == 'string' then
24916
+ if string.find(value, tostring(entry), 1, true) == nil then
24917
+ fail('expected ' .. describeValue(value) .. ' to contain ' .. describeValue(entry))
24918
+ end
24919
+ return
24920
+ end
24921
+ if type(value) ~= 'table' then
24922
+ fail('expected a string or a table, got ' .. type(value))
24923
+ return
24924
+ end
24925
+ for _, candidate in pairs(value) do
24926
+ if deepEqual(candidate, entry) then
24927
+ return
24928
+ end
24929
+ end
24930
+ fail('expected the table to contain ' .. describeValue(entry))
24931
+ end
24932
+
24933
+ function matchers.toThrow(message)
24934
+ if type(value) ~= 'function' then
24935
+ fail('expected a function, got ' .. type(value))
24936
+ return
24937
+ end
24938
+ local ok, err = pcall(value)
24939
+ if ok then
24940
+ fail('expected the function to throw')
24941
+ return
24942
+ end
24943
+ if message ~= nil and string.find(tostring(err), message, 1, true) == nil then
24944
+ fail('expected the error to contain ' .. describeValue(message) .. ', got ' .. describeValue(tostring(err)))
24945
+ end
24946
+ end
24947
+
24948
+ return matchers
24949
+ end
24950
+ `;
24951
+
24952
+ // src/testing/harness-runner.ts
24953
+ var RUNNER_SOURCE = String.raw`local function fullName(name)
24954
+ if #groups == 0 then
24955
+ return name
24956
+ end
24957
+ return table.concat(groups, ' > ') .. ' > ' .. name
24958
+ end
24959
+
24960
+ local function snapshot(list)
24961
+ local copy = {}
24962
+ for index = 1, #list do
24963
+ copy[index] = list[index]
24964
+ end
24965
+ return copy
24966
+ end
24967
+
24968
+ local function describe(name, body)
24969
+ groups[#groups + 1] = name
24970
+ local beforeDepth = #beforeHooks
24971
+ local afterDepth = #afterHooks
24972
+ body()
24973
+ for index = #beforeHooks, beforeDepth + 1, -1 do
24974
+ beforeHooks[index] = nil
24975
+ end
24976
+ for index = #afterHooks, afterDepth + 1, -1 do
24977
+ afterHooks[index] = nil
24978
+ end
24979
+ groups[#groups] = nil
24980
+ end
24981
+
24982
+ local function test(name, body)
24983
+ tests[#tests + 1] = { name = fullName(name), body = body, before = snapshot(beforeHooks), after = snapshot(afterHooks) }
24984
+ end
24985
+
24986
+ local function beforeEach(body)
24987
+ beforeHooks[#beforeHooks + 1] = body
24988
+ end
24989
+
24990
+ local function afterEach(body)
24991
+ afterHooks[#afterHooks + 1] = body
24992
+ end
24993
+
24994
+ local function resetStubs()
24995
+ callLog = {}
24996
+ behaviour = {}
24997
+ end
24998
+
24999
+ local function record(name, ...)
25000
+ local entries = callLog[name]
25001
+ if entries == nil then
25002
+ entries = {}
25003
+ callLog[name] = entries
25004
+ end
25005
+ entries[#entries + 1] = { n = select('#', ...), ... }
25006
+ end
25007
+
25008
+ local function makeStub(name)
25009
+ return function(...)
25010
+ record(name, ...)
25011
+ local configured = behaviour[name]
25012
+ if configured == nil then
25013
+ return nil
25014
+ end
25015
+ if configured.kind == 'function' then
25016
+ return configured.body(...)
25017
+ end
25018
+ return configured.value
25019
+ end
25020
+ end
25021
+
25022
+ local mta = {}
25023
+
25024
+ function mta.stub(name, implementation)
25025
+ behaviour[name] = { kind = 'function', body = implementation }
25026
+ end
25027
+
25028
+ function mta.returns(name, value)
25029
+ behaviour[name] = { kind = 'value', value = value }
25030
+ end
25031
+
25032
+ function mta.calls(name)
25033
+ return callLog[name] or {}
25034
+ end
25035
+
25036
+ function mta.reset()
25037
+ resetStubs()
25038
+ end
25039
+
25040
+ local function runAll()
25041
+ local passed = 0
25042
+ local failed = 0
25043
+ for _, entry in ipairs(tests) do
25044
+ resetStubs()
25045
+ local result = nil
25046
+ for _, hook in ipairs(entry.before) do
25047
+ if result == nil then
25048
+ result = attempt(hook)
25049
+ end
25050
+ end
25051
+ if result == nil then
25052
+ result = attempt(entry.body)
25053
+ end
25054
+ for _, hook in ipairs(entry.after) do
25055
+ local hookResult = attempt(hook)
25056
+ if result == nil then
25057
+ result = hookResult
25058
+ end
25059
+ end
25060
+ if result == nil then
25061
+ passed = passed + 1
25062
+ emit('pass', entry.name)
25063
+ else
25064
+ failed = failed + 1
25065
+ emit('fail', entry.name, result.file, result.line, result.message)
25066
+ end
25067
+ end
25068
+ emit('done', passed, failed)
25069
+ return failed
25070
+ end
25071
+
25072
+ rawset(_G, 'describe', describe)
25073
+ rawset(_G, 'test', test)
25074
+ rawset(_G, 'beforeEach', beforeEach)
25075
+ rawset(_G, 'afterEach', afterEach)
25076
+ rawset(_G, 'expect', expect)
25077
+ rawset(_G, 'mta', mta)
25078
+
25079
+ setmetatable(_G, {
25080
+ __index = function(_, key)
25081
+ if stubNames[key] == nil then
25082
+ return nil
25083
+ end
25084
+ local stub = makeStub(key)
25085
+ rawset(_G, key, stub)
25086
+ return stub
25087
+ end,
25088
+ })
25089
+
25090
+ rawset(_G, '__luamTest', {
25091
+ discard = function()
25092
+ tests = {}
25093
+ groups = {}
25094
+ beforeHooks = {}
25095
+ afterHooks = {}
25096
+ end,
25097
+ run = runAll,
25098
+ })
25099
+ `;
25100
+
25101
+ // src/testing/harness-source.ts
25102
+ var HARNESS_FILE = "harness.lua";
25103
+ var ENTRY_PREFIX = "main-";
25104
+ var SENTINEL = "##luam:test";
25105
+ var HARNESS_SOURCE = `${ASSERTIONS_SOURCE}
25106
+ ${RUNNER_SOURCE}`;
25107
+ function entryFile(environment) {
25108
+ return `${ENTRY_PREFIX}${environment}.lua`;
25109
+ }
25110
+ function stubbedGlobals(environment) {
25111
+ return globalsFor(environment).filter((declaration) => declaration.source === "mta" && declaration.type.kind === "function").map((declaration) => declaration.name).sort();
25112
+ }
25113
+ function entrySource(environment, preload, target) {
25114
+ const names = stubbedGlobals(environment).map((name) => ` ['${name}'] = true,`);
25115
+ const discard = preload.length === 0 ? [] : ["__luamTest.discard()"];
25116
+ const lines = [
25117
+ "__luamTestNames = {",
25118
+ ...names,
25119
+ "}",
25120
+ `dofile('${HARNESS_FILE}')`,
25121
+ ...preload.map((path) => `dofile('${path}')`),
25122
+ ...discard,
25123
+ ...target.map((path) => `dofile('${path}')`),
25124
+ "local failed = __luamTest.run()",
25125
+ "os.exit(failed == 0 and 0 or 1)"
25126
+ ];
25127
+ return `${lines.join("\n")}
25128
+ `;
25129
+ }
25130
+
25131
+ // src/testing/test-runner.ts
25132
+ function runLua(executable, args, cwd) {
25133
+ const result = spawnSync3(executable, [...args], { cwd, encoding: "utf8", shell: false, windowsHide: true });
25134
+ return {
25135
+ status: result.status,
25136
+ stdout: result.stdout ?? "",
25137
+ stderr: result.stderr ?? "",
25138
+ failure: result.error === void 0 ? null : result.error.message
25139
+ };
25140
+ }
25141
+ function unescape(value) {
25142
+ return value.replace(/\\(.)/g, (match, character) => {
25143
+ if (character === "n") {
25144
+ return "\n";
25145
+ }
25146
+ if (character === "r") {
25147
+ return "\r";
25148
+ }
25149
+ return character === "t" ? " " : character;
25150
+ });
25151
+ }
25152
+ function parseLine(environment, line2) {
25153
+ const parts = line2.split(" ");
25154
+ if (parts[0] !== SENTINEL || parts[1] !== "pass" && parts[1] !== "fail") {
25155
+ return null;
25156
+ }
25157
+ const passed = parts[1] === "pass";
25158
+ const file = unescape(parts[3] ?? "");
25159
+ const parsedLine = Number(parts[4] ?? "");
25160
+ return {
25161
+ environment,
25162
+ name: unescape(parts[2] ?? ""),
25163
+ passed,
25164
+ file: file.length === 0 ? null : file,
25165
+ line: Number.isSafeInteger(parsedLine) && parsedLine > 0 ? parsedLine : null,
25166
+ message: unescape(parts[5] ?? "")
25167
+ };
25168
+ }
25169
+ function parseRunOutput(environment, stdout) {
25170
+ const results = [];
25171
+ const output = [];
25172
+ for (const line2 of stdout.split(/\r?\n/)) {
25173
+ const result = parseLine(environment, line2);
25174
+ if (result !== null) {
25175
+ results.push(result);
25176
+ continue;
25177
+ }
25178
+ if (line2.startsWith(SENTINEL) || line2.length === 0) {
25179
+ continue;
25180
+ }
25181
+ output.push(line2);
25182
+ }
25183
+ return { results, output };
25184
+ }
25185
+ function writeWorkspace(directory, scripts) {
25186
+ writeFileSync8(join6(directory, HARNESS_FILE), HARNESS_SOURCE, "utf8");
25187
+ for (const script of scripts) {
25188
+ const target = join6(directory, script.path);
25189
+ mkdirSync7(dirname7(target), { recursive: true });
25190
+ writeFileSync8(target, script.content, "utf8");
25191
+ }
25192
+ }
25193
+ function loadOrder(available, environment) {
25194
+ const shared = bundlePath("shared");
25195
+ const own = bundlePath(environment);
25196
+ const target = available.has(own) ? [own] : [];
25197
+ if (environment === "shared") {
25198
+ return { preload: [], target };
25199
+ }
25200
+ return { preload: available.has(shared) ? [shared] : [], target };
25201
+ }
25202
+ function runTests(request) {
25203
+ const spawn2 = request.spawn ?? runLua;
25204
+ const directory = mkdtempSync2(join6(tmpdir2(), "luam-test-"));
25205
+ const available = new Set(request.scripts.map((script) => script.path));
25206
+ try {
25207
+ writeWorkspace(directory, request.scripts);
25208
+ return ALL_ENVIRONMENTS.filter((environment) => request.environments.includes(environment)).map((environment) => {
25209
+ const { preload, target } = loadOrder(available, environment);
25210
+ const entry = entryFile(environment);
25211
+ writeFileSync8(join6(directory, entry), entrySource(environment, preload, target), "utf8");
25212
+ const execution = spawn2(request.executable, [entry], directory);
25213
+ const parsed = parseRunOutput(environment, execution.stdout);
25214
+ const failure3 = execution.failure ?? (parsed.results.length === 0 && execution.stderr.length > 0 ? execution.stderr.trim() : null);
25215
+ return { environment, results: parsed.results, output: parsed.output, failure: failure3 };
25216
+ });
25217
+ } finally {
25218
+ rmSync2(directory, { recursive: true, force: true });
25219
+ }
25220
+ }
25221
+
25222
+ // src/commands/test-command.ts
25223
+ function testEnvironments(build, paths) {
25224
+ const found = build.bundles.flatMap(
25225
+ (bundle) => bundle.members.flatMap((member) => member.kind === "module" && paths.has(member.module.source) ? [member.module.environment] : [])
25226
+ );
25227
+ return [...new Set(found)];
25228
+ }
25229
+ function runTestCommand(context, options = {}) {
25230
+ const reporter = commandReporter(context);
25231
+ const excluded = [context.config.outDir, context.config.contracts];
25232
+ const discovered = discoverTests(context.root, context.config.sources, excluded);
25233
+ reportCliDiagnostics(reporter, discovered.diagnostics);
25234
+ if (hasCliErrors(discovered.diagnostics)) {
25235
+ return EXIT_DIAGNOSTICS;
25236
+ }
25237
+ if (discovered.files.length === 0) {
25238
+ reporter.warn(`No "${TEST_EXTENSION}" files were found. Write one next to the module it covers.`);
25239
+ return EXIT_OK;
25240
+ }
25241
+ const request = { explicit: options.lua ?? null, env: options.env ?? null, ...options.probe === void 0 ? {} : { probe: options.probe } };
25242
+ const interpreter = findLuaInterpreter(request);
25243
+ if (interpreter === null) {
25244
+ reporter.error(describeMissingInterpreter(request));
25245
+ reporter.detail(INSTALL_HINT);
25246
+ return EXIT_USAGE;
25247
+ }
25248
+ const started = performance.now();
25249
+ const outcome = runCompile(context.root, context.config, { additionalFiles: discovered.files, development: true, layout: "bundle", map: true });
25250
+ reportCliDiagnostics(reporter, outcome.diagnostics);
25251
+ reportFileDiagnostics(reporter, outcome.fileDiagnostics, outcome.sources);
25252
+ if (outcome.build === null) {
25253
+ reporter.error("Tests failed: the project did not compile.");
25254
+ return EXIT_DIAGNOSTICS;
25255
+ }
25256
+ const paths = new Set(discovered.files.map((file) => file.path));
25257
+ const environments = testEnvironments(outcome.build, paths);
25258
+ const runs = runTests({
25259
+ executable: interpreter.executable,
25260
+ scripts: outcome.build.scripts,
25261
+ environments,
25262
+ ...options.spawn === void 0 ? {} : { spawn: options.spawn }
25263
+ });
25264
+ return reportTestResults(reporter, runs, context.root, outcome.map, performance.now() - started) ? EXIT_OK : EXIT_DIAGNOSTICS;
25265
+ }
25266
+
25267
+ // src/cli/registry/test-registration.ts
25268
+ function registerTestCommand(program2, runtime) {
25269
+ const command = program2.command("test").description("Compile the project with its test files and run them on a Lua 5.1 interpreter.");
25270
+ addProjectOptions(command);
25271
+ command.option("--lua <path>", "Path to the Lua 5.1 interpreter that runs the tests.");
25272
+ command.action((options) => {
25273
+ const project = createProjectContext(runtime, "test", options);
25274
+ if (project.context === null) {
25275
+ runtime.exitCode = project.error;
25276
+ return;
25277
+ }
25278
+ runtime.exitCode = runTestCommand(project.context, { lua: options.lua ?? null, env: runtime.env[LUA_ENV_VARIABLE] ?? null });
25279
+ });
25280
+ }
25281
+
23077
25282
  // src/commands/trace-command.ts
23078
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
25283
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
23079
25284
  function defaultMapPath(root, options, reporter) {
23080
25285
  const loaded = loadManifest(root, { path: options.manifestPath, mode: manifestMode("trace"), env: options.env });
23081
25286
  if (loaded.config !== null) {
23082
25287
  const configured = resourceMapPath(root, loaded.config);
23083
- if (existsSync12(configured)) {
25288
+ if (existsSync13(configured)) {
23084
25289
  return configured;
23085
25290
  }
23086
25291
  }
@@ -23100,7 +25305,7 @@ function readStandardInput() {
23100
25305
  return "";
23101
25306
  }
23102
25307
  try {
23103
- return readFileSync9(0, "utf8");
25308
+ return readFileSync13(0, "utf8");
23104
25309
  } catch {
23105
25310
  return "";
23106
25311
  }
@@ -23134,30 +25339,30 @@ function runTraceCommand(root, reporter, options) {
23134
25339
  reporter.detail('A generated line cannot be resolved there. Reproduce the error under "luam dev" or "luam ensure" for a source position.');
23135
25340
  return EXIT_DIAGNOSTICS;
23136
25341
  }
23137
- let failed2 = false;
25342
+ let failed3 = false;
23138
25343
  for (const line2 of lines) {
23139
25344
  const generated = parseTracePosition(line2);
23140
25345
  if (generated === null) {
23141
25346
  reporter.error(`Trace could not find a file and line in "${line2}".`);
23142
- failed2 = true;
25347
+ failed3 = true;
23143
25348
  continue;
23144
25349
  }
23145
25350
  const file = generatedFile(loaded.map.resource, generated.file);
23146
25351
  const resolution = resolveResourcePosition(loaded.map, file, generated.line);
23147
25352
  if (resolution.status === "unknown-version") {
23148
25353
  reporter.error(`Resource map version ${resolution.version} is not supported.`);
23149
- failed2 = true;
25354
+ failed3 = true;
23150
25355
  continue;
23151
25356
  }
23152
25357
  if (resolution.status === "uncovered") {
23153
25358
  reporter.error(`Resource map does not cover "${generated.file}:${generated.line}".`);
23154
- failed2 = true;
25359
+ failed3 = true;
23155
25360
  continue;
23156
25361
  }
23157
25362
  const symbol = resolution.position.symbol === void 0 ? "" : ` (${resolution.position.symbol})`;
23158
25363
  reporter.raw(`${resolution.position.file}:${resolution.position.line}${symbol}`);
23159
25364
  }
23160
- return failed2 ? EXIT_DIAGNOSTICS : EXIT_OK;
25365
+ return failed3 ? EXIT_DIAGNOSTICS : EXIT_OK;
23161
25366
  }
23162
25367
 
23163
25368
  // src/cli/registry/trace-registration.ts
@@ -23178,12 +25383,14 @@ function registerTraceCommand(program2, runtime) {
23178
25383
  var COMMAND_REGISTRARS = [
23179
25384
  registerBuildCommand,
23180
25385
  registerCheckCommand,
25386
+ registerConfigCommand,
23181
25387
  registerDevCommand,
23182
25388
  registerDoctorCommand,
23183
25389
  registerEnsureCommand,
23184
25390
  registerInitCommand,
23185
25391
  registerServerCommand,
23186
25392
  registerSetupCommand,
25393
+ registerTestCommand,
23187
25394
  registerTraceCommand
23188
25395
  ];
23189
25396