@thigasdevelopment/luam 0.2.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/luam.mjs CHANGED
@@ -190,7 +190,7 @@ function parseArguments(argv) {
190
190
  continue;
191
191
  }
192
192
  if (parsed.command !== null) {
193
- if (parsed.command === "trace" && parsed.operand === null) {
193
+ if ((parsed.command === "init" || parsed.command === "trace") && parsed.operand === null) {
194
194
  parsed.operand = token;
195
195
  continue;
196
196
  }
@@ -212,12 +212,13 @@ var EXIT_DIAGNOSTICS = 1;
212
212
  var EXIT_USAGE = 2;
213
213
 
214
214
  // src/cli/usage.ts
215
- var VERSION = true ? "0.2.1" : "0.0.0-dev";
215
+ var VERSION = true ? "0.4.1" : "0.0.0-dev";
216
216
  var USAGE = [
217
217
  "luam \u2014 the Luam compiler for Multi Theft Auto resources.",
218
218
  "",
219
219
  "Usage:",
220
220
  " luam <command> [options]",
221
+ " luam init [path] [options]",
221
222
  "",
222
223
  "Commands:",
223
224
  " build Compile the project and write the resource to the output directory.",
@@ -225,16 +226,16 @@ var USAGE = [
225
226
  " dev Build, sync, restart, watch, and stream resource development logs.",
226
227
  " doctor Check the CLI, supported editors, and the Luam extension.",
227
228
  " ensure Build, sync the resource into the MTA server, and restart it.",
228
- " init Scaffold a new resource project with the framework and example handlers.",
229
+ " init Interactively scaffold a new resource project.",
229
230
  " setup Detect supported editors and install the Luam extension with consent.",
230
231
  " trace Resolve generated Lua positions back to Luam source positions.",
231
232
  "",
232
233
  "Options:",
233
234
  " --cwd <path> Project directory that contains luam.json. Defaults to the current directory.",
234
235
  " --config <path> Configuration file to load instead of luam.json.",
235
- " --name <name> Resource name for init. Defaults to the project directory name.",
236
+ " --name <name> Resource name for init. Defaults to the destination directory name.",
236
237
  " --force Let init overwrite files that already exist.",
237
- " -y, --yes Install the extension in every detected editor without prompting.",
238
+ " -y, --yes Accept init defaults or install every detected editor without prompting.",
238
239
  " --watch Keep ensure or dev running and rebuild on source changes.",
239
240
  " --no-watch Run ensure or dev once and exit.",
240
241
  " --bundle Build or ensure with bundled output.",
@@ -916,6 +917,15 @@ function createNamed(name) {
916
917
  function createRecord(name, members, origin = null) {
917
918
  return { kind: "record", name, origin, members };
918
919
  }
920
+ function renameRecord(type, name) {
921
+ return type.kind === "record" ? createRecord(name, type.members, type.origin) : type;
922
+ }
923
+ function createObjectType(members) {
924
+ const keys = [...members].map(
925
+ ([name, member]) => member.kind === "optional" ? `${name}?: ${typeToString(member.element)}` : `${name}: ${typeToString(member)}`
926
+ );
927
+ return createRecord(keys.length === 0 ? "{}" : `{ ${keys.join(", ")} }`, members);
928
+ }
919
929
  function createFunction(parameters, returnType, minimumArguments2, isVariadic = false) {
920
930
  return { kind: "function", parameters, returnType, minimumArguments: minimumArguments2 ?? parameters.length, isVariadic };
921
931
  }
@@ -1022,6 +1032,21 @@ function isFunctionAssignable(source, target, options) {
1022
1032
  }
1023
1033
  return target.returnType.kind === "void" || isAssignable(source.returnType, target.returnType, options);
1024
1034
  }
1035
+ function isRecordAssignable(source, target, options) {
1036
+ for (const [name, member] of target.members) {
1037
+ const value = source.members.get(name);
1038
+ if (value === void 0) {
1039
+ if (member.kind !== "optional") {
1040
+ return false;
1041
+ }
1042
+ continue;
1043
+ }
1044
+ if (!isAssignable(value, member, options)) {
1045
+ return false;
1046
+ }
1047
+ }
1048
+ return true;
1049
+ }
1025
1050
  function isAssignable(source, target, options = { allowNil: false }) {
1026
1051
  if (source.kind === "any" || target.kind === "any" || target.kind === "unknown") {
1027
1052
  return true;
@@ -1053,6 +1078,9 @@ function isAssignable(source, target, options = { allowNil: false }) {
1053
1078
  if (target.kind === "table") {
1054
1079
  return isTableLike(source) || source.kind === "record";
1055
1080
  }
1081
+ if (target.kind === "record") {
1082
+ return source.kind === "record" ? isRecordAssignable(source, target, options) : source.kind === "table";
1083
+ }
1056
1084
  if (target.kind === "array") {
1057
1085
  return source.kind === "table" || source.kind === "array" && isAssignable(source.element, target.element, options);
1058
1086
  }
@@ -1137,8 +1165,8 @@ var Binder = class {
1137
1165
  isDeclaredInCurrentScope(name) {
1138
1166
  return this.scopes[this.scopes.length - 1]?.has(name) ?? false;
1139
1167
  }
1140
- declareAlias(name, type) {
1141
- this.aliases.set(name, type);
1168
+ declareAlias(name, type, parameters = []) {
1169
+ this.aliases.set(name, { type, parameters });
1142
1170
  }
1143
1171
  lookupAlias(name) {
1144
1172
  return this.aliases.get(name) ?? null;
@@ -4831,6 +4859,14 @@ var DeclarationRegistry = class {
4831
4859
  lookupInterface(name) {
4832
4860
  return this.interfaces.get(name) ?? null;
4833
4861
  }
4862
+ interfaceExtends(name, target, visited = /* @__PURE__ */ new Set()) {
4863
+ const current = this.lookupInterface(name);
4864
+ if (current === null || visited.has(name)) {
4865
+ return false;
4866
+ }
4867
+ visited.add(name);
4868
+ return current.superInterfaces.some((parent) => parent === target || this.interfaceExtends(parent, target, visited));
4869
+ }
4834
4870
  declareEnum(info) {
4835
4871
  this.enums.set(info.name, info);
4836
4872
  }
@@ -4846,24 +4882,42 @@ var DeclarationRegistry = class {
4846
4882
  allEnums() {
4847
4883
  return [...this.enums.values()];
4848
4884
  }
4849
- collectMembers(className) {
4885
+ collectInterfaceMembers(name, collected, visited) {
4886
+ const current = this.lookupInterface(name);
4887
+ if (current === null || visited.has(current.name)) {
4888
+ return;
4889
+ }
4890
+ visited.add(current.name);
4891
+ for (const [memberName, member] of current.members) {
4892
+ if (!collected.has(memberName)) {
4893
+ collected.set(memberName, member);
4894
+ }
4895
+ }
4896
+ for (const parent of current.superInterfaces) {
4897
+ this.collectInterfaceMembers(parent, collected, visited);
4898
+ }
4899
+ }
4900
+ collectMembers(name) {
4850
4901
  const collected = /* @__PURE__ */ new Map();
4851
4902
  const visited = /* @__PURE__ */ new Set();
4852
- let current = this.lookupClass(className);
4903
+ let current = this.lookupClass(name);
4853
4904
  while (current !== null && !visited.has(current.name)) {
4854
- for (const [name, member] of current.members) {
4855
- if (!collected.has(name)) {
4856
- collected.set(name, member);
4905
+ for (const [name2, member] of current.members) {
4906
+ if (!collected.has(name2)) {
4907
+ collected.set(name2, member);
4857
4908
  }
4858
4909
  }
4859
4910
  visited.add(current.name);
4860
4911
  current = current.superClass === null ? null : this.lookupClass(current.superClass);
4861
4912
  }
4913
+ if (current === null && this.lookupClass(name) === null) {
4914
+ this.collectInterfaceMembers(name, collected, visited);
4915
+ }
4862
4916
  return [...collected.values()];
4863
4917
  }
4864
- lookupMember(className, member) {
4918
+ lookupMember(name, member) {
4865
4919
  const visited = /* @__PURE__ */ new Set();
4866
- let current = this.lookupClass(className);
4920
+ let current = this.lookupClass(name);
4867
4921
  while (current !== null && !visited.has(current.name)) {
4868
4922
  const found = current.members.get(member);
4869
4923
  if (found !== void 0) {
@@ -4872,7 +4926,9 @@ var DeclarationRegistry = class {
4872
4926
  visited.add(current.name);
4873
4927
  current = current.superClass === null ? null : this.lookupClass(current.superClass);
4874
4928
  }
4875
- return null;
4929
+ const collected = /* @__PURE__ */ new Map();
4930
+ this.collectInterfaceMembers(name, collected, visited);
4931
+ return collected.get(member) ?? null;
4876
4932
  }
4877
4933
  };
4878
4934
 
@@ -4950,6 +5006,35 @@ function mtaMember(className, member) {
4950
5006
  return mtaClassRegistry().lookupMember(className, member);
4951
5007
  }
4952
5008
 
5009
+ // ../compiler/src/checker/type-substitution.ts
5010
+ function substituteType(type, substitutions) {
5011
+ if (type.kind === "named") {
5012
+ return substitutions.get(type.name) ?? type;
5013
+ }
5014
+ if (type.kind === "array") {
5015
+ return createArray(substituteType(type.element, substitutions));
5016
+ }
5017
+ if (type.kind === "optional") {
5018
+ return createOptional(substituteType(type.element, substitutions));
5019
+ }
5020
+ if (type.kind === "union") {
5021
+ return createUnion(type.options.map((option) => substituteType(option, substitutions)));
5022
+ }
5023
+ if (type.kind === "function") {
5024
+ const parameters = type.parameters.map((parameter) => substituteType(parameter, substitutions));
5025
+ const returnType = substituteType(type.returnType, substitutions);
5026
+ return createFunction(parameters, returnType, type.minimumArguments, type.isVariadic);
5027
+ }
5028
+ if (type.kind === "tuple") {
5029
+ return createTuple(type.elements.map((element) => substituteType(element, substitutions)));
5030
+ }
5031
+ if (type.kind === "record") {
5032
+ const members = new Map([...type.members].map(([name, member]) => [name, substituteType(member, substitutions)]));
5033
+ return createRecord(type.name, members, type.origin);
5034
+ }
5035
+ return type;
5036
+ }
5037
+
4953
5038
  // ../compiler/src/checker/context.ts
4954
5039
  var BUILTIN_TYPES = {
4955
5040
  any: ANY_TYPE,
@@ -4987,6 +5072,7 @@ var CheckContext = class {
4987
5072
  methodStack = [];
4988
5073
  projectEnvironments = /* @__PURE__ */ new Map();
4989
5074
  ambientClasses = /* @__PURE__ */ new Set();
5075
+ ambientInterfaces = /* @__PURE__ */ new Set();
4990
5076
  constructor(mode, environment, ambient = EMPTY_AMBIENT, project = EMPTY_PROJECT_DECLARATIONS, oop = false) {
4991
5077
  this.mode = mode;
4992
5078
  this.environment = environment;
@@ -5008,6 +5094,9 @@ var CheckContext = class {
5008
5094
  isAmbientClass(name) {
5009
5095
  return this.ambientClasses.has(name);
5010
5096
  }
5097
+ isAmbientInterface(name) {
5098
+ return this.ambientInterfaces.has(name);
5099
+ }
5011
5100
  noteExternalReference(name, position2) {
5012
5101
  if (!this.externalReferences.has(name)) {
5013
5102
  this.externalReferences.set(name, position2);
@@ -5027,13 +5116,32 @@ var CheckContext = class {
5027
5116
  return this.types.get(expression) ?? UNKNOWN_TYPE;
5028
5117
  }
5029
5118
  pushReturnType(type) {
5030
- this.returnStack.push(type);
5119
+ this.returnStack.push({ expected: type, inferred: [] });
5031
5120
  }
5032
5121
  popReturnType() {
5033
- this.returnStack.pop();
5122
+ const frame = this.returnStack.pop();
5123
+ if (frame === void 0 || frame.inferred.length === 0) {
5124
+ return VOID_TYPE;
5125
+ }
5126
+ const tuples = frame.inferred.filter((type) => type.kind === "tuple");
5127
+ const tupleSize = tuples[0]?.kind === "tuple" ? tuples[0].elements.length : null;
5128
+ const sameTupleSize = tuples.length === frame.inferred.length && tuples.every((type) => type.kind === "tuple" && type.elements.length === tupleSize);
5129
+ if (sameTupleSize) {
5130
+ const first = tuples[0];
5131
+ if (first?.kind === "tuple") {
5132
+ const elements = first.elements.map(
5133
+ (_, index) => createUnion(tuples.map((type) => type.kind === "tuple" ? type.elements[index] ?? ANY_TYPE : ANY_TYPE))
5134
+ );
5135
+ return createTuple(elements);
5136
+ }
5137
+ }
5138
+ return createUnion(frame.inferred);
5034
5139
  }
5035
5140
  currentReturnType() {
5036
- return this.returnStack[this.returnStack.length - 1] ?? null;
5141
+ return this.returnStack[this.returnStack.length - 1]?.expected ?? null;
5142
+ }
5143
+ inferReturnType(type) {
5144
+ this.returnStack[this.returnStack.length - 1]?.inferred.push(type);
5037
5145
  }
5038
5146
  pushClassMethod(frame) {
5039
5147
  this.methodStack.push(frame);
@@ -5057,13 +5165,23 @@ var CheckContext = class {
5057
5165
  if (annotation.kind === "type-union") {
5058
5166
  return createUnion(annotation.options.map((option) => this.resolveAnnotation(option)));
5059
5167
  }
5168
+ if (annotation.kind === "type-object") {
5169
+ const members = /* @__PURE__ */ new Map();
5170
+ for (const member of annotation.members) {
5171
+ members.set(member.name, this.resolveAnnotation(member.annotation));
5172
+ }
5173
+ return createObjectType(members);
5174
+ }
5175
+ if (annotation.kind === "type-tuple") {
5176
+ return createTuple(annotation.elements.map((element) => this.resolveAnnotation(element)));
5177
+ }
5060
5178
  if (annotation.kind === "type-function") {
5061
5179
  const parameters = annotation.parameters.map((parameter) => this.resolveAnnotation(parameter));
5062
5180
  const optional = parameters.findIndex((parameter) => parameter.kind === "optional");
5063
5181
  const minimum = optional === -1 ? parameters.length : optional;
5064
5182
  return createFunction(parameters, this.resolveAnnotation(annotation.returnType), minimum, annotation.isVariadic);
5065
5183
  }
5066
- return this.resolveNamedAnnotation(annotation.name);
5184
+ return this.resolveNamedAnnotation(annotation.name, annotation.typeArguments, annotation.position);
5067
5185
  }
5068
5186
  expectAssignable(source, target, position2, subject) {
5069
5187
  if (isAssignable(source, target, { allowNil: this.allowNil })) {
@@ -5088,6 +5206,7 @@ var CheckContext = class {
5088
5206
  this.binder.declareGlobal({ name: info.name, type: createNamed(info.name), isLocal: false, position: info.position });
5089
5207
  }
5090
5208
  for (const info of ambient.interfaces) {
5209
+ this.ambientInterfaces.add(info.name);
5091
5210
  this.declarations.declareInterface(info);
5092
5211
  }
5093
5212
  for (const info of ambient.enums) {
@@ -5098,12 +5217,25 @@ var CheckContext = class {
5098
5217
  this.binder.declareGlobal({ name: info.name, type: info.type, isLocal: false, position: info.position });
5099
5218
  }
5100
5219
  }
5101
- resolveNamedAnnotation(name) {
5220
+ resolveNamedAnnotation(name, typeArguments, position2) {
5102
5221
  const builtin = BUILTIN_TYPES[name];
5103
5222
  if (builtin !== void 0) {
5223
+ if (typeArguments.length > 0) {
5224
+ this.report("check-generic-arity", `Type "${name}" does not accept type arguments.`, position2);
5225
+ }
5104
5226
  return builtin;
5105
5227
  }
5106
- return this.binder.lookupAlias(name) ?? createNamed(name);
5228
+ const alias = this.binder.lookupAlias(name);
5229
+ if (alias === null) {
5230
+ return createNamed(name);
5231
+ }
5232
+ if (typeArguments.length !== alias.parameters.length) {
5233
+ const expected = alias.parameters.length === 1 ? "1 type argument" : `${alias.parameters.length} type arguments`;
5234
+ this.report("check-generic-arity", `Type "${name}" expects ${expected} but received ${typeArguments.length}.`, position2);
5235
+ }
5236
+ const resolved = typeArguments.map((argument) => this.resolveAnnotation(argument));
5237
+ const substitutions = new Map(alias.parameters.map((parameter, index) => [parameter, resolved[index] ?? ANY_TYPE]));
5238
+ return substituteType(alias.type, substitutions);
5107
5239
  }
5108
5240
  };
5109
5241
 
@@ -5387,7 +5519,7 @@ function expandClassDecorators(context, statement, fieldTypes) {
5387
5519
  const occupied = new Map(statement.members.filter((member) => member.kind === "class-method").map((member) => [member.name, member.name]));
5388
5520
  const classDecorators = validateDecorators(context, statement.decorators, "class");
5389
5521
  for (const member of statement.members) {
5390
- if (member.kind === "class-method") {
5522
+ if (member.kind === "class-method" || member.name === "constructor") {
5391
5523
  validateDecorators(context, member.decorators, "method");
5392
5524
  continue;
5393
5525
  }
@@ -5969,7 +6101,11 @@ function resolveNamedMember(context, name, expression) {
5969
6101
  return resolveMtaMember(context, name, expression.property, expression.position)?.type ?? ANY_TYPE;
5970
6102
  }
5971
6103
  const contract = context.declarations.lookupClass(name) === null ? context.declarations.lookupInterface(name) : null;
5972
- return contract === null ? null : checkInterfaceMember(context, name, contract.members, expression);
6104
+ if (contract === null) {
6105
+ return null;
6106
+ }
6107
+ const members = new Map(context.declarations.collectMembers(name).map((member2) => [member2.name, member2]));
6108
+ return checkInterfaceMember(context, name, members, expression);
5973
6109
  }
5974
6110
  var NATIVE_CONSTRUCTOR = "new";
5975
6111
  function nativeConstructor(context, name) {
@@ -6273,7 +6409,7 @@ function checkMultiValueExpression(context, expression) {
6273
6409
  return checkTable(context, expression);
6274
6410
  case "function-expression": {
6275
6411
  const type = buildFunctionType(context, expression.parameters, expression.returnAnnotation);
6276
- checkFunctionBody(context, expression.parameters, expression.returnAnnotation, expression.body, null);
6412
+ checkFunctionBody(context, expression.parameters, expression.returnAnnotation, expression.body, type, null);
6277
6413
  return context.record(expression, type);
6278
6414
  }
6279
6415
  case "binary-expression": {
@@ -6334,8 +6470,16 @@ function registerMembers(context, info, statement) {
6334
6470
  return generated;
6335
6471
  }
6336
6472
  function checkMethodBody(context, info, member) {
6473
+ const explicitSelf = member.parameters.find((parameter) => parameter.name === "self");
6474
+ if (explicitSelf !== void 0) {
6475
+ context.report("check-explicit-self-parameter", 'Class methods receive "self" automatically; remove it from the parameter list.', explicitSelf.position);
6476
+ }
6477
+ const signature = info.members.get(member.name)?.type;
6478
+ if (signature?.kind !== "function") {
6479
+ return;
6480
+ }
6337
6481
  context.pushClassMethod({ className: info.name, methodName: member.name });
6338
- checkFunctionBody(context, member.parameters, member.returnAnnotation, member.body, createNamed(info.name));
6482
+ checkFunctionBody(context, member.parameters, member.returnAnnotation, member.body, signature, createNamed(info.name));
6339
6483
  context.popClassMethod();
6340
6484
  }
6341
6485
  function checkContract(context, info, contract, member) {
@@ -6360,7 +6504,7 @@ function checkInterfaces(context, info) {
6360
6504
  context.report("check-unknown-interface", `Class "${info.name}" implements "${name}", which is not defined.`, info.position);
6361
6505
  continue;
6362
6506
  }
6363
- for (const member of contract.members.values()) {
6507
+ for (const member of context.declarations.collectMembers(contract.name)) {
6364
6508
  checkContract(context, info, name, member);
6365
6509
  }
6366
6510
  }
@@ -6418,11 +6562,46 @@ function checkInterfaceDeclaration(context, statement) {
6418
6562
  return;
6419
6563
  }
6420
6564
  const members = /* @__PURE__ */ new Map();
6565
+ for (const name of new Set(statement.superInterfaces)) {
6566
+ const parent = context.declarations.lookupInterface(name);
6567
+ if (name === statement.name || context.declarations.interfaceExtends(name, statement.name)) {
6568
+ context.report("check-interface-cycle", `Interface "${statement.name}" creates an inheritance cycle through "${name}".`, statement.position);
6569
+ } else if (parent === null) {
6570
+ context.noteExternalReference(name, statement.position);
6571
+ context.report("check-unknown-interface", `Interface "${statement.name}" extends "${name}", which is not defined.`, statement.position);
6572
+ } else if (context.isAmbientInterface(name)) {
6573
+ context.noteExternalReference(name, statement.position);
6574
+ }
6575
+ }
6576
+ if (new Set(statement.superInterfaces).size !== statement.superInterfaces.length) {
6577
+ context.report("check-duplicate-interface-parent", `Interface "${statement.name}" extends the same interface more than once.`, statement.position);
6578
+ }
6421
6579
  for (const member of statement.members) {
6422
6580
  const type = member.kind === "interface-field" ? context.resolveAnnotation(member.annotation) : buildFunctionType(context, member.parameters, member.returnAnnotation);
6423
- members.set(member.name, { name: member.name, type, isMethod: member.kind === "interface-method", position: member.position });
6581
+ const info = { name: member.name, type, isMethod: member.kind === "interface-method", position: member.position };
6582
+ if (members.has(member.name)) {
6583
+ context.report("check-duplicate-interface-member", `Interface "${statement.name}" declares member "${member.name}" more than once.`, member.position);
6584
+ } else {
6585
+ members.set(member.name, info);
6586
+ }
6424
6587
  }
6425
- context.declarations.declareInterface({ name: statement.name, members, position: statement.position });
6588
+ const inherited = /* @__PURE__ */ new Map();
6589
+ for (const parent of statement.superInterfaces) {
6590
+ for (const member of context.declarations.collectMembers(parent)) {
6591
+ const existing = members.get(member.name) ?? inherited.get(member.name);
6592
+ if (existing !== void 0 && (existing.isMethod !== member.isMethod || typeToString(existing.type) !== typeToString(member.type))) {
6593
+ context.report("check-conflicting-interface-member", `Interface "${statement.name}" inherits conflicting declarations of "${member.name}".`, statement.position);
6594
+ } else if (existing === void 0) {
6595
+ inherited.set(member.name, member);
6596
+ }
6597
+ }
6598
+ }
6599
+ context.declarations.declareInterface({
6600
+ name: statement.name,
6601
+ superInterfaces: statement.superInterfaces,
6602
+ members,
6603
+ position: statement.position
6604
+ });
6426
6605
  }
6427
6606
  function checkEnumDeclaration(context, statement) {
6428
6607
  if (context.declarations.lookupEnum(statement.name) !== null) {
@@ -6487,9 +6666,9 @@ function buildFunctionType(context, parameters, returnAnnotation) {
6487
6666
  const types = parameters.filter((parameter) => !parameter.isVararg).map((parameter) => context.resolveAnnotation(parameter.annotation));
6488
6667
  return createFunction(types, context.resolveAnnotation(returnAnnotation), minimumArguments(context, parameters), isVariadic);
6489
6668
  }
6490
- function checkFunctionBody(context, parameters, returnAnnotation, body, selfType) {
6669
+ function checkFunctionBody(context, parameters, returnAnnotation, body, signature, selfType) {
6491
6670
  context.binder.pushScope();
6492
- context.pushReturnType(context.resolveAnnotation(returnAnnotation));
6671
+ context.pushReturnType(returnAnnotation === null ? null : context.resolveAnnotation(returnAnnotation));
6493
6672
  if (selfType !== null) {
6494
6673
  context.binder.declare({ name: "self", type: selfType, isLocal: true, position: ORIGIN });
6495
6674
  }
@@ -6498,7 +6677,10 @@ function checkFunctionBody(context, parameters, returnAnnotation, body, selfType
6498
6677
  context.binder.declare({ name: parameter.name, type, isLocal: true, position: parameter.position });
6499
6678
  }
6500
6679
  checkStatements(context, body);
6501
- context.popReturnType();
6680
+ const inferred = context.popReturnType();
6681
+ if (returnAnnotation === null) {
6682
+ signature.returnType = inferred;
6683
+ }
6502
6684
  context.binder.popScope();
6503
6685
  }
6504
6686
  function valueAt(values, valueTypes, index) {
@@ -6568,12 +6750,16 @@ function checkFunctionDeclaration(context, statement) {
6568
6750
  }
6569
6751
  }
6570
6752
  context.record(statement.name, type);
6571
- checkFunctionBody(context, statement.parameters, statement.returnAnnotation, statement.body, statement.isMethod ? ANY_TYPE : null);
6753
+ checkFunctionBody(context, statement.parameters, statement.returnAnnotation, statement.body, type, statement.isMethod ? ANY_TYPE : null);
6572
6754
  }
6573
6755
  function checkReturn(context, statement) {
6574
6756
  const valueTypes = checkValueList(context, statement.values);
6575
6757
  const expected = context.currentReturnType();
6576
- if (expected === null || expected.kind === "any") {
6758
+ if (expected === null) {
6759
+ context.inferReturnType(createTuple(valueTypes));
6760
+ return;
6761
+ }
6762
+ if (expected.kind === "any") {
6577
6763
  return;
6578
6764
  }
6579
6765
  if (expected.kind === "void") {
@@ -6582,6 +6768,23 @@ function checkReturn(context, statement) {
6582
6768
  }
6583
6769
  return;
6584
6770
  }
6771
+ if (expected.kind === "tuple") {
6772
+ if (valueTypes.length !== expected.elements.length) {
6773
+ const message = `This function declares ${expected.elements.length} return values but returns ${valueTypes.length}.`;
6774
+ context.report("check-return-mismatch", message, statement.position);
6775
+ return;
6776
+ }
6777
+ valueTypes.forEach((type, index) => {
6778
+ const value2 = statement.values[Math.min(index, statement.values.length - 1)];
6779
+ context.expectAssignable(type, expected.elements[index] ?? ANY_TYPE, value2?.position ?? statement.position, `Return value ${index + 1}`);
6780
+ });
6781
+ return;
6782
+ }
6783
+ if (valueTypes.length > 1) {
6784
+ const message = `This function declares one return value of type "${typeToString(expected)}" but returns ${valueTypes.length} values.`;
6785
+ context.report("check-return-mismatch", message, statement.position);
6786
+ return;
6787
+ }
6585
6788
  const first = valueTypes[0];
6586
6789
  const value = statement.values[0];
6587
6790
  if (first === void 0 || value === void 0) {
@@ -6628,9 +6831,12 @@ function checkStatement(context, statement) {
6628
6831
  return checkNumericFor(context, statement);
6629
6832
  case "generic-for-statement":
6630
6833
  return checkGenericFor(context, statement);
6631
- case "type-alias-statement":
6632
- context.binder.declareAlias(statement.name, context.resolveAnnotation(statement.annotation));
6834
+ case "type-alias-statement": {
6835
+ const resolved = context.resolveAnnotation(statement.annotation);
6836
+ const named2 = statement.annotation.kind === "type-object" ? renameRecord(resolved, statement.name) : resolved;
6837
+ context.binder.declareAlias(statement.name, named2, statement.typeParameters);
6633
6838
  return;
6839
+ }
6634
6840
  case "declare-statement":
6635
6841
  return checkDeclareStatement(context, statement);
6636
6842
  case "class-declaration":
@@ -7781,13 +7987,45 @@ function parseFunctionType(stream) {
7781
7987
  return { kind: "type-function", parameters, isVariadic, returnType, position: position2 };
7782
7988
  }
7783
7989
  function parseGroupedType(stream) {
7784
- stream.expect("punctuation", "(");
7990
+ const position2 = stream.expect("punctuation", "(").position;
7785
7991
  const inner = parseTypeAnnotation(stream);
7786
- if (stream.check("punctuation", ",")) {
7787
- throw stream.error("A parenthesized type must contain a single type.", "parse-invalid-type");
7992
+ if (!stream.match("punctuation", ",")) {
7993
+ stream.expect("punctuation", ")");
7994
+ return inner;
7788
7995
  }
7996
+ const elements = [inner];
7997
+ do {
7998
+ elements.push(parseTypeAnnotation(stream));
7999
+ } while (stream.match("punctuation", ","));
7789
8000
  stream.expect("punctuation", ")");
7790
- return inner;
8001
+ return { kind: "type-tuple", elements, position: position2 };
8002
+ }
8003
+ function parseObjectMember(stream) {
8004
+ const token = stream.expectName();
8005
+ const annotation = parseNamedAnnotation(stream, "field");
8006
+ if (annotation === null) {
8007
+ throw stream.error(`Key "${token.value}" of an object type requires a type annotation.`, "parse-invalid-type");
8008
+ }
8009
+ return { name: token.value, annotation, position: token.position };
8010
+ }
8011
+ function parseObjectType(stream) {
8012
+ const position2 = stream.expect("punctuation", "{").position;
8013
+ const members = [];
8014
+ const declared = /* @__PURE__ */ new Set();
8015
+ while (!stream.check("punctuation", "}") && !stream.isEof()) {
8016
+ if (stream.match("punctuation", ";") || stream.match("punctuation", ",")) {
8017
+ continue;
8018
+ }
8019
+ const member = parseObjectMember(stream);
8020
+ if (declared.has(member.name)) {
8021
+ stream.report("parse-duplicate-key", `Object type key "${member.name}" is declared more than once.`, member.position);
8022
+ continue;
8023
+ }
8024
+ declared.add(member.name);
8025
+ members.push(member);
8026
+ }
8027
+ stream.expect("punctuation", "}");
8028
+ return { kind: "type-object", members, position: position2 };
7791
8029
  }
7792
8030
  function parsePrimaryType(stream) {
7793
8031
  if (stream.check("keyword", "function")) {
@@ -7796,6 +8034,9 @@ function parsePrimaryType(stream) {
7796
8034
  if (stream.check("identifier", FUNCTION_TYPE)) {
7797
8035
  return parseFunctionType(stream);
7798
8036
  }
8037
+ if (stream.check("punctuation", "{")) {
8038
+ return parseObjectType(stream);
8039
+ }
7799
8040
  return stream.check("punctuation", "(") ? parseGroupedType(stream) : parseTypeName(stream);
7800
8041
  }
7801
8042
  function parsePostfixType(stream) {
@@ -8125,16 +8366,24 @@ function parseForStatement(stream) {
8125
8366
  }
8126
8367
 
8127
8368
  // ../compiler/src/parser/recovery.ts
8128
- function recoverInBlock(stream, error) {
8369
+ var BRACE_TERMINATORS = /* @__PURE__ */ new Set(["}"]);
8370
+ function isTerminator(stream, terminators) {
8371
+ const token = stream.current();
8372
+ if (token.kind !== "punctuation" && token.kind !== "keyword") {
8373
+ return false;
8374
+ }
8375
+ return terminators.has(token.value);
8376
+ }
8377
+ function recoverInBlock(stream, error, terminators = BRACE_TERMINATORS) {
8129
8378
  stream.diagnostics.push(error.diagnostic);
8130
8379
  if (stream.isEof()) {
8131
8380
  return;
8132
8381
  }
8133
8382
  const start = stream.current();
8134
- while (!stream.isEof() && !stream.check("punctuation", "}") && stream.current().position.line === start.position.line) {
8383
+ while (!stream.isEof() && !isTerminator(stream, terminators) && stream.current().position.line === start.position.line) {
8135
8384
  stream.next();
8136
8385
  }
8137
- if (!stream.isEof() && !stream.check("punctuation", "}") && stream.current().position.offset === start.position.offset) {
8386
+ if (!stream.isEof() && !isTerminator(stream, terminators) && stream.current().position.offset === start.position.offset) {
8138
8387
  stream.next();
8139
8388
  }
8140
8389
  }
@@ -8244,7 +8493,10 @@ function isDeclarationStart(stream) {
8244
8493
  if (offset > 0 && token.value !== "class") {
8245
8494
  return false;
8246
8495
  }
8247
- return token.value === "class" ? isClassHeader(stream, offset) : stream.checkAhead(offset + 2, "punctuation", "{");
8496
+ if (token.value === "class") {
8497
+ return isClassHeader(stream, offset);
8498
+ }
8499
+ return token.value === "interface" ? stream.checkAhead(offset + 2, "punctuation", "{") || stream.checkAhead(offset + 2, "keyword", "extends") : stream.checkAhead(offset + 2, "punctuation", "{");
8248
8500
  }
8249
8501
  function skipDecoratorArguments(stream) {
8250
8502
  let depth = 0;
@@ -8271,6 +8523,21 @@ function parseDecorators(stream) {
8271
8523
  return decorators;
8272
8524
  }
8273
8525
  function parseClassMethod(stream, token, decorators) {
8526
+ stream.expect("operator", "=");
8527
+ const expression = parseFunctionExpression(stream);
8528
+ return {
8529
+ kind: "class-method",
8530
+ name: token.value,
8531
+ isConstructor: token.value === "constructor",
8532
+ isSynthetic: false,
8533
+ parameters: expression.parameters,
8534
+ returnAnnotation: expression.returnAnnotation,
8535
+ body: expression.body,
8536
+ decorators,
8537
+ position: token.position
8538
+ };
8539
+ }
8540
+ function parseLegacyClassMethod(stream, token, decorators) {
8274
8541
  const parameters = parseParameters(stream);
8275
8542
  const returnAnnotation = parseOptionalAnnotation(stream);
8276
8543
  const body = parseBraceBlock(stream);
@@ -8289,14 +8556,26 @@ function parseClassMethod(stream, token, decorators) {
8289
8556
  function parseFieldAnnotation(stream) {
8290
8557
  return parseNamedAnnotation(stream, "field");
8291
8558
  }
8559
+ function expectClassFieldBoundary(stream, token) {
8560
+ const current = stream.current();
8561
+ const delimiter = current.kind === "punctuation" && (current.value === "}" || current.value === ";" || current.value === ",");
8562
+ if (current.kind === "eof" || delimiter || current.position.line > token.position.line) {
8563
+ return;
8564
+ }
8565
+ throw stream.error(`Expected a line break or separator after class member "${token.value}".`, "parse-unexpected-token");
8566
+ }
8292
8567
  function parseClassMember(stream) {
8293
8568
  const decorators = parseDecorators(stream);
8294
8569
  const token = stream.expectName();
8295
8570
  if (stream.check("punctuation", "(")) {
8571
+ return parseLegacyClassMethod(stream, token, decorators);
8572
+ }
8573
+ if (stream.check("operator", "=") && stream.checkAhead(1, "keyword", "function")) {
8296
8574
  return parseClassMethod(stream, token, decorators);
8297
8575
  }
8298
8576
  const annotation = parseFieldAnnotation(stream);
8299
8577
  const value = stream.match("operator", "=") ? parseExpression(stream) : null;
8578
+ expectClassFieldBoundary(stream, token);
8300
8579
  return { kind: "class-field", name: token.value, annotation, value, decorators, position: token.position };
8301
8580
  }
8302
8581
  function parseClassModifiers(stream, declaration) {
@@ -8348,7 +8627,13 @@ function parseInterfaceMember(stream) {
8348
8627
  function parseInterfaceDeclaration(stream) {
8349
8628
  const position2 = stream.next().position;
8350
8629
  const name = stream.expect("identifier").value;
8630
+ const superInterfaces = [];
8351
8631
  const members = [];
8632
+ if (stream.match("keyword", "extends")) {
8633
+ do {
8634
+ superInterfaces.push(stream.expect("identifier").value);
8635
+ } while (stream.match("punctuation", ","));
8636
+ }
8352
8637
  stream.expect("punctuation", "{");
8353
8638
  while (!stream.check("punctuation", "}") && !stream.isEof()) {
8354
8639
  if (stream.match("punctuation", ";") || stream.match("punctuation", ",")) {
@@ -8364,7 +8649,7 @@ function parseInterfaceDeclaration(stream) {
8364
8649
  }
8365
8650
  }
8366
8651
  stream.expect("punctuation", "}");
8367
- return { kind: "interface-declaration", name, members, position: position2 };
8652
+ return { kind: "interface-declaration", name, superInterfaces, members, position: position2 };
8368
8653
  }
8369
8654
  function parseEnumDeclaration(stream) {
8370
8655
  const position2 = stream.next().position;
@@ -8564,14 +8849,14 @@ function parseStatement(stream) {
8564
8849
  }
8565
8850
  return parseExpressionStatement(stream);
8566
8851
  }
8567
- function parseBlockStatement(stream) {
8852
+ function parseBlockStatement(stream, terminators = BRACE_TERMINATORS) {
8568
8853
  try {
8569
8854
  return parseStatement(stream);
8570
8855
  } catch (error) {
8571
8856
  if (!(error instanceof ParserError)) {
8572
8857
  throw error;
8573
8858
  }
8574
- recoverInBlock(stream, error);
8859
+ recoverInBlock(stream, error, terminators);
8575
8860
  return null;
8576
8861
  }
8577
8862
  }
@@ -8604,7 +8889,14 @@ function parseBlock(stream, terminators) {
8604
8889
  if (stream.current().kind === "keyword" && stop.has(stream.current().value)) {
8605
8890
  return body;
8606
8891
  }
8607
- const statement = parseStatement(stream);
8892
+ const offset = stream.current().position.offset;
8893
+ const statement = parseBlockStatement(stream, stop);
8894
+ if (statement === null) {
8895
+ if (stream.current().position.offset === offset) {
8896
+ return body;
8897
+ }
8898
+ continue;
8899
+ }
8608
8900
  body.push(statement);
8609
8901
  if (statement.kind === "return-statement") {
8610
8902
  return body;
@@ -8751,7 +9043,7 @@ function classText(info) {
8751
9043
  return `class ${info.name}:${info.superClass ?? ""}:${[...info.interfaces].sort(compareText).join(",")}{${membersText(info.members)}}`;
8752
9044
  }
8753
9045
  function interfaceText(info) {
8754
- return `interface ${info.name}{${membersText(info.members)}}`;
9046
+ return `interface ${info.name}:${[...info.superInterfaces].sort(compareText).join(",")}{${membersText(info.members)}}`;
8755
9047
  }
8756
9048
  function enumText(info) {
8757
9049
  return `enum ${info.name}{${info.members.join(",")}}`;
@@ -10792,6 +11084,28 @@ async function runDevCommand(context, options) {
10792
11084
  // src/commands/init-command.ts
10793
11085
  import { basename } from "node:path";
10794
11086
 
11087
+ // src/commands/init-prompt.ts
11088
+ import { createInterface } from "node:readline/promises";
11089
+ async function question(prompt, label2, fallback) {
11090
+ const answer = (await prompt.question(`${label2}${fallback === "" ? "" : ` (${fallback})`}: `)).trim();
11091
+ return answer === "" ? fallback : answer;
11092
+ }
11093
+ async function promptForProject(defaults, nameLocked) {
11094
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
11095
+ return defaults;
11096
+ }
11097
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
11098
+ try {
11099
+ const name = nameLocked ? defaults.name : await question(prompt, "Project name", defaults.name);
11100
+ const version = await question(prompt, "Version", defaults.version);
11101
+ const description = await question(prompt, "Description", defaults.description ?? "");
11102
+ const author = await question(prompt, "Author", defaults.author ?? "");
11103
+ return { name, version, description: description || null, author: author || null };
11104
+ } finally {
11105
+ prompt.close();
11106
+ }
11107
+ }
11108
+
10795
11109
  // src/config/config-schema.ts
10796
11110
  var CONFIG_FILE_NAME = "luam.json";
10797
11111
  var DEFAULT_SOURCE_DIRS = ["src"];
@@ -10839,21 +11153,29 @@ function readTemplateSource(source) {
10839
11153
  }
10840
11154
 
10841
11155
  // src/scaffold/scaffold-plan.ts
10842
- function renderConfig(source, name) {
11156
+ function renderConfig(source, details) {
10843
11157
  const parsed = JSON.parse(source);
10844
11158
  const config = typeof parsed === "object" && parsed !== null ? parsed : {};
10845
- return `${JSON.stringify({ ...config, name, description: `The ${name} resource, scaffolded by luam init.` }, null, 4)}
11159
+ const { name: _name, version: _version, description: _description, author: _author, ...template } = config;
11160
+ const metadata = {
11161
+ ...template,
11162
+ name: details.name,
11163
+ version: details.version,
11164
+ ...details.description === null ? {} : { description: details.description },
11165
+ ...details.author === null ? {} : { author: details.author }
11166
+ };
11167
+ return `${JSON.stringify(metadata, null, 4)}
10846
11168
  `;
10847
11169
  }
10848
- function renderFile(file, name) {
11170
+ function renderFile(file, details) {
10849
11171
  const source = readTemplateSource(file.source);
10850
11172
  if (file.path !== CONFIG_FILE_NAME) {
10851
11173
  return { path: file.path, content: source };
10852
11174
  }
10853
- return { path: file.path, content: renderConfig(source, name) };
11175
+ return { path: file.path, content: renderConfig(source, details) };
10854
11176
  }
10855
- function buildScaffoldPlan(name) {
10856
- return { name, files: TEMPLATE_FILES.map((file) => renderFile(file, name)) };
11177
+ function buildScaffoldPlan(details) {
11178
+ return { name: details.name, files: TEMPLATE_FILES.map((file) => renderFile(file, details)) };
10857
11179
  }
10858
11180
 
10859
11181
  // src/scaffold/scaffold-writer.ts
@@ -10884,21 +11206,31 @@ function resolveResourceName(root, requested) {
10884
11206
  const folder = basename(root);
10885
11207
  return isValidResourceName(folder) ? folder : FALLBACK_RESOURCE_NAME;
10886
11208
  }
10887
- function runInitCommand(root, logger, options) {
11209
+ async function runInitCommand(root, logger, options) {
10888
11210
  if (options.name !== null && !isValidResourceName(options.name)) {
10889
11211
  logger.error(`"--name" must be a valid MTA resource name but received "${options.name}".`);
10890
11212
  return EXIT_USAGE;
10891
11213
  }
10892
- const name = resolveResourceName(root, options.name);
10893
- const result = writeScaffold(root, buildScaffoldPlan(name), options.force);
11214
+ const defaults = {
11215
+ name: resolveResourceName(root, options.name),
11216
+ version: "1.0.0",
11217
+ description: null,
11218
+ author: null
11219
+ };
11220
+ const details = options.yes ? defaults : await options.prompt(defaults, options.name !== null);
11221
+ if (!isValidResourceName(details.name)) {
11222
+ logger.error(`"Project name" must be a valid MTA resource name but received "${details.name}".`);
11223
+ return EXIT_USAGE;
11224
+ }
11225
+ const result = writeScaffold(root, buildScaffoldPlan(details), options.force);
10894
11226
  for (const path of result.skipped) {
10895
11227
  logger.warn(`Kept the existing "${path}". Pass "--force" to overwrite it.`);
10896
11228
  }
10897
11229
  if (result.written.length === 0) {
10898
- logger.info(`The project "${name}" already exists in "${root}". Nothing was written.`);
11230
+ logger.info(`The project "${details.name}" already exists in "${root}". Nothing was written.`);
10899
11231
  return EXIT_OK;
10900
11232
  }
10901
- logger.info(`Scaffolded "${name}" into "${root}" (${result.written.length} files, ${result.skipped.length} kept).`);
11233
+ logger.info(`Scaffolded "${details.name}" into "${root}" (${result.written.length} files, ${result.skipped.length} kept).`);
10902
11234
  logger.info('Next: run "luam check", then "luam build".');
10903
11235
  return EXIT_OK;
10904
11236
  }
@@ -11510,12 +11842,12 @@ function createEditorService() {
11510
11842
  }
11511
11843
 
11512
11844
  // src/editor/installation-prompt.ts
11513
- import { createInterface } from "node:readline/promises";
11845
+ import { createInterface as createInterface2 } from "node:readline/promises";
11514
11846
  async function promptForInstallation(editorName) {
11515
11847
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
11516
11848
  return null;
11517
11849
  }
11518
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
11850
+ const prompt = createInterface2({ input: process.stdin, output: process.stdout });
11519
11851
  try {
11520
11852
  const answer = (await prompt.question(`Install the Luam extension in ${editorName}? [Y/n] `)).trim().toLowerCase();
11521
11853
  return answer === "" || answer === "y" || answer === "yes";
@@ -11638,7 +11970,8 @@ async function runCli(argv, overrides = {}) {
11638
11970
  }
11639
11971
  const root = resolve12(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
11640
11972
  if (parsed.command === "init") {
11641
- return runInitCommand(root, logger, { name: parsed.name, force: parsed.force });
11973
+ const target = parsed.operand === null ? root : resolve12(root, parsed.operand);
11974
+ return runInitCommand(target, logger, { name: parsed.name, force: parsed.force, yes: parsed.yes, prompt: overrides.initPrompt ?? promptForProject });
11642
11975
  }
11643
11976
  const editorService = overrides.editorService ?? createEditorService();
11644
11977
  if (parsed.command === "doctor") {