@thigasdevelopment/luam 0.18.2 → 0.19.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.
Files changed (2) hide show
  1. package/luam.mjs +304 -77
  2. package/package.json +1 -1
package/luam.mjs CHANGED
@@ -6584,7 +6584,7 @@ import { tmpdir } from "node:os";
6584
6584
  import { join as join2 } from "node:path";
6585
6585
 
6586
6586
  // src/cli/version.ts
6587
- var VERSION = true ? "0.18.2" : "0.0.0-dev";
6587
+ var VERSION = true ? "0.19.0" : "0.0.0-dev";
6588
6588
  var PROGRAM_NAME = "luam";
6589
6589
  var PROGRAM_DESCRIPTION = "luam \u2014 the Luam compiler for Multi Theft Auto resources.";
6590
6590
 
@@ -13296,13 +13296,13 @@ function chooseCandidate(source, candidates3) {
13296
13296
  return matching.length === 1 && best !== void 0 ? best : null;
13297
13297
  }
13298
13298
  function missingKeys(source, target) {
13299
- const missing = [];
13299
+ const missing2 = [];
13300
13300
  for (const [name, member] of target.members) {
13301
13301
  if (member.kind !== "optional" && !source.members.has(name)) {
13302
- missing.push(`"${name}"`);
13302
+ missing2.push(`"${name}"`);
13303
13303
  }
13304
13304
  }
13305
- return missing;
13305
+ return missing2;
13306
13306
  }
