@thigasdevelopment/luam 0.2.1 → 0.4.0

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
@@ -212,7 +212,7 @@ 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.0" : "0.0.0-dev";
216
216
  var USAGE = [
217
217
  "luam \u2014 the Luam compiler for Multi Theft Auto resources.",
218
218
  "",
@@ -916,6 +916,15 @@ function createNamed(name) {
916
916
  function createRecord(name, members, origin = null) {
917
917
  return { kind: "record", name, origin, members };
918
918
  }
919
+ function renameRecord(type, name) {
920
+ return type.kind === "record" ? createRecord(name, type.members, type.origin) : type;
921
+ }
922
+ function createObjectType(members) {
923
+ const keys = [...members].map(
924
+ ([name, member]) => member.kind === "optional" ? `${name}?: ${typeToString(member.element)}` : `${name}: ${typeToString(member)}`
925
+ );
926
+ return createRecord(keys.length === 0 ? "{}" : `{ ${keys.join(", ")} }`, members);
927
+ }
919
928
  function createFunction(parameters, returnType, minimumArguments2, isVariadic = false) {
920
929
  return { kind: "function", parameters, returnType, minimumArguments: minimumArguments2 ?? parameters.length, isVariadic };
921
930
  }
@@ -1022,6 +1031,21 @@ function isFunctionAssignable(source, target, options) {
1022
1031
  }
1023
1032
  return target.returnType.kind === "void" || isAssignable(source.returnType, target.returnType, options);
1024
1033
  }
1034
+ function isRecordAssignable(source, target, options) {
1035
+ for (const [name, member] of target.members) {
1036
+ const value = source.members.get(name);
1037
+ if (value === void 0) {
1038
+ if (member.kind !== "optional") {
1039
+ return false;
1040
+ }
1041
+ continue;
1042
+ }
1043
+ if (!isAssignable(value, member, options)) {
1044
+ return false;
1045
+ }
1046
+ }
1047
+ return true;
1048
+ }
1025
1049
  function isAssignable(source, target, options = { allowNil: false }) {
1026
1050
  if (source.kind === "any" || target.kind === "any" || target.kind === "unknown") {
1027
1051
  return true;
@@ -1053,6 +1077,9 @@ function isAssignable(source, target, options = { allowNil: false }) {
1053
1077
  if (target.kind === "table") {
1054
1078
  return isTableLike(source) || source.kind === "record";
1055
1079
  }
1080
+ if (target.kind === "record") {
1081
+ return source.kind === "record" ? isRecordAssignable(source, target, options) : source.kind === "table";
1082
+ }
1056
1083
  if (target.kind === "array") {
1057
1084
  return source.kind === "table" || source.kind === "array" && isAssignable(source.element, target.element, options);
1058
1085
  }
@@ -4831,6 +4858,14 @@ var DeclarationRegistry = class {
4831
4858
  lookupInterface(name) {
4832
4859
  return this.interfaces.get(name) ?? null;
4833
4860
  }
4861
+ interfaceExtends(name, target, visited = /* @__PURE__ */ new Set()) {
4862
+ const current = this.lookupInterface(name);
4863
+ if (current === null || visited.has(name)) {
4864
+ return false;
4865
+ }
4866
+ visited.add(name);
4867
+ return current.superInterfaces.some((parent) => parent === target || this.interfaceExtends(parent, target, visited));
4868
+ }
4834
4869
  declareEnum(info) {
4835
4870
  this.enums.set(info.name, info);
4836
4871
  }
@@ -4846,24 +4881,42 @@ var DeclarationRegistry = class {
4846
4881
  allEnums() {
4847
4882
  return [...this.enums.values()];
4848
4883
  }
4849
- collectMembers(className) {
4884
+ collectInterfaceMembers(name, collected, visited) {
4885
+ const current = this.lookupInterface(name);
4886
+ if (current === null || visited.has(current.name)) {
4887
+ return;
4888
+ }
4889
+ visited.add(current.name);
4890
+ for (const [memberName, member] of current.members) {
4891
+ if (!collected.has(memberName)) {
4892
+ collected.set(memberName, member);
4893
+ }
4894
+ }
4895
+ for (const parent of current.superInterfaces) {
4896
+ this.collectInterfaceMembers(parent, collected, visited);
4897
+ }
4898
+ }
4899
+ collectMembers(name) {
4850
4900
  const collected = /* @__PURE__ */ new Map();
4851
4901
  const visited = /* @__PURE__ */ new Set();
4852
- let current = this.lookupClass(className);
4902
+ let current = this.lookupClass(name);
4853
4903
  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);
4904
+ for (const [name2, member] of current.members) {
4905
+ if (!collected.has(name2)) {
4906
+ collected.set(name2, member);
4857
4907
  }
4858
4908
  }
4859
4909
  visited.add(current.name);
4860
4910
  current = current.superClass === null ? null : this.lookupClass(current.superClass);
4861
4911
  }
4912
+ if (current === null && this.lookupClass(name) === null) {
4913
+ this.collectInterfaceMembers(name, collected, visited);
4914
+ }
4862
4915
  return [...collected.values()];
4863
4916
  }
4864
- lookupMember(className, member) {
4917
+ lookupMember(name, member) {
4865
4918
  const visited = /* @__PURE__ */ new Set();
4866
- let current = this.lookupClass(className);
4919
+ let current = this.lookupClass(name);
4867
4920
  while (current !== null && !visited.has(current.name)) {
4868
4921
  const found = current.members.get(member);
4869
4922
  if (found !== void 0) {
@@ -4872,7 +4925,9 @@ var DeclarationRegistry = class {
4872
4925
  visited.add(current.name);
4873
4926
  current = current.superClass === null ? null : this.lookupClass(current.superClass);
4874
4927
  }
4875
- return null;
4928
+ const collected = /* @__PURE__ */ new Map();
4929
+ this.collectInterfaceMembers(name, collected, visited);
4930
+ return collected.get(member) ?? null;
4876
4931
  }
4877
4932
  };
4878
4933
 
@@ -4987,6 +5042,7 @@ var CheckContext = class {
4987
5042
  methodStack = [];
4988
5043
  projectEnvironments = /* @__PURE__ */ new Map();
4989
5044
  ambientClasses = /* @__PURE__ */ new Set();
5045
+ ambientInterfaces = /* @__PURE__ */ new Set();
4990
5046
  constructor(mode, environment, ambient = EMPTY_AMBIENT, project = EMPTY_PROJECT_DECLARATIONS, oop = false) {
4991
5047
  this.mode = mode;
4992
5048
  this.environment = environment;
@@ -5008,6 +5064,9 @@ var CheckContext = class {
5008
5064
  isAmbientClass(name) {
5009
5065
  return this.ambientClasses.has(name);
5010
5066
  }
5067
+ isAmbientInterface(name) {
5068
+ return this.ambientInterfaces.has(name);
5069
+ }
5011
5070
  noteExternalReference(name, position2) {
5012
5071
  if (!this.externalReferences.has(name)) {
5013
5072
  this.externalReferences.set(name, position2);
@@ -5057,6 +5116,16 @@ var CheckContext = class {
5057
5116
  if (annotation.kind === "type-union") {
5058
5117
  return createUnion(annotation.options.map((option) => this.resolveAnnotation(option)));
5059
5118
  }
5119
+ if (annotation.kind === "type-object") {
5120
+ const members = /* @__PURE__ */ new Map();
5121
+ for (const member of annotation.members) {
5122
+ members.set(member.name, this.resolveAnnotation(member.annotation));
5123
+ }
5124
+ return createObjectType(members);
5125
+ }
5126
+ if (annotation.kind === "type-tuple") {
5127
+ return createTuple(annotation.elements.map((element) => this.resolveAnnotation(element)));
5128
+ }
5060
5129
  if (annotation.kind === "type-function") {
5061
5130
  const parameters = annotation.parameters.map((parameter) => this.resolveAnnotation(parameter));
5062
5131
  const optional = parameters.findIndex((parameter) => parameter.kind === "optional");
@@ -5088,6 +5157,7 @@ var CheckContext = class {
5088
5157
  this.binder.declareGlobal({ name: info.name, type: createNamed(info.name), isLocal: false, position: info.position });
5089
5158
  }
5090
5159
  for (const info of ambient.interfaces) {
5160
+ this.ambientInterfaces.add(info.name);
5091
5161
  this.declarations.declareInterface(info);
5092
5162
  }
5093
5163
  for (const info of ambient.enums) {
@@ -5387,7 +5457,7 @@ function expandClassDecorators(context, statement, fieldTypes) {
5387
5457
  const occupied = new Map(statement.members.filter((member) => member.kind === "class-method").map((member) => [member.name, member.name]));
5388
5458
  const classDecorators = validateDecorators(context, statement.decorators, "class");
5389
5459
  for (const member of statement.members) {
5390
- if (member.kind === "class-method") {
5460
+ if (member.kind === "class-method" || member.name === "constructor") {
5391
5461
  validateDecorators(context, member.decorators, "method");
5392
5462
  continue;
5393
5463
  }
@@ -5969,7 +6039,11 @@ function resolveNamedMember(context, name, expression) {
5969
6039
  return resolveMtaMember(context, name, expression.property, expression.position)?.type ?? ANY_TYPE;
5970
6040
  }
5971
6041
  const contract = context.declarations.lookupClass(name) === null ? context.declarations.lookupInterface(name) : null;
5972
- return contract === null ? null : checkInterfaceMember(context, name, contract.members, expression);
6042
+ if (contract === null) {
6043
+ return null;
6044
+ }
6045
+ const members = new Map(context.declarations.collectMembers(name).map((member2) => [member2.name, member2]));
6046
+ return checkInterfaceMember(context, name, members, expression);
5973
6047
  }
5974
6048
  var NATIVE_CONSTRUCTOR = "new";
5975
6049
  function nativeConstructor(context, name) {
@@ -6334,6 +6408,10 @@ function registerMembers(context, info, statement) {
6334
6408
  return generated;
6335
6409
  }
6336
6410
  function checkMethodBody(context, info, member) {
6411
+ const explicitSelf = member.parameters.find((parameter) => parameter.name === "self");
6412
+ if (explicitSelf !== void 0) {
6413
+ context.report("check-explicit-self-parameter", 'Class methods receive "self" automatically; remove it from the parameter list.', explicitSelf.position);
6414
+ }
6337
6415
  context.pushClassMethod({ className: info.name, methodName: member.name });
6338
6416
  checkFunctionBody(context, member.parameters, member.returnAnnotation, member.body, createNamed(info.name));
6339
6417
  context.popClassMethod();
@@ -6360,7 +6438,7 @@ function checkInterfaces(context, info) {
6360
6438
  context.report("check-unknown-interface", `Class "${info.name}" implements "${name}", which is not defined.`, info.position);
6361
6439
  continue;
6362
6440
  }
6363
- for (const member of contract.members.values()) {
6441
+ for (const member of context.declarations.collectMembers(contract.name)) {
6364
6442
  checkContract(context, info, name, member);
6365
6443
  }
6366
6444
  }
@@ -6418,11 +6496,46 @@ function checkInterfaceDeclaration(context, statement) {
6418
6496
  return;
6419
6497
  }
6420
6498
  const members = /* @__PURE__ */ new Map();
6499
+ for (const name of new Set(statement.superInterfaces)) {
6500
+ const parent = context.declarations.lookupInterface(name);
6501
+ if (name === statement.name || context.declarations.interfaceExtends(name, statement.name)) {
6502
+ context.report("check-interface-cycle", `Interface "${statement.name}" creates an inheritance cycle through "${name}".`, statement.position);
6503
+ } else if (parent === null) {
6504
+ context.noteExternalReference(name, statement.position);
6505
+ context.report("check-unknown-interface", `Interface "${statement.name}" extends "${name}", which is not defined.`, statement.position);
6506
+ } else if (context.isAmbientInterface(name)) {
6507
+ context.noteExternalReference(name, statement.position);
6508
+ }
6509
+ }
6510
+ if (new Set(statement.superInterfaces).size !== statement.superInterfaces.length) {
6511
+ context.report("check-duplicate-interface-parent", `Interface "${statement.name}" extends the same interface more than once.`, statement.position);
6512
+ }
6421
6513
  for (const member of statement.members) {
6422
6514
  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 });
6515
+ const info = { name: member.name, type, isMethod: member.kind === "interface-method", position: member.position };
6516
+ if (members.has(member.name)) {
6517
+ context.report("check-duplicate-interface-member", `Interface "${statement.name}" declares member "${member.name}" more than once.`, member.position);
6518
+ } else {
6519
+ members.set(member.name, info);
6520
+ }
6521
+ }
6522
+ const inherited = /* @__PURE__ */ new Map();
6523
+ for (const parent of statement.superInterfaces) {
6524
+ for (const member of context.declarations.collectMembers(parent)) {
6525
+ const existing = members.get(member.name) ?? inherited.get(member.name);
6526
+ if (existing !== void 0 && (existing.isMethod !== member.isMethod || typeToString(existing.type) !== typeToString(member.type))) {
6527
+ context.report("check-conflicting-interface-member", `Interface "${statement.name}" inherits conflicting declarations of "${member.name}".`, statement.position);
6528
+ } else if (existing === void 0) {
6529
+ inherited.set(member.name, member);
6530
+ }
6531
+ }
6424
6532
  }
6425
- context.declarations.declareInterface({ name: statement.name, members, position: statement.position });
6533
+ context.declarations.declareInterface({
6534
+ name: statement.name,
6535
+ superInterfaces: statement.superInterfaces,
6536
+ members,
6537
+ position: statement.position
6538
+ });
6426
6539
  }
6427
6540
  function checkEnumDeclaration(context, statement) {
6428
6541
  if (context.declarations.lookupEnum(statement.name) !== null) {
@@ -6582,6 +6695,23 @@ function checkReturn(context, statement) {
6582
6695
  }
6583
6696
  return;
6584
6697
  }
6698
+ if (expected.kind === "tuple") {
6699
+ if (valueTypes.length !== expected.elements.length) {
6700
+ const message = `This function declares ${expected.elements.length} return values but returns ${valueTypes.length}.`;
6701
+ context.report("check-return-mismatch", message, statement.position);
6702
+ return;
6703
+ }
6704
+ valueTypes.forEach((type, index) => {
6705
+ const value2 = statement.values[Math.min(index, statement.values.length - 1)];
6706
+ context.expectAssignable(type, expected.elements[index] ?? ANY_TYPE, value2?.position ?? statement.position, `Return value ${index + 1}`);
6707
+ });
6708
+ return;
6709
+ }
6710
+ if (valueTypes.length > 1) {
6711
+ const message = `This function declares one return value of type "${typeToString(expected)}" but returns ${valueTypes.length} values.`;
6712
+ context.report("check-return-mismatch", message, statement.position);
6713
+ return;
6714
+ }
6585
6715
  const first = valueTypes[0];
6586
6716
  const value = statement.values[0];
6587
6717
  if (first === void 0 || value === void 0) {
@@ -6628,9 +6758,12 @@ function checkStatement(context, statement) {
6628
6758
  return checkNumericFor(context, statement);
6629
6759
  case "generic-for-statement":
6630
6760
  return checkGenericFor(context, statement);
6631
- case "type-alias-statement":
6632
- context.binder.declareAlias(statement.name, context.resolveAnnotation(statement.annotation));
6761
+ case "type-alias-statement": {
6762
+ const resolved = context.resolveAnnotation(statement.annotation);
6763
+ const named2 = statement.annotation.kind === "type-object" ? renameRecord(resolved, statement.name) : resolved;
6764
+ context.binder.declareAlias(statement.name, named2);
6633
6765
  return;
6766
+ }
6634
6767
  case "declare-statement":
6635
6768
  return checkDeclareStatement(context, statement);
6636
6769
  case "class-declaration":
@@ -7781,13 +7914,45 @@ function parseFunctionType(stream) {
7781
7914
  return { kind: "type-function", parameters, isVariadic, returnType, position: position2 };
7782
7915
  }
7783
7916
  function parseGroupedType(stream) {
7784
- stream.expect("punctuation", "(");
7917
+ const position2 = stream.expect("punctuation", "(").position;
7785
7918
  const inner = parseTypeAnnotation(stream);
7786
- if (stream.check("punctuation", ",")) {
7787
- throw stream.error("A parenthesized type must contain a single type.", "parse-invalid-type");
7919
+ if (!stream.match("punctuation", ",")) {
7920
+ stream.expect("punctuation", ")");
7921
+ return inner;
7788
7922
  }
7923
+ const elements = [inner];
7924
+ do {
7925
+ elements.push(parseTypeAnnotation(stream));
7926
+ } while (stream.match("punctuation", ","));
7789
7927
  stream.expect("punctuation", ")");
7790
- return inner;
7928
+ return { kind: "type-tuple", elements, position: position2 };
7929
+ }
7930
+ function parseObjectMember(stream) {
7931
+ const token = stream.expectName();
7932
+ const annotation = parseNamedAnnotation(stream, "field");
7933
+ if (annotation === null) {
7934
+ throw stream.error(`Key "${token.value}" of an object type requires a type annotation.`, "parse-invalid-type");
7935
+ }
7936
+ return { name: token.value, annotation, position: token.position };
7937
+ }
7938
+ function parseObjectType(stream) {
7939
+ const position2 = stream.expect("punctuation", "{").position;
7940
+ const members = [];
7941
+ const declared = /* @__PURE__ */ new Set();
7942
+ while (!stream.check("punctuation", "}") && !stream.isEof()) {
7943
+ if (stream.match("punctuation", ";") || stream.match("punctuation", ",")) {
7944
+ continue;
7945
+ }
7946
+ const member = parseObjectMember(stream);
7947
+ if (declared.has(member.name)) {
7948
+ stream.report("parse-duplicate-key", `Object type key "${member.name}" is declared more than once.`, member.position);
7949
+ continue;
7950
+ }
7951
+ declared.add(member.name);
7952
+ members.push(member);
7953
+ }
7954
+ stream.expect("punctuation", "}");
7955
+ return { kind: "type-object", members, position: position2 };
7791
7956
  }
7792
7957
  function parsePrimaryType(stream) {
7793
7958
  if (stream.check("keyword", "function")) {
@@ -7796,6 +7961,9 @@ function parsePrimaryType(stream) {
7796
7961
  if (stream.check("identifier", FUNCTION_TYPE)) {
7797
7962
  return parseFunctionType(stream);
7798
7963
  }
7964
+ if (stream.check("punctuation", "{")) {
7965
+ return parseObjectType(stream);
7966
+ }
7799
7967
  return stream.check("punctuation", "(") ? parseGroupedType(stream) : parseTypeName(stream);
7800
7968
  }
7801
7969
  function parsePostfixType(stream) {
@@ -8125,16 +8293,24 @@ function parseForStatement(stream) {
8125
8293
  }
8126
8294
 
8127
8295
  // ../compiler/src/parser/recovery.ts
8128
- function recoverInBlock(stream, error) {
8296
+ var BRACE_TERMINATORS = /* @__PURE__ */ new Set(["}"]);
8297
+ function isTerminator(stream, terminators) {
8298
+ const token = stream.current();
8299
+ if (token.kind !== "punctuation" && token.kind !== "keyword") {
8300
+ return false;
8301
+ }
8302
+ return terminators.has(token.value);
8303
+ }
8304
+ function recoverInBlock(stream, error, terminators = BRACE_TERMINATORS) {
8129
8305
  stream.diagnostics.push(error.diagnostic);
8130
8306
  if (stream.isEof()) {
8131
8307
  return;
8132
8308
  }
8133
8309
  const start = stream.current();
8134
- while (!stream.isEof() && !stream.check("punctuation", "}") && stream.current().position.line === start.position.line) {
8310
+ while (!stream.isEof() && !isTerminator(stream, terminators) && stream.current().position.line === start.position.line) {
8135
8311
  stream.next();
8136
8312
  }
8137
- if (!stream.isEof() && !stream.check("punctuation", "}") && stream.current().position.offset === start.position.offset) {
8313
+ if (!stream.isEof() && !isTerminator(stream, terminators) && stream.current().position.offset === start.position.offset) {
8138
8314
  stream.next();
8139
8315
  }
8140
8316
  }
@@ -8244,7 +8420,10 @@ function isDeclarationStart(stream) {
8244
8420
  if (offset > 0 && token.value !== "class") {
8245
8421
  return false;
8246
8422
  }
8247
- return token.value === "class" ? isClassHeader(stream, offset) : stream.checkAhead(offset + 2, "punctuation", "{");
8423
+ if (token.value === "class") {
8424
+ return isClassHeader(stream, offset);
8425
+ }
8426
+ return token.value === "interface" ? stream.checkAhead(offset + 2, "punctuation", "{") || stream.checkAhead(offset + 2, "keyword", "extends") : stream.checkAhead(offset + 2, "punctuation", "{");
8248
8427
  }
8249
8428
  function skipDecoratorArguments(stream) {
8250
8429
  let depth = 0;
@@ -8271,6 +8450,21 @@ function parseDecorators(stream) {
8271
8450
  return decorators;
8272
8451
  }
8273
8452
  function parseClassMethod(stream, token, decorators) {
8453
+ stream.expect("operator", "=");
8454
+ const expression = parseFunctionExpression(stream);
8455
+ return {
8456
+ kind: "class-method",
8457
+ name: token.value,
8458
+ isConstructor: token.value === "constructor",
8459
+ isSynthetic: false,
8460
+ parameters: expression.parameters,
8461
+ returnAnnotation: expression.returnAnnotation,
8462
+ body: expression.body,
8463
+ decorators,
8464
+ position: token.position
8465
+ };
8466
+ }
8467
+ function parseLegacyClassMethod(stream, token, decorators) {
8274
8468
  const parameters = parseParameters(stream);
8275
8469
  const returnAnnotation = parseOptionalAnnotation(stream);
8276
8470
  const body = parseBraceBlock(stream);
@@ -8293,6 +8487,9 @@ function parseClassMember(stream) {
8293
8487
  const decorators = parseDecorators(stream);
8294
8488
  const token = stream.expectName();
8295
8489
  if (stream.check("punctuation", "(")) {
8490
+ return parseLegacyClassMethod(stream, token, decorators);
8491
+ }
8492
+ if (stream.check("operator", "=") && stream.checkAhead(1, "keyword", "function")) {
8296
8493
  return parseClassMethod(stream, token, decorators);
8297
8494
  }
8298
8495
  const annotation = parseFieldAnnotation(stream);
@@ -8348,7 +8545,13 @@ function parseInterfaceMember(stream) {
8348
8545
  function parseInterfaceDeclaration(stream) {
8349
8546
  const position2 = stream.next().position;
8350
8547
  const name = stream.expect("identifier").value;
8548
+ const superInterfaces = [];
8351
8549
  const members = [];
8550
+ if (stream.match("keyword", "extends")) {
8551
+ do {
8552
+ superInterfaces.push(stream.expect("identifier").value);
8553
+ } while (stream.match("punctuation", ","));
8554
+ }
8352
8555
  stream.expect("punctuation", "{");
8353
8556
  while (!stream.check("punctuation", "}") && !stream.isEof()) {
8354
8557
  if (stream.match("punctuation", ";") || stream.match("punctuation", ",")) {
@@ -8364,7 +8567,7 @@ function parseInterfaceDeclaration(stream) {
8364
8567
  }
8365
8568
  }
8366
8569
  stream.expect("punctuation", "}");
8367
- return { kind: "interface-declaration", name, members, position: position2 };
8570
+ return { kind: "interface-declaration", name, superInterfaces, members, position: position2 };
8368
8571
  }
8369
8572
  function parseEnumDeclaration(stream) {
8370
8573
  const position2 = stream.next().position;
@@ -8564,14 +8767,14 @@ function parseStatement(stream) {
8564
8767
  }
8565
8768
  return parseExpressionStatement(stream);
8566
8769
  }
8567
- function parseBlockStatement(stream) {
8770
+ function parseBlockStatement(stream, terminators = BRACE_TERMINATORS) {
8568
8771
  try {
8569
8772
  return parseStatement(stream);
8570
8773
  } catch (error) {
8571
8774
  if (!(error instanceof ParserError)) {
8572
8775
  throw error;
8573
8776
  }
8574
- recoverInBlock(stream, error);
8777
+ recoverInBlock(stream, error, terminators);
8575
8778
  return null;
8576
8779
  }
8577
8780
  }
@@ -8604,7 +8807,14 @@ function parseBlock(stream, terminators) {
8604
8807
  if (stream.current().kind === "keyword" && stop.has(stream.current().value)) {
8605
8808
  return body;
8606
8809
  }
8607
- const statement = parseStatement(stream);
8810
+ const offset = stream.current().position.offset;
8811
+ const statement = parseBlockStatement(stream, stop);
8812
+ if (statement === null) {
8813
+ if (stream.current().position.offset === offset) {
8814
+ return body;
8815
+ }
8816
+ continue;
8817
+ }
8608
8818
  body.push(statement);
8609
8819
  if (statement.kind === "return-statement") {
8610
8820
  return body;
@@ -8751,7 +8961,7 @@ function classText(info) {
8751
8961
  return `class ${info.name}:${info.superClass ?? ""}:${[...info.interfaces].sort(compareText).join(",")}{${membersText(info.members)}}`;
8752
8962
  }
8753
8963
  function interfaceText(info) {
8754
- return `interface ${info.name}{${membersText(info.members)}}`;
8964
+ return `interface ${info.name}:${[...info.superInterfaces].sort(compareText).join(",")}{${membersText(info.members)}}`;
8755
8965
  }
8756
8966
  function enumText(info) {
8757
8967
  return `enum ${info.name}{${info.members.join(",")}}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thigasdevelopment/luam",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "The Luam compiler for Multi Theft Auto resources.",
5
5
  "keywords": [
6
6
  "mta",
@@ -1,120 +0,0 @@
1
- # luam-resource
2
-
3
- A Luam resource scaffolded by `luam init`. Write `.luam` sources under `src`, run
4
- `luam build`, and MTA gets a plain Lua 5.1 resource in `build/luam-resource`.
5
-
6
- ## Commands
7
-
8
- | Command | Behavior |
9
- | ------- | -------- |
10
- | `luam check` | Compiles and reports diagnostics. Writes nothing. |
11
- | `luam build` | Writes the resource into `build/luam-resource`. |
12
- | `luam ensure` | Builds, syncs into the MTA server, restarts, and watches sources. |
13
-
14
- Set `serverPath` in `luam.json` before running `luam ensure`.
15
-
16
- ## Layout
17
-
18
- ```
19
- .env Declares deployment keys and safe defaults
20
- config.lua Structural settings the resource owns
21
- assets/ Files copied verbatim and downloaded by clients
22
- src/
23
- shared/
24
- config.luam Constants and helpers both sides use
25
- framework/ Core, Event, Listener, Command, Loader, ThreadPool
26
- server/
27
- main.luam Creates the server Core and starts it
28
- handlers/ One file per server listener
29
- commands/ One file per server command
30
- client/
31
- main.luam Creates the client Core and starts it
32
- commands/ One file per client command
33
- ```
34
-
35
- The generated resource mirrors this tree. `src/server/main.luam` becomes
36
- `src/server/main.lua`, and runtime helpers land in `src/<environment>/lib`, so a
37
- server-only helper is never downloaded by a client.
38
-
39
- ## Configuration
40
-
41
- Two files hold settings, and they have different owners.
42
-
43
- `config.lua` belongs to the resource author. It is plain Lua, copied verbatim,
44
- listed in `meta.xml` as a shared script, and readable by clients. Anything a
45
- player may see belongs here.
46
-
47
- `.env` belongs to the deployment. It declares the keys and safe defaults, and it
48
- is the source of truth for their types: an unquoted number is a number, `true`
49
- and `false` are booleans, and quoting forces a string. Override values locally in
50
- `.env.local`, which is never committed.
51
-
52
- The first `luam build` writes `build/<resource>/.env` from those keys, leaving
53
- sensitive values blank, and never overwrites it again — that file belongs to the
54
- server administrator.
55
-
56
- Values reach Luam through `process.env`, which exists only on the server:
57
-
58
- ```lua
59
- outputServerLog(tostring(process.env.SERVER_NAME))
60
- ```
61
-
62
- A key that `.env` does not declare is a compile error, and `process` in a client
63
- or shared file is a compile error too.
64
-
65
- ## Framework
66
-
67
- `Core` owns the resource lifecycle. `main.luam` creates it with the side it runs
68
- on and calls `start()` once the resource has finished loading. `start()` scans
69
- the class registry, instantiates every `Listener` and `Command` subclass with the
70
- `Core` instance, and registers them.
71
-
72
- ### Listeners
73
-
74
- A listener declares the MTA event it handles and overrides `handle`. Nothing
75
- registers it by hand — being a `Listener` subclass is enough.
76
-
77
- ```lua
78
- class PlayerQuitListener extends Listener {
79
- event: string = 'onPlayerQuit'
80
-
81
- handle(...): void {
82
- outputDebugString(getPlayerName(source) .. ' left.')
83
- }
84
- }
85
- ```
86
-
87
- `element` defaults to `root`. Set it to narrow the handler to one element.
88
-
89
- ### Commands
90
-
91
- A command declares its name, optional aliases, and overrides `execute`. Every
92
- name and alias is registered.
93
-
94
- ```lua
95
- class KickCommand extends Command {
96
- name: string = 'kick'
97
- aliases: string[] = { 'k' }
98
- description: string = 'Kicks a player by name.'
99
-
100
- execute(player: any, ...): void {
101
- local args: any = { ... }
102
-
103
- kickPlayer(getPlayerFromName(args[1]), player, 'kicked')
104
- }
105
- }
106
- ```
107
-
108
- On the server `execute` receives the calling player; on the client the first
109
- parameter is `nil`.
110
-
111
- ### Threads
112
-
113
- `ThreadPool` wraps the `threads.lua` runtime helper, which is opt-in. Add it to
114
- `luam.json` before using the class:
115
-
116
- ```json
117
- {
118
- "helpers": ["threads"]
119
- }
120
- ```
@@ -1,14 +0,0 @@
1
- Config = {
2
- greeting = 'Welcome to the server.',
3
-
4
- spawn = {
5
- x = 1959.55,
6
- y = -1714.46,
7
- z = 10.0,
8
- },
9
-
10
- ping = {
11
- limit = 250,
12
- warnAt = 180,
13
- },
14
- }
package/template/env DELETED
@@ -1,11 +0,0 @@
1
- # Deployment values. This file is versioned and declares the keys and safe
2
- # defaults. Types come from this file: an unquoted number is a number, "true"
3
- # and "false" are booleans, and quoting forces a string.
4
- #
5
- # Override values on your machine in ".env.local", which is never committed.
6
- # The server administrator edits the generated "build/.env" instead.
7
-
8
- # Server
9
- SERVER_NAME="Luam Server"
10
- MAX_PLAYERS=32
11
- DEBUG=false
@@ -1,9 +0,0 @@
1
- local core: Core = new Core('client')
2
-
3
- addEventHandler('onClientResourceStart', resourceRoot, function()
4
- core:start()
5
- end)
6
-
7
- addEventHandler('onClientResourceStop', resourceRoot, function()
8
- core:stop()
9
- end)
@@ -1,9 +0,0 @@
1
- local core: Core = new Core('server')
2
-
3
- addEventHandler('onResourceStart', resourceRoot, function()
4
- core:start()
5
- end)
6
-
7
- addEventHandler('onResourceStop', resourceRoot, function()
8
- core:stop()
9
- end)