13307
13307
  function missingKeyHint(source, target) {
13308
13308
  if (source.kind !== "record" || source.isLiteral !== true) {
@@ -13314,11 +13314,11 @@ function missingKeyHint(source, target) {
13314
13314
  if (candidate === null) {
13315
13315
  return "";
13316
13316
  }
13317
- const missing = missingKeys(source, candidate);
13318
- if (missing.length === 0) {
13317
+ const missing2 = missingKeys(source, candidate);
13318
+ if (missing2.length === 0) {
13319
13319
  return "";
13320
13320
  }
13321
- const label2 = missing.length === 1 ? `Key ${missing[0]} is` : `Keys ${missing.join(", ")} are`;
13321
+ const label2 = missing2.length === 1 ? `Key ${missing2[0]} is` : `Keys ${missing2.join(", ")} are`;
13322
13322
  return ` ${label2} missing from "${typeToString(candidate)}".`;
13323
13323
  }
13324
13324
 
@@ -13458,17 +13458,24 @@ var CheckContext = class {
13458
13458
  pushNarrowing(facts) {
13459
13459
  this.narrowings.push(new Map(facts));
13460
13460
  }
13461
+ get isNarrowed() {
13462
+ return this.narrowings.length > 0;
13463
+ }
13461
13464
  popNarrowing() {
13462
13465
  this.narrowings.pop();
13463
13466
  }
13464
- forgetNarrowing(name) {
13467
+ forgetNarrowing(path) {
13465
13468
  for (const frame of this.narrowings) {
13466
- frame.delete(name);
13469
+ for (const key of [...frame.keys()]) {
13470
+ if (key === path || key.startsWith(`${path}.`) || path.startsWith(`${key}.`)) {
13471
+ frame.delete(key);
13472
+ }
13473
+ }
13467
13474
  }
13468
13475
  }
13469
- narrowedType(name) {
13476
+ narrowedType(path) {
13470
13477
  for (let index = this.narrowings.length - 1; index >= 0; index -= 1) {
13471
- const found = this.narrowings[index]?.get(name);
13478
+ const found = this.narrowings[index]?.get(path);
13472
13479
  if (found !== void 0) {
13473
13480
  return found;
13474
13481
  }
@@ -14272,6 +14279,139 @@ function expandClassDecorators(context, statement, fieldTypes) {
14272
14279
  return generated;
14273
14280
  }
14274
14281
 
14282
+ // ../compiler/src/checker/access-path.ts
14283
+ function pathOf2(expression) {
14284
+ if (expression.kind === "identifier") {
14285
+ return expression.name;
14286
+ }
14287
+ if (expression.kind !== "member-expression") {
14288
+ return null;
14289
+ }
14290
+ const object = pathOf2(expression.object);
14291
+ return object === null ? null : `${object}.${expression.property}`;
14292
+ }
14293
+ function assignedPath(target) {
14294
+ const direct = pathOf2(target);
14295
+ if (direct !== null) {
14296
+ return direct;
14297
+ }
14298
+ return target.kind === "member-expression" || target.kind === "index-expression" ? assignedPath(target.object) : null;
14299
+ }
14300
+ function pathType(context, expression) {
14301
+ if (expression.kind === "identifier") {
14302
+ return context.binder.lookup(expression.name)?.type ?? null;
14303
+ }
14304
+ return expression.kind === "member-expression" ? context.types.get(expression) ?? null : null;
14305
+ }
14306
+ function childExpressions2(expression) {
14307
+ switch (expression.kind) {
14308
+ case "member-expression":
14309
+ return [expression.object];
14310
+ case "index-expression":
14311
+ return [expression.object, expression.index];
14312
+ case "call-expression":
14313
+ return [expression.callee, ...expression.args];
14314
+ case "new-expression":
14315
+ return expression.args;
14316
+ case "table-expression":
14317
+ return expression.fields.flatMap((field2) => field2.key === null ? [field2.value] : [field2.key, field2.value]);
14318
+ case "binary-expression":
14319
+ return [expression.left, expression.right];
14320
+ case "unary-expression":
14321
+ return [expression.operand];
14322
+ case "group-expression":
14323
+ return [expression.expression];
14324
+ default:
14325
+ return [];
14326
+ }
14327
+ }
14328
+ function statementExpressions2(statement) {
14329
+ switch (statement.kind) {
14330
+ case "local-statement":
14331
+ case "return-statement":
14332
+ return statement.values;
14333
+ case "assignment-statement":
14334
+ return [...statement.targets, ...statement.values];
14335
+ case "call-statement":
14336
+ return [statement.expression];
14337
+ case "while-statement":
14338
+ case "repeat-statement":
14339
+ return [statement.condition];
14340
+ case "if-statement":
14341
+ return statement.clauses.map((clause) => clause.condition);
14342
+ case "numeric-for-statement":
14343
+ return statement.step === null ? [statement.start, statement.limit] : [statement.start, statement.limit, statement.step];
14344
+ case "generic-for-statement":
14345
+ return statement.iterators;
14346
+ case "class-declaration":
14347
+ return statement.members.flatMap((member) => member.kind === "class-field" && member.value !== null ? [member.value] : []);
14348
+ default:
14349
+ return [];
14350
+ }
14351
+ }
14352
+ function blockBodies(statement) {
14353
+ switch (statement.kind) {
14354
+ case "do-statement":
14355
+ case "while-statement":
14356
+ case "repeat-statement":
14357
+ case "numeric-for-statement":
14358
+ case "generic-for-statement":
14359
+ case "function-declaration":
14360
+ return [statement.body];
14361
+ case "if-statement":
14362
+ return [...statement.clauses.map((clause) => clause.body), ...statement.alternate === null ? [] : [statement.alternate]];
14363
+ case "class-declaration":
14364
+ return statement.members.filter((member) => member.kind === "class-method").map((member) => member.body);
14365
+ default:
14366
+ return [];
14367
+ }
14368
+ }
14369
+ function collectExpressionPaths(expression, paths) {
14370
+ if (expression.kind === "function-expression") {
14371
+ collectPaths(expression.body, paths);
14372
+ }
14373
+ for (const child of childExpressions2(expression)) {
14374
+ collectExpressionPaths(child, paths);
14375
+ }
14376
+ }
14377
+ function addTargets(statement, paths) {
14378
+ if (statement.kind === "assignment-statement") {
14379
+ for (const target of statement.targets) {
14380
+ const path = assignedPath(target);
14381
+ if (path !== null) {
14382
+ paths.add(path);
14383
+ }
14384
+ }
14385
+ }
14386
+ if (statement.kind === "local-statement") {
14387
+ statement.declarations.forEach((declaration) => paths.add(declaration.name));
14388
+ }
14389
+ }
14390
+ function collectPaths(body, paths) {
14391
+ for (const statement of body) {
14392
+ addTargets(statement, paths);
14393
+ for (const expression of statementExpressions2(statement)) {
14394
+ collectExpressionPaths(expression, paths);
14395
+ }
14396
+ for (const block of blockBodies(statement)) {
14397
+ collectPaths(block, paths);
14398
+ }
14399
+ }
14400
+ }
14401
+ function assignedPaths(body) {
14402
+ const paths = /* @__PURE__ */ new Set();
14403
+ collectPaths(body, paths);
14404
+ return paths;
14405
+ }
14406
+ function forgetAssignedPaths(context, body) {
14407
+ if (!context.isNarrowed) {
14408
+ return;
14409
+ }
14410
+ for (const path of assignedPaths(body)) {
14411
+ context.forgetNarrowing(path);
14412
+ }
14413
+ }
14414
+
14275
14415
  // ../compiler/src/extensions/native-extensions.ts
14276
14416
  var NATIVE_EXTENSIONS = [
14277
14417
  { receiver: "table", property: "count", target: "table.size", style: "property", result: "number", helper: "table" },
@@ -17934,19 +18074,19 @@ function resolveUnionMember(context, type, expression) {
17934
18074
  return null;
17935
18075
  }
17936
18076
  const resolved2 = [];
17937
- const missing = [];
18077
+ const missing2 = [];
17938
18078
  for (const option of type.options) {
17939
18079
  const member = memberOf(context, option, expression.property);
17940
18080
  if (member === null) {
17941
- missing.push(`"${typeToString(option)}"`);
18081
+ missing2.push(`"${typeToString(option)}"`);
17942
18082
  } else {
17943
18083
  resolved2.push(member);
17944
18084
  }
17945
18085
  }
17946
- if (missing.length === 0) {
18086
+ if (missing2.length === 0) {
17947
18087
  return createUnion(resolved2);
17948
18088
  }
17949
- const message = `"${expression.property}" is not a key of every member of "${typeToString(type)}". It is missing from ${missing.join(", ")}.`;
18089
+ const message = `"${expression.property}" is not a key of every member of "${typeToString(type)}". It is missing from ${missing2.join(", ")}.`;
17950
18090
  context.report("check-unknown-union-key", message, expression.position);
17951
18091
  return ANY_TYPE;
17952
18092
  }
@@ -17962,18 +18102,19 @@ function literalValue(expression) {
17962
18102
  return expression.kind === "number-literal" ? createNumberLiteral(expression.value) : null;
17963
18103
  }
17964
18104
  function discriminantTest(left, right) {
17965
- if (left.kind !== "member-expression" || left.object.kind !== "identifier") {
18105
+ if (left.kind !== "member-expression") {
17966
18106
  return null;
17967
18107
  }
18108
+ const path = pathOf2(left.object);
17968
18109
  const value = literalValue(right);
17969
- return value === null ? null : { name: left.object.name, property: left.property, value };
18110
+ return path === null || value === null ? null : { path, subject: left.object, property: left.property, value };
17970
18111
  }
17971
- function subjectType(context, name) {
17972
- const type = context.narrowedType(name) ?? context.binder.lookup(name)?.type ?? null;
18112
+ function subjectType(context, test) {
18113
+ const type = context.narrowedType(test.path) ?? pathType(context, test.subject);
17973
18114
  return type === null ? null : withoutNil(type);
17974
18115
  }
17975
18116
  function filterOptions(context, test, keep) {
17976
- const subject = subjectType(context, test.name);
18117
+ const subject = subjectType(context, test);
17977
18118
  if (subject === null || subject.kind !== "union") {
17978
18119
  return null;
17979
18120
  }
@@ -17995,7 +18136,7 @@ function discriminantFact(context, left, right, keep) {
17995
18136
  return null;
17996
18137
  }
17997
18138
  const narrowed = filterOptions(context, test, keep);
17998
- return narrowed === null ? null : [test.name, narrowed];
18139
+ return narrowed === null ? null : [test.path, narrowed];
17999
18140
  }
18000
18141
 
18001
18142
  // ../compiler/src/checker/narrowing.ts
@@ -18010,38 +18151,42 @@ var GUARD_TYPES = {
18010
18151
  thread: THREAD_TYPE,
18011
18152
  userdata: USERDATA_TYPE
18012
18153
  };
18013
- function declaredType(context, name) {
18014
- return context.binder.lookup(name)?.type ?? null;
18015
- }
18016
- function guardedName(expression) {
18154
+ function guardedPath(expression) {
18017
18155
  if (expression.kind !== "call-expression" || expression.callee.kind !== "identifier" || expression.callee.name !== GUARD_FUNCTION) {
18018
18156
  return null;
18019
18157
  }
18020
18158
  const [argument] = expression.args;
18021
- return argument !== void 0 && argument.kind === "identifier" ? argument.name : null;
18159
+ return argument === void 0 ? null : pathOf2(argument);
18022
18160
  }
18023
18161
  function guardFact(left, right) {
18024
- const name = guardedName(left);
18162
+ const path = guardedPath(left);
18025
18163
  const value = right.kind === "string-literal" ? GUARD_TYPES[right.value] : void 0;
18026
- return name === null || value === void 0 ? null : [name, value];
18164
+ return path === null || value === void 0 ? null : [path, value];
18027
18165
  }
18028
18166
  function nilComparison(left, right) {
18029
- if (left.kind === "identifier" && right.kind === "nil-literal") {
18030
- return left.name;
18167
+ if (right.kind === "nil-literal" && pathOf2(left) !== null) {
18168
+ return left;
18169
+ }
18170
+ return left.kind === "nil-literal" && pathOf2(right) !== null ? right : null;
18171
+ }
18172
+ function present2(context, expression, facts) {
18173
+ const path = pathOf2(expression);
18174
+ const declared = path === null ? null : pathType(context, expression);
18175
+ if (path !== null && declared !== null) {
18176
+ facts.set(path, withoutNil(declared));
18031
18177
  }
18032
- return right.kind === "identifier" && left.kind === "nil-literal" ? right.name : null;
18033
18178
  }
18034
- function present2(context, name, facts) {
18035
- const declared = declaredType(context, name);
18036
- if (declared !== null) {
18037
- facts.set(name, withoutNil(declared));
18179
+ function missing(expression, facts) {
18180
+ const path = pathOf2(expression);
18181
+ if (path !== null) {
18182
+ facts.set(path, NIL_TYPE);
18038
18183
  }
18039
18184
  }
18040
18185
  function mergeAlternatives(left, right, facts) {
18041
- for (const [name, type] of left) {
18042
- const other = right.get(name);
18186
+ for (const [path, type] of left) {
18187
+ const other = right.get(path);
18043
18188
  if (other !== void 0) {
18044
- facts.set(name, createUnion([type, other]));
18189
+ facts.set(path, createUnion([type, other]));
18045
18190
  }
18046
18191
  }
18047
18192
  }
@@ -18056,8 +18201,8 @@ function collectFacts(context, expression, facts) {
18056
18201
  collectFacts(context, expression.expression, facts);
18057
18202
  return;
18058
18203
  }
18059
- if (expression.kind === "identifier") {
18060
- present2(context, expression.name, facts);
18204
+ if (expression.kind === "identifier" || expression.kind === "member-expression") {
18205
+ present2(context, expression, facts);
18061
18206
  return;
18062
18207
  }
18063
18208
  if (expression.kind !== "binary-expression") {
@@ -18074,20 +18219,20 @@ function collectFacts(context, expression, facts) {
18074
18219
  }
18075
18220
  if (expression.operator === "==") {
18076
18221
  const guard = guardFact(expression.left, expression.right) ?? guardFact(expression.right, expression.left);
18077
- const missing = nilComparison(expression.left, expression.right);
18222
+ const absent = nilComparison(expression.left, expression.right);
18078
18223
  if (guard !== null) {
18079
18224
  facts.set(guard[0], guard[1]);
18080
- } else if (missing !== null) {
18081
- facts.set(missing, NIL_TYPE);
18225
+ } else if (absent !== null) {
18226
+ missing(absent, facts);
18082
18227
  } else {
18083
18228
  applyDiscriminant(context, expression.left, expression.right, true, facts);
18084
18229
  }
18085
18230
  return;
18086
18231
  }
18087
18232
  if (expression.operator === "~=") {
18088
- const name = nilComparison(expression.left, expression.right);
18089
- if (name !== null) {
18090
- present2(context, name, facts);
18233
+ const tested = nilComparison(expression.left, expression.right);
18234
+ if (tested !== null) {
18235
+ present2(context, tested, facts);
18091
18236
  } else {
18092
18237
  applyDiscriminant(context, expression.left, expression.right, false, facts);
18093
18238
  }
@@ -18121,15 +18266,15 @@ function guardFacts(context, statement) {
18121
18266
  function negatedFacts(context, condition) {
18122
18267
  const facts = /* @__PURE__ */ new Map();
18123
18268
  if (condition.kind === "binary-expression" && condition.operator === "or") {
18124
- for (const [name, type] of [...negatedFacts(context, condition.left), ...negatedFacts(context, condition.right)]) {
18125
- facts.set(name, type);
18269
+ for (const [path, type] of [...negatedFacts(context, condition.left), ...negatedFacts(context, condition.right)]) {
18270
+ facts.set(path, type);
18126
18271
  }
18127
18272
  return facts;
18128
18273
  }
18129
18274
  if (condition.kind === "binary-expression" && condition.operator === "==") {
18130
- const name = nilComparison(condition.left, condition.right);
18131
- if (name !== null) {
18132
- present2(context, name, facts);
18275
+ const tested = nilComparison(condition.left, condition.right);
18276
+ if (tested !== null) {
18277
+ present2(context, tested, facts);
18133
18278
  } else {
18134
18279
  applyDiscriminant(context, condition.left, condition.right, false, facts);
18135
18280
  }
@@ -18137,8 +18282,8 @@ function negatedFacts(context, condition) {
18137
18282
  if (condition.kind === "binary-expression" && condition.operator === "~=") {
18138
18283
  applyDiscriminant(context, condition.left, condition.right, true, facts);
18139
18284
  }
18140
- if (condition.kind === "unary-expression" && condition.operator === "not" && condition.operand.kind === "identifier") {
18141
- present2(context, condition.operand.name, facts);
18285
+ if (condition.kind === "unary-expression" && condition.operator === "not") {
18286
+ present2(context, condition.operand, facts);
18142
18287
  }
18143
18288
  return facts;
18144
18289
  }
@@ -18174,6 +18319,7 @@ function checkNumericFor(context, statement) {
18174
18319
  context.report("check-invalid-operand", `Numeric "for" expects "number" but received "${typeToString(type)}".`, bound.position);
18175
18320
  }
18176
18321
  }
18322
+ forgetAssignedPaths(context, statement.body);
18177
18323
  context.binder.pushScope();
18178
18324
  context.binder.declare({ name: statement.variable.name, type: NUMBER_TYPE, isLocal: true, position: statement.variable.position, origin: "local" });
18179
18325
  checkStatements(context, statement.body);
@@ -18197,6 +18343,7 @@ function iteratedTypes(context, iterators) {
18197
18343
  function checkGenericFor(context, statement) {
18198
18344
  statement.iterators.forEach((iterator) => checkExpression(context, iterator));
18199
18345
  const iterated = iteratedTypes(context, statement.iterators);
18346
+ forgetAssignedPaths(context, statement.body);
18200
18347
  context.binder.pushScope();
18201
18348
  statement.variables.forEach((variable, index) => {
18202
18349
  const inferred = iterated[index] ?? ANY_TYPE;
@@ -18269,6 +18416,7 @@ function buildFunctionType(context, parameters, returnAnnotation, contextual = n
18269
18416
  return createFunction(types, context.resolveAnnotation(returnAnnotation), minimumArguments(context, parameters), isVariadic);
18270
18417
  }
18271
18418
  function checkFunctionBody(context, parameters, returnAnnotation, body, signature, selfType) {
18419
+ forgetAssignedPaths(context, body);
18272
18420
  context.binder.pushScope();
18273
18421
  context.pushReturnType(returnAnnotation === null ? null : context.resolveAnnotation(returnAnnotation));
18274
18422
  if (selfType !== null) {
@@ -18327,6 +18475,10 @@ function checkCompoundOperand(context, operator, type, position2) {
18327
18475
  function checkAssignment(context, statement) {
18328
18476
  const valueTypes = checkValueList(context, statement.values);
18329
18477
  statement.targets.forEach((target, index) => {
18478
+ const assigned = assignedPath(target);
18479
+ if (assigned !== null) {
18480
+ context.forgetNarrowing(assigned);
18481
+ }
18330
18482
  const targetType = checkExpression(context, target);
18331
18483
  const valueType = valueTypes[index] ?? NIL_TYPE;
18332
18484
  const value = valueAt(statement.values, valueTypes, index);
@@ -18355,9 +18507,6 @@ function checkAssignment(context, statement) {
18355
18507
  if (value !== void 0) {
18356
18508
  context.expectAssignable(valueType, declared, value.position, "Assignment");
18357
18509
  }
18358
- if (target.kind === "identifier") {
18359
- context.forgetNarrowing(target.name);
18360
- }
18361
18510
  });
18362
18511
  }
18363
18512
  function checkFunctionDeclaration(context, statement) {
@@ -18441,8 +18590,10 @@ function checkStatement(context, statement) {
18441
18590
  return checkBlock(context, statement.body);
18442
18591
  case "while-statement":
18443
18592
  checkExpression(context, statement.condition);
18593
+ forgetAssignedPaths(context, statement.body);
18444
18594
  return checkBlock(context, statement.body);
18445
18595
  case "repeat-statement":
18596
+ forgetAssignedPaths(context, statement.body);
18446
18597
  checkBlock(context, statement.body);
18447
18598
  checkExpression(context, statement.condition);
18448
18599
  return;
@@ -18501,20 +18652,12 @@ function parseInterpolation(segment) {
18501
18652
  function collectInterpolations(segments) {
18502
18653
  return segments.filter((segment) => segment.kind === "interpolation").map(parseInterpolation);
18503
18654
  }
18504
- function collectTemplateRoots(segments) {
18505
- const roots = [];
18506
- for (const interpolation of collectInterpolations(segments)) {
18507
- if (interpolation.root.length > 0 && !roots.includes(interpolation.root)) {
18508
- roots.push(interpolation.root);
18509
- }
18510
- }
18511
- return roots;
18512
- }
18513
18655
  function templateLiteralText(segments) {
18514
18656
  return segments.map((segment) => segment.kind === "text" ? segment.value : `\${${segment.value}}`).join("");
18515
18657
  }
18516
18658
 
18517
18659
  // ../compiler/src/checker/expressions.ts
18660
+ var NAME_PATH_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
18518
18661
  var EXTENSION_RESULTS = {
18519
18662
  boolean: BOOLEAN_TYPE,
18520
18663
  number: NUMBER_TYPE,
@@ -18528,7 +18671,16 @@ function isNativeConstruction(context, expression) {
18528
18671
  }
18529
18672
  return nativeConstructor(context, object.name) !== null;
18530
18673
  }
18674
+ function narrowedMember(context, expression) {
18675
+ const path = pathOf2(expression);
18676
+ return path === null ? null : context.narrowedType(path);
18677
+ }
18531
18678
  function checkMember(context, expression) {
18679
+ const narrowed = narrowedMember(context, expression);
18680
+ if (narrowed !== null) {
18681
+ checkExpression(context, expression.object);
18682
+ return context.record(expression, narrowed);
18683
+ }
18532
18684
  if (expression.object.kind === "identifier" && isMtaClassReference(context, expression.object.name)) {
18533
18685
  context.references.add(expression.object.name);
18534
18686
  return context.record(expression, resolveMtaStaticMember(context, expression.object.name, expression.property, expression.position)?.type ?? ANY_TYPE);
@@ -18691,13 +18843,18 @@ function checkTable(context, expression) {
18691
18843
  }
18692
18844
  function checkTemplate(context, expression) {
18693
18845
  for (const interpolation of collectInterpolations(expression.segments)) {
18694
- if (interpolation.root.length === 0) {
18846
+ if (interpolation.path.length === 0) {
18695
18847
  const message = 'Template interpolation is empty. Write "${name}", "${object.field}", or "${name:fallback}".';
18696
18848
  context.report("check-empty-interpolation", message, interpolation.position);
18697
18849
  continue;
18698
18850
  }
18699
- if (context.binder.lookup(interpolation.root) === null) {
18700
- const message = `Template interpolation "${interpolation.path}" refers to "${interpolation.root}", which is not in scope.`;
18851
+ if (!NAME_PATH_PATTERN.test(interpolation.path)) {
18852
+ const message = `Template interpolation "${interpolation.path}" is not a name or a member path. Compute the value first, then interpolate the name.`;
18853
+ context.report("check-unknown-template-root", message, interpolation.position);
18854
+ continue;
18855
+ }
18856
+ if (interpolation.fallback === null && context.binder.lookup(interpolation.root) === null) {
18857
+ const message = `Template interpolation "${interpolation.path}" refers to "${interpolation.root}", which is not in scope. Write "\${${interpolation.path}:fallback}" to accept a value that may be missing.`;
18701
18858
  context.report("check-unknown-template-root", message, interpolation.position);
18702
18859
  }
18703
18860
  }
@@ -19238,16 +19395,86 @@ function withSymbol(state, symbol, action) {
19238
19395
  }
19239
19396
 
19240
19397
  // ../compiler/src/emitter/template.ts
19398
+ var SELF_PATH_PATTERN = /^self((?:\.[A-Za-z_][A-Za-z0-9_]*)+)$/;
19399
+ function flattenedName(path) {
19400
+ const fields = SELF_PATH_PATTERN.exec(path)?.[1] ?? null;
19401
+ if (fields === null) {
19402
+ return null;
19403
+ }
19404
+ const name = fields.slice(1).split(".").join("_");
19405
+ return LUA_KEYWORDS.has(name) ? null : name;
19406
+ }
19407
+ function collectFlattened(segments) {
19408
+ const interpolations = collectInterpolations(segments);
19409
+ const reserved = /* @__PURE__ */ new Set();
19410
+ const claims = /* @__PURE__ */ new Map();
19411
+ const conflicting = /* @__PURE__ */ new Set();
19412
+ for (const interpolation of interpolations) {
19413
+ const name = flattenedName(interpolation.path);
19414
+ if (name === null) {
19415
+ reserved.add(interpolation.root);
19416
+ continue;
19417
+ }
19418
+ const claimed = claims.get(name) ?? null;
19419
+ if (claimed !== null && claimed !== interpolation.path) {
19420
+ conflicting.add(name);
19421
+ continue;
19422
+ }
19423
+ claims.set(name, interpolation.path);
19424
+ }
19425
+ const flattened = /* @__PURE__ */ new Map();
19426
+ for (const interpolation of interpolations) {
19427
+ const name = flattenedName(interpolation.path);
19428
+ if (name !== null && !reserved.has(name) && !conflicting.has(name)) {
19429
+ flattened.set(interpolation.raw, name);
19430
+ }
19431
+ }
19432
+ return flattened;
19433
+ }
19434
+ function rewriteInterpolation(raw, name) {
19435
+ const separator = raw.indexOf(":");
19436
+ const key = separator === -1 ? raw : raw.slice(0, separator);
19437
+ const rest = separator === -1 ? "" : raw.slice(separator);
19438
+ const path = key.trim();
19439
+ const start = key.indexOf(path);
19440
+ return `${key.slice(0, start)}${name}${key.slice(start + path.length)}${rest}`;
19441
+ }
19442
+ function flattenedLiteral(segments, flattened) {
19443
+ if (flattened.size === 0) {
19444
+ return templateLiteralText(segments);
19445
+ }
19446
+ return segments.map((segment) => {
19447
+ if (segment.kind === "text") {
19448
+ return segment.value;
19449
+ }
19450
+ const name = flattened.get(segment.value) ?? null;
19451
+ return `\${${name === null ? segment.value : rewriteInterpolation(segment.value, name)}}`;
19452
+ }).join("");
19453
+ }
19454
+ function collectBindings(segments, flattened) {
19455
+ const bindings = [];
19456
+ const seen = /* @__PURE__ */ new Set();
19457
+ for (const interpolation of collectInterpolations(segments)) {
19458
+ const flatName = flattened.get(interpolation.raw) ?? null;
19459
+ const name = flatName ?? interpolation.root;
19460
+ if (name.length === 0 || seen.has(name)) {
19461
+ continue;
19462
+ }
19463
+ seen.add(name);
19464
+ bindings.push({ name, value: flatName === null ? name : interpolation.path });
19465
+ }
19466
+ return bindings;
19467
+ }
19241
19468
  function lowerTemplate(segments) {
19242
- const roots = collectTemplateRoots(segments);
19469
+ const flattened = collectFlattened(segments);
19243
19470
  const isInterpolated = segments.some((segment) => segment.kind === "interpolation");
19244
- return { literal: templateLiteralText(segments), roots, isInterpolated };
19471
+ return { literal: flattenedLiteral(segments, flattened), bindings: collectBindings(segments, flattened), isInterpolated };
19245
19472
  }
19246
- function templateContext(roots) {
19247
- if (roots.length === 0) {
19473
+ function templateContext(bindings) {
19474
+ if (bindings.length === 0) {
19248
19475
  return "{}";
19249
19476
  }
19250
- return `{ ${roots.map((root) => `${root} = ${root}`).join(", ")} }`;
19477
+ return `{ ${bindings.map((binding) => `${binding.name} = ${binding.value}`).join(", ")} }`;
19251
19478
  }
19252
19479
 
19253
19480
  // ../compiler/src/emitter/expressions.ts
@@ -19320,7 +19547,7 @@ function emitTemplate(state, segments) {
19320
19547
  return emitString(lowered.literal);
19321
19548
  }
19322
19549
  requireHelper(state, "string");
19323
- return `string.template(${emitString(lowered.literal)}, ${templateContext(lowered.roots)})`;
19550
+ return `string.template(${emitString(lowered.literal)}, ${templateContext(lowered.bindings)})`;
19324
19551
  }
19325
19552
  function emitBinary(state, expression, limit) {
19326
19553
  const precedence = binaryPrecedence(expression.operator);
@@ -19956,11 +20183,11 @@ function canonicalEdit(input, statement, span) {
19956
20183
  }
19957
20184
  const authored = source.slice(span.start, span.end);
19958
20185
  const replacement = reindent(trimmed, indentOf(source, span.start));
19959
- const missing = lineCount2(authored) - lineCount2(replacement);
19960
- if (missing < 0 && input.development) {
20186
+ const missing2 = lineCount2(authored) - lineCount2(replacement);
20187
+ if (missing2 < 0 && input.development) {
19961
20188
  return null;
19962
20189
  }
19963
- const padded = missing > 0 && source.slice(span.end).trim().length > 0 ? `${replacement}${"\n".repeat(missing)}` : replacement;
20190
+ const padded = missing2 > 0 && source.slice(span.end).trim().length > 0 ? `${replacement}${"\n".repeat(missing2)}` : replacement;
19964
20191
  return { start: span.start, end: span.end, replacement: padded, lines: emitted.lines };
19965
20192
  }
19966
20193
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thigasdevelopment/luam",
3
- "version": "0.18.2",
3
+ "version": "0.19.0",
4
4
  "description": "The Luam compiler for Multi Theft Auto resources.",
5
5
  "keywords": [
6
6
  "mta",