@neocompose/cli 0.17.0 → 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 (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/neo.mjs +3334 -267
  3. package/package.json +1 -1
package/dist/neo.mjs CHANGED
@@ -1502,7 +1502,7 @@ function vectorTypes() {
1502
1502
  members: structuredLeafFieldProperties(name)
1503
1503
  }));
1504
1504
  }
1505
- var NEOSCRIPT_KEYWORDS, NEOSCRIPT_INFERRED_LOCAL_KEYWORD, NEOSCRIPT_CONSTRUCTOR_KEYWORD, NEOSCRIPT_PRIMITIVE_TYPES, NEOSCRIPT_BUILTIN_TYPES, NEOSCRIPT_OPERATORS, STRING_TYPE, INT_TYPE, FLOAT_TYPE, BOOL_TYPE, IMAGE_REF_TYPE, FLOAT_OPTIONAL_TYPE, NEOSCRIPT_STRUCTURED_LEAF_FIELDS, NEOSCRIPT_IMAGE_REGISTRY_TYPE, NEOSCRIPT_AUDIO_CLIP_REGISTRY_TYPE, NEOSCRIPT_PENDING_SYMBOL_ID_PREFIX, REGISTRY_ENTRY_TYPES, NEOSCRIPT_GLOBALS, NEOSCRIPT_BUILTIN_TYPE_SYMBOLS;
1505
+ var NEOSCRIPT_KEYWORDS, NEOSCRIPT_INFERRED_LOCAL_KEYWORD, NEOSCRIPT_CONSTRUCTOR_KEYWORD, NEOSCRIPT_PRIMITIVE_TYPES, NEOSCRIPT_BUILTIN_TYPES, NEOSCRIPT_OPERATORS, NEOSCRIPT_STATEMENT_SNIPPETS, STRING_TYPE, INT_TYPE, FLOAT_TYPE, BOOL_TYPE, IMAGE_REF_TYPE, FLOAT_OPTIONAL_TYPE, NEOSCRIPT_STRUCTURED_LEAF_FIELDS, NEOSCRIPT_IMAGE_REGISTRY_TYPE, NEOSCRIPT_AUDIO_CLIP_REGISTRY_TYPE, NEOSCRIPT_PENDING_SYMBOL_ID_PREFIX, REGISTRY_ENTRY_TYPES, NEOSCRIPT_GLOBALS, NEOSCRIPT_BUILTIN_TYPE_SYMBOLS;
1506
1506
  var init_language_spec = __esm({
1507
1507
  "../packages/neoscript-language/src/language-spec.ts"() {
1508
1508
  "use strict";
@@ -1513,6 +1513,16 @@ var init_language_spec = __esm({
1513
1513
  "enum",
1514
1514
  "if",
1515
1515
  "else",
1516
+ "try",
1517
+ "catch",
1518
+ "for",
1519
+ "foreach",
1520
+ "in",
1521
+ "break",
1522
+ "continue",
1523
+ "switch",
1524
+ "case",
1525
+ "default",
1516
1526
  "extends",
1517
1527
  "interface",
1518
1528
  "native",
@@ -1595,6 +1605,58 @@ var init_language_spec = __esm({
1595
1605
  "<",
1596
1606
  ">"
1597
1607
  ];
1608
+ NEOSCRIPT_STATEMENT_SNIPPETS = [
1609
+ {
1610
+ label: "if",
1611
+ insertText: "if (${1:condition}) {\n ${0}\n}",
1612
+ detail: "Conditional statement"
1613
+ },
1614
+ {
1615
+ label: "try / catch",
1616
+ insertText: "try {\n ${1}\n}\ncatch (string ${2:message}) {\n ${0}\n}",
1617
+ detail: "Handle a string error"
1618
+ },
1619
+ {
1620
+ label: "for",
1621
+ insertText: "for (${1:int} ${2:i} = ${3:0}; ${2:i} < ${4:count}; ${2:i}++) {\n ${0}\n}",
1622
+ detail: "Counted loop"
1623
+ },
1624
+ {
1625
+ label: "foreach",
1626
+ insertText: "foreach (var ${1:item} in ${2:collection}) {\n ${0}\n}",
1627
+ detail: "Collection loop"
1628
+ },
1629
+ {
1630
+ label: "switch",
1631
+ insertText: "switch (${1:value}) {\n case ${2:constant}:\n ${3}\n break;\n default:\n ${0}\n break;\n}",
1632
+ detail: "Scalar or enum switch statement"
1633
+ },
1634
+ {
1635
+ label: "break",
1636
+ insertText: "break;",
1637
+ detail: "Exit the nearest loop or switch"
1638
+ },
1639
+ {
1640
+ label: "continue",
1641
+ insertText: "continue;",
1642
+ detail: "Advance the nearest loop"
1643
+ },
1644
+ {
1645
+ label: "if / else",
1646
+ insertText: "if (${1:condition}) {\n ${2}\n} else {\n ${0}\n}",
1647
+ detail: "Conditional statement with fallback"
1648
+ },
1649
+ {
1650
+ label: "return",
1651
+ insertText: "return ${0:value};",
1652
+ detail: "Return from the current script"
1653
+ },
1654
+ {
1655
+ label: "throw",
1656
+ insertText: 'throw "${0:message}";',
1657
+ detail: "Stop execution with an error"
1658
+ }
1659
+ ];
1598
1660
  STRING_TYPE = { kind: "primitive", name: "string" };
1599
1661
  INT_TYPE = { kind: "primitive", name: "int" };
1600
1662
  FLOAT_TYPE = { kind: "primitive", name: "float" };
@@ -2287,7 +2349,29 @@ function complete(snapshot, position) {
2287
2349
  const tokens = significantTokensBefore(snapshot.lexed.tokens, word.start);
2288
2350
  const tail = tokens[tokens.length - 1];
2289
2351
  let candidates;
2290
- if (tail && (tail.kind === "punctuation" && tail.text === "." || tail.kind === "operator" && tail.text === "?.")) {
2352
+ const catchFilterCandidates = catchFilterCompletionItems(
2353
+ snapshot,
2354
+ word.start,
2355
+ word
2356
+ );
2357
+ const catchClauseCandidates = catchClauseCompletionItems(
2358
+ snapshot,
2359
+ word.start,
2360
+ word
2361
+ );
2362
+ const switchCaseCandidates = switchCaseCompletionItems(
2363
+ snapshot,
2364
+ word.start,
2365
+ prefix,
2366
+ word
2367
+ );
2368
+ if (catchFilterCandidates) {
2369
+ candidates = catchFilterCandidates;
2370
+ } else if (catchClauseCandidates) {
2371
+ candidates = catchClauseCandidates;
2372
+ } else if (switchCaseCandidates) {
2373
+ candidates = switchCaseCandidates;
2374
+ } else if (tail && (tail.kind === "punctuation" && tail.text === "." || tail.kind === "operator" && tail.text === "?.")) {
2291
2375
  const receiverTokens = expressionTokensBefore(tokens, tokens.length - 1);
2292
2376
  const resolved = resolveChain(snapshot, receiverTokens, offset);
2293
2377
  candidates = completionItemsForResolution(snapshot, resolved, word);
@@ -2503,13 +2587,28 @@ function semanticTokens(snapshot) {
2503
2587
  }
2504
2588
  if (token.kind !== "identifier") continue;
2505
2589
  if (snapshot.parsed.locals.some(
2506
- (local) => local.inferred && local.contextualKeywordRange?.start.line === token.range.start.line && local.contextualKeywordRange.start.character === token.range.start.character
2590
+ (local) => local.contextualKeywordRange?.start.line === token.range.start.line && local.contextualKeywordRange.start.character === token.range.start.character
2507
2591
  ) || snapshot.parsed.calls.some(
2508
2592
  (call) => call.kind === "constructor" && call.contextualKeywordRange?.start.line === token.range.start.line && call.contextualKeywordRange.start.character === token.range.start.character
2509
2593
  )) {
2510
2594
  result.push({ range: token.range, type: "keyword", modifiers: [] });
2511
2595
  continue;
2512
2596
  }
2597
+ const declaredLocal = snapshot.parsed.locals.find(
2598
+ (local) => local.nameRange.start.line === token.range.start.line && local.nameRange.start.character === token.range.start.character
2599
+ );
2600
+ if (declaredLocal) {
2601
+ const symbol2 = localSymbol(declaredLocal, snapshot);
2602
+ result.push({
2603
+ range: token.range,
2604
+ type: semanticSymbolKind(symbol2),
2605
+ modifiers: [
2606
+ "declaration",
2607
+ ...symbol2.writable === false ? ["readonly"] : []
2608
+ ]
2609
+ });
2610
+ continue;
2611
+ }
2513
2612
  const reference2 = snapshot.parsed.references.find(
2514
2613
  (candidate) => candidate.start === token.start
2515
2614
  );
@@ -2572,6 +2671,19 @@ function isValidRenameIdentifier(name) {
2572
2671
  }
2573
2672
  function topLevelCompletionItems(snapshot, offset, word) {
2574
2673
  const items = [];
2674
+ if (insideCatchBody(snapshot, offset)) {
2675
+ items.push(
2676
+ completion(
2677
+ "throw;",
2678
+ "snippet",
2679
+ "throw;",
2680
+ "Rethrow the current catch message",
2681
+ word,
2682
+ snapshot,
2683
+ "snippet"
2684
+ )
2685
+ );
2686
+ }
2575
2687
  for (const keyword of NEOSCRIPT_KEYWORDS) {
2576
2688
  items.push(
2577
2689
  completion(
@@ -2584,6 +2696,19 @@ function topLevelCompletionItems(snapshot, offset, word) {
2584
2696
  )
2585
2697
  );
2586
2698
  }
2699
+ for (const snippet of NEOSCRIPT_STATEMENT_SNIPPETS) {
2700
+ items.push(
2701
+ completion(
2702
+ snippet.label,
2703
+ "snippet",
2704
+ snippet.insertText,
2705
+ snippet.detail,
2706
+ word,
2707
+ snapshot,
2708
+ "snippet"
2709
+ )
2710
+ );
2711
+ }
2587
2712
  items.push(
2588
2713
  completion(
2589
2714
  NEOSCRIPT_INFERRED_LOCAL_KEYWORD,
@@ -2638,6 +2763,135 @@ function topLevelCompletionItems(snapshot, offset, word) {
2638
2763
  }
2639
2764
  return items;
2640
2765
  }
2766
+ function catchFilterCompletionItems(snapshot, offset, word) {
2767
+ const prefix = snapshot.source.text.slice(0, offset);
2768
+ if (!/\bcatch\s*\(\s*string\s+[A-Za-z_][A-Za-z0-9_]*\s*\)\s*$/.test(prefix)) {
2769
+ return null;
2770
+ }
2771
+ return [
2772
+ completion(
2773
+ "when",
2774
+ "snippet",
2775
+ "when (${1:condition})",
2776
+ "Filter this catch clause",
2777
+ word,
2778
+ snapshot,
2779
+ "snippet"
2780
+ )
2781
+ ];
2782
+ }
2783
+ function catchClauseCompletionItems(snapshot, offset, word) {
2784
+ if (!catchClauseCanFollow(snapshot, offset)) return null;
2785
+ return [
2786
+ completion(
2787
+ "catch",
2788
+ "snippet",
2789
+ "catch (string ${1:message}) {\n ${0}\n}",
2790
+ "Handle another string error",
2791
+ word,
2792
+ snapshot,
2793
+ "snippet"
2794
+ )
2795
+ ];
2796
+ }
2797
+ function catchClauseCanFollow(snapshot, offset) {
2798
+ const tokens = significantTokensBefore(snapshot.lexed.tokens, offset);
2799
+ const closeIndex = tokens.length - 1;
2800
+ const close = tokens[closeIndex];
2801
+ if (close?.kind !== "punctuation" || close.text !== "}") return false;
2802
+ const closedCatch = catchEndingAt(snapshot, close.start);
2803
+ if (closedCatch && closedCatch.contextualKeywordRange === void 0) {
2804
+ return false;
2805
+ }
2806
+ return closesTryOrCatchChain(snapshot, tokens, closeIndex);
2807
+ }
2808
+ function closesTryOrCatchChain(snapshot, tokens, closeIndex) {
2809
+ const close = tokens[closeIndex];
2810
+ if (close?.kind !== "punctuation" || close.text !== "}") return false;
2811
+ const closedCatch = catchEndingAt(snapshot, close.start);
2812
+ if (closedCatch) {
2813
+ const catchKeywordStart = closedCatch.catchKeywordStart;
2814
+ if (catchKeywordStart === void 0) return false;
2815
+ const catchIndex = tokens.findIndex(
2816
+ (token) => token.start === catchKeywordStart
2817
+ );
2818
+ if (catchIndex <= 0) return false;
2819
+ const precedingClose = tokens[catchIndex - 1];
2820
+ if (precedingClose?.kind !== "punctuation" || precedingClose.text !== "}") {
2821
+ return false;
2822
+ }
2823
+ return closesTryOrCatchChain(snapshot, tokens, catchIndex - 1);
2824
+ }
2825
+ const openIndex = matchingOpenBraceIndex(tokens, closeIndex);
2826
+ if (openIndex === null) return false;
2827
+ const keyword = tokens[openIndex - 1];
2828
+ return keyword?.kind === "keyword" && keyword.text === "try";
2829
+ }
2830
+ function catchEndingAt(snapshot, closeStart) {
2831
+ return snapshot.parsed.locals.find(
2832
+ (local) => local.catchParameter && local.scopeEnd === closeStart
2833
+ );
2834
+ }
2835
+ function matchingOpenBraceIndex(tokens, closeIndex) {
2836
+ let depth = 0;
2837
+ for (let index = closeIndex; index >= 0; index--) {
2838
+ const token = tokens[index];
2839
+ if (token?.kind !== "punctuation") continue;
2840
+ if (token.text === "}") depth++;
2841
+ if (token.text !== "{") continue;
2842
+ depth--;
2843
+ if (depth === 0) return index;
2844
+ }
2845
+ return null;
2846
+ }
2847
+ function insideCatchBody(snapshot, offset) {
2848
+ let clause;
2849
+ for (const local of snapshot.parsed.locals) {
2850
+ if (!local.catchParameter || local.catchBodyStart === void 0 || local.catchBodyStart > offset || offset > local.scopeEnd) {
2851
+ continue;
2852
+ }
2853
+ if (clause?.catchBodyStart === void 0 || local.catchBodyStart > clause.catchBodyStart) {
2854
+ clause = local;
2855
+ }
2856
+ }
2857
+ if (clause?.catchBodyStart === void 0) return false;
2858
+ return !insideLambdaBlock(
2859
+ snapshot.lexed.tokens,
2860
+ clause.catchBodyStart,
2861
+ offset
2862
+ );
2863
+ }
2864
+ function insideLambdaBlock(tokens, catchBodyStart, offset) {
2865
+ const significant = tokens.filter(
2866
+ (token) => token.kind !== "comment" && token.kind !== "eof"
2867
+ );
2868
+ for (let index = 0; index < significant.length; index++) {
2869
+ const arrow = significant[index];
2870
+ if (arrow?.kind !== "operator" || arrow.text !== "=>" || arrow.start < catchBodyStart || arrow.start >= offset) {
2871
+ continue;
2872
+ }
2873
+ const open = significant[index + 1];
2874
+ if (open?.kind !== "punctuation" || open.text !== "{") continue;
2875
+ const closeIndex = matchingCloseBraceIndex(significant, index + 1);
2876
+ const close = closeIndex === null ? void 0 : significant[closeIndex];
2877
+ if (open.end <= offset && (close === void 0 || offset <= close.start)) {
2878
+ return true;
2879
+ }
2880
+ }
2881
+ return false;
2882
+ }
2883
+ function matchingCloseBraceIndex(tokens, openIndex) {
2884
+ let depth = 0;
2885
+ for (let index = openIndex; index < tokens.length; index++) {
2886
+ const token = tokens[index];
2887
+ if (token?.kind !== "punctuation") continue;
2888
+ if (token.text === "{") depth++;
2889
+ if (token.text !== "}") continue;
2890
+ depth--;
2891
+ if (depth === 0) return index;
2892
+ }
2893
+ return null;
2894
+ }
2641
2895
  function typeCompletionItems(snapshot, word) {
2642
2896
  const items = NEOSCRIPT_PRIMITIVE_TYPES.map(
2643
2897
  (name) => completion(name, "keyword", name, "Primitive type", word, snapshot)
@@ -2753,9 +3007,62 @@ function resolveReference(snapshot, reference2) {
2753
3007
  const dotIndex = tokenIndex - 1;
2754
3008
  const chainTokens = expressionTokensBefore(tokens, dotIndex);
2755
3009
  const receiver = resolveChain(snapshot, chainTokens, reference2.start);
2756
- if (!receiver) return null;
3010
+ if (!receiver) {
3011
+ return resolveContextualSwitchEnumReference(snapshot, reference2);
3012
+ }
2757
3013
  return resolveMember(snapshot, receiver, reference2.name);
2758
3014
  }
3015
+ function switchCaseCompletionItems(snapshot, offset, prefix, word) {
3016
+ const context = switchCaseEnumContext(snapshot, offset);
3017
+ if (!context) return null;
3018
+ const preceding = snapshot.source.text[word.start - 1];
3019
+ const authoredDot = context.label.expressionTokens.some(
3020
+ (token) => token.kind === "punctuation" && token.text === "."
3021
+ );
3022
+ if (prefix.length > 0 && preceding !== "." && !authoredDot) return null;
3023
+ const needsContextualDot = !authoredDot && preceding !== ".";
3024
+ return context.type.members.filter((member) => member.kind === "enumMember").map((member) => {
3025
+ const item = symbolCompletion(member, word, snapshot);
3026
+ if (!needsContextualDot) return item;
3027
+ const insertText = `.${member.name}`;
3028
+ return {
3029
+ ...item,
3030
+ label: insertText,
3031
+ insertText,
3032
+ textEdit: {
3033
+ range: snapshot.source.range(word.start, word.end),
3034
+ newText: insertText
3035
+ }
3036
+ };
3037
+ });
3038
+ }
3039
+ function resolveContextualSwitchEnumReference(snapshot, reference2) {
3040
+ const context = switchCaseEnumContext(snapshot, reference2.start);
3041
+ if (!context) return null;
3042
+ const symbol = context.type.members.find(
3043
+ (member) => member.kind === "enumMember" && member.name === reference2.name
3044
+ );
3045
+ return symbol ? { type: symbol.type, symbol } : null;
3046
+ }
3047
+ function switchCaseEnumContext(snapshot, offset) {
3048
+ for (const statement of snapshot.parsed.switches) {
3049
+ for (const section of statement.sections) {
3050
+ for (const label of section.labels) {
3051
+ if (label.kind !== "case") continue;
3052
+ if (offset < label.start || offset > label.end) continue;
3053
+ const selectorType = inferExpressionType(
3054
+ statement.selectorTokens,
3055
+ snapshot,
3056
+ statement.start
3057
+ );
3058
+ if (selectorType.kind !== "named") return null;
3059
+ const type = snapshot.project.typeById.get(selectorType.typeId);
3060
+ return type?.kind === "enum" ? { type, label } : null;
3061
+ }
3062
+ }
3063
+ }
3064
+ return null;
3065
+ }
2759
3066
  function resolveChain(snapshot, tokens, offset) {
2760
3067
  if (tokens.length === 0) return null;
2761
3068
  const constructorType = tokens[0]?.kind === "identifier" && tokens[0].text === NEOSCRIPT_CONSTRUCTOR_KEYWORD && tokens[1]?.kind === "identifier" ? snapshot.project.typeByName.get(tokens[1].text) : void 0;
@@ -3058,11 +3365,17 @@ function resolveDeclaredType(local, snapshot) {
3058
3365
  function inferLocalType(local, snapshot) {
3059
3366
  const tokens = local.initializerTokens ?? [];
3060
3367
  if (tokens.length === 0) return UNKNOWN_TYPE;
3061
- return inferExpressionType(
3368
+ const inferred = inferExpressionType(
3062
3369
  tokens,
3063
3370
  snapshot,
3064
3371
  tokens[0]?.start ?? snapshot.source.offsetAt(local.nameRange.end)
3065
3372
  );
3373
+ if (!local.collectionIteration) return inferred;
3374
+ if (inferred.kind === "dictionary") return inferred.valueType;
3375
+ if (inferred.kind === "list" || inferred.kind === "set") {
3376
+ return inferred.elementType;
3377
+ }
3378
+ return UNKNOWN_TYPE;
3066
3379
  }
3067
3380
  function inferExpressionType(rawTokens, snapshot, offset) {
3068
3381
  const tokens = unwrapExpressionTokens(rawTokens);
@@ -3467,7 +3780,8 @@ function localSymbol(local, snapshot) {
3467
3780
  name: local.name,
3468
3781
  kind: local.lambdaParameter ? "parameter" : "local",
3469
3782
  type: local.lambdaParameter ? inferLambdaParameterType(local, snapshot) : resolveDeclaredType(local, snapshot),
3470
- location: { uri: snapshot.uri, range: local.nameRange }
3783
+ location: { uri: snapshot.uri, range: local.nameRange },
3784
+ ...local.collectionIteration || local.catchParameter ? { writable: false } : {}
3471
3785
  },
3472
3786
  local.scopeStart,
3473
3787
  local.scopeEnd
@@ -4217,14 +4531,20 @@ function parseNeoScript(lexed, kind) {
4217
4531
  const locals = [];
4218
4532
  const references = [];
4219
4533
  const calls = [];
4534
+ const switches = [];
4220
4535
  for (const executable of executableRegions) {
4221
4536
  const executablePairs = pairBrackets(executable);
4222
- const regionLocals = parseLocals(
4537
+ const regionSwitches = parseSwitchStatements(
4223
4538
  executable,
4224
4539
  executablePairs,
4225
4540
  lexed,
4226
4541
  diagnostics
4227
4542
  );
4543
+ const regionLocals = applySwitchSectionScopes(
4544
+ parseLocals(executable, executablePairs, lexed, diagnostics),
4545
+ regionSwitches,
4546
+ lexed
4547
+ );
4228
4548
  const regionCalls = parseCalls(executable, executablePairs, lexed);
4229
4549
  const declarations = new Set(
4230
4550
  regionLocals.map(
@@ -4246,9 +4566,10 @@ function parseNeoScript(lexed, kind) {
4246
4566
  ...parseReferences(executable, declarations, contextualKeywords)
4247
4567
  );
4248
4568
  calls.push(...regionCalls);
4569
+ switches.push(...regionSwitches);
4249
4570
  validateTerminatedStatements(executable, diagnostics);
4250
4571
  }
4251
- return { units, locals, references, calls, diagnostics };
4572
+ return { units, locals, references, calls, switches, diagnostics };
4252
4573
  }
4253
4574
  function parseUnits(tokens, pairs, kind, lexed) {
4254
4575
  if (kind !== "property") {
@@ -4413,6 +4734,158 @@ function pairBrackets(tokens) {
4413
4734
  }
4414
4735
  return pairs;
4415
4736
  }
4737
+ function parseSwitchStatements(tokens, pairs, lexed, diagnostics) {
4738
+ const switches = [];
4739
+ for (let index = 0; index < tokens.length; index++) {
4740
+ const keyword = tokens[index];
4741
+ const openParen = tokens[index + 1];
4742
+ if (keyword?.kind !== "keyword" || keyword.text !== "switch") continue;
4743
+ if (!openParen || openParen.kind !== "punctuation" || openParen.text !== "(") {
4744
+ continue;
4745
+ }
4746
+ const closeParenIndex = pairs.get(index + 1);
4747
+ if (closeParenIndex === void 0) continue;
4748
+ const openBraceIndex = closeParenIndex + 1;
4749
+ const openBrace = tokens[openBraceIndex];
4750
+ if (!openBrace || openBrace.kind !== "punctuation" || openBrace.text !== "{") {
4751
+ continue;
4752
+ }
4753
+ const closeBraceIndex = pairs.get(openBraceIndex);
4754
+ const limit = closeBraceIndex ?? tokens.length;
4755
+ const closeBrace = closeBraceIndex === void 0 ? void 0 : tokens[closeBraceIndex];
4756
+ const selectorTokens = tokens.slice(index + 2, closeParenIndex);
4757
+ const selectorStart = selectorTokens[0]?.start ?? openParen.end;
4758
+ const selectorEnd = selectorTokens.at(-1)?.end ?? selectorStart;
4759
+ const sections = [];
4760
+ let cursor = openBraceIndex + 1;
4761
+ while (cursor < limit) {
4762
+ const firstLabelIndex = findNextSwitchLabel(tokens, cursor, limit, pairs);
4763
+ if (firstLabelIndex === null) break;
4764
+ cursor = firstLabelIndex;
4765
+ const labels = [];
4766
+ while (cursor < limit) {
4767
+ const labelKeyword = tokens[cursor];
4768
+ if (labelKeyword?.kind !== "keyword" || labelKeyword.text !== "case" && labelKeyword.text !== "default") {
4769
+ break;
4770
+ }
4771
+ const nextLabelIndex = findNextSwitchLabel(
4772
+ tokens,
4773
+ cursor + 1,
4774
+ limit,
4775
+ pairs
4776
+ );
4777
+ const labelLimit = nextLabelIndex ?? limit;
4778
+ const colonIndex = findSwitchLabelColon(
4779
+ tokens,
4780
+ cursor + 1,
4781
+ labelLimit,
4782
+ pairs
4783
+ );
4784
+ const colon = colonIndex === null ? void 0 : tokens[colonIndex];
4785
+ const expressionEndIndex = colonIndex ?? labelLimit;
4786
+ const expressionTokens = labelKeyword.text === "case" ? tokens.slice(cursor + 1, expressionEndIndex) : [];
4787
+ if (!colon) {
4788
+ diagnostics.push({
4789
+ range: labelKeyword.range,
4790
+ severity: "error",
4791
+ source: "neoscript",
4792
+ code: "missing-switch-label-colon",
4793
+ message: `The \`${labelKeyword.text}\` label must end with \`:\`.`,
4794
+ suggestions: ["Add `:` after the switch label."]
4795
+ });
4796
+ }
4797
+ const labelEnd = colon?.end ?? tokens[labelLimit]?.start ?? closeBrace?.start ?? lexed.source.text.length;
4798
+ labels.push({
4799
+ kind: labelKeyword.text,
4800
+ keywordRange: labelKeyword.range,
4801
+ expressionTokens,
4802
+ start: labelKeyword.start,
4803
+ end: labelEnd,
4804
+ colonEnd: colon?.end ?? labelEnd
4805
+ });
4806
+ cursor = colonIndex === null ? labelLimit : colonIndex + 1;
4807
+ if (labelKeyword.text === "default") break;
4808
+ const adjacent = tokens[cursor];
4809
+ if (adjacent?.kind !== "keyword" || adjacent.text !== "case") break;
4810
+ }
4811
+ const lastLabel = labels.at(-1);
4812
+ if (!lastLabel) {
4813
+ cursor++;
4814
+ continue;
4815
+ }
4816
+ const nextSectionIndex = findNextSwitchLabel(
4817
+ tokens,
4818
+ cursor,
4819
+ limit,
4820
+ pairs
4821
+ );
4822
+ sections.push({
4823
+ labels,
4824
+ bodyStart: lastLabel.colonEnd,
4825
+ bodyEnd: nextSectionIndex === null ? closeBrace?.start ?? lexed.source.text.length : tokens[nextSectionIndex]?.start ?? lexed.source.text.length
4826
+ });
4827
+ cursor = nextSectionIndex ?? limit;
4828
+ }
4829
+ switches.push({
4830
+ selectorTokens,
4831
+ selectorRange: lexed.source.range(selectorStart, selectorEnd),
4832
+ sections,
4833
+ start: keyword.start,
4834
+ end: closeBrace?.end ?? lexed.source.text.length
4835
+ });
4836
+ }
4837
+ return switches;
4838
+ }
4839
+ function findNextSwitchLabel(tokens, start, limit, pairs) {
4840
+ let cursor = start;
4841
+ while (cursor < limit) {
4842
+ const token = tokens[cursor];
4843
+ if (!token) return null;
4844
+ if (token.kind === "keyword" && (token.text === "case" || token.text === "default")) {
4845
+ return cursor;
4846
+ }
4847
+ if (token.kind === "punctuation" && (token.text === "(" || token.text === "[" || token.text === "{")) {
4848
+ const close = pairs.get(cursor);
4849
+ if (close !== void 0 && close < limit) {
4850
+ cursor = close + 1;
4851
+ continue;
4852
+ }
4853
+ }
4854
+ cursor++;
4855
+ }
4856
+ return null;
4857
+ }
4858
+ function findSwitchLabelColon(tokens, start, limit, pairs) {
4859
+ let cursor = start;
4860
+ while (cursor < limit) {
4861
+ const token = tokens[cursor];
4862
+ if (!token) return null;
4863
+ if (token.kind === "punctuation" && token.text === ":") return cursor;
4864
+ if (token.kind === "punctuation" && (token.text === "(" || token.text === "[" || token.text === "{")) {
4865
+ const close = pairs.get(cursor);
4866
+ if (close !== void 0 && close < limit) {
4867
+ cursor = close + 1;
4868
+ continue;
4869
+ }
4870
+ }
4871
+ cursor++;
4872
+ }
4873
+ return null;
4874
+ }
4875
+ function applySwitchSectionScopes(locals, switches, lexed) {
4876
+ return locals.map((local) => {
4877
+ const declaration = lexed.source.offsetAt(local.nameRange.start);
4878
+ let scopeEnd = local.scopeEnd;
4879
+ for (const statement of switches) {
4880
+ for (const section of statement.sections) {
4881
+ if (declaration >= section.bodyStart && declaration < section.bodyEnd) {
4882
+ scopeEnd = Math.min(scopeEnd, section.bodyEnd);
4883
+ }
4884
+ }
4885
+ }
4886
+ return scopeEnd === local.scopeEnd ? local : { ...local, scopeEnd };
4887
+ });
4888
+ }
4416
4889
  function validatePropertyUnits(units, diagnostics, lexed) {
4417
4890
  const getters = units.filter((unit) => unit.kind === "getter");
4418
4891
  const setters = units.filter((unit) => unit.kind === "setter");
@@ -4450,6 +4923,18 @@ function duplicateUnitDiagnostic(label, units, diagnostics) {
4450
4923
  function parseLocals(tokens, pairs, lexed, diagnostics) {
4451
4924
  const locals = [];
4452
4925
  for (let index = 0; index < tokens.length; index++) {
4926
+ const caught = tryParseCatchParameter(tokens, index, pairs, lexed);
4927
+ if (caught) {
4928
+ locals.push(caught.local);
4929
+ index = caught.resumeTokenIndex;
4930
+ continue;
4931
+ }
4932
+ const loop = tryParseLoopLocal(tokens, index, pairs, lexed, diagnostics);
4933
+ if (loop) {
4934
+ locals.push(loop.local);
4935
+ index = loop.resumeTokenIndex;
4936
+ continue;
4937
+ }
4453
4938
  const lambda = tryParseLambdaParameters(tokens, index, pairs, lexed);
4454
4939
  if (lambda) {
4455
4940
  locals.push(...lambda.locals);
@@ -4711,13 +5196,17 @@ function validateTerminatedStatements(tokens, diagnostics) {
4711
5196
  for (let index = 0; index < tokens.length; index++) {
4712
5197
  const token = tokens[index];
4713
5198
  if (!token || token.kind !== "keyword") continue;
4714
- if (token.text !== "return" && token.text !== "throw") continue;
5199
+ if (token.text !== "return" && token.text !== "throw" && token.text !== "break" && token.text !== "continue")
5200
+ continue;
4715
5201
  let cursor = index + 1;
4716
5202
  let nested = 0;
4717
5203
  let terminated = false;
4718
5204
  while (cursor < tokens.length) {
4719
5205
  const current = tokens[cursor];
4720
5206
  if (!current) break;
5207
+ if (nested === 0 && current.kind === "keyword" && (current.text === "case" || current.text === "default")) {
5208
+ break;
5209
+ }
4721
5210
  if (current.kind === "punctuation" && ["(", "[", "{"].includes(current.text))
4722
5211
  nested++;
4723
5212
  if (current.kind === "punctuation" && current.text === "}") {
@@ -4778,6 +5267,126 @@ function containingBraceRange(tokens, tokenIndex, documentEnd) {
4778
5267
  }
4779
5268
  return { start, end };
4780
5269
  }
5270
+ function tryParseCatchParameter(tokens, start, pairs, lexed) {
5271
+ const keyword = tokens[start];
5272
+ const open = tokens[start + 1];
5273
+ if (keyword?.kind !== "keyword" || keyword.text !== "catch" || open?.kind !== "punctuation" || open.text !== "(") {
5274
+ return null;
5275
+ }
5276
+ const closeIndex = pairs.get(start + 1);
5277
+ if (closeIndex === void 0) return null;
5278
+ const type = tokens[start + 2];
5279
+ const name = tokens[start + 3];
5280
+ if (closeIndex !== start + 4 || type?.kind !== "type" || type.text !== "string" || name?.kind !== "identifier") {
5281
+ return null;
5282
+ }
5283
+ const possibleWhen = tokens[closeIndex + 1];
5284
+ const when = possibleWhen?.kind === "identifier" && possibleWhen.text === "when" ? possibleWhen : void 0;
5285
+ let bodyIndex = closeIndex + 1;
5286
+ if (when) {
5287
+ const filterOpen = tokens[closeIndex + 2];
5288
+ if (filterOpen?.kind === "punctuation" && filterOpen.text === "(") {
5289
+ const filterCloseIndex = pairs.get(closeIndex + 2);
5290
+ bodyIndex = filterCloseIndex === void 0 ? tokens.length : filterCloseIndex + 1;
5291
+ } else {
5292
+ bodyIndex = closeIndex + 2;
5293
+ }
5294
+ }
5295
+ const scopeEnd = loopBodyScopeEnd(
5296
+ tokens,
5297
+ bodyIndex,
5298
+ pairs,
5299
+ lexed.source.text.length
5300
+ );
5301
+ const body = tokens[bodyIndex];
5302
+ const catchBodyStart = body?.kind === "punctuation" && body.text === "{" ? body.end : body?.start;
5303
+ return {
5304
+ local: {
5305
+ name: name.text,
5306
+ nameRange: name.range,
5307
+ typeTokens: [type],
5308
+ inferred: false,
5309
+ ...when ? { contextualKeywordRange: when.range } : {},
5310
+ catchParameter: true,
5311
+ catchKeywordStart: keyword.start,
5312
+ ...catchBodyStart === void 0 ? {} : { catchBodyStart },
5313
+ declarationRange: lexed.source.range(type.start, name.end),
5314
+ scopeStart: name.start,
5315
+ scopeEnd,
5316
+ lambdaParameter: false
5317
+ },
5318
+ resumeTokenIndex: closeIndex
5319
+ };
5320
+ }
5321
+ function tryParseLoopLocal(tokens, start, pairs, lexed, diagnostics) {
5322
+ const keyword = tokens[start];
5323
+ if (!keyword || keyword.kind !== "keyword" || keyword.text !== "for" && keyword.text !== "foreach") {
5324
+ return null;
5325
+ }
5326
+ const open = tokens[start + 1];
5327
+ if (!open || open.kind !== "punctuation" || open.text !== "(") return null;
5328
+ const closeIndex = pairs.get(start + 1);
5329
+ if (closeIndex === void 0) return null;
5330
+ const scopeEnd = loopBodyScopeEnd(
5331
+ tokens,
5332
+ closeIndex + 1,
5333
+ pairs,
5334
+ lexed.source.text.length
5335
+ );
5336
+ if (keyword.text === "for") {
5337
+ const declaration = tryParseVariableDeclaration(tokens, start + 2, lexed);
5338
+ if (!declaration || declaration.endTokenIndex >= closeIndex) return null;
5339
+ if (declaration.missingInitializer) {
5340
+ diagnostics.push({
5341
+ range: declaration.local.nameRange,
5342
+ severity: "error",
5343
+ source: "neoscript",
5344
+ code: "missing-var-initializer",
5345
+ message: `Cannot infer the type of \`${declaration.local.name}\` without an initializer; use \`var ${declaration.local.name} = ...;\` or an explicit type.`
5346
+ });
5347
+ }
5348
+ return {
5349
+ local: { ...declaration.local, scopeEnd },
5350
+ resumeTokenIndex: declaration.endTokenIndex
5351
+ };
5352
+ }
5353
+ const header = tokens.slice(start + 2, closeIndex);
5354
+ const inOffset = header.findIndex(
5355
+ (token) => token.kind === "keyword" && token.text === "in"
5356
+ );
5357
+ if (inOffset < 2) return null;
5358
+ const inIndex = start + 2 + inOffset;
5359
+ const name = tokens[inIndex - 1];
5360
+ const first = tokens[start + 2];
5361
+ if (!name || name.kind !== "identifier" || !first) return null;
5362
+ const inferred = first.kind === "identifier" && first.text === NEOSCRIPT_INFERRED_LOCAL_KEYWORD;
5363
+ return {
5364
+ local: {
5365
+ name: name.text,
5366
+ nameRange: name.range,
5367
+ typeTokens: inferred ? [] : tokens.slice(start + 2, inIndex - 1),
5368
+ inferred,
5369
+ ...inferred ? { contextualKeywordRange: first.range } : {},
5370
+ initializerTokens: tokens.slice(inIndex + 1, closeIndex),
5371
+ collectionIteration: true,
5372
+ declarationRange: lexed.source.range(first.start, name.end),
5373
+ scopeStart: tokens[closeIndex]?.end ?? name.end,
5374
+ scopeEnd,
5375
+ lambdaParameter: false
5376
+ },
5377
+ resumeTokenIndex: inIndex - 1
5378
+ };
5379
+ }
5380
+ function loopBodyScopeEnd(tokens, bodyStart, pairs, documentEnd) {
5381
+ const first = tokens[bodyStart];
5382
+ if (!first) return documentEnd;
5383
+ if (first.kind === "punctuation" && first.text === "{") {
5384
+ const closeIndex = pairs.get(bodyStart);
5385
+ return closeIndex === void 0 ? documentEnd : tokens[closeIndex]?.start ?? documentEnd;
5386
+ }
5387
+ const endIndex = statementEndTokenIndex(tokens, bodyStart);
5388
+ return tokens[endIndex]?.end ?? documentEnd;
5389
+ }
4781
5390
  var OPEN_TO_CLOSE, CLOSE_TO_OPEN;
4782
5391
  var init_parser = __esm({
4783
5392
  "../packages/neoscript-language/src/parser.ts"() {
@@ -5285,6 +5894,16 @@ var init_strict_lexer = __esm({
5285
5894
  // statement keywords
5286
5895
  "if",
5287
5896
  "else",
5897
+ "for",
5898
+ "foreach",
5899
+ "in",
5900
+ "break",
5901
+ "continue",
5902
+ "switch",
5903
+ "case",
5904
+ "default",
5905
+ "try",
5906
+ "catch",
5288
5907
  "abstract",
5289
5908
  "async",
5290
5909
  "class",
@@ -5436,12 +6055,34 @@ var init_strict_parser = __esm({
5436
6055
  if (t.kind === "keyword" && t.text === "if") {
5437
6056
  return this.parseIf();
5438
6057
  }
6058
+ if (t.kind === "keyword" && t.text === "try") {
6059
+ return this.parseTry();
6060
+ }
6061
+ if (t.kind === "keyword" && t.text === "for") {
6062
+ return this.parseFor();
6063
+ }
6064
+ if (t.kind === "keyword" && t.text === "foreach") {
6065
+ return this.parseForEach();
6066
+ }
6067
+ if (t.kind === "keyword" && t.text === "switch") {
6068
+ return this.parseSwitch();
6069
+ }
5439
6070
  if (t.kind === "keyword" && t.text === "return") {
5440
6071
  return this.parseReturn();
5441
6072
  }
5442
6073
  if (t.kind === "keyword" && t.text === "throw") {
5443
6074
  return this.parseThrow();
5444
6075
  }
6076
+ if (t.kind === "keyword" && t.text === "break") {
6077
+ this.next();
6078
+ this.expect("punct", ";");
6079
+ return { kind: "break", pos: t.pos };
6080
+ }
6081
+ if (t.kind === "keyword" && t.text === "continue") {
6082
+ this.next();
6083
+ this.expect("punct", ";");
6084
+ return { kind: "continue", pos: t.pos };
6085
+ }
5445
6086
  if (this.looksLikeVarDecl()) {
5446
6087
  return this.parseVarDecl();
5447
6088
  }
@@ -5505,6 +6146,226 @@ var init_strict_parser = __esm({
5505
6146
  const body = this.parseStatementBody();
5506
6147
  return { cond, body, pos };
5507
6148
  }
6149
+ parseFor() {
6150
+ const token = this.expect("keyword", "for");
6151
+ this.expect("punct", "(");
6152
+ if (!this.looksLikeVarDecl()) {
6153
+ throw new CompileError(
6154
+ "NeoScript for loops require one initialized local declaration.",
6155
+ this.peek().pos
6156
+ );
6157
+ }
6158
+ const initializer = this.parseVarDecl(false);
6159
+ this.expectSingleForClause(";");
6160
+ if (this.peek().kind === "punct" && this.peek().text === ";") {
6161
+ throw new CompileError(
6162
+ "NeoScript for loops require a condition.",
6163
+ this.peek().pos
6164
+ );
6165
+ }
6166
+ const condition = this.parseExpr();
6167
+ this.expectSingleForClause(";");
6168
+ if (this.peek().kind === "punct" && this.peek().text === ")") {
6169
+ throw new CompileError(
6170
+ "NeoScript for loops require one iterator.",
6171
+ this.peek().pos
6172
+ );
6173
+ }
6174
+ const iterator = this.parseForIterator();
6175
+ this.expectSingleForClause(")");
6176
+ return {
6177
+ kind: "for",
6178
+ initializer,
6179
+ condition,
6180
+ iterator,
6181
+ body: this.parseStatementBody(),
6182
+ pos: token.pos
6183
+ };
6184
+ }
6185
+ parseForEach() {
6186
+ const token = this.expect("keyword", "foreach");
6187
+ this.expect("punct", "(");
6188
+ const inferred = this.peek().kind === "ident" && this.peek().text === NEOSCRIPT_INFERRED_LOCAL_KEYWORD;
6189
+ let type;
6190
+ if (inferred) {
6191
+ this.next();
6192
+ type = null;
6193
+ } else {
6194
+ type = this.parseType();
6195
+ }
6196
+ const name = this.expect("ident");
6197
+ this.expect("keyword", "in");
6198
+ const collection = this.parseExpr();
6199
+ this.expect("punct", ")");
6200
+ return {
6201
+ kind: "forEach",
6202
+ type,
6203
+ name: name.text,
6204
+ collection,
6205
+ body: this.parseStatementBody(),
6206
+ pos: token.pos
6207
+ };
6208
+ }
6209
+ parseTry() {
6210
+ const token = this.expect("keyword", "try");
6211
+ const body = this.parseBlock();
6212
+ const catches = [];
6213
+ while (this.peek().kind === "keyword" && this.peek().text === "catch") {
6214
+ catches.push(this.parseCatchClause());
6215
+ }
6216
+ if (catches.length === 0) {
6217
+ throw new CompileError(
6218
+ "`try` must be followed by at least one `catch` clause.",
6219
+ token.pos
6220
+ );
6221
+ }
6222
+ return { kind: "try", body, catches, pos: token.pos };
6223
+ }
6224
+ parseCatchClause() {
6225
+ const token = this.expect("keyword", "catch");
6226
+ const open = this.peek();
6227
+ if (open.kind !== "punct" || open.text !== "(") {
6228
+ throw new CompileError(
6229
+ "NeoScript catch parameters must have the form `catch (string name)`.",
6230
+ open.pos
6231
+ );
6232
+ }
6233
+ this.next();
6234
+ const type = this.peek();
6235
+ const name = this.peek(1);
6236
+ const close = this.peek(2);
6237
+ if (type.kind !== "keyword" || type.text !== "string" || name.kind !== "ident" || close.kind !== "punct" || close.text !== ")") {
6238
+ throw new CompileError(
6239
+ "NeoScript catch parameters must have the form `catch (string name)`.",
6240
+ type.pos
6241
+ );
6242
+ }
6243
+ this.next();
6244
+ this.next();
6245
+ this.next();
6246
+ let filter = null;
6247
+ if (this.peek().kind === "ident" && this.peek().text === "when") {
6248
+ this.next();
6249
+ this.expect("punct", "(");
6250
+ filter = this.parseExpr();
6251
+ this.expect("punct", ")");
6252
+ }
6253
+ return {
6254
+ name: name.text,
6255
+ filter,
6256
+ body: this.parseBlock(),
6257
+ pos: token.pos
6258
+ };
6259
+ }
6260
+ parseSwitch() {
6261
+ const token = this.expect("keyword", "switch");
6262
+ this.expect("punct", "(");
6263
+ const selector = this.parseExpr();
6264
+ this.expect("punct", ")");
6265
+ this.expect("punct", "{");
6266
+ const sections = [];
6267
+ while (!(this.peek().kind === "punct" && this.peek().text === "}")) {
6268
+ const sectionStart = this.peek();
6269
+ if (sectionStart.kind === "eof") {
6270
+ throw new CompileError(
6271
+ "Unexpected end of input \u2014 missing `}`?",
6272
+ sectionStart.pos
6273
+ );
6274
+ }
6275
+ if (sectionStart.kind !== "keyword" || sectionStart.text !== "case" && sectionStart.text !== "default") {
6276
+ throw new CompileError(
6277
+ "Expected `case` or `default` in switch statement.",
6278
+ sectionStart.pos
6279
+ );
6280
+ }
6281
+ if (sectionStart.text === "default") {
6282
+ this.next();
6283
+ this.expect("punct", ":");
6284
+ sections.push({
6285
+ labels: [{ kind: "default", pos: sectionStart.pos }],
6286
+ body: this.parseSwitchSectionBody(),
6287
+ pos: sectionStart.pos
6288
+ });
6289
+ continue;
6290
+ }
6291
+ const labels = [];
6292
+ while (this.peek().kind === "keyword" && this.peek().text === "case") {
6293
+ const caseToken = this.next();
6294
+ labels.push({
6295
+ kind: "case",
6296
+ expression: this.parseExpr(),
6297
+ pos: caseToken.pos
6298
+ });
6299
+ this.expect("punct", ":");
6300
+ }
6301
+ sections.push({
6302
+ labels,
6303
+ body: this.parseSwitchSectionBody(),
6304
+ pos: sectionStart.pos
6305
+ });
6306
+ }
6307
+ this.expect("punct", "}");
6308
+ return { kind: "switch", selector, sections, pos: token.pos };
6309
+ }
6310
+ parseSwitchSectionBody() {
6311
+ const body = [];
6312
+ while (true) {
6313
+ const token = this.peek();
6314
+ if (token.kind === "punct" && token.text === "}") return body;
6315
+ if (token.kind === "keyword" && (token.text === "case" || token.text === "default")) {
6316
+ return body;
6317
+ }
6318
+ body.push(this.parseStatement());
6319
+ }
6320
+ }
6321
+ parseForIterator() {
6322
+ const start = this.peek();
6323
+ if (isIncrementOp(start)) {
6324
+ const op = this.next();
6325
+ return {
6326
+ kind: "assign",
6327
+ target: this.parseExpr(),
6328
+ op: op.text,
6329
+ value: null,
6330
+ pos: start.pos
6331
+ };
6332
+ }
6333
+ const target = this.parseExpr();
6334
+ const operator = this.peek();
6335
+ if (isIncrementOp(operator)) {
6336
+ this.next();
6337
+ return {
6338
+ kind: "assign",
6339
+ target,
6340
+ op: operator.text,
6341
+ value: null,
6342
+ pos: start.pos
6343
+ };
6344
+ }
6345
+ if (!isAssignmentOp(operator)) {
6346
+ throw new CompileError(
6347
+ "NeoScript for loop iterator must be an assignment, increment, or decrement.",
6348
+ operator.pos
6349
+ );
6350
+ }
6351
+ this.next();
6352
+ return {
6353
+ kind: "assign",
6354
+ target,
6355
+ op: operator.text,
6356
+ value: this.parseExpr(),
6357
+ pos: start.pos
6358
+ };
6359
+ }
6360
+ expectSingleForClause(terminator) {
6361
+ if (this.peek().kind === "punct" && this.peek().text === ",") {
6362
+ throw new CompileError(
6363
+ "NeoScript for loops support one initializer and one iterator.",
6364
+ this.peek().pos
6365
+ );
6366
+ }
6367
+ this.expect("punct", terminator);
6368
+ }
5508
6369
  parseStatementBody() {
5509
6370
  if (this.peek().kind === "punct" && this.peek().text === "{") {
5510
6371
  return this.parseBlock();
@@ -5528,11 +6389,11 @@ var init_strict_parser = __esm({
5528
6389
  }
5529
6390
  parseThrow() {
5530
6391
  const thrTok = this.expect("keyword", "throw");
5531
- const expr = this.parseExpr();
6392
+ const expr = this.peek().kind === "punct" && this.peek().text === ";" ? null : this.parseExpr();
5532
6393
  this.expect("punct", ";");
5533
6394
  return { kind: "throw", expr, pos: thrTok.pos };
5534
6395
  }
5535
- parseVarDecl() {
6396
+ parseVarDecl(terminated = true) {
5536
6397
  const startPos = this.peek().pos;
5537
6398
  const inferred = this.peek().kind === "ident" && this.peek().text === NEOSCRIPT_INFERRED_LOCAL_KEYWORD;
5538
6399
  let type;
@@ -5550,7 +6411,7 @@ var init_strict_parser = __esm({
5550
6411
  );
5551
6412
  }
5552
6413
  const init = this.parseExpr();
5553
- this.expect("punct", ";");
6414
+ if (terminated) this.expect("punct", ";");
5554
6415
  return {
5555
6416
  kind: "varDecl",
5556
6417
  type,
@@ -5925,7 +6786,7 @@ var init_strict_parser = __esm({
5925
6786
  }
5926
6787
  while (true) {
5927
6788
  let name = null;
5928
- if (this.peek().kind === "ident" && this.peek(1).kind === "punct" && this.peek(1).text === ":") {
6789
+ if ((this.peek().kind === "ident" || this.peek().kind === "keyword" && this.peek().text === "default") && this.peek(1).kind === "punct" && this.peek(1).text === ":") {
5929
6790
  name = this.next().text;
5930
6791
  this.next();
5931
6792
  }
@@ -6369,7 +7230,7 @@ var NEOSCRIPT_COMPILER_REVISION;
6369
7230
  var init_strict_ir = __esm({
6370
7231
  "../packages/neoscript-language/src/strict-ir.ts"() {
6371
7232
  "use strict";
6372
- NEOSCRIPT_COMPILER_REVISION = 3;
7233
+ NEOSCRIPT_COMPILER_REVISION = 6;
6373
7234
  }
6374
7235
  });
6375
7236
 
@@ -6555,16 +7416,176 @@ function callMatchesDeclaredOverloadNames(declared, expression) {
6555
7416
  ) === callKey
6556
7417
  );
6557
7418
  }
6558
- function scopeVariable(name, value, sourceType, writability, ownership, writeRoot) {
7419
+ function scopeVariable(name, value, sourceType, writability, ownership, writeRoot, readonlyBinding) {
6559
7420
  return {
6560
7421
  name,
6561
7422
  type: sourceType ?? fromWireType(value.typeInfo),
6562
7423
  variableId: value.id,
6563
7424
  ...writability ? { writability } : {},
6564
7425
  ...ownership ? { ownership } : {},
6565
- ...writeRoot ? { writeRoot } : {}
7426
+ ...writeRoot ? { writeRoot } : {},
7427
+ ...readonlyBinding ? { readonlyBinding } : {}
6566
7428
  };
6567
7429
  }
7430
+ function invalidateLoopEntryNarrowingsAssignedBy(loopEntryScope, outerScope, statements) {
7431
+ for (const rootName of collectAssignedPathRoots(statements)) {
7432
+ const outerEntry = outerScope.lookup(rootName);
7433
+ if (outerEntry) {
7434
+ loopEntryScope.hideInheritedNarrowing(outerEntry);
7435
+ }
7436
+ }
7437
+ }
7438
+ function collectAssignedPathRoots(statements, roots = /* @__PURE__ */ new Set()) {
7439
+ for (const statement of statements) {
7440
+ if (statement.kind === "assign") {
7441
+ const assignedPath = canonicalPath(statement.target);
7442
+ if (assignedPath) roots.add(pathRoot(assignedPath));
7443
+ collectAssignedPathRootsInExpression(statement.target, roots);
7444
+ if (statement.value)
7445
+ collectAssignedPathRootsInExpression(statement.value, roots);
7446
+ continue;
7447
+ }
7448
+ if (statement.kind === "varDecl") {
7449
+ collectAssignedPathRootsInExpression(statement.init, roots);
7450
+ continue;
7451
+ }
7452
+ if (statement.kind === "if") {
7453
+ for (const branch of statement.branches) {
7454
+ collectAssignedPathRootsInExpression(branch.cond, roots);
7455
+ collectAssignedPathRoots(branch.body, roots);
7456
+ }
7457
+ if (statement.elseBody) {
7458
+ collectAssignedPathRoots(statement.elseBody, roots);
7459
+ }
7460
+ continue;
7461
+ }
7462
+ if (statement.kind === "try") {
7463
+ collectAssignedPathRoots(statement.body, roots);
7464
+ for (const clause of statement.catches) {
7465
+ if (clause.filter) {
7466
+ collectAssignedPathRootsInExpression(clause.filter, roots);
7467
+ }
7468
+ collectAssignedPathRoots(clause.body, roots);
7469
+ }
7470
+ continue;
7471
+ }
7472
+ if (statement.kind === "for") {
7473
+ collectAssignedPathRootsInExpression(statement.initializer.init, roots);
7474
+ collectAssignedPathRootsInExpression(statement.condition, roots);
7475
+ collectAssignedPathRoots([statement.iterator], roots);
7476
+ collectAssignedPathRoots(statement.body, roots);
7477
+ continue;
7478
+ }
7479
+ if (statement.kind === "forEach") {
7480
+ collectAssignedPathRootsInExpression(statement.collection, roots);
7481
+ collectAssignedPathRoots(statement.body, roots);
7482
+ continue;
7483
+ }
7484
+ if (statement.kind === "switch") {
7485
+ collectAssignedPathRootsInExpression(statement.selector, roots);
7486
+ for (const section of statement.sections) {
7487
+ for (const label of section.labels) {
7488
+ if (label.kind === "case") {
7489
+ collectAssignedPathRootsInExpression(label.expression, roots);
7490
+ }
7491
+ }
7492
+ collectAssignedPathRoots(section.body, roots);
7493
+ }
7494
+ continue;
7495
+ }
7496
+ if (statement.kind === "return" || statement.kind === "throw") {
7497
+ if (statement.expr)
7498
+ collectAssignedPathRootsInExpression(statement.expr, roots);
7499
+ continue;
7500
+ }
7501
+ if (statement.kind === "exprStmt") {
7502
+ collectAssignedPathRootsInExpression(statement.expr, roots);
7503
+ }
7504
+ }
7505
+ return roots;
7506
+ }
7507
+ function collectAssignedPathRootsInExpression(expression, roots) {
7508
+ switch (expression.kind) {
7509
+ case "litInterp":
7510
+ for (const part of expression.parts) {
7511
+ if (part.kind === "expr") {
7512
+ collectAssignedPathRootsInExpression(part.expr, roots);
7513
+ }
7514
+ }
7515
+ return;
7516
+ case "litList":
7517
+ for (const element of expression.elements) {
7518
+ collectAssignedPathRootsInExpression(element, roots);
7519
+ }
7520
+ return;
7521
+ case "litDict":
7522
+ for (const entry of expression.entries) {
7523
+ collectAssignedPathRootsInExpression(entry.key, roots);
7524
+ collectAssignedPathRootsInExpression(entry.value, roots);
7525
+ }
7526
+ return;
7527
+ case "member":
7528
+ collectAssignedPathRootsInExpression(expression.receiver, roots);
7529
+ return;
7530
+ case "index":
7531
+ collectAssignedPathRootsInExpression(expression.receiver, roots);
7532
+ collectAssignedPathRootsInExpression(expression.index, roots);
7533
+ return;
7534
+ case "call":
7535
+ collectAssignedPathRootsInExpression(expression.callee, roots);
7536
+ for (const argument2 of expression.args) {
7537
+ collectAssignedPathRootsInExpression(argument2, roots);
7538
+ }
7539
+ return;
7540
+ case "new":
7541
+ for (const argument2 of expression.args) {
7542
+ collectAssignedPathRootsInExpression(argument2, roots);
7543
+ }
7544
+ for (const initializer of expression.initializer ?? []) {
7545
+ collectAssignedPathRootsInExpression(initializer.value, roots);
7546
+ }
7547
+ return;
7548
+ case "annotated":
7549
+ for (const annotation2 of expression.annotations) {
7550
+ for (const argument2 of annotation2.args) {
7551
+ collectAssignedPathRootsInExpression(argument2, roots);
7552
+ }
7553
+ }
7554
+ collectAssignedPathRootsInExpression(expression.expression, roots);
7555
+ return;
7556
+ case "binary":
7557
+ case "coalesce":
7558
+ collectAssignedPathRootsInExpression(expression.left, roots);
7559
+ collectAssignedPathRootsInExpression(expression.right, roots);
7560
+ return;
7561
+ case "unary":
7562
+ case "force":
7563
+ collectAssignedPathRootsInExpression(expression.operand, roots);
7564
+ return;
7565
+ case "is":
7566
+ collectAssignedPathRootsInExpression(expression.operand, roots);
7567
+ return;
7568
+ case "lambda":
7569
+ collectAssignedPathRoots(expression.body, roots);
7570
+ return;
7571
+ case "litNull":
7572
+ case "litBool":
7573
+ case "litInt":
7574
+ case "litFloat":
7575
+ case "litString":
7576
+ case "litTripleString":
7577
+ case "contextualEnum":
7578
+ case "ident":
7579
+ return;
7580
+ }
7581
+ }
7582
+ function mergeBranchInvalidations(parent, branches) {
7583
+ for (const branch of branches) {
7584
+ for (const entry of branch.invalidatedRootEntries()) {
7585
+ if (parent.containsEntry(entry)) parent.invalidate(entry);
7586
+ }
7587
+ }
7588
+ }
6568
7589
  function mergeBranchOwnership(parent, branches) {
6569
7590
  if (branches.length === 0) return;
6570
7591
  const changed = /* @__PURE__ */ new Set();
@@ -7094,8 +8115,19 @@ function isAssignmentTarget(expression) {
7094
8115
  }
7095
8116
  function instructionsTerminate(instructions) {
7096
8117
  for (const instruction of instructions) {
7097
- if (instruction.type === "return" /* Return */ || instruction.type === "throw" /* Throw */)
8118
+ if (instruction.type === "return" /* Return */ || instruction.type === "throw" /* Throw */ || instruction.type === "break" /* Break */ || instruction.type === "continue" /* Continue */)
7098
8119
  return true;
8120
+ if (instruction.type === "try" /* Try */ && instructionsTerminate(instruction.instructions) && instruction.catches.every(
8121
+ (clause) => instructionsTerminate(clause.instructions)
8122
+ )) {
8123
+ return true;
8124
+ }
8125
+ if (instruction.type === "switch" /* Switch */) {
8126
+ if (instruction.defaultInstructions !== void 0 && instruction.defaultInstructions !== null && instruction.sections.every(
8127
+ (section) => instructionsPropagatePastSwitch(section.instructions)
8128
+ ) && instructionsPropagatePastSwitch(instruction.defaultInstructions))
8129
+ return true;
8130
+ }
7099
8131
  if (instruction.type === "if" /* If */) {
7100
8132
  if (!instruction.else) continue;
7101
8133
  if (instruction.branches.every(
@@ -7106,6 +8138,35 @@ function instructionsTerminate(instructions) {
7106
8138
  }
7107
8139
  return false;
7108
8140
  }
8141
+ function instructionsPropagatePastSwitch(instructions) {
8142
+ for (const instruction of instructions) {
8143
+ if (instruction.type === "return" /* Return */ || instruction.type === "throw" /* Throw */ || instruction.type === "continue" /* Continue */) {
8144
+ return true;
8145
+ }
8146
+ if (instruction.type === "break" /* Break */) return false;
8147
+ if (instruction.type === "try" /* Try */ && instructionsPropagatePastSwitch(instruction.instructions) && instruction.catches.every(
8148
+ (clause) => instructionsPropagatePastSwitch(clause.instructions)
8149
+ )) {
8150
+ return true;
8151
+ }
8152
+ if (instruction.type === "switch" /* Switch */) {
8153
+ if (instruction.defaultInstructions !== void 0 && instruction.defaultInstructions !== null && instruction.sections.every(
8154
+ (section) => instructionsPropagatePastSwitch(section.instructions)
8155
+ ) && instructionsPropagatePastSwitch(instruction.defaultInstructions)) {
8156
+ return true;
8157
+ }
8158
+ }
8159
+ if (instruction.type === "if" /* If */) {
8160
+ if (!instruction.else) continue;
8161
+ if (instruction.branches.every(
8162
+ (branch) => instructionsPropagatePastSwitch(branch.instructions)
8163
+ ) && instructionsPropagatePastSwitch(instruction.else)) {
8164
+ return true;
8165
+ }
8166
+ }
8167
+ }
8168
+ return false;
8169
+ }
7109
8170
  function collectReturns(statements) {
7110
8171
  const result = [];
7111
8172
  for (const statement of statements) {
@@ -7116,6 +8177,18 @@ function collectReturns(statements) {
7116
8177
  if (statement.elseBody)
7117
8178
  result.push(...collectReturns(statement.elseBody));
7118
8179
  }
8180
+ if (statement.kind === "for" || statement.kind === "forEach") {
8181
+ result.push(...collectReturns(statement.body));
8182
+ }
8183
+ if (statement.kind === "switch") {
8184
+ for (const section of statement.sections)
8185
+ result.push(...collectReturns(section.body));
8186
+ }
8187
+ if (statement.kind === "try") {
8188
+ result.push(...collectReturns(statement.body));
8189
+ for (const clause of statement.catches)
8190
+ result.push(...collectReturns(clause.body));
8191
+ }
7119
8192
  }
7120
8193
  return result;
7121
8194
  }
@@ -7152,6 +8225,13 @@ function canonicalDecimal(raw, pos) {
7152
8225
  const zero = whole === "0" && /^0*$/.test(fractionalRaw);
7153
8226
  return `${negative && !zero ? "-" : ""}${whole}${fractionalRaw ? `.${fractionalRaw}` : ""}`;
7154
8227
  }
8228
+ function isSwitchSelectorType(type, project) {
8229
+ return isPrimitive(type, "int") || isPrimitive(type, "string") || isPrimitive(type, "bool") || isNamedEnum(type, project);
8230
+ }
8231
+ function switchValueKey(value) {
8232
+ const enumId = value.typeInfo.type === 8 /* Enum */ ? value.typeInfo.enumId : null;
8233
+ return JSON.stringify([value.typeInfo.type, enumId, value.value]);
8234
+ }
7155
8235
  var CONSTRUCTING_FUNCTION_KINDS, WRITE_TARGET_DESCRIPTIONS, FALLTHROUGH_LABELS, Scope, StrictNeoScriptResolver, SPRITE_DERIVED_MEMBERS;
7156
8236
  var init_strict_resolver = __esm({
7157
8237
  "../packages/neoscript-language/src/strict-resolver.ts"() {
@@ -7189,6 +8269,7 @@ var init_strict_resolver = __esm({
7189
8269
  parent;
7190
8270
  entries = /* @__PURE__ */ new Map();
7191
8271
  invalidatedRoots = /* @__PURE__ */ new Set();
8272
+ hiddenNarrowingRoots = /* @__PURE__ */ new Set();
7192
8273
  narrowedPaths = /* @__PURE__ */ new Map();
7193
8274
  ownershipOverrides = /* @__PURE__ */ new Map();
7194
8275
  define(entry) {
@@ -7197,10 +8278,19 @@ var init_strict_resolver = __esm({
7197
8278
  lookup(name) {
7198
8279
  return this.entries.get(name) ?? this.parent?.lookup(name) ?? null;
7199
8280
  }
8281
+ containsEntry(entry) {
8282
+ for (const candidate of this.entries.values()) {
8283
+ if (candidate === entry) return true;
8284
+ }
8285
+ return this.parent?.containsEntry(entry) ?? false;
8286
+ }
7200
8287
  /**
7201
8288
  * P43 §7.1. The write-target root recorded for the binding a pointer names.
7202
8289
  * Innermost scope wins, so a shadowing declaration governs its own block.
7203
8290
  */
8291
+ parentScope() {
8292
+ return this.parent;
8293
+ }
7204
8294
  writeRootOf(variableId) {
7205
8295
  for (const entry of this.entries.values()) {
7206
8296
  if (entry.variableId === variableId) return entry.writeRoot;
@@ -7216,15 +8306,34 @@ var init_strict_resolver = __esm({
7216
8306
  ownershipChanges() {
7217
8307
  return this.ownershipOverrides;
7218
8308
  }
8309
+ invalidatedRootEntries() {
8310
+ return this.invalidatedRoots;
8311
+ }
8312
+ effectiveNarrowedPaths() {
8313
+ const effective = this.parent ? new Map(this.parent.effectiveNarrowedPaths()) : /* @__PURE__ */ new Map();
8314
+ for (const invalidated of this.invalidatedRoots) {
8315
+ for (const [path, narrowed] of effective) {
8316
+ if (narrowed.rootEntry === invalidated) effective.delete(path);
8317
+ }
8318
+ }
8319
+ for (const [path, narrowed] of this.narrowedPaths) {
8320
+ effective.set(path, narrowed);
8321
+ }
8322
+ return effective;
8323
+ }
7219
8324
  lookupNarrowedType(path, rootEntry) {
7220
- if (this.invalidatedRoots.has(rootEntry)) return null;
7221
8325
  const local = this.narrowedPaths.get(path);
7222
8326
  if (local?.rootEntry === rootEntry) return local.type;
8327
+ if (this.hiddenNarrowingRoots.has(rootEntry)) return null;
8328
+ if (this.invalidatedRoots.has(rootEntry)) return null;
7223
8329
  return this.parent?.lookupNarrowedType(path, rootEntry) ?? null;
7224
8330
  }
7225
8331
  narrow(path, rootEntry, type) {
7226
8332
  this.narrowedPaths.set(path, { type, rootEntry });
7227
8333
  }
8334
+ hideInheritedNarrowing(rootEntry) {
8335
+ this.hiddenNarrowingRoots.add(rootEntry);
8336
+ }
7228
8337
  invalidate(rootEntry) {
7229
8338
  this.invalidatedRoots.add(rootEntry);
7230
8339
  for (const [path, narrowed] of this.narrowedPaths) {
@@ -7280,7 +8389,15 @@ var init_strict_resolver = __esm({
7280
8389
  argumentVariables;
7281
8390
  callSiteCounter = 0;
7282
8391
  lambdaDepth = 0;
8392
+ unreachableControlEffectDepth = 0;
8393
+ catchBindingCounter = 0;
8394
+ catchContexts = [];
7283
8395
  expectedReturnStack = [];
8396
+ controlContexts = [];
8397
+ loopFlowContexts = [];
8398
+ switchFlowContexts = [];
8399
+ tryMutationContexts = [];
8400
+ functionControlBoundaries = [];
7284
8401
  compile(body) {
7285
8402
  const scope = new Scope(null);
7286
8403
  scope.define(
@@ -7424,9 +8541,36 @@ var init_strict_resolver = __esm({
7424
8541
  }
7425
8542
  }
7426
8543
  resolveStatements(statements, scope) {
7427
- return statements.map(
7428
- (statement) => this.resolveStatement(statement, scope)
7429
- );
8544
+ const instructions = [];
8545
+ let reachable = true;
8546
+ let unreachableScope = null;
8547
+ for (const statement of statements) {
8548
+ const statementScope = reachable ? scope : unreachableScope ??= new Scope(scope);
8549
+ const instruction = this.resolveSequencedStatement(
8550
+ statement,
8551
+ statementScope,
8552
+ reachable
8553
+ );
8554
+ instructions.push(instruction);
8555
+ if (reachable && instructionsTerminate([instruction])) reachable = false;
8556
+ if (reachable) {
8557
+ for (const context of this.tryMutationContexts) {
8558
+ context.effects.push(
8559
+ this.captureScopeEffects(statementScope, context.boundary)
8560
+ );
8561
+ }
8562
+ }
8563
+ }
8564
+ return instructions;
8565
+ }
8566
+ resolveSequencedStatement(statement, scope, reachable) {
8567
+ if (reachable) return this.resolveStatement(statement, scope);
8568
+ this.unreachableControlEffectDepth++;
8569
+ try {
8570
+ return this.resolveStatement(statement, scope);
8571
+ } finally {
8572
+ this.unreachableControlEffectDepth--;
8573
+ }
7430
8574
  }
7431
8575
  resolveStatement(statement, scope) {
7432
8576
  switch (statement.kind) {
@@ -7437,6 +8581,7 @@ var init_strict_resolver = __esm({
7437
8581
  statement.pos
7438
8582
  );
7439
8583
  }
8584
+ this.assertLocalNameAvailable(statement.name, scope, statement.pos);
7440
8585
  const inferred = statement.type === null;
7441
8586
  if (inferred && (statement.init.kind === "litList" || statement.init.kind === "litDict") && (statement.init.kind === "litList" ? statement.init.elements.length === 0 : statement.init.entries.length === 0)) {
7442
8587
  throw new CompileError(
@@ -7519,6 +8664,7 @@ var init_strict_resolver = __esm({
7519
8664
  branchExitNarrowings.push(new Map(noBranchScope.narrowedPaths));
7520
8665
  branchExitOwnershipScopes.push(noBranchScope);
7521
8666
  }
8667
+ mergeBranchInvalidations(scope, branchExitOwnershipScopes);
7522
8668
  if (branchExitNarrowings.length > 0) {
7523
8669
  for (const [path, narrowed] of mergeNarrowingMaps(
7524
8670
  branchExitNarrowings
@@ -7536,6 +8682,332 @@ var init_strict_resolver = __esm({
7536
8682
  else: otherwise
7537
8683
  };
7538
8684
  }
8685
+ case "try":
8686
+ return this.resolveTryStatement(statement, scope);
8687
+ case "switch": {
8688
+ const selector = this.resolveExpression(statement.selector, scope);
8689
+ if (!isSwitchSelectorType(selector.type, this.project)) {
8690
+ throw new CompileError(
8691
+ `switch selector must be int, string, bool, enum, or an optional form; got ${this.describe(selector.type)}`,
8692
+ statement.selector.pos
8693
+ );
8694
+ }
8695
+ const switchFlow = {
8696
+ boundary: scope,
8697
+ breakEffects: []
8698
+ };
8699
+ const sections = [];
8700
+ let defaultInstructions;
8701
+ const normalizedLabels = /* @__PURE__ */ new Set();
8702
+ this.controlContexts.push("switch");
8703
+ this.switchFlowContexts.push(switchFlow);
8704
+ try {
8705
+ for (const section of statement.sections) {
8706
+ const defaultLabels = section.labels.filter(
8707
+ (label) => label.kind === "default"
8708
+ );
8709
+ if (defaultLabels.length > 0 && section.labels.length !== 1) {
8710
+ throw new CompileError(
8711
+ "`default` must be a switch section of its own.",
8712
+ defaultLabels[0]?.pos ?? section.pos
8713
+ );
8714
+ }
8715
+ if (defaultLabels.length > 0 && defaultInstructions !== void 0) {
8716
+ throw new CompileError(
8717
+ "A switch statement can contain only one default section.",
8718
+ defaultLabels[0]?.pos ?? section.pos
8719
+ );
8720
+ }
8721
+ const labels = section.labels.flatMap((label) => {
8722
+ if (label.kind === "default") return [];
8723
+ const normalized = this.resolveSwitchCaseLabel(
8724
+ label.expression,
8725
+ selector.type,
8726
+ scope
8727
+ );
8728
+ const key = switchValueKey(normalized);
8729
+ if (normalizedLabels.has(key)) {
8730
+ throw new CompileError(
8731
+ "Duplicate switch case label.",
8732
+ label.pos
8733
+ );
8734
+ }
8735
+ normalizedLabels.add(key);
8736
+ return [normalized];
8737
+ });
8738
+ const sectionScope = new Scope(scope);
8739
+ const instructions = this.resolveStatements(
8740
+ section.body,
8741
+ sectionScope
8742
+ );
8743
+ if (!instructionsTerminate(instructions)) {
8744
+ throw new CompileError(
8745
+ "Switch section can reach its end; add break, return, or throw.",
8746
+ section.pos
8747
+ );
8748
+ }
8749
+ if (defaultLabels.length > 0) {
8750
+ defaultInstructions = instructions;
8751
+ } else {
8752
+ sections.push({ labels, instructions });
8753
+ }
8754
+ }
8755
+ } finally {
8756
+ this.switchFlowContexts.pop();
8757
+ this.controlContexts.pop();
8758
+ }
8759
+ const exitNarrowings = switchFlow.breakEffects.map(
8760
+ (effect) => effect.narrowedPaths
8761
+ );
8762
+ const exitEffects = [...switchFlow.breakEffects];
8763
+ if (defaultInstructions === void 0) {
8764
+ exitEffects.push({
8765
+ invalidatedRoots: /* @__PURE__ */ new Set(),
8766
+ ownershipOverrides: /* @__PURE__ */ new Map(),
8767
+ narrowedPaths: scope.effectiveNarrowedPaths()
8768
+ });
8769
+ exitNarrowings.push(scope.effectiveNarrowedPaths());
8770
+ }
8771
+ this.applyMergedScopeEffects(scope, exitEffects);
8772
+ this.mergeLoopNarrowings(scope, exitNarrowings);
8773
+ return {
8774
+ type: "switch" /* Switch */,
8775
+ selector: selector.pointer,
8776
+ selectorTypeInfo: toWireType(selector.type, this.project),
8777
+ sections,
8778
+ ...defaultInstructions !== void 0 ? { defaultInstructions } : {}
8779
+ };
8780
+ }
8781
+ case "for": {
8782
+ const loopScope = new Scope(scope);
8783
+ const initializer = this.resolveStatement(
8784
+ statement.initializer,
8785
+ loopScope
8786
+ );
8787
+ if (initializer.type !== "variable" /* Variable */) {
8788
+ throw new Error(
8789
+ "NeoScript for-loop initializer did not resolve to a variable instruction."
8790
+ );
8791
+ }
8792
+ const loopEntryScope = new Scope(loopScope);
8793
+ invalidateLoopEntryNarrowingsAssignedBy(loopEntryScope, scope, [
8794
+ ...statement.body,
8795
+ statement.iterator
8796
+ ]);
8797
+ const condition = this.toBool(
8798
+ this.resolveExpression(statement.condition, loopEntryScope),
8799
+ statement.condition.pos
8800
+ );
8801
+ const iterationScope = new Scope(loopEntryScope);
8802
+ this.applyFactsToScope(loopEntryScope, iterationScope, [
8803
+ ...factsWhenTrue(statement.condition)
8804
+ ]);
8805
+ const bodyScope = new Scope(iterationScope);
8806
+ const loopFlow = {
8807
+ boundary: iterationScope,
8808
+ continueEffects: [],
8809
+ breakEffects: []
8810
+ };
8811
+ this.controlContexts.push("loop");
8812
+ this.loopFlowContexts.push(loopFlow);
8813
+ let instructions;
8814
+ try {
8815
+ instructions = this.resolveStatements(statement.body, bodyScope);
8816
+ } finally {
8817
+ this.loopFlowContexts.pop();
8818
+ this.controlContexts.pop();
8819
+ }
8820
+ const iteratorEffects = [...loopFlow.continueEffects];
8821
+ if (!instructionsTerminate(instructions)) {
8822
+ iteratorEffects.push(
8823
+ this.captureScopeEffects(bodyScope, iterationScope)
8824
+ );
8825
+ }
8826
+ this.applyMergedScopeEffects(iterationScope, iteratorEffects);
8827
+ const iteratorScope = new Scope(iterationScope);
8828
+ const iterator = this.resolveAssignment(
8829
+ statement.iterator,
8830
+ iteratorScope
8831
+ );
8832
+ const initialConditionFalseScope = new Scope(loopScope);
8833
+ this.applyFactsToScope(loopScope, initialConditionFalseScope, [
8834
+ ...factsWhenFalse(statement.condition)
8835
+ ]);
8836
+ const effectScopes = [loopScope];
8837
+ const normalExitScopes = [initialConditionFalseScope];
8838
+ if (iteratorEffects.length > 0) {
8839
+ const postIterationScope = new Scope(loopScope);
8840
+ this.applyMergedScopeEffects(postIterationScope, [
8841
+ this.captureScopeEffects(iteratorScope, loopScope)
8842
+ ]);
8843
+ effectScopes.push(postIterationScope);
8844
+ const subsequentConditionFalseScope = new Scope(postIterationScope);
8845
+ this.applyFactsToScope(
8846
+ postIterationScope,
8847
+ subsequentConditionFalseScope,
8848
+ [...factsWhenFalse(statement.condition)]
8849
+ );
8850
+ normalExitScopes.push(subsequentConditionFalseScope);
8851
+ }
8852
+ this.mergeLoopEffects(scope, effectScopes, loopFlow.breakEffects);
8853
+ this.mergeLoopNarrowings(scope, [
8854
+ ...normalExitScopes.map((exit) => exit.effectiveNarrowedPaths()),
8855
+ ...loopFlow.breakEffects.map((exit) => exit.narrowedPaths)
8856
+ ]);
8857
+ return {
8858
+ type: "for" /* For */,
8859
+ initializer: initializer.variable,
8860
+ condition: pointerToBooleanExpression(condition.pointer),
8861
+ iterator,
8862
+ instructions
8863
+ };
8864
+ }
8865
+ case "forEach": {
8866
+ const collection = this.resolveExpression(statement.collection, scope);
8867
+ if (isNullable(collection.type)) {
8868
+ throw new CompileError(
8869
+ `foreach requires a non-optional collection, got ${this.describe(collection.type)}`,
8870
+ statement.collection.pos
8871
+ );
8872
+ }
8873
+ const parts = collectionPartsFor(collection);
8874
+ if (!parts) {
8875
+ throw new CompileError(
8876
+ `foreach requires a List, Dictionary, Set, lookup, or derived collection, got ${this.describe(collection.type)}`,
8877
+ statement.collection.pos
8878
+ );
8879
+ }
8880
+ this.assertLocalNameAvailable(statement.name, scope, statement.pos);
8881
+ const inferredType = parts.valueType;
8882
+ const bindingType = statement.type ? this.resolveType(statement.type) : inferredType;
8883
+ if (statement.type) {
8884
+ this.requireAssignable(
8885
+ inferredType,
8886
+ bindingType,
8887
+ statement.pos,
8888
+ "foreach iterator"
8889
+ );
8890
+ }
8891
+ const binding = variable(
8892
+ statement.name,
8893
+ bindingType,
8894
+ void 0,
8895
+ this.project
8896
+ );
8897
+ const bindingWritability = collection.entryWritability ?? collection.writability ?? "runtime" /* Runtime */;
8898
+ const loopScope = new Scope(scope);
8899
+ loopScope.define(
8900
+ scopeVariable(
8901
+ statement.name,
8902
+ binding,
8903
+ bindingType,
8904
+ bindingWritability,
8905
+ bindingWritability,
8906
+ collection.writeRoot ?? this.writeThroughRoot(collection.pointer, scope),
8907
+ "foreach"
8908
+ )
8909
+ );
8910
+ const iterationScope = new Scope(loopScope);
8911
+ invalidateLoopEntryNarrowingsAssignedBy(
8912
+ iterationScope,
8913
+ scope,
8914
+ statement.body
8915
+ );
8916
+ const bodyScope = new Scope(iterationScope);
8917
+ const loopFlow = {
8918
+ boundary: iterationScope,
8919
+ continueEffects: [],
8920
+ breakEffects: []
8921
+ };
8922
+ this.controlContexts.push("loop");
8923
+ this.loopFlowContexts.push(loopFlow);
8924
+ let instructions;
8925
+ try {
8926
+ instructions = this.resolveStatements(statement.body, bodyScope);
8927
+ } finally {
8928
+ this.loopFlowContexts.pop();
8929
+ this.controlContexts.pop();
8930
+ }
8931
+ const nextItemEffects = [...loopFlow.continueEffects];
8932
+ if (!instructionsTerminate(instructions)) {
8933
+ nextItemEffects.push(
8934
+ this.captureScopeEffects(bodyScope, iterationScope)
8935
+ );
8936
+ }
8937
+ const nextItemScope = new Scope(loopScope);
8938
+ this.applyMergedScopeEffects(nextItemScope, nextItemEffects);
8939
+ this.mergeLoopEffects(
8940
+ scope,
8941
+ [loopScope, nextItemScope],
8942
+ [...loopFlow.breakEffects]
8943
+ );
8944
+ this.mergeLoopNarrowings(scope, [
8945
+ loopScope.effectiveNarrowedPaths(),
8946
+ nextItemScope.effectiveNarrowedPaths(),
8947
+ ...loopFlow.breakEffects.map((exit) => exit.narrowedPaths)
8948
+ ]);
8949
+ return {
8950
+ type: "forEach" /* ForEach */,
8951
+ binding: {
8952
+ id: binding.id,
8953
+ typeInfo: binding.typeInfo,
8954
+ readonly: true,
8955
+ writability: bindingWritability
8956
+ },
8957
+ collectionPointer: collection.pointer,
8958
+ collectionTypeInfo: collection.wireType ?? toWireType(collection.type, this.project),
8959
+ instructions
8960
+ };
8961
+ }
8962
+ case "break": {
8963
+ const functionBoundary = this.functionControlBoundaries.at(-1);
8964
+ const controlDepth = functionBoundary?.controlDepth ?? 0;
8965
+ if (this.controlContexts.length <= controlDepth) {
8966
+ throw new CompileError(
8967
+ "`break;` is only valid inside a loop or switch.",
8968
+ statement.pos
8969
+ );
8970
+ }
8971
+ const control = this.controlContexts.at(-1);
8972
+ if (control === "switch") {
8973
+ const switchFlow = this.switchFlowContexts.at(-1);
8974
+ if (!switchFlow) {
8975
+ throw new Error("NeoScript switch flow stack is unbalanced.");
8976
+ }
8977
+ if (this.unreachableControlEffectDepth === 0) {
8978
+ switchFlow.breakEffects.push(
8979
+ this.captureScopeEffects(scope, switchFlow.boundary)
8980
+ );
8981
+ }
8982
+ } else {
8983
+ const loopFlow = this.loopFlowContexts.at(-1);
8984
+ if (!loopFlow) {
8985
+ throw new Error("NeoScript loop flow stack is unbalanced.");
8986
+ }
8987
+ if (this.unreachableControlEffectDepth === 0) {
8988
+ loopFlow.breakEffects.push(
8989
+ this.captureScopeEffects(scope, loopFlow.boundary)
8990
+ );
8991
+ }
8992
+ }
8993
+ return { type: "break" /* Break */ };
8994
+ }
8995
+ case "continue": {
8996
+ const functionBoundary = this.functionControlBoundaries.at(-1);
8997
+ const loopFlow = this.loopFlowContexts.length > (functionBoundary?.loopFlowDepth ?? 0) ? this.loopFlowContexts.at(-1) : void 0;
8998
+ if (loopFlow === void 0) {
8999
+ throw new CompileError(
9000
+ "`continue;` is only valid inside a loop.",
9001
+ statement.pos
9002
+ );
9003
+ }
9004
+ if (this.unreachableControlEffectDepth === 0) {
9005
+ loopFlow.continueEffects.push(
9006
+ this.captureScopeEffects(scope, loopFlow.boundary)
9007
+ );
9008
+ }
9009
+ return { type: "continue" /* Continue */ };
9010
+ }
7539
9011
  case "return": {
7540
9012
  const expected = this.currentExpectedReturn();
7541
9013
  if (statement.expr === null) {
@@ -7580,6 +9052,21 @@ var init_strict_resolver = __esm({
7580
9052
  };
7581
9053
  }
7582
9054
  case "throw": {
9055
+ if (statement.expr === null) {
9056
+ const functionBoundary = this.functionControlBoundaries.at(-1);
9057
+ const catchDepth = functionBoundary?.catchDepth ?? 0;
9058
+ const rethrowPointer = this.catchContexts.length > catchDepth ? this.catchContexts.at(-1) : void 0;
9059
+ if (!rethrowPointer) {
9060
+ throw new CompileError(
9061
+ "`throw;` is only valid inside a catch body.",
9062
+ statement.pos
9063
+ );
9064
+ }
9065
+ return {
9066
+ type: "throw" /* Throw */,
9067
+ pointer: rethrowPointer
9068
+ };
9069
+ }
7583
9070
  const message = this.resolveExpression(statement.expr, scope);
7584
9071
  if (!isPrimitive(message.type, "string") || isNullable(message.type)) {
7585
9072
  throw new CompileError(
@@ -7618,6 +9105,271 @@ var init_strict_resolver = __esm({
7618
9105
  }
7619
9106
  }
7620
9107
  }
9108
+ resolveTryStatement(statement, scope) {
9109
+ const tryScope = new Scope(scope);
9110
+ const mutations = {
9111
+ boundary: scope,
9112
+ effects: []
9113
+ };
9114
+ this.tryMutationContexts.push(mutations);
9115
+ let instructions;
9116
+ try {
9117
+ instructions = this.resolveStatements(statement.body, tryScope);
9118
+ } finally {
9119
+ this.tryMutationContexts.pop();
9120
+ }
9121
+ const normalEffects = [];
9122
+ const normalNarrowings = [];
9123
+ if (!instructionsTerminate(instructions)) {
9124
+ const exitEffect = this.captureScopeEffects(tryScope, scope);
9125
+ normalEffects.push(exitEffect);
9126
+ normalNarrowings.push(exitEffect.narrowedPaths);
9127
+ }
9128
+ const baseline = {
9129
+ invalidatedRoots: /* @__PURE__ */ new Set(),
9130
+ ownershipOverrides: /* @__PURE__ */ new Map(),
9131
+ narrowedPaths: scope.effectiveNarrowedPaths()
9132
+ };
9133
+ const incomingCatchEffects = [baseline, ...mutations.effects];
9134
+ const catches = statement.catches.map((clause, index) => {
9135
+ const resolved = this.resolveCatchClause(
9136
+ clause,
9137
+ scope,
9138
+ incomingCatchEffects,
9139
+ index,
9140
+ statement.catches.length
9141
+ );
9142
+ if (resolved.exitEffect) {
9143
+ normalEffects.push(resolved.exitEffect);
9144
+ normalNarrowings.push(resolved.exitEffect.narrowedPaths);
9145
+ }
9146
+ return resolved.catch;
9147
+ });
9148
+ this.applyMergedScopeEffects(scope, normalEffects);
9149
+ this.mergeLoopNarrowings(scope, normalNarrowings);
9150
+ return {
9151
+ type: "try" /* Try */,
9152
+ instructions,
9153
+ catches
9154
+ };
9155
+ }
9156
+ resolveCatchClause(clause, outerScope, incomingEffects, index, catchCount) {
9157
+ if (clause.filter === null && index !== catchCount - 1) {
9158
+ throw new CompileError(
9159
+ "An unfiltered catch clause must be the final catch clause.",
9160
+ clause.pos
9161
+ );
9162
+ }
9163
+ const catchScope = new Scope(outerScope);
9164
+ this.applyMergedScopeEffects(catchScope, incomingEffects);
9165
+ this.assertLocalNameAvailable(clause.name, catchScope, clause.pos);
9166
+ const catchType = {
9167
+ kind: "primitive",
9168
+ name: "string"
9169
+ };
9170
+ const bindingId = `__catch:${this.catchBindingCounter++}:${clause.name}`;
9171
+ const binding = variable(bindingId, catchType, void 0, this.project);
9172
+ catchScope.define(
9173
+ scopeVariable(
9174
+ clause.name,
9175
+ binding,
9176
+ catchType,
9177
+ "local" /* Local */,
9178
+ "local" /* Local */,
9179
+ void 0,
9180
+ "catch"
9181
+ )
9182
+ );
9183
+ let filter;
9184
+ if (clause.filter) {
9185
+ const condition = this.toBool(
9186
+ this.resolveExpression(clause.filter, catchScope),
9187
+ clause.filter.pos
9188
+ );
9189
+ filter = pointerToBooleanExpression(condition.pointer);
9190
+ }
9191
+ const bodyScope = new Scope(catchScope);
9192
+ if (clause.filter) {
9193
+ this.applyFactsToScope(catchScope, bodyScope, [
9194
+ ...factsWhenTrue(clause.filter)
9195
+ ]);
9196
+ }
9197
+ const catchPointer = {
9198
+ type: "variable" /* Variable */,
9199
+ variableId: bindingId
9200
+ };
9201
+ this.catchContexts.push(catchPointer);
9202
+ let instructions;
9203
+ try {
9204
+ instructions = this.resolveStatements(clause.body, bodyScope);
9205
+ } finally {
9206
+ this.catchContexts.pop();
9207
+ }
9208
+ const exitEffect = instructionsTerminate(instructions) ? null : this.captureScopeEffects(bodyScope, outerScope);
9209
+ return {
9210
+ catch: {
9211
+ binding: {
9212
+ id: bindingId,
9213
+ typeInfo: binding.typeInfo,
9214
+ readonly: true
9215
+ },
9216
+ ...filter ? { filter } : {},
9217
+ instructions
9218
+ },
9219
+ exitEffect
9220
+ };
9221
+ }
9222
+ assertLocalNameAvailable(name, scope, pos) {
9223
+ if (!scope.lookup(name)) return;
9224
+ throw new CompileError(
9225
+ `Cannot declare local '${name}' because that name is already used in an enclosing or current scope.`,
9226
+ pos
9227
+ );
9228
+ }
9229
+ captureScopeEffects(effect, boundary) {
9230
+ const invalidatedRoots = /* @__PURE__ */ new Set();
9231
+ const ownershipOverrides = /* @__PURE__ */ new Map();
9232
+ let current = effect;
9233
+ while (current !== null && current !== boundary) {
9234
+ for (const entry of current.invalidatedRootEntries()) {
9235
+ invalidatedRoots.add(entry);
9236
+ }
9237
+ for (const [entry, ownership] of current.ownershipChanges()) {
9238
+ if (!ownershipOverrides.has(entry)) {
9239
+ ownershipOverrides.set(entry, ownership);
9240
+ }
9241
+ }
9242
+ current = current.parentScope();
9243
+ }
9244
+ return {
9245
+ invalidatedRoots,
9246
+ ownershipOverrides,
9247
+ narrowedPaths: effect.effectiveNarrowedPaths()
9248
+ };
9249
+ }
9250
+ applyMergedScopeEffects(target, effects) {
9251
+ const invalidatedRoots = /* @__PURE__ */ new Set();
9252
+ const changedOwnership = /* @__PURE__ */ new Set();
9253
+ for (const effect of effects) {
9254
+ for (const entry of effect.invalidatedRoots) {
9255
+ if (target.containsEntry(entry)) invalidatedRoots.add(entry);
9256
+ }
9257
+ for (const entry of effect.ownershipOverrides.keys()) {
9258
+ if (target.containsEntry(entry)) changedOwnership.add(entry);
9259
+ }
9260
+ }
9261
+ for (const entry of invalidatedRoots) target.invalidate(entry);
9262
+ for (const entry of changedOwnership) {
9263
+ const original = target.ownership(entry) ?? "runtime" /* Runtime */;
9264
+ const ownerships = effects.map(
9265
+ (effect) => effect.ownershipOverrides.get(entry) ?? original
9266
+ );
9267
+ const first = ownerships[0] ?? original;
9268
+ target.setOwnership(
9269
+ entry,
9270
+ ownerships.every((ownership) => ownership === first) ? first : "runtime" /* Runtime */
9271
+ );
9272
+ }
9273
+ }
9274
+ mergeLoopEffects(parent, effects, capturedEffects = []) {
9275
+ const changedOwnership = /* @__PURE__ */ new Set();
9276
+ const invalidated = /* @__PURE__ */ new Set();
9277
+ for (const effect of effects) {
9278
+ for (const entry of effect.ownershipChanges().keys()) {
9279
+ if (parent.containsEntry(entry)) changedOwnership.add(entry);
9280
+ }
9281
+ for (const entry of effect.invalidatedRootEntries()) {
9282
+ if (parent.containsEntry(entry)) invalidated.add(entry);
9283
+ }
9284
+ }
9285
+ for (const effect of capturedEffects) {
9286
+ for (const entry of effect.ownershipOverrides.keys()) {
9287
+ if (parent.containsEntry(entry)) changedOwnership.add(entry);
9288
+ }
9289
+ for (const entry of effect.invalidatedRoots) {
9290
+ if (parent.containsEntry(entry)) invalidated.add(entry);
9291
+ }
9292
+ }
9293
+ for (const entry of invalidated) parent.invalidate(entry);
9294
+ for (const entry of changedOwnership) {
9295
+ const entryOwnership = parent.ownership(entry) ?? "runtime" /* Runtime */;
9296
+ const ownerships = [
9297
+ entryOwnership,
9298
+ ...effects.map((effect) => effect.ownership(entry) ?? entryOwnership),
9299
+ ...capturedEffects.map(
9300
+ (effect) => effect.ownershipOverrides.get(entry) ?? entryOwnership
9301
+ )
9302
+ ];
9303
+ const first = ownerships[0] ?? "runtime" /* Runtime */;
9304
+ parent.setOwnership(
9305
+ entry,
9306
+ ownerships.every((ownership) => ownership === first) ? first : "runtime" /* Runtime */
9307
+ );
9308
+ }
9309
+ }
9310
+ mergeLoopNarrowings(parent, exitPaths) {
9311
+ if (exitPaths.length === 0) return;
9312
+ const merged = mergeNarrowingMaps(exitPaths);
9313
+ for (const [path, narrowed] of parent.effectiveNarrowedPaths()) {
9314
+ const common = merged.get(path);
9315
+ if (parent.containsEntry(narrowed.rootEntry) && (common?.rootEntry !== narrowed.rootEntry || !typeRefsEqual(common.type, narrowed.type))) {
9316
+ parent.invalidate(narrowed.rootEntry);
9317
+ }
9318
+ }
9319
+ for (const [path, narrowed] of merged) {
9320
+ if (!parent.containsEntry(narrowed.rootEntry)) continue;
9321
+ const rootEntry = parent.lookup(pathRoot(path));
9322
+ if (rootEntry === narrowed.rootEntry) {
9323
+ parent.narrow(path, rootEntry, narrowed.type);
9324
+ }
9325
+ }
9326
+ }
9327
+ resolveSwitchCaseLabel(expression, selectorType, scope) {
9328
+ if (expression.kind === "litNull") {
9329
+ if (!isNullable(selectorType)) {
9330
+ throw new CompileError(
9331
+ `Cannot use null as switch case of required type ${this.describe(selectorType)}`,
9332
+ expression.pos
9333
+ );
9334
+ }
9335
+ return {
9336
+ typeInfo: { type: 0 /* Null */, required: true },
9337
+ value: null
9338
+ };
9339
+ }
9340
+ const expected = { ...selectorType, nullable: false };
9341
+ const integerValue = expression.kind === "litInt" ? expression.value : expression.kind === "unary" && expression.op === "-" && expression.operand.kind === "litInt" ? -expression.operand.value : null;
9342
+ if (integerValue !== null && !Number.isSafeInteger(integerValue)) {
9343
+ throw new CompileError(
9344
+ "Switch integer case labels must be safe integers.",
9345
+ expression.pos
9346
+ );
9347
+ }
9348
+ let resolved;
9349
+ if (integerValue !== null) {
9350
+ resolved = literal({ kind: "primitive", name: "int" }, integerValue);
9351
+ } else if (expression.kind === "litString" || expression.kind === "litBool" || expression.kind === "contextualEnum" || expression.kind === "member" && expression.receiver.kind === "ident") {
9352
+ resolved = this.resolveExpression(expression, scope, expected);
9353
+ } else {
9354
+ throw new CompileError(
9355
+ "Switch case labels must be compile-time int, string, bool, enum, or null constants.",
9356
+ expression.pos
9357
+ );
9358
+ }
9359
+ this.requireAssignable(
9360
+ resolved.type,
9361
+ expected,
9362
+ expression.pos,
9363
+ "switch case"
9364
+ );
9365
+ if (resolved.pointer.type !== "value" /* Value */) {
9366
+ throw new CompileError(
9367
+ "Switch case labels must be compile-time int, string, bool, enum, or null constants.",
9368
+ expression.pos
9369
+ );
9370
+ }
9371
+ return resolved.pointer.value;
9372
+ }
7621
9373
  resolveAssignment(statement, scope) {
7622
9374
  if (!isAssignmentTarget(statement.target)) {
7623
9375
  throw new CompileError(
@@ -7626,8 +9378,21 @@ var init_strict_resolver = __esm({
7626
9378
  );
7627
9379
  }
7628
9380
  const local = statement.target.kind === "ident" ? scope.lookup(statement.target.name) : null;
9381
+ if (local?.readonlyBinding) {
9382
+ const label = local.readonlyBinding === "catch" ? "catch parameter" : "foreach iterator";
9383
+ throw new CompileError(
9384
+ `Cannot assign to ${label} '${local.name}' because it is read-only.`,
9385
+ statement.pos
9386
+ );
9387
+ }
7629
9388
  const resolvedTarget = this.resolveExpression(statement.target, scope);
7630
- const target = local && statement.op === "=" ? { ...resolvedTarget, type: local.type } : resolvedTarget;
9389
+ const storageType = this.assignmentStorageType(
9390
+ statement.target,
9391
+ resolvedTarget,
9392
+ local,
9393
+ scope
9394
+ );
9395
+ const target = statement.op === "=" ? { ...resolvedTarget, type: storageType } : resolvedTarget;
7631
9396
  const writability = local?.writability ?? target.writability;
7632
9397
  if (target.symbol?.computed === true && target.symbol.writable !== true) {
7633
9398
  throw new CompileError(
@@ -7682,7 +9447,15 @@ var init_strict_resolver = __esm({
7682
9447
  const assignedPath = canonicalPath(statement.target);
7683
9448
  if (assignedPath) {
7684
9449
  const rootEntry = scope.lookup(pathRoot(assignedPath));
7685
- if (rootEntry) scope.invalidate(rootEntry);
9450
+ if (rootEntry) {
9451
+ scope.invalidate(rootEntry);
9452
+ if (statement.op === "=" && !isNullable(value.type)) {
9453
+ scope.narrow(assignedPath, rootEntry, {
9454
+ ...target.type,
9455
+ nullable: false
9456
+ });
9457
+ }
9458
+ }
7686
9459
  }
7687
9460
  return {
7688
9461
  type: "assign" /* Assign */,
@@ -7695,6 +9468,23 @@ var init_strict_resolver = __esm({
7695
9468
  pointer: value.pointer
7696
9469
  };
7697
9470
  }
9471
+ /**
9472
+ * The declared type of an assignment target, with any flow narrowing on the
9473
+ * path stripped back off. Locals carry their declaration on the scope entry;
9474
+ * a member re-resolves through `resolveMemberNatural`, which is the same
9475
+ * lookup `resolveMember` performs before it overlays a narrowed type.
9476
+ */
9477
+ assignmentStorageType(targetAst, resolvedTarget, local, scope) {
9478
+ if (targetAst.kind === "ident") return local?.type ?? resolvedTarget.type;
9479
+ if (targetAst.kind !== "member") return resolvedTarget.type;
9480
+ return this.resolveMemberNatural(
9481
+ targetAst.receiver,
9482
+ targetAst.name,
9483
+ scope,
9484
+ targetAst.pos,
9485
+ targetAst.optional === true
9486
+ ).type;
9487
+ }
7698
9488
  resolveCompoundAssignment(operation, valueAst, target, scope, pos) {
7699
9489
  const binaryOperation = operation === "++" ? "+" : operation === "--" ? "-" : operation.slice(0, 1);
7700
9490
  const right = operation === "++" || operation === "--" ? literal({ kind: "primitive", name: "int" }, 1) : valueAst ? this.resolveExpression(valueAst, scope, target.type) : null;
@@ -8120,7 +9910,8 @@ var init_strict_resolver = __esm({
8120
9910
  variableId: entry.variableId
8121
9911
  },
8122
9912
  type: scope.lookupNarrowedType(name, entry) ?? entry.type,
8123
- ...ownership ? { writability: ownership } : {}
9913
+ ...ownership ? { writability: ownership } : {},
9914
+ ...entry.writeRoot ? { writeRoot: entry.writeRoot } : {}
8124
9915
  };
8125
9916
  }
8126
9917
  const global = this.context.project.globals.find(
@@ -8325,6 +10116,12 @@ var init_strict_resolver = __esm({
8325
10116
  pos
8326
10117
  );
8327
10118
  }
10119
+ if (!optional && isNullable(receiver.type)) {
10120
+ throw new CompileError(
10121
+ `Member '${name}' is read on ${this.describe(receiver.type)}, which may be null. Handle null before reading through it \u2014 use \`!\` if it is always set, or \`?.\`, \`??\`, or a null check that narrows it.`,
10122
+ pos
10123
+ );
10124
+ }
8328
10125
  if (receiver.staticType?.kind === "enum") {
8329
10126
  if (optional) {
8330
10127
  throw new CompileError(
@@ -8485,6 +10282,7 @@ var init_strict_resolver = __esm({
8485
10282
  };
8486
10283
  }
8487
10284
  const writability = this.memberWritability(member, receiver);
10285
+ const entryWritability = member.lookup?.multiselect === true ? this.lookupEntryWritability(member, receiver) : void 0;
8488
10286
  const lookupWire = member.lookup?.multiselect === true && memberType2.kind === "set" ? {
8489
10287
  type: 9 /* Lookup */,
8490
10288
  required: !isNullable(memberType2),
@@ -8505,7 +10303,8 @@ var init_strict_resolver = __esm({
8505
10303
  ),
8506
10304
  symbol: member,
8507
10305
  ...writability ? { writability } : {},
8508
- ...lookupWire ? { wireType: lookupWire } : {}
10306
+ ...lookupWire ? { wireType: lookupWire } : {},
10307
+ ...entryWritability ? { entryWritability } : {}
8509
10308
  };
8510
10309
  }
8511
10310
  throw new CompileError(
@@ -8583,6 +10382,10 @@ var init_strict_resolver = __esm({
8583
10382
  if (member.lookup.multiselect) {
8584
10383
  return member.writability ? toWritability(member.writability) : receiver.writability ?? "save" /* Save */;
8585
10384
  }
10385
+ return this.lookupEntryWritability(member, receiver);
10386
+ }
10387
+ lookupEntryWritability(member, receiver) {
10388
+ if (!member.lookup) return "readOnly" /* ReadOnly */;
8586
10389
  const target = this.project.symbolById.get(
8587
10390
  member.lookup.collectionMemberId
8588
10391
  );
@@ -9380,11 +11183,18 @@ var init_strict_resolver = __esm({
9380
11183
  pos
9381
11184
  );
9382
11185
  const filteredType = receiver.type.kind === "list" || receiver.type.kind === "dictionary" || receiver.type.kind === "set" ? { ...receiver.type, readOnly: true } : receiver.type;
9383
- return intrinsic(
11186
+ const filtered = intrinsic(
9384
11187
  "where" /* Where */,
9385
11188
  { collectionPointer: receiver.pointer, function: fn },
9386
11189
  filteredType
9387
11190
  );
11191
+ return {
11192
+ ...filtered,
11193
+ ...receiver.writability ? { writability: receiver.writability } : {},
11194
+ ...receiver.wireType ? { wireType: receiver.wireType } : {},
11195
+ writeRoot: receiver.writeRoot ?? this.writeThroughRoot(receiver.pointer, scope),
11196
+ ...receiver.entryWritability ? { entryWritability: receiver.entryWritability } : {}
11197
+ };
9388
11198
  }
9389
11199
  if (name === "First" || name === "FirstOrDefault" || name === "Find") {
9390
11200
  if (args.length > 1) {
@@ -9401,7 +11211,7 @@ var init_strict_resolver = __esm({
9401
11211
  pos
9402
11212
  ) : void 0;
9403
11213
  const result = name === "FirstOrDefault" || name === "Find" ? { ...collection.valueType, nullable: true } : collection.valueType;
9404
- return intrinsic(
11214
+ const first = intrinsic(
9405
11215
  name === "First" ? "first" /* First */ : "firstOrDefault" /* FirstOrDefault */,
9406
11216
  {
9407
11217
  collectionPointer: receiver.pointer,
@@ -9409,6 +11219,12 @@ var init_strict_resolver = __esm({
9409
11219
  },
9410
11220
  result
9411
11221
  );
11222
+ const entryWritability = receiver.entryWritability ?? receiver.writability ?? "runtime" /* Runtime */;
11223
+ return {
11224
+ ...first,
11225
+ writability: entryWritability,
11226
+ writeRoot: this.expressionWriteRoot(receiver, scope)
11227
+ };
9412
11228
  }
9413
11229
  if (name === "Select") {
9414
11230
  requireArgCount(name, args, 1, pos);
@@ -9416,7 +11232,13 @@ var init_strict_resolver = __esm({
9416
11232
  if (lambda.kind !== "lambda") {
9417
11233
  throw new CompileError("Select requires a lambda argument.", pos);
9418
11234
  }
9419
- const resultType = this.inferLambdaReturnType(lambda, collection, scope);
11235
+ const inferredReturn = this.inferLambdaReturn(
11236
+ lambda,
11237
+ collection,
11238
+ receiver,
11239
+ scope
11240
+ );
11241
+ const resultType = inferredReturn.type;
9420
11242
  const fn = this.resolveCollectionLambda(
9421
11243
  lambda,
9422
11244
  collection,
@@ -9424,11 +11246,16 @@ var init_strict_resolver = __esm({
9424
11246
  scope,
9425
11247
  pos
9426
11248
  );
9427
- return intrinsic(
11249
+ const selected2 = intrinsic(
9428
11250
  "select" /* Select */,
9429
11251
  { collectionPointer: receiver.pointer, function: fn },
9430
11252
  { kind: "list", elementType: resultType, readOnly: true }
9431
11253
  );
11254
+ return {
11255
+ ...selected2,
11256
+ entryWritability: inferredReturn.writability,
11257
+ writeRoot: inferredReturn.writeRoot
11258
+ };
9432
11259
  }
9433
11260
  return null;
9434
11261
  }
@@ -9594,6 +11421,7 @@ var init_strict_resolver = __esm({
9594
11421
  const scope = new Scope(outerScope);
9595
11422
  const parameterTypes = collection.kind === "dictionary" ? [collection.keyType, collection.valueType] : [collection.valueType];
9596
11423
  const parameters = ast.params.map((parameter3, index) => {
11424
+ this.assertLocalNameAvailable(parameter3.name, scope, parameter3.pos);
9597
11425
  const inferred = requiredAt(parameterTypes, index);
9598
11426
  const declared = parameter3.type ? this.resolveType(parameter3.type) : inferred;
9599
11427
  if (parameter3.type)
@@ -9615,6 +11443,11 @@ var init_strict_resolver = __esm({
9615
11443
  );
9616
11444
  return item;
9617
11445
  });
11446
+ this.functionControlBoundaries.push({
11447
+ controlDepth: this.controlContexts.length,
11448
+ loopFlowDepth: this.loopFlowContexts.length,
11449
+ catchDepth: this.catchContexts.length
11450
+ });
9618
11451
  this.lambdaDepth++;
9619
11452
  this.expectedReturnStack.push(returnType);
9620
11453
  try {
@@ -9634,31 +11467,42 @@ var init_strict_resolver = __esm({
9634
11467
  } finally {
9635
11468
  this.expectedReturnStack.pop();
9636
11469
  this.lambdaDepth--;
11470
+ this.functionControlBoundaries.pop();
9637
11471
  }
9638
11472
  }
9639
- inferLambdaReturnType(lambda, collection, outerScope) {
11473
+ inferLambdaReturn(lambda, collection, source, outerScope) {
9640
11474
  const returns = collectReturns(lambda.body);
9641
11475
  if (returns.length === 0) {
9642
11476
  throw new CompileError("Select lambda must return a value.", lambda.pos);
9643
11477
  }
9644
11478
  const scope = new Scope(outerScope);
9645
11479
  const parameterTypes = collection.kind === "dictionary" ? [collection.keyType, collection.valueType] : [collection.valueType];
11480
+ const valueParameterIndex = collection.kind === "dictionary" ? 1 : 0;
11481
+ const sourceWritability = source.entryWritability ?? source.writability ?? "runtime" /* Runtime */;
11482
+ const sourceWriteRoot = this.expressionWriteRoot(source, outerScope);
9646
11483
  lambda.params.forEach((parameter3, index) => {
11484
+ this.assertLocalNameAvailable(parameter3.name, scope, parameter3.pos);
9647
11485
  const type = parameter3.type ? this.resolveType(parameter3.type) : requiredAt(parameterTypes, index);
11486
+ const isValueParameter = index === valueParameterIndex;
9648
11487
  scope.define(
9649
11488
  scopeVariable(
9650
11489
  parameter3.name,
9651
11490
  variable(parameter3.name, type, void 0, this.project),
9652
11491
  type,
9653
- void 0,
9654
- "runtime" /* Runtime */
11492
+ isValueParameter ? sourceWritability : "runtime" /* Runtime */,
11493
+ isValueParameter ? sourceWritability : "runtime" /* Runtime */,
11494
+ isValueParameter ? sourceWriteRoot : "local"
9655
11495
  )
9656
11496
  );
9657
11497
  });
9658
11498
  let inferred = null;
11499
+ let inferredWritability = null;
11500
+ let inferredWriteRoot = null;
9659
11501
  for (const returned of returns) {
9660
11502
  if (!returned.expr) continue;
9661
11503
  const current = this.resolveExpression(returned.expr, scope);
11504
+ const currentWritability = current.writability ?? "runtime" /* Runtime */;
11505
+ const currentWriteRoot = this.expressionWriteRoot(current, scope);
9662
11506
  if (!inferred) inferred = current.type;
9663
11507
  else {
9664
11508
  const common = commonNeoScriptAssignableType(
@@ -9674,8 +11518,14 @@ var init_strict_resolver = __esm({
9674
11518
  }
9675
11519
  inferred = common;
9676
11520
  }
11521
+ inferredWritability = inferredWritability === null ? currentWritability : inferredWritability === currentWritability ? inferredWritability : "runtime" /* Runtime */;
11522
+ inferredWriteRoot = inferredWriteRoot === null ? currentWriteRoot : inferredWriteRoot === currentWriteRoot ? inferredWriteRoot : "unknown";
9677
11523
  }
9678
- return inferred ?? { kind: "primitive", name: "null", nullable: true };
11524
+ return {
11525
+ type: inferred ?? { kind: "primitive", name: "null", nullable: true },
11526
+ writability: inferredWritability ?? "runtime" /* Runtime */,
11527
+ writeRoot: inferredWriteRoot ?? "unknown"
11528
+ };
9679
11529
  }
9680
11530
  resolveFunctionStatement(expression, scope) {
9681
11531
  if (expression.kind !== "call" || expression.callee.kind !== "member")
@@ -10129,6 +11979,14 @@ var init_strict_resolver = __esm({
10129
11979
  }
10130
11980
  return this.writeTargetRoot(pointer, scope);
10131
11981
  }
11982
+ expressionWriteRoot(expression, scope) {
11983
+ if (expression.writeRoot) return expression.writeRoot;
11984
+ if (expression.pointer.type === "value" /* Value */) return "local";
11985
+ if (expression.pointer.type === "function" /* Function */ && CONSTRUCTING_FUNCTION_KINDS.has(expression.pointer.function.type)) {
11986
+ return "local";
11987
+ }
11988
+ return this.writeThroughRoot(expression.pointer, scope);
11989
+ }
10132
11990
  /**
10133
11991
  * P43 §7.1. The write-target root a newly declared local inherits.
10134
11992
  *
@@ -10478,10 +12336,17 @@ function formatNeoScript(text, options = {}) {
10478
12336
  const tabSize = options.tabSize ?? 2;
10479
12337
  const indentUnit = options.insertSpaces === false ? " " : " ".repeat(tabSize);
10480
12338
  const lines = text.replace(/\r\n/g, "\n").split("\n");
12339
+ const normalizedText = lines.join("\n");
10481
12340
  const syntax = analyzeNeoScriptSyntax(
10482
- lines.join("\n"),
12341
+ normalizedText,
10483
12342
  options.kind ?? inferDocumentKind(text)
10484
12343
  );
12344
+ const splitCatchText = splitSameLineCatchClauses(
12345
+ normalizedText,
12346
+ syntax.lexed.tokens
12347
+ );
12348
+ if (splitCatchText !== normalizedText)
12349
+ return formatNeoScript(splitCatchText, options);
10485
12350
  const delimiterTokensByLine = /* @__PURE__ */ new Map();
10486
12351
  for (const token of syntax.lexed.tokens) {
10487
12352
  if (token.kind !== "punctuation" || !isDelimiter(token.text)) continue;
@@ -10505,8 +12370,14 @@ function formatNeoScript(text, options = {}) {
10505
12370
  lineTokens,
10506
12371
  firstContentCharacter
10507
12372
  );
10508
- const lineDepth = Math.max(0, depth - leadingClosers);
10509
- formatted.push(`${indentUnit.repeat(lineDepth)}${content}`);
12373
+ const contentOffset = syntax.lexed.source.offsetAt({
12374
+ line: lineIndex,
12375
+ character: firstContentCharacter
12376
+ });
12377
+ const sectionDepth = switchSectionIndentDepth(syntax.parsed, contentOffset);
12378
+ const lineDepth = Math.max(0, depth - leadingClosers) + sectionDepth;
12379
+ const normalized = normalizeControlHeaderSpacing(content);
12380
+ formatted.push(`${indentUnit.repeat(lineDepth)}${normalized}`);
10510
12381
  for (const token of lineTokens) {
10511
12382
  depth = isOpeningDelimiter(token.text) ? depth + 1 : Math.max(0, depth - 1);
10512
12383
  }
@@ -10523,6 +12394,53 @@ function formattingEdit(text, options) {
10523
12394
  const source = new SourceText(text);
10524
12395
  return [{ range: source.fullRange(), newText: formatted }];
10525
12396
  }
12397
+ function normalizeControlHeaderSpacing(content) {
12398
+ const withKeywordSpace = content.replace(
12399
+ /^(for|foreach|switch|catch)\s*\(/,
12400
+ "$1 ("
12401
+ );
12402
+ const withCatchFilterSpace = withKeywordSpace.replace(
12403
+ /\)\s*when\s*\(/,
12404
+ ") when ("
12405
+ );
12406
+ if (!withCatchFilterSpace.startsWith("foreach ("))
12407
+ return withCatchFilterSpace;
12408
+ return withCatchFilterSpace.replace(/\s+in\s+/, " in ");
12409
+ }
12410
+ function splitSameLineCatchClauses(text, tokens) {
12411
+ const boundaries = [];
12412
+ for (let index = 1; index < tokens.length; index++) {
12413
+ const catchKeyword = tokens[index];
12414
+ const preceding = tokens[index - 1];
12415
+ if (catchKeyword?.kind !== "keyword" || catchKeyword.text !== "catch" || preceding?.kind !== "punctuation" || preceding.text !== "}" || preceding.range.end.line !== catchKeyword.range.start.line) {
12416
+ continue;
12417
+ }
12418
+ if (!/^\s*$/.test(text.slice(preceding.end, catchKeyword.start))) continue;
12419
+ boundaries.push({ start: preceding.end, end: catchKeyword.start });
12420
+ }
12421
+ if (boundaries.length === 0) return text;
12422
+ let result = "";
12423
+ let cursor = 0;
12424
+ for (const boundary of boundaries) {
12425
+ result += `${text.slice(cursor, boundary.start)}
12426
+ `;
12427
+ cursor = boundary.end;
12428
+ }
12429
+ return result + text.slice(cursor);
12430
+ }
12431
+ function switchSectionIndentDepth(parsed, contentOffset) {
12432
+ let depth = 0;
12433
+ for (const statement of parsed.switches) {
12434
+ for (const section of statement.sections) {
12435
+ const lastLabel = section.labels.at(-1);
12436
+ if (!lastLabel) continue;
12437
+ if (contentOffset > lastLabel.colonEnd && contentOffset < section.bodyEnd) {
12438
+ depth++;
12439
+ }
12440
+ }
12441
+ }
12442
+ return depth;
12443
+ }
10526
12444
  function countLeadingClosers(lineTokens, firstContentCharacter) {
10527
12445
  let count = 0;
10528
12446
  let expectedCharacter = firstContentCharacter;
@@ -16028,7 +17946,8 @@ function parseProjectRootInitializer(source) {
16028
17946
  name: name.text,
16029
17947
  type: type.text,
16030
17948
  storage: storage.text,
16031
- initializer
17949
+ initializer,
17950
+ start: expressionStart.pos
16032
17951
  });
16033
17952
  }
16034
17953
  take("}");
@@ -16398,11 +18317,55 @@ function collectIdentifierReads(expression, names) {
16398
18317
  for (const inner of statement.elseBody ?? [])
16399
18318
  walkStatement(inner, visible);
16400
18319
  return;
18320
+ case "for": {
18321
+ walk(statement.initializer.init, visible);
18322
+ const inner = new Set(visible);
18323
+ inner.delete(statement.initializer.name);
18324
+ walk(statement.condition, inner);
18325
+ walkStatement(statement.iterator, inner);
18326
+ for (const bodyStatement of statement.body) {
18327
+ walkStatement(bodyStatement, inner);
18328
+ }
18329
+ return;
18330
+ }
18331
+ case "forEach": {
18332
+ walk(statement.collection, visible);
18333
+ const inner = new Set(visible);
18334
+ inner.delete(statement.name);
18335
+ for (const bodyStatement of statement.body) {
18336
+ walkStatement(bodyStatement, inner);
18337
+ }
18338
+ return;
18339
+ }
18340
+ case "switch":
18341
+ walk(statement.selector, visible);
18342
+ for (const section of statement.sections) {
18343
+ for (const label of section.labels) {
18344
+ if (label.kind === "case") walk(label.expression, visible);
18345
+ }
18346
+ for (const bodyStatement of section.body) {
18347
+ walkStatement(bodyStatement, visible);
18348
+ }
18349
+ }
18350
+ return;
18351
+ case "try":
18352
+ for (const bodyStatement of statement.body) {
18353
+ walkStatement(bodyStatement, visible);
18354
+ }
18355
+ for (const clause of statement.catches) {
18356
+ const catchScope = new Set(visible);
18357
+ catchScope.delete(clause.name);
18358
+ if (clause.filter) walk(clause.filter, catchScope);
18359
+ for (const bodyStatement of clause.body) {
18360
+ walkStatement(bodyStatement, catchScope);
18361
+ }
18362
+ }
18363
+ return;
16401
18364
  case "return":
16402
18365
  if (statement.expr) walk(statement.expr, visible);
16403
18366
  return;
16404
18367
  case "throw":
16405
- walk(statement.expr, visible);
18368
+ if (statement.expr) walk(statement.expr, visible);
16406
18369
  return;
16407
18370
  case "assign":
16408
18371
  walk(statement.target, visible);
@@ -16411,6 +18374,9 @@ function collectIdentifierReads(expression, names) {
16411
18374
  case "exprStmt":
16412
18375
  walk(statement.expr, visible);
16413
18376
  return;
18377
+ case "break":
18378
+ case "continue":
18379
+ return;
16414
18380
  }
16415
18381
  };
16416
18382
  walk(expression, names);
@@ -16470,10 +18436,20 @@ function validateProjectRootGlobal(uri, declaration, environment, diagnostics) {
16470
18436
  scope,
16471
18437
  environment,
16472
18438
  declaration.initializer.range,
16473
- diagnostics
18439
+ diagnostics,
18440
+ {
18441
+ text: actual.initializer,
18442
+ start: slotPosition(declaration.initializer.range.start, actual.start)
18443
+ }
16474
18444
  );
16475
18445
  }
16476
18446
  }
18447
+ function slotPosition(start, pos) {
18448
+ if (pos.line === 1) {
18449
+ return { line: start.line, character: start.character + pos.column - 1 };
18450
+ }
18451
+ return { line: start.line + pos.line - 1, character: pos.column - 1 };
18452
+ }
16477
18453
  function validateMember(uri, member, documentKind, ownerScope, environment, diagnostics) {
16478
18454
  const memberType2 = semanticType(member.type);
16479
18455
  validateAnnotations(
@@ -17098,7 +19074,7 @@ function validateStatements(statements, returnType, scope, environment, uri, ran
17098
19074
  range2,
17099
19075
  diagnostics
17100
19076
  );
17101
- } else if (statement.kind === "throw") {
19077
+ } else if (statement.kind === "throw" && statement.expr) {
17102
19078
  validateExpression(
17103
19079
  statement.expr,
17104
19080
  primitiveType("string"),
@@ -17108,6 +19084,40 @@ function validateStatements(statements, returnType, scope, environment, uri, ran
17108
19084
  range2,
17109
19085
  diagnostics
17110
19086
  );
19087
+ } else if (statement.kind === "try") {
19088
+ validateStatements(
19089
+ statement.body,
19090
+ returnType,
19091
+ new Map(scope),
19092
+ environment,
19093
+ uri,
19094
+ range2,
19095
+ diagnostics
19096
+ );
19097
+ for (const clause of statement.catches) {
19098
+ const catchScope = new Map(scope);
19099
+ catchScope.set(clause.name, primitiveType("string"));
19100
+ if (clause.filter) {
19101
+ validateExpression(
19102
+ clause.filter,
19103
+ primitiveType("bool"),
19104
+ catchScope,
19105
+ environment,
19106
+ uri,
19107
+ range2,
19108
+ diagnostics
19109
+ );
19110
+ }
19111
+ validateStatements(
19112
+ clause.body,
19113
+ returnType,
19114
+ catchScope,
19115
+ environment,
19116
+ uri,
19117
+ range2,
19118
+ diagnostics
19119
+ );
19120
+ }
17111
19121
  } else if (statement.kind === "if") {
17112
19122
  for (const branch of statement.branches) {
17113
19123
  validateExpression(
@@ -17490,7 +19500,11 @@ function constructionSiteRange(expression, target, fallback, anchor) {
17490
19500
  if (!entry) return fallback;
17491
19501
  return anchorSpan(anchor, entry.pos, entry.name.length);
17492
19502
  }
17493
- return fallback;
19503
+ return anchorSpan(
19504
+ anchor,
19505
+ expression.pos,
19506
+ NEOSCRIPT_CONSTRUCTOR_KEYWORD.length
19507
+ );
17494
19508
  }
17495
19509
  function namedArgumentRange(anchor, valuePos, name) {
17496
19510
  const prefix = anchor.text.slice(0, anchorOffset(anchor, valuePos));
@@ -18053,6 +20067,7 @@ var primitiveType, IDENTIFIER_PATTERN2, LIST_COLUMN_INHERITANCE_KEY;
18053
20067
  var init_project_source_semantics = __esm({
18054
20068
  "../packages/neoscript-language/src/project-source-semantics.ts"() {
18055
20069
  "use strict";
20070
+ init_language_spec();
18056
20071
  init_project_schema_contract_generated();
18057
20072
  init_strict_compile_error();
18058
20073
  init_strict_parser();
@@ -24705,7 +26720,9 @@ function memberType(member, environment, visiting, field) {
24705
26720
  string2(member.collectionMemberId, `${field}.collectionMemberId`)
24706
26721
  );
24707
26722
  const collectionType = collection ? memberType(collection, environment, next, `${field}.collection`) : unknownType();
24708
- const entryType = collectionElementType2(collectionType) ?? unknownType();
26723
+ const declared = optionalRecord(member.declaredType);
26724
+ const declaredType = declared ? manifestType2(declared, environment, `${field}.declaredType`) : null;
26725
+ const entryType = declaredType && !isUnknownType2(declaredType) ? declaredType : collectionElementType2(collectionType) ?? unknownType();
24709
26726
  return member.multiselect === true ? { kind: "set", elementType: entryType, nullable } : { ...entryType, nullable };
24710
26727
  }
24711
26728
  if (kind === "dialogueLookup") {
@@ -25033,6 +27050,9 @@ function primitive(name, nullable) {
25033
27050
  function unknownType() {
25034
27051
  return { kind: "primitive", name: "unknown" };
25035
27052
  }
27053
+ function isUnknownType2(type) {
27054
+ return type.kind === "primitive" && type.name === "unknown";
27055
+ }
25036
27056
  function indexById(values, field) {
25037
27057
  const result = /* @__PURE__ */ new Map();
25038
27058
  values.forEach((value, index) => {
@@ -33513,6 +35533,9 @@ function isNSTypeInfoCollectionInternal(value, ancestors) {
33513
35533
  }
33514
35534
  return true;
33515
35535
  }
35536
+ function isNSTypeInfoLookup(value) {
35537
+ return isNSTypeInfoLookupInternal(value, /* @__PURE__ */ new Set());
35538
+ }
33516
35539
  function isNSTypeInfoLookupInternal(value, ancestors) {
33517
35540
  if (!isNSTypeInfoBase(value)) return false;
33518
35541
  if (value.type !== 9 /* Lookup */) return false;
@@ -33781,6 +35804,20 @@ function isNSFunctionWithReturnType(value) {
33781
35804
  if (!Array.isArray(v.parameters)) return false;
33782
35805
  if (!v.parameters.every(isNSVariable)) return false;
33783
35806
  if (!isNSInstructions(v.instructions)) return false;
35807
+ if ([
35808
+ "for" /* for */,
35809
+ "forEach" /* forEach */,
35810
+ "break" /* break */,
35811
+ "continue" /* continue */
35812
+ ].some((type) => nsInstructionsContainType(v.instructions ?? [], type)) && (v.compilerRevision ?? 1) < 4) {
35813
+ return false;
35814
+ }
35815
+ if (nsInstructionsContainType(v.instructions, "switch" /* switch */) && (v.compilerRevision ?? 1) < 5) {
35816
+ return false;
35817
+ }
35818
+ if (nsInstructionsContainType(v.instructions, "try" /* try */) && (v.compilerRevision ?? 1) < 6) {
35819
+ return false;
35820
+ }
33784
35821
  if (!isNSTypeInfo(v.typeInfo)) return false;
33785
35822
  return true;
33786
35823
  }
@@ -34038,8 +36075,159 @@ function isNSInstructionFunctionCall(value) {
34038
36075
  if (v?.type !== "functionCall" /* functionCall */) return false;
34039
36076
  return isNSPointerCallFunction(v.call);
34040
36077
  }
36078
+ function isNSLoopBinding(value) {
36079
+ const binding = value;
36080
+ if (typeof binding !== "object" || binding === null) return false;
36081
+ if (typeof binding.id !== "string" || binding.id.length === 0) return false;
36082
+ if (!isNSTypeInfo(binding.typeInfo)) return false;
36083
+ if (binding.readonly !== true) return false;
36084
+ if (binding.writability === void 0) return true;
36085
+ return Object.values(NSWritability).includes(binding.writability);
36086
+ }
36087
+ function isNSInstructionFor(value) {
36088
+ const instruction = value;
36089
+ if (instruction?.type !== "for" /* for */) return false;
36090
+ if (!isNSVariable(instruction.initializer)) return false;
36091
+ if (!isNSBooleanExpression(instruction.condition)) return false;
36092
+ if (!isNSInstructionAssign(instruction.iterator)) return false;
36093
+ return isNSInstructions(instruction.instructions);
36094
+ }
36095
+ function isNSInstructionForEach(value) {
36096
+ const instruction = value;
36097
+ if (instruction?.type !== "forEach" /* forEach */) return false;
36098
+ if (!isNSLoopBinding(instruction.binding)) return false;
36099
+ if (!isNSPointer(instruction.collectionPointer)) return false;
36100
+ if (!isNSTypeInfoCollection(instruction.collectionTypeInfo) && !isNSTypeInfoLookup(instruction.collectionTypeInfo)) {
36101
+ return false;
36102
+ }
36103
+ if (instruction.collectionTypeInfo.required !== true) return false;
36104
+ return isNSInstructions(instruction.instructions);
36105
+ }
36106
+ function isNSInstructionBreak(value) {
36107
+ const instruction = value;
36108
+ return instruction?.type === "break" /* break */;
36109
+ }
36110
+ function isNSInstructionContinue(value) {
36111
+ const instruction = value;
36112
+ return instruction?.type === "continue" /* continue */;
36113
+ }
36114
+ function isNSSwitchSelectorType(value) {
36115
+ if (!isNSTypeInfo(value)) return false;
36116
+ return value.type === 2 /* Int */ || value.type === 3 /* String */ || value.type === 1 /* Bool */ || value.type === 8 /* Enum */;
36117
+ }
36118
+ function nsSwitchLabelKey(value, selectorTypeInfo) {
36119
+ if (!isNSValue(value)) return null;
36120
+ if (value.typeInfo.required !== true) return null;
36121
+ if (value.typeInfo.type === 0 /* Null */) {
36122
+ return value.value === null && selectorTypeInfo.required === false ? "null" : null;
36123
+ }
36124
+ if (value.typeInfo.type !== selectorTypeInfo.type) return null;
36125
+ switch (value.typeInfo.type) {
36126
+ case 2 /* Int */:
36127
+ return typeof value.value === "number" && Number.isSafeInteger(value.value) ? `int:${String(value.value)}` : null;
36128
+ case 3 /* String */:
36129
+ return typeof value.value === "string" ? `string:${JSON.stringify(value.value)}` : null;
36130
+ case 1 /* Bool */:
36131
+ return typeof value.value === "boolean" ? `bool:${String(value.value)}` : null;
36132
+ case 8 /* Enum */:
36133
+ if (selectorTypeInfo.type !== 8 /* Enum */ || value.typeInfo.enumId !== selectorTypeInfo.enumId || !Array.isArray(value.value) || value.value.length !== 1 || typeof value.value[0] !== "string" || value.value[0].length === 0) {
36134
+ return null;
36135
+ }
36136
+ return `enum:${value.typeInfo.enumId}:${JSON.stringify(value.value[0])}`;
36137
+ default:
36138
+ return null;
36139
+ }
36140
+ }
36141
+ function isNSSwitchSection(value, selectorTypeInfo, seenLabels) {
36142
+ const section = value;
36143
+ if (typeof section !== "object" || section === null) return false;
36144
+ if (!Array.isArray(section.labels) || section.labels.length === 0) {
36145
+ return false;
36146
+ }
36147
+ for (const label of section.labels) {
36148
+ const key = nsSwitchLabelKey(label, selectorTypeInfo);
36149
+ if (key === null || seenLabels.has(key)) return false;
36150
+ seenLabels.add(key);
36151
+ }
36152
+ return isNSInstructions(section.instructions);
36153
+ }
36154
+ function isNSInstructionSwitch(value) {
36155
+ const instruction = value;
36156
+ if (instruction?.type !== "switch" /* switch */) return false;
36157
+ if (!isNSPointer(instruction.selector)) return false;
36158
+ if (!isNSSwitchSelectorType(instruction.selectorTypeInfo)) return false;
36159
+ if (!Array.isArray(instruction.sections)) return false;
36160
+ const selectorTypeInfo = instruction.selectorTypeInfo;
36161
+ const seenLabels = /* @__PURE__ */ new Set();
36162
+ if (!instruction.sections.every(
36163
+ (section) => isNSSwitchSection(section, selectorTypeInfo, seenLabels)
36164
+ )) {
36165
+ return false;
36166
+ }
36167
+ return instruction.defaultInstructions === void 0 || instruction.defaultInstructions === null || isNSInstructions(instruction.defaultInstructions);
36168
+ }
36169
+ function isNSCatchClause(value, bindingIds, isFinal) {
36170
+ const clause = value;
36171
+ if (typeof clause !== "object" || clause === null) return false;
36172
+ const binding = clause.binding;
36173
+ if (typeof binding !== "object" || binding === null) return false;
36174
+ if (typeof binding.id !== "string" || binding.id.length === 0) return false;
36175
+ if (bindingIds.has(binding.id)) return false;
36176
+ bindingIds.add(binding.id);
36177
+ if (!isNSTypeInfo(binding.typeInfo)) return false;
36178
+ if (binding.typeInfo.type !== 3 /* String */) return false;
36179
+ if (binding.typeInfo.required !== true) return false;
36180
+ if (binding.readonly !== true) return false;
36181
+ const hasFilter = clause.filter !== void 0 && clause.filter !== null;
36182
+ if (!hasFilter && !isFinal) return false;
36183
+ if (hasFilter && !isNSBooleanExpression(clause.filter)) return false;
36184
+ return isNSInstructions(clause.instructions);
36185
+ }
36186
+ function isNSInstructionTry(value) {
36187
+ const instruction = value;
36188
+ if (instruction?.type !== "try" /* try */) return false;
36189
+ if (!isNSInstructions(instruction.instructions)) return false;
36190
+ if (!Array.isArray(instruction.catches)) return false;
36191
+ if (instruction.catches.length === 0) return false;
36192
+ const bindingIds = /* @__PURE__ */ new Set();
36193
+ for (let index = 0; index < instruction.catches.length; index += 1) {
36194
+ const clause = instruction.catches[index];
36195
+ if (!isNSCatchClause(
36196
+ clause,
36197
+ bindingIds,
36198
+ index === instruction.catches.length - 1
36199
+ )) {
36200
+ return false;
36201
+ }
36202
+ }
36203
+ return true;
36204
+ }
34041
36205
  function isNSInstruction(value) {
34042
- return isNSInstructionVariable(value) || isNSInstructionIfBranch(value) || isNSInstructionReturn(value) || isNSInstructionThrow(value) || isNSInstructionAssign(value) || isNSInstructionCollectionCall(value) || isNSInstructionFunctionCall(value);
36206
+ return isNSInstructionVariable(value) || isNSInstructionIfBranch(value) || isNSInstructionReturn(value) || isNSInstructionThrow(value) || isNSInstructionAssign(value) || isNSInstructionCollectionCall(value) || isNSInstructionFunctionCall(value) || isNSInstructionFor(value) || isNSInstructionForEach(value) || isNSInstructionBreak(value) || isNSInstructionContinue(value) || isNSInstructionSwitch(value) || isNSInstructionTry(value);
36207
+ }
36208
+ function nsInstructionsContainType(instructions, type) {
36209
+ return instructions.some((instruction) => {
36210
+ if (instruction.type === type) return true;
36211
+ if (instruction.type === "if" /* if */) {
36212
+ return instruction.branches.some(
36213
+ (branch) => nsInstructionsContainType(branch.instructions, type)
36214
+ ) || instruction.else !== null && instruction.else !== void 0 && nsInstructionsContainType(instruction.else, type);
36215
+ }
36216
+ if (instruction.type === "for" /* for */ || instruction.type === "forEach" /* forEach */) {
36217
+ return nsInstructionsContainType(instruction.instructions, type);
36218
+ }
36219
+ if (instruction.type === "switch" /* switch */) {
36220
+ return instruction.sections.some(
36221
+ (section) => nsInstructionsContainType(section.instructions, type)
36222
+ ) || instruction.defaultInstructions !== null && instruction.defaultInstructions !== void 0 && nsInstructionsContainType(instruction.defaultInstructions, type);
36223
+ }
36224
+ if (instruction.type === "try" /* try */) {
36225
+ return nsInstructionsContainType(instruction.instructions, type) || instruction.catches.some(
36226
+ (clause) => nsInstructionsContainType(clause.instructions, type)
36227
+ );
36228
+ }
36229
+ return false;
36230
+ });
34043
36231
  }
34044
36232
  function isNSInstructions(value) {
34045
36233
  return Array.isArray(value) && value.every(isNSInstruction);
@@ -48711,6 +50899,7 @@ function drainValueReferenceObligationsV4(state, manifest, options) {
48711
50899
  for (const recorded of options.registry.loweringFailures) {
48712
50900
  failures.push({
48713
50901
  message: recorded.message,
50902
+ ...recorded.code === void 0 ? {} : { code: recorded.code },
48714
50903
  ...composeReferenceSitePosition(recorded.site, options.sourceTextByUri)
48715
50904
  });
48716
50905
  }
@@ -50146,59 +52335,96 @@ function resolveConstructorProjectionArguments(context, schemaClass2, expression
50146
52335
  context.classes,
50147
52336
  schemaClass2.id
50148
52337
  );
52338
+ const signature = describeProjectedConstructor(schemaClass2.name, projections);
50149
52339
  if (projections.length === 0) {
50150
52340
  if (expression.args.length > 0) {
50151
- throw new Error(
50152
- `Class ${schemaClass2.name} does not accept constructor arguments.`
50153
- );
52341
+ context.loweringFailures.push({
52342
+ message: `Class '${schemaClass2.name}' projects no constructor parameters, so '${schemaClass2.name}()' takes no arguments \u2014 this call passes ${expression.args.length}.`,
52343
+ code: "unbound-constructor-argument",
52344
+ site: referenceSite(source, expression)
52345
+ });
50154
52346
  }
50155
52347
  return [];
50156
52348
  }
50157
52349
  const names = expression.argumentNames ?? [];
50158
- if (expression.args.length !== projections.length || names.length !== projections.length) {
50159
- throw new Error(
50160
- `Class ${schemaClass2.name} requires named constructor argument${projections.length === 1 ? "" : "s"} ${projections.map((entry) => entry.parameterName).join(", ")}.`
50161
- );
52350
+ if (expression.args.length !== projections.length) {
52351
+ context.loweringFailures.push({
52352
+ message: `Constructor '${signature}' takes ${projections.length} argument${projections.length === 1 ? "" : "s"}, but this call passes ${expression.args.length} \u2014 parameters: ${describeQuotedNames(projections.map((entry) => entry.parameterName))}.`,
52353
+ code: "missing-constructor-argument",
52354
+ site: referenceSite(source, expression)
52355
+ });
52356
+ return [];
52357
+ }
52358
+ if (names.length !== projections.length) {
52359
+ context.loweringFailures.push({
52360
+ message: `Constructor '${signature}' takes named arguments only, so this call must name all ${projections.length} of them and names ${names.length} \u2014 parameters: ${describeQuotedNames(projections.map((entry) => entry.parameterName))}.`,
52361
+ code: "unbound-constructor-argument",
52362
+ site: referenceSite(source, expression)
52363
+ });
52364
+ return [];
50162
52365
  }
50163
52366
  const seen = /* @__PURE__ */ new Set();
50164
52367
  const resolved = [];
50165
52368
  for (let index = 0; index < expression.args.length; index++) {
52369
+ const argument2 = expression.args[index];
50166
52370
  const parameterName = names[index];
50167
52371
  if (parameterName === null || parameterName === void 0) {
50168
- throw new Error(
50169
- `Class ${schemaClass2.name} constructor arguments must be named.`
50170
- );
52372
+ context.loweringFailures.push({
52373
+ message: `Constructor '${signature}' takes named arguments only, so the argument at position ${index + 1} binds to no parameter \u2014 candidates: ${describeQuotedNames(projections.map((entry) => entry.parameterName))}.`,
52374
+ code: "unbound-constructor-argument",
52375
+ site: referenceSite(source, argument2)
52376
+ });
52377
+ continue;
50171
52378
  }
50172
52379
  if (seen.has(parameterName)) {
50173
- throw new Error(
50174
- `Class ${schemaClass2.name} constructor argument ${parameterName} is duplicated.`
50175
- );
52380
+ context.loweringFailures.push({
52381
+ message: `Constructor '${signature}' is passed '${parameterName}' more than once, and each parameter binds exactly one argument.`,
52382
+ code: "duplicate-constructor-argument",
52383
+ site: namedArgumentSite(source, argument2, parameterName)
52384
+ });
52385
+ continue;
50176
52386
  }
50177
52387
  seen.add(parameterName);
50178
52388
  const projection = projections.find(
50179
52389
  (candidate) => candidate.parameterName === parameterName
50180
52390
  );
50181
52391
  if (projection === void 0) {
50182
- throw new Error(
50183
- `Class ${schemaClass2.name} has no constructor argument ${parameterName}.`
50184
- );
52392
+ context.loweringFailures.push({
52393
+ message: `Constructor '${signature}' has no parameter '${parameterName}' \u2014 candidates: ${describeQuotedNames(projections.map((entry) => entry.parameterName))}.`,
52394
+ code: "unknown-constructor-argument",
52395
+ site: namedArgumentSite(source, argument2, parameterName)
52396
+ });
52397
+ continue;
50185
52398
  }
50186
- const argument2 = expression.args[index];
50187
52399
  if (argument2.kind !== "litString") {
50188
- throw new Error(
50189
- `Class ${schemaClass2.name} constructor argument ${parameterName} must be a value-row id string.`
50190
- );
52400
+ context.loweringFailures.push({
52401
+ message: `Constructor '${signature}' parameter '${parameterName}' projects a row identity, so its argument must be a value-row id string literal.`,
52402
+ code: "non-literal-constructor-argument",
52403
+ site: referenceSite(source, argument2)
52404
+ });
52405
+ continue;
50191
52406
  }
50192
52407
  const schemaKey = inheritedProjectionSchemaKey(
50193
52408
  context.classes,
50194
52409
  schemaClass2.id,
50195
52410
  projection.memberId
50196
52411
  );
52412
+ if (schemaKey === null) {
52413
+ context.loweringFailures.push({
52414
+ message: `Constructor '${signature}' parameter '${parameterName}' projects member ${projection.memberId}, which no class in '${schemaClass2.name}'s inheritance chain declares under a schema key.`,
52415
+ code: "unprojectable-constructor-argument",
52416
+ site: referenceSite(source, argument2)
52417
+ });
52418
+ continue;
52419
+ }
50197
52420
  const projectedMember = context.members.get(projection.memberId);
50198
- if (schemaKey === null || projectedMember?.kind !== "lookup") {
50199
- throw new Error(
50200
- `Class ${schemaClass2.name} constructor projection ${parameterName} does not resolve to a Lookup field.`
50201
- );
52421
+ if (projectedMember?.kind !== "lookup") {
52422
+ context.loweringFailures.push({
52423
+ message: `Constructor '${signature}' parameter '${parameterName}' projects member ${projection.memberId}, which is ${projectedMember === void 0 ? "in no member of this project" : `a ${projectedMember.kind} member`} and not the Lookup field a projection writes.`,
52424
+ code: "unprojectable-constructor-argument",
52425
+ site: referenceSite(source, argument2)
52426
+ });
52427
+ continue;
50202
52428
  }
50203
52429
  context.referenceObligations.push({
50204
52430
  kind: "constructorProjection",
@@ -50219,6 +52445,14 @@ function resolveConstructorProjectionArguments(context, schemaClass2, expression
50219
52445
  }
50220
52446
  return resolved;
50221
52447
  }
52448
+ function describeProjectedConstructor(className, projections) {
52449
+ return `${className}(${projections.map((entry) => entry.parameterName).join(", ")})`;
52450
+ }
52451
+ function describeQuotedNames(names) {
52452
+ const quoted = names.map((name) => `'${name}'`);
52453
+ if (quoted.length <= 1) return quoted.join("") || "none";
52454
+ return `${quoted.slice(0, -1).join(", ")} and ${quoted.at(-1)}`;
52455
+ }
50222
52456
  function validateAnimationProjectionBinding(context, schemaClass2, environment, targetValueId) {
50223
52457
  if (schemaClass2.system?.worldKind !== "animationChildOverride") return;
50224
52458
  const parameter3 = schemaClass2.genericParameters[0];
@@ -50653,16 +52887,42 @@ function lowerReferences(context, member, expression, ownerValueId, source) {
50653
52887
  return ids;
50654
52888
  }
50655
52889
  function referenceSite(source, expression) {
52890
+ return referenceSiteAt(source, expressionAnchorPos(expression));
52891
+ }
52892
+ function referenceSiteAt(source, pos) {
50656
52893
  return {
50657
52894
  uri: source.source.uri,
50658
52895
  declarationStart: source.source.range.start,
50659
- expression: {
50660
- initializer: source.initializer,
50661
- pos: expressionAnchorPos(expression)
50662
- },
52896
+ expression: { initializer: source.initializer, pos },
50663
52897
  label: source.label
50664
52898
  };
50665
52899
  }
52900
+ function namedArgumentSite(source, argument2, name) {
52901
+ const value = expressionAnchorPos(argument2);
52902
+ const text = source.initializer;
52903
+ const prefix = text.slice(0, initializerOffsetAtPos(text, value));
52904
+ const colon = prefix.lastIndexOf(":");
52905
+ if (colon < 0) return referenceSiteAt(source, value);
52906
+ if (prefix.slice(colon + 1).trim().length > 0) {
52907
+ return referenceSiteAt(source, value);
52908
+ }
52909
+ const beforeColon = prefix.slice(0, colon).trimEnd();
52910
+ if (!beforeColon.endsWith(name)) return referenceSiteAt(source, value);
52911
+ const start = sourcePositionAtOffset(text, beforeColon.length - name.length);
52912
+ return referenceSiteAt(source, {
52913
+ line: start.line + 1,
52914
+ column: start.character + 1
52915
+ });
52916
+ }
52917
+ function initializerOffsetAtPos(text, pos) {
52918
+ let offset = 0;
52919
+ for (let line = 1; line < pos.line; line += 1) {
52920
+ const next = text.indexOf("\n", offset);
52921
+ if (next < 0) return text.length;
52922
+ offset = next + 1;
52923
+ }
52924
+ return Math.min(offset + pos.column - 1, text.length);
52925
+ }
50666
52926
  function expressionAnchorPos(expression) {
50667
52927
  if (expression.kind === "annotated") {
50668
52928
  return expressionAnchorPos(expression.expression);
@@ -50874,7 +53134,14 @@ function validateReferenceContract(context, member, target, ownerValueId) {
50874
53134
  const actualClassId = targetData && typeof targetData.classId === "string" ? targetData.classId : null;
50875
53135
  if (expectedClassId && actualClassId && !classAssignableToClass(context, actualClassId, expectedClassId)) {
50876
53136
  throw new Error(
50877
- `Lookup member ${member.name} target ${target.id} is ${context.classes.get(actualClassId)?.name ?? actualClassId}, but collection ${collectionMemberName(context, member)} holds ${context.classes.get(expectedClassId)?.name ?? expectedClassId}.`
53137
+ `Lookup member ${member.name} target ${target.id} is ${classDisplayName(context, actualClassId)}, but collection ${collectionMemberName(context, member)} holds ${classDisplayName(context, expectedClassId)}.`
53138
+ );
53139
+ }
53140
+ const declaredClassId = lookupDeclaredClassId(context, member);
53141
+ if (declaredClassId && actualClassId && !classAssignableToClass(context, actualClassId, declaredClassId)) {
53142
+ const narrowedFrom = expectedClassId === null ? `the collection ${collectionMemberName(context, member)}` : `the collection ${collectionMemberName(context, member)} holds ${classDisplayName(context, expectedClassId)}`;
53143
+ throw new Error(
53144
+ `Lookup member ${member.name} target ${target.id} is ${classDisplayName(context, actualClassId)}, but the member declares ${classDisplayName(context, declaredClassId)} \u2014 ${narrowedFrom} and this member narrows it. Point it at a ${classDisplayName(context, declaredClassId)} row.`
50878
53145
  );
50879
53146
  }
50880
53147
  if (target.genericTypeName && expectedTypeName && !referenceTypeNameAssignable(
@@ -50901,6 +53168,15 @@ function validateReferenceContract(context, member, target, ownerValueId) {
50901
53168
  }
50902
53169
  validateDialogueEligibility(context, member, target.id);
50903
53170
  }
53171
+ function lookupDeclaredClassId(context, member) {
53172
+ const declared = member.declaredType;
53173
+ if (declared === null) return null;
53174
+ if (declared.kind !== "class") return null;
53175
+ return context.classes.has(declared.classId) ? declared.classId : null;
53176
+ }
53177
+ function classDisplayName(context, classId) {
53178
+ return context.classes.get(classId)?.name ?? classId;
53179
+ }
50904
53180
  function lookupEntryTypeName(context, member) {
50905
53181
  const collection = context.members.get(member.collectionMemberId);
50906
53182
  const entry = collection?.kind === "list" || collection?.kind === "dictionary" ? context.members.get(collection.entryMemberId) : null;
@@ -56756,12 +59032,14 @@ function computeWorkspaceStatus(workspace, options = {}) {
56756
59032
  }
56757
59033
  );
56758
59034
  for (const failure of referenceFailures) {
59035
+ const message = failure.code === void 0 ? failure.message : `${failure.code}: ${failure.message}`;
56759
59036
  parseErrors.push(
56760
59037
  new SchemaSourceError(
56761
- failure.message,
59038
+ message,
56762
59039
  failure.file,
56763
59040
  failure.line,
56764
- failure.column
59041
+ failure.column,
59042
+ failure.code
56765
59043
  )
56766
59044
  );
56767
59045
  }
@@ -62513,6 +64791,20 @@ function memberToSymbol(record3, schemaKey, containingClass2, ownerClassId2, ind
62513
64791
  writability: writable ? "setter" : "readOnly"
62514
64792
  };
62515
64793
  }
64794
+ if (isMemberNSPropertyContractBase(resolved)) {
64795
+ return {
64796
+ ...common,
64797
+ kind: "property",
64798
+ type: toLanguageType(
64799
+ resolved.returnTypeInfo,
64800
+ context,
64801
+ genericEnvironment
64802
+ ),
64803
+ writable: false,
64804
+ computed: true,
64805
+ writability: "readOnly"
64806
+ };
64807
+ }
62516
64808
  if (isMemberLookupBase(resolved)) {
62517
64809
  const collectionWritability = uniqueEffectiveWritability(
62518
64810
  resolved.collectionMemberId,
@@ -62861,27 +65153,26 @@ function memberRuntimeType(record3, context, seen = /* @__PURE__ */ new Set(), g
62861
65153
  }
62862
65154
  case 9 /* Lookup */: {
62863
65155
  if (!isMemberLookupBase(member)) return UNKNOWN_TYPE2;
62864
- const collection = analyzerMemberById(
62865
- context.vm,
62866
- member.collectionMemberId
62867
- );
62868
- if (!collection) return UNKNOWN_TYPE2;
62869
- const collectionType = memberRuntimeType(
62870
- collection,
65156
+ const declared = member.declaredTypeInfo ? toLanguageType(member.declaredTypeInfo, context, genericEnvironment) : null;
65157
+ const narrowed = declared && !isUnknownType3(declared) ? declared : null;
65158
+ const entry = narrowed ?? lookupCollectionEntryType(
65159
+ member.collectionMemberId,
62871
65160
  context,
62872
65161
  nextSeen,
62873
65162
  genericEnvironment
62874
65163
  );
62875
- const entry = collectionEntryType(collectionType);
62876
65164
  if (!entry) return UNKNOWN_TYPE2;
62877
65165
  return member.multiselect ? {
62878
65166
  kind: "set",
62879
65167
  elementType: requiredType(entry),
62880
65168
  nullable: !required2
62881
- } : { ...entry, nullable: true };
65169
+ } : { ...entry, nullable: !required2 };
62882
65170
  }
62883
65171
  case 10 /* NSProperty */:
62884
- return isMemberNSPropertyBase(member) ? toLanguageType(member.returnTypeInfo, context) : UNKNOWN_TYPE2;
65172
+ if (isMemberNSPropertyBase(member) || isMemberNSPropertyContractBase(member)) {
65173
+ return toLanguageType(member.returnTypeInfo, context);
65174
+ }
65175
+ return UNKNOWN_TYPE2;
62885
65176
  case 13 /* Function */:
62886
65177
  case 23 /* NSFunction */:
62887
65178
  return primitive2("void", true);
@@ -63132,6 +65423,16 @@ function collectionEntryType(type) {
63132
65423
  if (type.kind === "dictionary") return type.valueType;
63133
65424
  return null;
63134
65425
  }
65426
+ function lookupCollectionEntryType(collectionMemberId, context, seen, genericEnvironment) {
65427
+ const collection = analyzerMemberById(context.vm, collectionMemberId);
65428
+ if (!collection) return null;
65429
+ return collectionEntryType(
65430
+ memberRuntimeType(collection, context, new Set(seen), genericEnvironment)
65431
+ );
65432
+ }
65433
+ function isUnknownType3(type) {
65434
+ return type.kind === "primitive" && type.name === "unknown";
65435
+ }
63135
65436
  function projectLanguageVersion(context) {
63136
65437
  const records2 = [
63137
65438
  ...context.vm.classes,
@@ -63495,6 +65796,165 @@ var init_compiler_adapter = __esm({
63495
65796
  }
63496
65797
  });
63497
65798
 
65799
+ // src/commands/push-body-diagnostics.ts
65800
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
65801
+ import { join as join12 } from "node:path";
65802
+ function createNeoScriptBodySourceLocator(workspace, status) {
65803
+ const textByFile = /* @__PURE__ */ new Map();
65804
+ return {
65805
+ originOf(site) {
65806
+ if (site.recordKind === "script") return null;
65807
+ const record3 = status.reconstructed.get(
65808
+ recordStateKey(site.recordKind, site.recordId)
65809
+ );
65810
+ if (record3 === void 0) return null;
65811
+ const span2 = record3.sourceSpan;
65812
+ if (span2 === void 0) {
65813
+ return {
65814
+ file: record3.file,
65815
+ declarationStart: { line: record3.line - 1, character: 0 }
65816
+ };
65817
+ }
65818
+ return { file: span2.path, declarationStart: span2.start };
65819
+ },
65820
+ textOf(file) {
65821
+ const cached = textByFile.get(file);
65822
+ if (cached !== void 0) return cached;
65823
+ const path = join12(workspace.root, file);
65824
+ const text = existsSync11(path) ? readFileSync10(path, "utf8") : null;
65825
+ textByFile.set(file, text);
65826
+ return text;
65827
+ }
65828
+ };
65829
+ }
65830
+ function compileNeoScriptBodyOrThrow(compile, site, locator) {
65831
+ try {
65832
+ return compile();
65833
+ } catch (error) {
65834
+ if (!(error instanceof CompileError)) throw error;
65835
+ throw positionNeoScriptBodyCompileError(error, site, locator);
65836
+ }
65837
+ }
65838
+ function describeBodySite(site) {
65839
+ if (site.recordKind === "script") {
65840
+ return `${unitLabel(site.unit)} compiled from ${site.origin}`;
65841
+ }
65842
+ if (site.recordKind === "migration") {
65843
+ if (site.ownerName === null) return `Migration '${site.memberName}'`;
65844
+ return `Migration '${site.memberName}' (target ${site.ownerName})`;
65845
+ }
65846
+ const unit = unitLabel(site.unit);
65847
+ if (site.ownerName === null) {
65848
+ return `${unit} '${site.memberName}' (member ${site.recordId}, whose declaring class is not in this push's schema)`;
65849
+ }
65850
+ return `${unit} '${site.ownerName}.${site.memberName}'`;
65851
+ }
65852
+ function unitLabel(unit) {
65853
+ return `${unit.charAt(0).toUpperCase()}${unit.slice(1)}`;
65854
+ }
65855
+ function positionNeoScriptBodyCompileError(error, site, locator) {
65856
+ const subject = describeBodySite(site);
65857
+ const message = error.message.replace(`${error.line}:${error.column}: `, "");
65858
+ const inBody = `at ${error.line}:${error.column} in the compiled body`;
65859
+ if (site.recordKind === "script") {
65860
+ return new NeoScriptBodyCompileError(
65861
+ `${subject}: ${message} (${inBody}; this body was handed to the CLI directly, so no declaring file exists to map it to)`,
65862
+ error.line,
65863
+ error.column
65864
+ );
65865
+ }
65866
+ if (locator === null) {
65867
+ return new NeoScriptBodyCompileError(
65868
+ `${subject}: ${message} (${inBody}; no workspace source locator was supplied for this compile, so it could not be mapped to a file)`,
65869
+ error.line,
65870
+ error.column
65871
+ );
65872
+ }
65873
+ const origin = locator.originOf(site);
65874
+ if (origin === null) {
65875
+ return new NeoScriptBodyCompileError(
65876
+ `${subject}: ${message} (${inBody}; this body has no reconstructed source record in the workspace, so it could not be mapped to a file)`,
65877
+ error.line,
65878
+ error.column
65879
+ );
65880
+ }
65881
+ const text = locator.textOf(origin.file);
65882
+ if (text === null) {
65883
+ return new SchemaSourceError(
65884
+ `${subject}: ${message} (${inBody}; ${origin.file} could not be read, so the position is the declaration's)`,
65885
+ origin.file,
65886
+ origin.declarationStart.line + 1,
65887
+ origin.declarationStart.character + 1
65888
+ );
65889
+ }
65890
+ const composed = composeBodyPosition(text, origin, site.code, error);
65891
+ if (composed === null) {
65892
+ return new SchemaSourceError(
65893
+ `${subject}: ${message} (${inBody}; the authored body text was not found in ${origin.file}, so the position is the declaration's)`,
65894
+ origin.file,
65895
+ origin.declarationStart.line + 1,
65896
+ origin.declarationStart.character + 1
65897
+ );
65898
+ }
65899
+ return new SchemaSourceError(
65900
+ `${subject}: ${message}`,
65901
+ origin.file,
65902
+ composed.line,
65903
+ composed.column
65904
+ );
65905
+ }
65906
+ function composeBodyPosition(text, origin, code, error) {
65907
+ const declarationOffset = offsetAtPosition(text, origin.declarationStart);
65908
+ if (declarationOffset === null) return null;
65909
+ const codeOffset = text.indexOf(code, declarationOffset);
65910
+ if (codeOffset < 0) return null;
65911
+ const start = positionAtOffset(text, codeOffset);
65912
+ if (error.line === 1) {
65913
+ return { line: start.line + 1, column: start.character + error.column };
65914
+ }
65915
+ return { line: start.line + error.line, column: error.column };
65916
+ }
65917
+ function offsetAtPosition(text, position) {
65918
+ let offset = 0;
65919
+ for (let line = 0; line < position.line; line += 1) {
65920
+ const next = text.indexOf("\n", offset);
65921
+ if (next < 0) return null;
65922
+ offset = next + 1;
65923
+ }
65924
+ const candidate = offset + position.character;
65925
+ return candidate > text.length ? null : candidate;
65926
+ }
65927
+ function positionAtOffset(text, offset) {
65928
+ let line = 0;
65929
+ let lineStart = 0;
65930
+ for (let cursor = 0; cursor < offset; cursor += 1) {
65931
+ if (text[cursor] === "\n") {
65932
+ line += 1;
65933
+ lineStart = cursor + 1;
65934
+ }
65935
+ }
65936
+ return { line, character: offset - lineStart };
65937
+ }
65938
+ var NeoScriptBodyCompileError;
65939
+ var init_push_body_diagnostics = __esm({
65940
+ "src/commands/push-body-diagnostics.ts"() {
65941
+ "use strict";
65942
+ init_compile_error();
65943
+ init_source_diagnostics();
65944
+ init_workspace();
65945
+ NeoScriptBodyCompileError = class extends Error {
65946
+ constructor(message, line, column) {
65947
+ super(message);
65948
+ this.line = line;
65949
+ this.column = column;
65950
+ this.name = "NeoScriptBodyCompileError";
65951
+ }
65952
+ line;
65953
+ column;
65954
+ };
65955
+ }
65956
+ });
65957
+
63498
65958
  // ../src/view-models/neoscript-evaluator/NSGetterRuntimeError.ts
63499
65959
  var NSGetterRuntimeError;
63500
65960
  var init_NSGetterRuntimeError = __esm({
@@ -63744,6 +66204,9 @@ var init_decimal = __esm({
63744
66204
  });
63745
66205
 
63746
66206
  // ../src/view-models/neoscript-evaluator/evaluateNSGetter.ts
66207
+ function isCatchableNSRuntimeError(error) {
66208
+ return error instanceof NSGetterRuntimeError && !(error instanceof NonCatchableNSGetterRuntimeError);
66209
+ }
63747
66210
  function makeEvaluatorLookups(members, values) {
63748
66211
  const attrMap = /* @__PURE__ */ new Map();
63749
66212
  for (const a of members) attrMap.set(a.id, a);
@@ -63958,6 +66421,7 @@ function evaluateNSGetterWithEffects(getter, ctx, argumentValues = []) {
63958
66421
  );
63959
66422
  return createdSessionValues.length === 0 ? { value: result.value, writes } : { value: result.value, writes, createdSessionValues };
63960
66423
  }
66424
+ rejectEscapedLoopTransfer(result, "NeoScript getter");
63961
66425
  } catch (error) {
63962
66426
  finalizeConstructorAllocations(runtimeCtx, void 0, ownsInvocationState);
63963
66427
  throw error;
@@ -63983,6 +66447,7 @@ function evaluateNSAction(action, ctx) {
63983
66447
  );
63984
66448
  }
63985
66449
  }
66450
+ rejectEscapedLoopTransfer(result, "NeoScript action");
63986
66451
  finalizeConstructorAllocations(runtimeCtx, void 0, ownsInvocationState);
63987
66452
  } catch (error) {
63988
66453
  finalizeConstructorAllocations(runtimeCtx, void 0, ownsInvocationState);
@@ -64052,7 +66517,8 @@ function withEvaluationRuntime(ctx, writes) {
64052
66517
  functionStack: [],
64053
66518
  constructorGroups: /* @__PURE__ */ new Map(),
64054
66519
  ownedValueAttachments: /* @__PURE__ */ new Map(),
64055
- constructionStack: []
66520
+ constructionStack: [],
66521
+ loopIterations: 0
64056
66522
  },
64057
66523
  __indexes: ctx.__executionState === void 0 || ctx.__valueOverlay === void 0 ? void 0 : ctx.__indexes
64058
66524
  };
@@ -64302,6 +66768,7 @@ function evaluateNSSetter(setter, ctx, value, writes) {
64302
66768
  `Setter returned a non-null value: ${String(result.value)}`
64303
66769
  );
64304
66770
  }
66771
+ rejectEscapedLoopTransfer(result, "NeoScript setter");
64305
66772
  }
64306
66773
  function executeCompiledFunction(action, ctx, args, allowFallthroughReturn, runtimeSignature) {
64307
66774
  if (action.parameters.length < 2) {
@@ -64345,6 +66812,7 @@ function executeCompiledFunction(action, ctx, args, allowFallthroughReturn, runt
64345
66812
  "NSFunction ended without returning its declared value."
64346
66813
  );
64347
66814
  }
66815
+ rejectEscapedLoopTransfer(result, "NeoScript function");
64348
66816
  let value = result.value;
64349
66817
  const runtimeReturnType = runtimeSignature?.returnTypeInfo ?? action.typeInfo;
64350
66818
  if (runtimeReturnType.type === NS_TYPE_VOID) {
@@ -64365,6 +66833,40 @@ function executeCompiledFunction(action, ctx, args, allowFallthroughReturn, runt
64365
66833
  }
64366
66834
  return value;
64367
66835
  }
66836
+ function createChildScope(parent) {
66837
+ const child = new Map(parent);
66838
+ const inherited = readonlyBindingErrorsByScope.get(parent);
66839
+ if (inherited !== void 0 && inherited.size > 0) {
66840
+ readonlyBindingErrorsByScope.set(child, new Map(inherited));
66841
+ }
66842
+ return child;
66843
+ }
66844
+ function markReadonlyBinding(scope, bindingId, errorMessage3) {
66845
+ const existing = readonlyBindingErrorsByScope.get(scope);
66846
+ if (existing === void 0) {
66847
+ readonlyBindingErrorsByScope.set(
66848
+ scope,
66849
+ /* @__PURE__ */ new Map([[bindingId, errorMessage3]])
66850
+ );
66851
+ return;
66852
+ }
66853
+ readonlyBindingErrorsByScope.set(
66854
+ scope,
66855
+ new Map([...existing, [bindingId, errorMessage3]])
66856
+ );
66857
+ }
66858
+ function assertVariableBindingWritable(scope, bindingId) {
66859
+ const errorMessage3 = readonlyBindingErrorsByScope.get(scope)?.get(bindingId);
66860
+ if (errorMessage3 === void 0) return;
66861
+ throw new NSGetterRuntimeError(errorMessage3);
66862
+ }
66863
+ function rejectEscapedLoopTransfer(result, bodyLabel) {
66864
+ if (result.kind === "break" || result.kind === "continue") {
66865
+ throw new NSGetterRuntimeError(
66866
+ `Corrupt ${bodyLabel} IR: '${result.kind}' escaped the nearest loop.`
66867
+ );
66868
+ }
66869
+ }
64368
66870
  function evaluationOptions(ctx, allowFallthroughReturn) {
64369
66871
  const writes = ctx.__executionState?.writes;
64370
66872
  if (writes === void 0) {
@@ -64387,6 +66889,186 @@ function createTopLevelScope(ctx) {
64387
66889
  );
64388
66890
  return scope;
64389
66891
  }
66892
+ function consumeLoopIteration(ctx) {
66893
+ const state = ctx.__executionState;
66894
+ if (state === void 0) {
66895
+ throw new NSGetterRuntimeError(
66896
+ "NeoScript loop executed without a shared execution state."
66897
+ );
66898
+ }
66899
+ if (state.loopIterations >= MAX_LOOP_ITERATIONS) {
66900
+ throw new NSGetterRuntimeError(
66901
+ `NeoScript loop iteration limit of ${MAX_LOOP_ITERATIONS} exceeded.`
66902
+ );
66903
+ }
66904
+ state.loopIterations += 1;
66905
+ }
66906
+ function synchronizeExistingBindings(parent, child, bindingIds) {
66907
+ for (const bindingId of bindingIds) {
66908
+ parent.set(bindingId, child.get(bindingId));
66909
+ }
66910
+ }
66911
+ function evalInstructionsInChildScope(instructions, parent, ctx, options) {
66912
+ const parentBindingIds = [...parent.keys()];
66913
+ const child = createChildScope(parent);
66914
+ try {
66915
+ return evalInstructions(instructions, child, ctx, options);
66916
+ } finally {
66917
+ synchronizeExistingBindings(parent, child, parentBindingIds);
66918
+ }
66919
+ }
66920
+ function switchSelectorTypeSupported(typeInfo) {
66921
+ return typeInfo.type === 2 /* Int */ || typeInfo.type === 3 /* String */ || typeInfo.type === 1 /* Bool */ || typeInfo.type === 8 /* Enum */;
66922
+ }
66923
+ function switchLabelKeyOrThrow(label, selectorTypeInfo) {
66924
+ if (label.typeInfo.required !== true) {
66925
+ throw new CorruptNeoScriptIRError(
66926
+ "Corrupt NeoScript switch IR: case labels must use canonical required type information."
66927
+ );
66928
+ }
66929
+ if (label.typeInfo.type === 0 /* Null */) {
66930
+ if (label.value !== null || selectorTypeInfo.required !== false) {
66931
+ throw new CorruptNeoScriptIRError(
66932
+ "Corrupt NeoScript switch IR: a null case label requires an optional selector."
66933
+ );
66934
+ }
66935
+ return "null";
66936
+ }
66937
+ if (label.typeInfo.type !== selectorTypeInfo.type) {
66938
+ throw new CorruptNeoScriptIRError(
66939
+ "Corrupt NeoScript switch IR: case label type does not match the selector type."
66940
+ );
66941
+ }
66942
+ switch (label.typeInfo.type) {
66943
+ case 2 /* Int */:
66944
+ if (typeof label.value !== "number" || !Number.isSafeInteger(label.value)) {
66945
+ throw new CorruptNeoScriptIRError(
66946
+ "Corrupt NeoScript switch IR: int case label is not a safe integer."
66947
+ );
66948
+ }
66949
+ return `int:${String(label.value)}`;
66950
+ case 3 /* String */:
66951
+ if (typeof label.value !== "string") {
66952
+ throw new CorruptNeoScriptIRError(
66953
+ "Corrupt NeoScript switch IR: string case label has a non-string value."
66954
+ );
66955
+ }
66956
+ return `string:${JSON.stringify(label.value)}`;
66957
+ case 1 /* Bool */:
66958
+ if (typeof label.value !== "boolean") {
66959
+ throw new CorruptNeoScriptIRError(
66960
+ "Corrupt NeoScript switch IR: bool case label has a non-boolean value."
66961
+ );
66962
+ }
66963
+ return `bool:${String(label.value)}`;
66964
+ case 8 /* Enum */:
66965
+ if (selectorTypeInfo.type !== 8 /* Enum */ || label.typeInfo.enumId !== selectorTypeInfo.enumId || !Array.isArray(label.value) || label.value.length !== 1 || typeof label.value[0] !== "string" || label.value[0].length === 0) {
66966
+ throw new CorruptNeoScriptIRError(
66967
+ "Corrupt NeoScript switch IR: enum case label does not match the selector enum."
66968
+ );
66969
+ }
66970
+ return `enum:${label.typeInfo.enumId}:${JSON.stringify(label.value[0])}`;
66971
+ default:
66972
+ throw new CorruptNeoScriptIRError(
66973
+ "Corrupt NeoScript switch IR: unsupported case label type."
66974
+ );
66975
+ }
66976
+ }
66977
+ function validateSwitchInstruction(instruction) {
66978
+ if (!switchSelectorTypeSupported(instruction.selectorTypeInfo)) {
66979
+ throw new CorruptNeoScriptIRError(
66980
+ "Corrupt NeoScript switch IR: selector type must be int, string, bool, or enum."
66981
+ );
66982
+ }
66983
+ const labels = /* @__PURE__ */ new Set();
66984
+ for (const section of instruction.sections) {
66985
+ if (section.labels.length === 0) {
66986
+ throw new CorruptNeoScriptIRError(
66987
+ "Corrupt NeoScript switch IR: case section has no labels."
66988
+ );
66989
+ }
66990
+ for (const label of section.labels) {
66991
+ const key = switchLabelKeyOrThrow(label, instruction.selectorTypeInfo);
66992
+ if (labels.has(key)) {
66993
+ throw new CorruptNeoScriptIRError(
66994
+ "Corrupt NeoScript switch IR: duplicate case label."
66995
+ );
66996
+ }
66997
+ labels.add(key);
66998
+ }
66999
+ }
67000
+ }
67001
+ function assertSwitchSelectorValue(value, typeInfo, ctx) {
67002
+ const matchesType = runtimeValueMatchesType(value, typeInfo, ctx) && (typeInfo.type !== 2 /* Int */ || value === null || typeof value === "number" && Number.isSafeInteger(value));
67003
+ if (!matchesType) {
67004
+ throw new NSGetterRuntimeError(
67005
+ "NeoScript switch selector does not match its compiled type."
67006
+ );
67007
+ }
67008
+ }
67009
+ function validateTryInstruction(instruction) {
67010
+ if (!Array.isArray(instruction.catches)) {
67011
+ throw new CorruptNeoScriptIRError(
67012
+ "Corrupt NeoScript try IR: catch clause list is missing."
67013
+ );
67014
+ }
67015
+ if (instruction.catches.length === 0) {
67016
+ throw new CorruptNeoScriptIRError(
67017
+ "Corrupt NeoScript try IR: at least one catch clause is required."
67018
+ );
67019
+ }
67020
+ const bindingIds = /* @__PURE__ */ new Set();
67021
+ for (let index = 0; index < instruction.catches.length; index += 1) {
67022
+ const clause = instruction.catches[index];
67023
+ if (typeof clause?.binding !== "object" || clause.binding === null) {
67024
+ throw new CorruptNeoScriptIRError(
67025
+ "Corrupt NeoScript try IR: catch clause is missing its binding."
67026
+ );
67027
+ }
67028
+ if (typeof clause.binding.id !== "string") {
67029
+ throw new CorruptNeoScriptIRError(
67030
+ "Corrupt NeoScript try IR: catch binding id must be a string."
67031
+ );
67032
+ }
67033
+ if (clause.binding.id.length === 0) {
67034
+ throw new CorruptNeoScriptIRError(
67035
+ "Corrupt NeoScript try IR: catch binding id must be non-empty."
67036
+ );
67037
+ }
67038
+ if (bindingIds.has(clause.binding.id)) {
67039
+ throw new CorruptNeoScriptIRError(
67040
+ "Corrupt NeoScript try IR: catch binding ids must be unique."
67041
+ );
67042
+ }
67043
+ bindingIds.add(clause.binding.id);
67044
+ if (clause.binding.typeInfo?.type !== 3 /* String */) {
67045
+ throw new CorruptNeoScriptIRError(
67046
+ "Corrupt NeoScript try IR: catch binding type must be string."
67047
+ );
67048
+ }
67049
+ if (clause.binding.typeInfo.required !== true) {
67050
+ throw new CorruptNeoScriptIRError(
67051
+ "Corrupt NeoScript try IR: catch binding string must be required."
67052
+ );
67053
+ }
67054
+ if (clause.binding.readonly !== true) {
67055
+ throw new CorruptNeoScriptIRError(
67056
+ "Corrupt NeoScript try IR: catch binding must be read-only."
67057
+ );
67058
+ }
67059
+ const unfiltered = clause.filter === void 0 || clause.filter === null;
67060
+ if (unfiltered && index !== instruction.catches.length - 1) {
67061
+ throw new CorruptNeoScriptIRError(
67062
+ "Corrupt NeoScript try IR: an unfiltered catch must be the final clause."
67063
+ );
67064
+ }
67065
+ }
67066
+ if (!isNSInstructionTry(instruction)) {
67067
+ throw new CorruptNeoScriptIRError(
67068
+ "Corrupt NeoScript try IR: catch filter or child instructions are malformed."
67069
+ );
67070
+ }
67071
+ }
64390
67072
  function evalInstructions(instructions, scope, ctx, options) {
64391
67073
  for (const ins of instructions) {
64392
67074
  switch (ins.type) {
@@ -64409,13 +67091,13 @@ function evalInstructions(instructions, scope, ctx, options) {
64409
67091
  ctx,
64410
67092
  options
64411
67093
  );
64412
- if (r.kind === "return") return r;
67094
+ if (r.kind !== "fallthrough") return r;
64413
67095
  break;
64414
67096
  }
64415
67097
  }
64416
67098
  if (!matched && ins.else) {
64417
67099
  const r = evalInstructions(ins.else, scope, ctx, options);
64418
- if (r.kind === "return") return r;
67100
+ if (r.kind !== "fallthrough") return r;
64419
67101
  }
64420
67102
  break;
64421
67103
  }
@@ -64433,6 +67115,7 @@ function evalInstructions(instructions, scope, ctx, options) {
64433
67115
  value = coerceDecimalOperand(value, "assignment");
64434
67116
  }
64435
67117
  if (ins.target.pointer.type === "variable" /* variable */) {
67118
+ assertVariableBindingWritable(scope, ins.target.pointer.variableId);
64436
67119
  scope.set(ins.target.pointer.variableId, value);
64437
67120
  break;
64438
67121
  }
@@ -64473,6 +67156,174 @@ function evalInstructions(instructions, scope, ctx, options) {
64473
67156
  evalPointer(ins.call, scope, ctx);
64474
67157
  break;
64475
67158
  }
67159
+ case "for" /* for */: {
67160
+ const parentBindingIds = [...scope.keys()];
67161
+ const loopScope = createChildScope(scope);
67162
+ try {
67163
+ const initialValue = coerceAssignmentValue(
67164
+ evalPointer(ins.initializer.pointer, loopScope, ctx),
67165
+ ins.initializer.typeInfo,
67166
+ "variable initialization"
67167
+ );
67168
+ loopScope.set(ins.initializer.id, initialValue);
67169
+ while (evalBooleanExpression(ins.condition, loopScope, ctx)) {
67170
+ consumeLoopIteration(ctx);
67171
+ const result = evalInstructionsInChildScope(
67172
+ ins.instructions,
67173
+ loopScope,
67174
+ ctx,
67175
+ options
67176
+ );
67177
+ if (result.kind === "return") return result;
67178
+ if (result.kind === "break") break;
67179
+ evalInstructions([ins.iterator], loopScope, ctx, options);
67180
+ }
67181
+ } finally {
67182
+ synchronizeExistingBindings(scope, loopScope, parentBindingIds);
67183
+ }
67184
+ break;
67185
+ }
67186
+ case "forEach" /* forEach */: {
67187
+ const collection = evalPointer(ins.collectionPointer, scope, ctx);
67188
+ const membership = snapshotCollectionMembership(collection);
67189
+ const parentBindingIds = [...scope.keys()];
67190
+ const loopScope = createChildScope(scope);
67191
+ markReadonlyBinding(
67192
+ loopScope,
67193
+ ins.binding.id,
67194
+ READONLY_FOREACH_BINDING_ERROR
67195
+ );
67196
+ try {
67197
+ for (const rawEntry of membership) {
67198
+ consumeLoopIteration(ctx);
67199
+ loopScope.set(
67200
+ ins.binding.id,
67201
+ coerceAssignmentValue(
67202
+ resolveValueIfId(rawEntry, ctx),
67203
+ ins.binding.typeInfo,
67204
+ "foreach binding"
67205
+ )
67206
+ );
67207
+ const result = evalInstructionsInChildScope(
67208
+ ins.instructions,
67209
+ loopScope,
67210
+ ctx,
67211
+ options
67212
+ );
67213
+ if (result.kind === "return") return result;
67214
+ if (result.kind === "break") break;
67215
+ }
67216
+ } finally {
67217
+ synchronizeExistingBindings(scope, loopScope, parentBindingIds);
67218
+ }
67219
+ break;
67220
+ }
67221
+ case "switch" /* switch */: {
67222
+ validateSwitchInstruction(ins);
67223
+ const selector = evalPointer(ins.selector, scope, ctx);
67224
+ assertSwitchSelectorValue(selector, ins.selectorTypeInfo, ctx);
67225
+ const matchedSection = ins.sections.find(
67226
+ (section) => section.labels.some((label) => jsEqual(selector, label.value))
67227
+ );
67228
+ const selectedInstructions = matchedSection?.instructions ?? ins.defaultInstructions;
67229
+ if (selectedInstructions === void 0 || selectedInstructions === null) {
67230
+ break;
67231
+ }
67232
+ const parentBindingIds = [...scope.keys()];
67233
+ const sectionScope = createChildScope(scope);
67234
+ const result = (() => {
67235
+ try {
67236
+ return evalInstructions(
67237
+ selectedInstructions,
67238
+ sectionScope,
67239
+ ctx,
67240
+ options
67241
+ );
67242
+ } finally {
67243
+ synchronizeExistingBindings(scope, sectionScope, parentBindingIds);
67244
+ }
67245
+ })();
67246
+ if (result.kind === "break") break;
67247
+ if (result.kind === "fallthrough") {
67248
+ throw new CorruptNeoScriptIRError(
67249
+ "Corrupt NeoScript switch IR: selected section reached its end."
67250
+ );
67251
+ }
67252
+ return result;
67253
+ }
67254
+ case "try" /* try */: {
67255
+ validateTryInstruction(ins);
67256
+ const parentBindingIds = [...scope.keys()];
67257
+ const tryScope = createChildScope(scope);
67258
+ let tryResult;
67259
+ try {
67260
+ try {
67261
+ tryResult = evalInstructions(
67262
+ ins.instructions,
67263
+ tryScope,
67264
+ ctx,
67265
+ options
67266
+ );
67267
+ } finally {
67268
+ synchronizeExistingBindings(scope, tryScope, parentBindingIds);
67269
+ }
67270
+ } catch (error) {
67271
+ if (!isCatchableNSRuntimeError(error)) throw error;
67272
+ let matched = false;
67273
+ for (const clause of ins.catches) {
67274
+ const catchScope = createChildScope(scope);
67275
+ catchScope.set(clause.binding.id, error.message);
67276
+ markReadonlyBinding(
67277
+ catchScope,
67278
+ clause.binding.id,
67279
+ READONLY_CATCH_BINDING_ERROR
67280
+ );
67281
+ const filter = clause.filter;
67282
+ let filterMatches = filter === null || filter === void 0;
67283
+ if (filter !== null && filter !== void 0) {
67284
+ try {
67285
+ filterMatches = evalBooleanExpression(filter, catchScope, ctx);
67286
+ } catch (filterError) {
67287
+ if (!isCatchableNSRuntimeError(filterError)) {
67288
+ synchronizeExistingBindings(
67289
+ scope,
67290
+ catchScope,
67291
+ parentBindingIds
67292
+ );
67293
+ throw filterError;
67294
+ }
67295
+ filterMatches = false;
67296
+ }
67297
+ }
67298
+ if (!filterMatches) {
67299
+ synchronizeExistingBindings(scope, catchScope, parentBindingIds);
67300
+ continue;
67301
+ }
67302
+ matched = true;
67303
+ let catchResult;
67304
+ try {
67305
+ catchResult = evalInstructions(
67306
+ clause.instructions,
67307
+ catchScope,
67308
+ ctx,
67309
+ options
67310
+ );
67311
+ } finally {
67312
+ synchronizeExistingBindings(scope, catchScope, parentBindingIds);
67313
+ }
67314
+ if (catchResult.kind !== "fallthrough") return catchResult;
67315
+ break;
67316
+ }
67317
+ if (!matched) throw error;
67318
+ break;
67319
+ }
67320
+ if (tryResult.kind !== "fallthrough") return tryResult;
67321
+ break;
67322
+ }
67323
+ case "break" /* break */:
67324
+ return { kind: "break" };
67325
+ case "continue" /* continue */:
67326
+ return { kind: "continue" };
64476
67327
  }
64477
67328
  void touchUnknown(ins);
64478
67329
  }
@@ -66226,6 +69077,9 @@ function applyArithmetic(op, operands, decimal2) {
66226
69077
  });
66227
69078
  }
66228
69079
  }
69080
+ function coerceAssignmentValue(value, targetTypeInfo, context) {
69081
+ return targetTypeInfo.type === 20 /* Decimal */ && typeof value === "number" ? coerceDecimalOperand(value, context) : value;
69082
+ }
66229
69083
  function coerceDecimalOperand(value, context) {
66230
69084
  if (typeof value === "string") {
66231
69085
  if (!isDecimalString(value)) {
@@ -68488,33 +71342,55 @@ function collectionLength(c) {
68488
71342
  `Cannot Count() ${typeof c}; expected list, dictionary, or string`
68489
71343
  );
68490
71344
  }
68491
- function collectionEntries(c, ctx) {
68492
- if (Array.isArray(c)) {
68493
- return c.map((entry) => resolveValueIfId(entry, ctx));
68494
- }
68495
- if (typeof c === "object" && c !== null) {
68496
- return Object.values(c).map((entry) => resolveValueIfId(entry, ctx));
68497
- }
68498
- return [];
71345
+ function snapshotCollectionMembership(collection) {
71346
+ const membership = [];
71347
+ const valid = forEachRawCollectionEntry(collection, ({ raw }) => {
71348
+ membership.push(raw);
71349
+ });
71350
+ if (valid) return membership;
71351
+ throw new NSGetterRuntimeError(
71352
+ "NeoScript foreach requires a present collection."
71353
+ );
68499
71354
  }
68500
- function iterateCollection(c, ctx, callback) {
71355
+ function forEachRawCollectionEntry(c, callback) {
68501
71356
  if (Array.isArray(c)) {
68502
- for (let i = 0; i < c.length; i++) {
68503
- const raw = c[i];
68504
- const entry = resolveValueIfId(raw, ctx);
68505
- callback(entry, i, typeof raw === "string" ? raw : null);
71357
+ for (let key = 0; key < c.length; key += 1) {
71358
+ const raw = c[key];
71359
+ callback({
71360
+ raw,
71361
+ key,
71362
+ valueId: typeof raw === "string" ? raw : null
71363
+ });
68506
71364
  }
68507
- return;
71365
+ return true;
68508
71366
  }
68509
71367
  if (typeof c === "object" && c !== null) {
68510
- for (const [k, raw] of Object.entries(c)) {
68511
- const entry = resolveValueIfId(raw, ctx);
68512
- callback(entry, k, typeof raw === "string" ? raw : null);
71368
+ for (const [key, raw] of Object.entries(c)) {
71369
+ callback({
71370
+ raw,
71371
+ key,
71372
+ valueId: typeof raw === "string" ? raw : null
71373
+ });
68513
71374
  }
71375
+ return true;
68514
71376
  }
71377
+ return false;
71378
+ }
71379
+ function collectionEntries(c, ctx) {
71380
+ const entries = [];
71381
+ forEachRawCollectionEntry(c, ({ raw }) => {
71382
+ entries.push(resolveValueIfId(raw, ctx));
71383
+ });
71384
+ return entries;
71385
+ }
71386
+ function iterateCollection(c, ctx, callback) {
71387
+ forEachRawCollectionEntry(c, ({ raw, key, valueId }) => {
71388
+ const entry = resolveValueIfId(raw, ctx);
71389
+ callback(entry, key, valueId);
71390
+ });
68515
71391
  }
68516
71392
  function pushParams(parent, parameters, positional, isList) {
68517
- const child = new Map(parent);
71393
+ const child = createChildScope(parent);
68518
71394
  if (parameters.length === 1) {
68519
71395
  child.set(parameters[0].id, positional[1]);
68520
71396
  } else if (parameters.length === 2) {
@@ -68524,7 +71400,7 @@ function pushParams(parent, parameters, positional, isList) {
68524
71400
  }
68525
71401
  return child;
68526
71402
  }
68527
- var NativeFunctionDelegateUnavailableError, liveListIndexesByProject, MAX_CONSTRUCTION_DEPTH, LazyValueOverlay;
71403
+ var NonCatchableNSGetterRuntimeError, NativeFunctionDelegateUnavailableError, CorruptNeoScriptIRError, liveListIndexesByProject, MAX_CONSTRUCTION_DEPTH, MAX_LOOP_ITERATIONS, LazyValueOverlay, readonlyBindingErrorsByScope, READONLY_FOREACH_BINDING_ERROR, READONLY_CATCH_BINDING_ERROR;
68528
71404
  var init_evaluateNSGetter = __esm({
68529
71405
  "../src/view-models/neoscript-evaluator/evaluateNSGetter.ts"() {
68530
71406
  "use strict";
@@ -68540,10 +71416,15 @@ var init_evaluateNSGetter = __esm({
68540
71416
  init_core();
68541
71417
  init_member_storage_key();
68542
71418
  init_project2();
68543
- NativeFunctionDelegateUnavailableError = class extends NSGetterRuntimeError {
71419
+ NonCatchableNSGetterRuntimeError = class extends NSGetterRuntimeError {
71420
+ };
71421
+ NativeFunctionDelegateUnavailableError = class extends NonCatchableNSGetterRuntimeError {
71422
+ };
71423
+ CorruptNeoScriptIRError = class extends NonCatchableNSGetterRuntimeError {
68544
71424
  };
68545
71425
  liveListIndexesByProject = /* @__PURE__ */ new WeakMap();
68546
71426
  MAX_CONSTRUCTION_DEPTH = 64;
71427
+ MAX_LOOP_ITERATIONS = 1e4;
68547
71428
  LazyValueOverlay = class extends Map {
68548
71429
  constructor(vm) {
68549
71430
  super();
@@ -68603,6 +71484,9 @@ var init_evaluateNSGetter = __esm({
68603
71484
  return indexes;
68604
71485
  }
68605
71486
  };
71487
+ readonlyBindingErrorsByScope = /* @__PURE__ */ new WeakMap();
71488
+ READONLY_FOREACH_BINDING_ERROR = "Cannot assign to a read-only foreach iterator binding.";
71489
+ READONLY_CATCH_BINDING_ERROR = "Cannot assign to a read-only catch message binding.";
68606
71490
  }
68607
71491
  });
68608
71492
 
@@ -69354,6 +72238,49 @@ function stringifyReturnInstructions(instructions, sourceType) {
69354
72238
  pointer: stringifyTextVariablePointer(instruction.pointer, sourceType)
69355
72239
  };
69356
72240
  }
72241
+ if (instruction.type === "switch" /* switch */) {
72242
+ return {
72243
+ ...instruction,
72244
+ sections: instruction.sections.map((section) => ({
72245
+ ...section,
72246
+ instructions: stringifyReturnInstructions(
72247
+ section.instructions,
72248
+ sourceType
72249
+ )
72250
+ })),
72251
+ ...instruction.defaultInstructions === void 0 ? {} : {
72252
+ defaultInstructions: instruction.defaultInstructions === null ? null : stringifyReturnInstructions(
72253
+ instruction.defaultInstructions,
72254
+ sourceType
72255
+ )
72256
+ }
72257
+ };
72258
+ }
72259
+ if (instruction.type === "try" /* try */) {
72260
+ return {
72261
+ ...instruction,
72262
+ instructions: stringifyReturnInstructions(
72263
+ instruction.instructions,
72264
+ sourceType
72265
+ ),
72266
+ catches: instruction.catches.map((clause) => ({
72267
+ ...clause,
72268
+ instructions: stringifyReturnInstructions(
72269
+ clause.instructions,
72270
+ sourceType
72271
+ )
72272
+ }))
72273
+ };
72274
+ }
72275
+ if (instruction.type === "for" /* for */ || instruction.type === "forEach" /* forEach */) {
72276
+ return {
72277
+ ...instruction,
72278
+ instructions: stringifyReturnInstructions(
72279
+ instruction.instructions,
72280
+ sourceType
72281
+ )
72282
+ };
72283
+ }
69357
72284
  if (instruction.type !== "if" /* if */) return instruction;
69358
72285
  return {
69359
72286
  ...instruction,
@@ -69928,7 +72855,7 @@ __export(script_exports, {
69928
72855
  readDocumentArrays: () => readDocumentArrays,
69929
72856
  runScript: () => runScript
69930
72857
  });
69931
- import { readFileSync as readFileSync10 } from "node:fs";
72858
+ import { readFileSync as readFileSync11 } from "node:fs";
69932
72859
  function readDocumentArrays(raw) {
69933
72860
  const arrayOf2 = (field) => {
69934
72861
  const value = raw[field];
@@ -70440,9 +73367,9 @@ function buildRootValue(document) {
70440
73367
  }
70441
73368
  function readSource(options, fallback) {
70442
73369
  if (options.source !== null) return options.source;
70443
- if (options.file !== null) return readFileSync10(options.file, "utf8");
73370
+ if (options.file !== null) return readFileSync11(options.file, "utf8");
70444
73371
  if (fallback !== void 0) return fallback;
70445
- const stdin = readFileSync10(0, "utf8");
73372
+ const stdin = readFileSync11(0, "utf8");
70446
73373
  if (stdin.trim().length === 0) {
70447
73374
  throw new Error(
70448
73375
  "Provide NeoScript source as an argument, --file, or stdin."
@@ -70450,6 +73377,20 @@ function readSource(options, fallback) {
70450
73377
  }
70451
73378
  return stdin;
70452
73379
  }
73380
+ function describeScriptBodyOrigin(options, storedBodyMemberLabel) {
73381
+ if (options.source !== null) return "the inline source argument";
73382
+ if (options.file !== null) return `--file ${options.file}`;
73383
+ if (storedBodyMemberLabel !== null) {
73384
+ return `the stored body of ${storedBodyMemberLabel}`;
73385
+ }
73386
+ return "stdin";
73387
+ }
73388
+ function qualifiedMemberLabel(thisClass, member) {
73389
+ const memberName = String(member.name ?? member.id);
73390
+ if (thisClass === null) return `'${memberName}'`;
73391
+ if (typeof thisClass.name !== "string") return `'${memberName}'`;
73392
+ return `'${thisClass.name}.${memberName}'`;
73393
+ }
70453
73394
  function analyzeWorkspaceProjectManifestV4(workspace) {
70454
73395
  const status = computeWorkspaceStatus(workspace, {
70455
73396
  skipProjectBinaryInspection: true
@@ -70532,6 +73473,14 @@ async function runScript(workspace, command, options, dependencies = {}) {
70532
73473
  );
70533
73474
  }
70534
73475
  const thisClass = setterMember !== null ? resolveMemberContainingClass(setterMember, document) : nsFunctionTarget !== null ? nsFunctionMember?.isStatic === true ? null : nsFunctionTarget.receiverClass : resolveThisClass(options.thisRef, document);
73476
+ const storedBodyMemberLabel = nsFunctionDefinition === null || nsFunctionMember === null ? null : qualifiedMemberLabel(thisClass, nsFunctionMember);
73477
+ const bodyOrigin = describeScriptBodyOrigin(options, storedBodyMemberLabel);
73478
+ const bodySite = (unit) => ({
73479
+ recordKind: "script",
73480
+ origin: bodyOrigin,
73481
+ unit,
73482
+ code: source
73483
+ });
70535
73484
  const compileContext = {
70536
73485
  project: document.project,
70537
73486
  members: document.members,
@@ -70565,53 +73514,76 @@ async function runScript(workspace, command, options, dependencies = {}) {
70565
73514
  source,
70566
73515
  sharedContext
70567
73516
  );
70568
- const error = diagnostics.find(
70569
- (diagnostic) => diagnostic.severity === "error"
73517
+ compileNeoScriptBodyOrThrow(
73518
+ () => {
73519
+ const diagnostic = diagnostics.find(
73520
+ (candidate) => candidate.severity === "error"
73521
+ );
73522
+ if (diagnostic === void 0) return;
73523
+ throw new CompileError(diagnostic.message, {
73524
+ line: diagnostic.range.start.line + 1,
73525
+ column: diagnostic.range.start.character + 1
73526
+ });
73527
+ },
73528
+ bodySite(sharedKind),
73529
+ null
70570
73530
  );
70571
- if (error !== void 0) {
70572
- throw new CompileError(error.message, {
70573
- line: error.range.start.line + 1,
70574
- column: error.range.start.character + 1
70575
- });
70576
- }
70577
73531
  }
70578
73532
  if (options.mode === "nsfunction") {
70579
73533
  if (nsFunctionMember === null || nsFunctionDefinition === null) {
70580
73534
  throw new Error("NSFunction mode requires a function target.");
70581
73535
  }
70582
- compiled = compileNSFunction(source, {
70583
- project: document.project,
70584
- members: document.members,
70585
- classes: document.classes,
70586
- enums: document.enums,
70587
- interfaces: document.interfaces,
70588
- thisClass,
70589
- returnTypeInfo: nsFunctionDefinition.returnTypeInfo,
70590
- argumentTypes: nsFunctionDefinition.argumentTypes,
70591
- deferred: nsFunctionDefinition.deferred,
70592
- functionName: String(nsFunctionMember.name ?? nsFunctionMember.id)
70593
- });
73536
+ compiled = compileNeoScriptBodyOrThrow(
73537
+ () => compileNSFunction(source, {
73538
+ project: document.project,
73539
+ members: document.members,
73540
+ classes: document.classes,
73541
+ enums: document.enums,
73542
+ interfaces: document.interfaces,
73543
+ thisClass,
73544
+ returnTypeInfo: nsFunctionDefinition.returnTypeInfo,
73545
+ argumentTypes: nsFunctionDefinition.argumentTypes,
73546
+ deferred: nsFunctionDefinition.deferred,
73547
+ functionName: String(nsFunctionMember.name ?? nsFunctionMember.id)
73548
+ }),
73549
+ bodySite("function"),
73550
+ null
73551
+ );
70594
73552
  } else if (options.mode === "setter") {
70595
73553
  if (setterMember === null) {
70596
73554
  throw new Error("Setter mode requires an NSProperty member target.");
70597
73555
  }
70598
- compiled = compileNSSetter(source, {
70599
- project: document.project,
70600
- members: document.members,
70601
- classes: document.classes,
70602
- enums: document.enums,
70603
- interfaces: document.interfaces,
70604
- thisClass,
70605
- valueTypeInfo: resolveScriptPropertyReturnTypeInfo(
70606
- setterMember,
70607
- document.members
70608
- )
70609
- });
73556
+ compiled = compileNeoScriptBodyOrThrow(
73557
+ () => compileNSSetter(source, {
73558
+ project: document.project,
73559
+ members: document.members,
73560
+ classes: document.classes,
73561
+ enums: document.enums,
73562
+ interfaces: document.interfaces,
73563
+ thisClass,
73564
+ valueTypeInfo: resolveScriptPropertyReturnTypeInfo(
73565
+ setterMember,
73566
+ document.members
73567
+ )
73568
+ }),
73569
+ bodySite("setter"),
73570
+ null
73571
+ );
73572
+ } else if (command === "apply" || options.mode === "action") {
73573
+ compiled = compileNeoScriptBodyOrThrow(
73574
+ () => compileNSAction(source, compileContext),
73575
+ bodySite("action"),
73576
+ null
73577
+ );
70610
73578
  } else {
70611
- compiled = command === "apply" || options.mode === "action" ? compileNSAction(source, compileContext) : compileNSGetter(source, compileContext);
73579
+ compiled = compileNeoScriptBodyOrThrow(
73580
+ () => compileNSGetter(source, compileContext),
73581
+ bodySite("getter"),
73582
+ null
73583
+ );
70612
73584
  }
70613
73585
  } catch (error) {
70614
- if (error instanceof CompileError) {
73586
+ if (error instanceof NeoScriptBodyCompileError) {
70615
73587
  const report2 = {
70616
73588
  ok: false,
70617
73589
  error: error.message,
@@ -70623,7 +73595,7 @@ async function runScript(workspace, command, options, dependencies = {}) {
70623
73595
  };
70624
73596
  const hint = options.returns === null && error.message.includes("not assignable to declared return type") ? '\n(eval defaults to an unknown return type \u2014 pass --returns, e.g. --returns "string[]")' : "";
70625
73597
  console.log(
70626
- options.json ? JSON.stringify(report2, null, 2) : `Compile error at ${error.line}:${error.column} \u2014 ${error.message}${hint}`
73598
+ options.json ? JSON.stringify(report2, null, 2) : `Compile error \u2014 ${error.message}${hint}`
70627
73599
  );
70628
73600
  process.exitCode = 1;
70629
73601
  return;
@@ -71482,6 +74454,7 @@ var init_script = __esm({
71482
74454
  init_workspace_status();
71483
74455
  init_source_diagnostics();
71484
74456
  init_project_documents();
74457
+ init_push_body_diagnostics();
71485
74458
  RETURN_SHORTHANDS = {
71486
74459
  // MemberKind numerics: Bool=1, Int=2, String=3, Float=4.
71487
74460
  bool: { type: 1, required: true },
@@ -71503,8 +74476,8 @@ __export(migrate_exports, {
71503
74476
  runMigrate: () => runMigrate
71504
74477
  });
71505
74478
  import { randomUUID as randomUUID3 } from "node:crypto";
71506
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, readdirSync as readdirSync5, writeFileSync as writeFileSync9 } from "node:fs";
71507
- import { join as join12 } from "node:path";
74479
+ import { existsSync as existsSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync5, writeFileSync as writeFileSync9 } from "node:fs";
74480
+ import { join as join13 } from "node:path";
71508
74481
  async function runMigrate(workspace, subcommand, positional, targetRef, json, dependencies = {}) {
71509
74482
  if (subcommand === "new") {
71510
74483
  const name = positional[0];
@@ -71513,10 +74486,10 @@ async function runMigrate(workspace, subcommand, positional, targetRef, json, de
71513
74486
  "Usage: neo migrate new <name> [--target <ClassName|project>]"
71514
74487
  );
71515
74488
  }
71516
- const migrationsDir = join12(workspace.root, "Migrations");
74489
+ const migrationsDir = join13(workspace.root, "Migrations");
71517
74490
  mkdirSync9(migrationsDir, { recursive: true });
71518
74491
  let nextOrder = 1;
71519
- if (existsSync11(migrationsDir)) {
74492
+ if (existsSync12(migrationsDir)) {
71520
74493
  for (const entry of readdirSync5(migrationsDir)) {
71521
74494
  const match = /^(\d+)-/.exec(entry);
71522
74495
  if (match !== null) {
@@ -71525,8 +74498,8 @@ async function runMigrate(workspace, subcommand, positional, targetRef, json, de
71525
74498
  }
71526
74499
  }
71527
74500
  const relPath = migrationFileName(nextOrder, name);
71528
- const absolute = join12(workspace.root, relPath);
71529
- if (existsSync11(absolute)) {
74501
+ const absolute = join13(workspace.root, relPath);
74502
+ if (existsSync12(absolute)) {
71530
74503
  throw new Error(`"${relPath}" already exists.`);
71531
74504
  }
71532
74505
  const target = targetRef ?? "project";
@@ -71573,12 +74546,17 @@ async function runMigrate(workspace, subcommand, positional, targetRef, json, de
71573
74546
  assertMigrationCheckWorkspaceIsValid(status);
71574
74547
  const candidate = localMigrationCheckCandidate(status);
71575
74548
  const compileAction = dependencies.compileAction ?? compileNSAction;
74549
+ const bodySourceLocator = createNeoScriptBodySourceLocator(
74550
+ workspace,
74551
+ status
74552
+ );
71576
74553
  let failures = 0;
71577
74554
  const results = [];
71578
- for (const { data: migration, file } of candidate.migrations) {
74555
+ for (const { data: migration, file, recordId } of candidate.migrations) {
71579
74556
  if (typeof migration.code !== "string" || migration.code.trim().length === 0) {
71580
74557
  continue;
71581
74558
  }
74559
+ const code = migration.code;
71582
74560
  const thisClass = typeof migration.targetClassId === "string" ? candidate.classes.find(
71583
74561
  (schemaClass2) => schemaClass2.id === migration.targetClassId
71584
74562
  ) ?? null : null;
@@ -71588,16 +74566,27 @@ async function runMigrate(workspace, subcommand, positional, targetRef, json, de
71588
74566
  `Migration targets unknown local class "${migration.targetClassId}". Fix its @target directive or restore the class declaration.`
71589
74567
  );
71590
74568
  }
71591
- compileAction(migration.code, {
71592
- project: raw.project,
71593
- members: candidate.members,
71594
- classes: candidate.classes,
71595
- enums: candidate.enums,
71596
- interfaces: candidate.interfaces,
71597
- thisClass,
71598
- dialogueContext: null,
71599
- migrationContext: true
71600
- });
74569
+ compileNeoScriptBodyOrThrow(
74570
+ () => compileAction(code, {
74571
+ project: raw.project,
74572
+ members: candidate.members,
74573
+ classes: candidate.classes,
74574
+ enums: candidate.enums,
74575
+ interfaces: candidate.interfaces,
74576
+ thisClass,
74577
+ dialogueContext: null,
74578
+ migrationContext: true
74579
+ }),
74580
+ {
74581
+ recordKind: "migration",
74582
+ recordId,
74583
+ ownerName: migrationTargetClassName(thisClass),
74584
+ memberName: String(migration.name ?? recordId),
74585
+ unit: "migration",
74586
+ code
74587
+ },
74588
+ bodySourceLocator
74589
+ );
71601
74590
  results.push({
71602
74591
  id: migration.id,
71603
74592
  name: migration.name,
@@ -71606,7 +74595,7 @@ async function runMigrate(workspace, subcommand, positional, targetRef, json, de
71606
74595
  });
71607
74596
  } catch (error) {
71608
74597
  failures += 1;
71609
- const message = error instanceof CompileError ? `${error.line}:${error.column} ${error.message}` : error instanceof Error ? error.message : String(error);
74598
+ const message = error instanceof Error ? error.message : String(error);
71610
74599
  results.push({
71611
74600
  id: migration.id,
71612
74601
  name: migration.name,
@@ -71708,7 +74697,11 @@ async function runMigrate(workspace, subcommand, positional, targetRef, json, de
71708
74697
  }
71709
74698
  function localMigrationCheckCandidate(status) {
71710
74699
  const byKind = (kind) => [...status.reconstructed.values()].filter((record3) => record3.recordKind === kind).map((record3) => record3.fullData);
71711
- const migrations = [...status.reconstructed.values()].filter((record3) => record3.recordKind === "migration").map((record3) => ({ data: record3.fullData, file: record3.file })).sort(
74700
+ const migrations = [...status.reconstructed.values()].filter((record3) => record3.recordKind === "migration").map((record3) => ({
74701
+ data: record3.fullData,
74702
+ file: record3.file,
74703
+ recordId: record3.recordId
74704
+ })).sort(
71712
74705
  (left, right) => Number(left.data.order ?? 0) - Number(right.data.order ?? 0)
71713
74706
  );
71714
74707
  return {
@@ -71896,6 +74889,11 @@ function resolveAssignTarget(write, instanceValue, migrationName) {
71896
74889
  }
71897
74890
  return { childKey };
71898
74891
  }
74892
+ function migrationTargetClassName(thisClass) {
74893
+ if (thisClass === null) return null;
74894
+ if (typeof thisClass.name !== "string") return null;
74895
+ return thisClass.name;
74896
+ }
71899
74897
  async function runPendingMigrations(workspace, client, raw, migrations, onlyRef, dryRunFlagUnused) {
71900
74898
  void dryRunFlagUnused;
71901
74899
  const dryRun = process.argv.includes("--dry-run");
@@ -71956,16 +74954,28 @@ async function runPendingMigrations(workspace, client, raw, migrations, onlyRef,
71956
74954
  );
71957
74955
  }
71958
74956
  const pinned = migration.action;
71959
- const compiled = pinned !== void 0 && pinned !== null ? pinned : compileNSAction(migration.code, {
71960
- project: document.project,
71961
- members: document.members,
71962
- classes: document.classes,
71963
- enums: document.enums,
71964
- interfaces: document.interfaces,
71965
- thisClass,
71966
- dialogueContext: null,
71967
- migrationContext: true
71968
- });
74957
+ const migrationCode = migration.code;
74958
+ const compiled = pinned !== void 0 && pinned !== null ? pinned : compileNeoScriptBodyOrThrow(
74959
+ () => compileNSAction(migrationCode, {
74960
+ project: document.project,
74961
+ members: document.members,
74962
+ classes: document.classes,
74963
+ enums: document.enums,
74964
+ interfaces: document.interfaces,
74965
+ thisClass,
74966
+ dialogueContext: null,
74967
+ migrationContext: true
74968
+ }),
74969
+ {
74970
+ recordKind: "migration",
74971
+ recordId: String(migration.id),
74972
+ ownerName: migrationTargetClassName(thisClass),
74973
+ memberName: migrationName,
74974
+ unit: "migration",
74975
+ code: migrationCode
74976
+ },
74977
+ null
74978
+ );
71969
74979
  const instances = targetClassId === null ? [null] : collectClassInstances(document, targetClassId, memberById, valueById);
71970
74980
  let instanceEditCount = 0;
71971
74981
  for (const instance of instances) {
@@ -72512,6 +75522,7 @@ var init_migrate = __esm({
72512
75522
  init_workspace_status();
72513
75523
  init_source_diagnostics();
72514
75524
  init_compiler_adapter();
75525
+ init_push_body_diagnostics();
72515
75526
  init_neoscript_evaluator();
72516
75527
  init_script();
72517
75528
  init_project_migration_created_values();
@@ -73928,7 +76939,7 @@ __export(content_exports, {
73928
76939
  runRecords: () => runRecords,
73929
76940
  runValues: () => runValues
73930
76941
  });
73931
- import { readFileSync as readFileSync11 } from "node:fs";
76942
+ import { readFileSync as readFileSync12 } from "node:fs";
73932
76943
  import { randomUUID as randomUUID4 } from "node:crypto";
73933
76944
  function versionPath(workspace, suffix) {
73934
76945
  return `/api/projects/${workspace.config.projectId}/versions/${workspace.config.versionId}/${suffix}`;
@@ -73936,9 +76947,9 @@ function versionPath(workspace, suffix) {
73936
76947
  function readBatch(file) {
73937
76948
  let raw = null;
73938
76949
  if (file !== null) {
73939
- raw = readFileSync11(file, "utf8");
76950
+ raw = readFileSync12(file, "utf8");
73940
76951
  } else if (!process.stdin.isTTY) {
73941
- raw = readFileSync11(0, "utf8");
76952
+ raw = readFileSync12(0, "utf8");
73942
76953
  if (raw.trim().length === 0) raw = null;
73943
76954
  }
73944
76955
  if (raw === null) return null;
@@ -75095,7 +78106,7 @@ async function runFiles(context, subcommand, positional) {
75095
78106
  "Usage: neo files texture-settings <fileId> --file <settings.json>"
75096
78107
  );
75097
78108
  }
75098
- const payload = JSON.parse(readFileSync11(payloadPath, "utf8"));
78109
+ const payload = JSON.parse(readFileSync12(payloadPath, "utf8"));
75099
78110
  const result = await context.client.post(
75100
78111
  versionPath(context.workspace, `files/${fileId}/unity-texture-settings`),
75101
78112
  payload
@@ -75124,7 +78135,7 @@ __export(export_exports, {
75124
78135
  runExportUnity: () => runExportUnity
75125
78136
  });
75126
78137
  import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "node:fs";
75127
- import { join as join13 } from "node:path";
78138
+ import { join as join14 } from "node:path";
75128
78139
  async function runExportUnity(workspace, outDir) {
75129
78140
  if (outDir === null) {
75130
78141
  throw new Error(
@@ -75136,23 +78147,23 @@ async function runExportUnity(workspace, outDir) {
75136
78147
  `/api/projects/${workspace.config.projectId}/export`,
75137
78148
  { versionId: workspace.config.versionId }
75138
78149
  );
75139
- const resourcesDir = join13(outDir, "Resources", "Neo");
75140
- const localizationDir = join13(resourcesDir, "Localization");
75141
- const scriptsDir = join13(outDir, "Scripts", "Neo");
78150
+ const resourcesDir = join14(outDir, "Resources", "Neo");
78151
+ const localizationDir = join14(resourcesDir, "Localization");
78152
+ const scriptsDir = join14(outDir, "Scripts", "Neo");
75142
78153
  mkdirSync10(localizationDir, { recursive: true });
75143
78154
  mkdirSync10(scriptsDir, { recursive: true });
75144
- writeFileSync10(join13(resourcesDir, "project.json"), response.projectJson);
78155
+ writeFileSync10(join14(resourcesDir, "project.json"), response.projectJson);
75145
78156
  writeFileSync10(
75146
- join13(scriptsDir, "NeoGeneratedTypes.cs"),
78157
+ join14(scriptsDir, "NeoGeneratedTypes.cs"),
75147
78158
  response.generatedTypes
75148
78159
  );
75149
78160
  for (const file of response.localizationFiles ?? []) {
75150
- writeFileSync10(join13(localizationDir, file.fileName), file.content);
78161
+ writeFileSync10(join14(localizationDir, file.fileName), file.content);
75151
78162
  }
75152
- console.log(`wrote ${join13(resourcesDir, "project.json")}`);
75153
- console.log(`wrote ${join13(scriptsDir, "NeoGeneratedTypes.cs")}`);
78163
+ console.log(`wrote ${join14(resourcesDir, "project.json")}`);
78164
+ console.log(`wrote ${join14(scriptsDir, "NeoGeneratedTypes.cs")}`);
75154
78165
  for (const file of response.localizationFiles ?? []) {
75155
- console.log(`wrote ${join13(localizationDir, file.fileName)}`);
78166
+ console.log(`wrote ${join14(localizationDir, file.fileName)}`);
75156
78167
  }
75157
78168
  const diagnostics = response.diagnostics ?? [];
75158
78169
  for (const diagnostic of diagnostics) {
@@ -75298,8 +78309,8 @@ var init_project_source_bundle = __esm({
75298
78309
  });
75299
78310
 
75300
78311
  // src/project-source/project-file-push.ts
75301
- import { basename as basename2, join as join14 } from "node:path";
75302
- import { readFileSync as readFileSync12 } from "node:fs";
78312
+ import { basename as basename2, join as join15 } from "node:path";
78313
+ import { readFileSync as readFileSync13 } from "node:fs";
75303
78314
  function ensureProjectFileBinaryChangesV4(args) {
75304
78315
  for (const binary of args.binaryChanges) {
75305
78316
  if (binary.action !== "upload") continue;
@@ -75344,8 +78355,8 @@ function prepareProjectFilePushesV4(args) {
75344
78355
  `Project file ${recordId} has upload bytes but its source change has no record data.`
75345
78356
  );
75346
78357
  }
75347
- const absolute = join14(args.workspace.root, binary.path);
75348
- const bytes = new Uint8Array(readFileSync12(absolute));
78358
+ const absolute = join15(args.workspace.root, binary.path);
78359
+ const bytes = new Uint8Array(readFileSync13(absolute));
75349
78360
  const digest = sha256Bytes(bytes);
75350
78361
  if (binary.localSha256 !== null && digest !== binary.localSha256) {
75351
78362
  throw new Error(
@@ -75677,9 +78688,9 @@ var init_project_file_push = __esm({
75677
78688
  // src/project-source/trusted-commit-verification.ts
75678
78689
  import { randomUUID as randomUUID5 } from "node:crypto";
75679
78690
  import { tmpdir } from "node:os";
75680
- import { join as join15 } from "node:path";
78691
+ import { join as join16 } from "node:path";
75681
78692
  function verifyProjectSourceCommitAgainstStateV4(args) {
75682
- const root = join15(tmpdir(), `neo-source-verify-virtual-${randomUUID5()}`);
78693
+ const root = join16(tmpdir(), `neo-source-verify-virtual-${randomUUID5()}`);
75683
78694
  const workspace = {
75684
78695
  root,
75685
78696
  config: {
@@ -76166,10 +79177,10 @@ import {
76166
79177
  mkdirSync as mkdirSync11,
76167
79178
  writeFileSync as writeFileSync11,
76168
79179
  rmSync as rmSync6,
76169
- existsSync as existsSync12,
76170
- readFileSync as readFileSync13
79180
+ existsSync as existsSync13,
79181
+ readFileSync as readFileSync14
76171
79182
  } from "node:fs";
76172
- import { dirname as dirname7, join as join16, relative as relative5, sep as sep5 } from "node:path";
79183
+ import { dirname as dirname7, join as join17, relative as relative5, sep as sep5 } from "node:path";
76173
79184
  function assignPendingIds(changes, staticValueSeeds, reconstructed3) {
76174
79185
  const assigned = /* @__PURE__ */ new Map();
76175
79186
  const assign = (pendingId2) => {
@@ -76734,9 +79745,11 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
76734
79745
  (change) => change.recordKind === "member" || change.recordKind === "class" || change.recordKind === "enum" || change.recordKind === "interface" || change.nextData !== void 0 && change.recordKind === "migration" && typeof change.nextData.code === "string" && change.nextData.code.trim().length > 0
76735
79746
  );
76736
79747
  const compileSchema = needsNeoScriptCompilation ? await buildPostPushCompileSchema(workspace, status) : null;
79748
+ const bodySourceLocator = createNeoScriptBodySourceLocator(workspace, status);
76737
79749
  if (compileSchema !== null) {
76738
79750
  prepareCompleteNeoScriptBodyChanges(workspace, status, compileSchema, {
76739
- completeSweep: workspaceChangesRequireCompleteBodySweep(status.changes)
79751
+ completeSweep: workspaceChangesRequireCompleteBodySweep(status.changes),
79752
+ bodySourceLocator
76740
79753
  });
76741
79754
  }
76742
79755
  const now = Date.now();
@@ -76745,19 +79758,25 @@ ${blockingErrors.map((error) => ` ${error.message}`).join("\n")}`
76745
79758
  if (change.recordKind === "migration" && typeof change.nextData.code === "string" && change.nextData.code.trim().length > 0) {
76746
79759
  change.nextData = {
76747
79760
  ...change.nextData,
76748
- action: compileMigrationAction(compileSchema, change.nextData)
79761
+ action: compileMigrationAction(
79762
+ compileSchema,
79763
+ change.nextData,
79764
+ bodySourceLocator
79765
+ )
76749
79766
  };
76750
79767
  }
76751
79768
  if (change.recordKind === "member" && change.nextData.kind === 10) {
76752
79769
  change.nextData = compileNSPropertyChange(
76753
79770
  compileSchema,
76754
- change.nextData
79771
+ change.nextData,
79772
+ bodySourceLocator
76755
79773
  );
76756
79774
  }
76757
79775
  if (change.recordKind === "member" && change.nextData.kind === 23) {
76758
79776
  change.nextData = compileNSFunctionChange(
76759
79777
  compileSchema,
76760
- change.nextData
79778
+ change.nextData,
79779
+ bodySourceLocator
76761
79780
  );
76762
79781
  }
76763
79782
  if (change.kind === "create") {
@@ -77672,13 +80691,13 @@ ${finalErrors.map(
77672
80691
  for (const recordState of Object.values(workspace.state.records)) {
77673
80692
  const previousPath = recordState.file;
77674
80693
  if (previousPath === void 0 || emittedPaths.has(previousPath)) continue;
77675
- const absolute = join16(workspace.root, previousPath);
77676
- if (existsSync12(absolute)) rmSync6(absolute);
80694
+ const absolute = join17(workspace.root, previousPath);
80695
+ if (existsSync13(absolute)) rmSync6(absolute);
77677
80696
  }
77678
80697
  for (const file of files) {
77679
- const absolute = join16(workspace.root, file.path);
80698
+ const absolute = join17(workspace.root, file.path);
77680
80699
  mkdirSync11(dirname7(absolute), { recursive: true });
77681
- const existing = existsSync12(absolute) ? readFileSync13(absolute, "utf8") : null;
80700
+ const existing = existsSync13(absolute) ? readFileSync14(absolute, "utf8") : null;
77682
80701
  if (existing !== file.content)
77683
80702
  writeFileSync11(absolute, file.content, "utf8");
77684
80703
  }
@@ -77715,7 +80734,7 @@ function materializeAssignedSchemaIdsInAuthoredSource(workspace, replacements) {
77715
80734
  return {
77716
80735
  uri,
77717
80736
  kind,
77718
- text: readFileSync13(absolutePath, "utf8")
80737
+ text: readFileSync14(absolutePath, "utf8")
77719
80738
  };
77720
80739
  }
77721
80740
  );
@@ -77905,72 +80924,107 @@ function containingClass(schema, memberId) {
77905
80924
  return isObjectRecord2(classSchema) && Object.values(classSchema).includes(memberId);
77906
80925
  }) ?? null;
77907
80926
  }
77908
- function compileNSPropertyChange(schema, memberData) {
80927
+ function bodyOwnerName(thisClass) {
80928
+ if (thisClass === null) return null;
80929
+ if (typeof thisClass.name !== "string") return null;
80930
+ return thisClass.name;
80931
+ }
80932
+ function compileNSPropertyChange(schema, memberData, locator) {
77909
80933
  const memberId = memberData.id;
77910
80934
  const thisClass = containingClass(schema, memberId);
77911
80935
  const returnTypeInfo = resolveNSPropertyReturnTypeInfo(
77912
80936
  memberData,
77913
80937
  schema.members
77914
80938
  );
80939
+ const bodyIdentity = {
80940
+ recordKind: "member",
80941
+ recordId: String(memberId),
80942
+ ownerName: bodyOwnerName(thisClass),
80943
+ memberName: String(memberData.name ?? memberId)
80944
+ };
77915
80945
  const next = { ...memberData };
77916
80946
  if (typeof memberData.code === "string") {
77917
- next.getter = compileNSGetter2(memberData.code, {
77918
- project: schema.project,
77919
- members: schema.members,
77920
- classes: schema.classes,
77921
- enums: schema.enums,
77922
- interfaces: schema.interfaces,
77923
- thisClass,
77924
- returnTypeInfo,
77925
- dialogueContext: null,
77926
- implicitMemberAccess: true,
77927
- staticMember: memberData.isStatic === true
77928
- });
80947
+ const code = memberData.code;
80948
+ next.getter = compileNeoScriptBodyOrThrow(
80949
+ () => compileNSGetter2(code, {
80950
+ project: schema.project,
80951
+ members: schema.members,
80952
+ classes: schema.classes,
80953
+ enums: schema.enums,
80954
+ interfaces: schema.interfaces,
80955
+ thisClass,
80956
+ returnTypeInfo,
80957
+ dialogueContext: null,
80958
+ implicitMemberAccess: true,
80959
+ staticMember: memberData.isStatic === true
80960
+ }),
80961
+ { ...bodyIdentity, unit: "getter", code },
80962
+ locator
80963
+ );
77929
80964
  } else {
77930
80965
  delete next.getter;
77931
80966
  }
77932
80967
  if (typeof memberData.setterCode === "string") {
77933
- next.setter = compileNSSetter2(memberData.setterCode, {
77934
- project: schema.project,
77935
- members: schema.members,
77936
- classes: schema.classes,
77937
- enums: schema.enums,
77938
- interfaces: schema.interfaces,
77939
- thisClass,
77940
- valueTypeInfo: returnTypeInfo,
77941
- implicitMemberAccess: true,
77942
- staticMember: memberData.isStatic === true
77943
- });
80968
+ const setterCode = memberData.setterCode;
80969
+ next.setter = compileNeoScriptBodyOrThrow(
80970
+ () => compileNSSetter2(setterCode, {
80971
+ project: schema.project,
80972
+ members: schema.members,
80973
+ classes: schema.classes,
80974
+ enums: schema.enums,
80975
+ interfaces: schema.interfaces,
80976
+ thisClass,
80977
+ valueTypeInfo: returnTypeInfo,
80978
+ implicitMemberAccess: true,
80979
+ staticMember: memberData.isStatic === true
80980
+ }),
80981
+ { ...bodyIdentity, unit: "setter", code: setterCode },
80982
+ locator
80983
+ );
77944
80984
  } else {
77945
80985
  delete next.setter;
77946
80986
  }
77947
80987
  return next;
77948
80988
  }
77949
- function compileNSFunctionChange(schema, memberData) {
80989
+ function compileNSFunctionChange(schema, memberData, locator) {
77950
80990
  const next = { ...memberData };
77951
80991
  if (typeof memberData.code !== "string") {
77952
80992
  delete next.action;
77953
80993
  return next;
77954
80994
  }
80995
+ const code = memberData.code;
77955
80996
  const contract = resolveNSFunctionContract(memberData, schema.members);
77956
- next.action = compileNSFunction2(memberData.code, {
77957
- project: schema.project,
77958
- members: schema.members,
77959
- classes: schema.classes,
77960
- enums: schema.enums,
77961
- interfaces: schema.interfaces,
77962
- thisClass: containingClass(schema, memberData.id),
77963
- returnTypeInfo: contract.returnTypeInfo,
77964
- argumentTypes: contract.argumentTypes,
77965
- deferred: contract.deferred,
77966
- functionName: String(memberData.name ?? memberData.id),
77967
- implicitMemberAccess: true,
77968
- staticMember: memberData.isStatic === true
77969
- });
80997
+ const thisClass = containingClass(schema, memberData.id);
80998
+ next.action = compileNeoScriptBodyOrThrow(
80999
+ () => compileNSFunction2(code, {
81000
+ project: schema.project,
81001
+ members: schema.members,
81002
+ classes: schema.classes,
81003
+ enums: schema.enums,
81004
+ interfaces: schema.interfaces,
81005
+ thisClass,
81006
+ returnTypeInfo: contract.returnTypeInfo,
81007
+ argumentTypes: contract.argumentTypes,
81008
+ deferred: contract.deferred,
81009
+ functionName: String(memberData.name ?? memberData.id),
81010
+ implicitMemberAccess: true,
81011
+ staticMember: memberData.isStatic === true
81012
+ }),
81013
+ {
81014
+ recordKind: "member",
81015
+ recordId: String(memberData.id),
81016
+ ownerName: bodyOwnerName(thisClass),
81017
+ memberName: String(memberData.name ?? memberData.id),
81018
+ unit: "function",
81019
+ code
81020
+ },
81021
+ locator
81022
+ );
77970
81023
  return next;
77971
81024
  }
77972
81025
  function prepareCompleteNeoScriptBodyChanges(workspace, status, schema, options = {}) {
77973
81026
  const completeSweep = options.completeSweep ?? true;
81027
+ const locator = options.bodySourceLocator ?? createNeoScriptBodySourceLocator(workspace, status);
77974
81028
  const changesById = new Map(
77975
81029
  status.changes.filter((change) => change.recordKind === "member").map((change) => [change.recordId, change])
77976
81030
  );
@@ -77980,10 +81034,10 @@ function prepareCompleteNeoScriptBodyChanges(workspace, status, schema, options
77980
81034
  return member;
77981
81035
  }
77982
81036
  if (member.kind === 10) {
77983
- return compileNSPropertyChange(schema, member);
81037
+ return compileNSPropertyChange(schema, member, locator);
77984
81038
  }
77985
81039
  if (member.kind === 23) {
77986
- return compileNSFunctionChange(schema, member);
81040
+ return compileNSFunctionChange(schema, member, locator);
77987
81041
  }
77988
81042
  return member;
77989
81043
  });
@@ -78322,21 +81376,33 @@ function resolveNSFunctionContract(member, members) {
78322
81376
  }
78323
81377
  return { returnTypeInfo, argumentTypes, deferred };
78324
81378
  }
78325
- function compileMigrationAction(schema, migrationData) {
81379
+ function compileMigrationAction(schema, migrationData, locator) {
78326
81380
  const targetClassId = migrationData.targetClassId;
78327
81381
  const thisClass = typeof targetClassId === "string" ? schema.classes.find(
78328
81382
  (schemaClass2) => schemaClass2.id === targetClassId
78329
81383
  ) ?? null : null;
78330
- return compileNSAction2(String(migrationData.code), {
78331
- project: schema.project,
78332
- members: schema.members,
78333
- classes: schema.classes,
78334
- enums: schema.enums,
78335
- interfaces: schema.interfaces,
78336
- thisClass,
78337
- dialogueContext: null,
78338
- migrationContext: true
78339
- });
81384
+ const code = String(migrationData.code);
81385
+ return compileNeoScriptBodyOrThrow(
81386
+ () => compileNSAction2(code, {
81387
+ project: schema.project,
81388
+ members: schema.members,
81389
+ classes: schema.classes,
81390
+ enums: schema.enums,
81391
+ interfaces: schema.interfaces,
81392
+ thisClass,
81393
+ dialogueContext: null,
81394
+ migrationContext: true
81395
+ }),
81396
+ {
81397
+ recordKind: "migration",
81398
+ recordId: String(migrationData.id),
81399
+ ownerName: bodyOwnerName(thisClass),
81400
+ memberName: String(migrationData.name ?? migrationData.id),
81401
+ unit: "migration",
81402
+ code
81403
+ },
81404
+ locator
81405
+ );
78340
81406
  }
78341
81407
  var compileNSAction2, compileNSFunction2, compileNSGetter2, compileNSSetter2, ProjectTransactionInterruptedError, ProjectTransactionFailedError, PROJECT_TRANSACTION_POLL_DELAYS_MS;
78342
81408
  var init_push = __esm({
@@ -78365,6 +81431,7 @@ var init_push = __esm({
78365
81431
  init_project_manifest();
78366
81432
  init_merge();
78367
81433
  init_push_change_intent();
81434
+ init_push_body_diagnostics();
78368
81435
  ({ compileNSAction: compileNSAction2, compileNSFunction: compileNSFunction2, compileNSGetter: compileNSGetter2, compileNSSetter: compileNSSetter2 } = compiler_adapter_exports);
78369
81436
  ProjectTransactionInterruptedError = class extends Error {
78370
81437
  constructor(transactionId) {
@@ -78403,7 +81470,7 @@ __export(dev_exports, {
78403
81470
  runDev: () => runDev
78404
81471
  });
78405
81472
  import { watch } from "node:fs";
78406
- import { join as join17 } from "node:path";
81473
+ import { join as join18 } from "node:path";
78407
81474
  import { emitKeypressEvents } from "node:readline";
78408
81475
  import { ConvexClient } from "convex/browser";
78409
81476
  function isSchemaSignal(value) {
@@ -78513,7 +81580,7 @@ async function runDev(workspace, options) {
78513
81580
  };
78514
81581
  for (const dir of ["Classes", "Enums"]) {
78515
81582
  try {
78516
- watch(join17(workspace.root, dir), { persistent: true }, onFileChange);
81583
+ watch(join18(workspace.root, dir), { persistent: true }, onFileChange);
78517
81584
  } catch {
78518
81585
  }
78519
81586
  }
@@ -78568,12 +81635,12 @@ __export(resolve_exports, {
78568
81635
  runResolve: () => runResolve,
78569
81636
  workspaceFilePath: () => workspaceFilePath
78570
81637
  });
78571
- import { readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "node:fs";
78572
- import { join as join18 } from "node:path";
81638
+ import { readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "node:fs";
81639
+ import { join as join19 } from "node:path";
78573
81640
  function runResolve(workspace, side) {
78574
81641
  let resolvedFiles = 0;
78575
81642
  for (const filePath of listProjectSourceFilesV4(workspace.root)) {
78576
- const source = readFileSync14(filePath, "utf8");
81643
+ const source = readFileSync15(filePath, "utf8");
78577
81644
  if (detectConflictMarkers(source) === null) continue;
78578
81645
  const resolved = resolveMarkers(source, side);
78579
81646
  writeFileSync12(filePath, resolved, "utf8");
@@ -78584,12 +81651,12 @@ function runResolve(workspace, side) {
78584
81651
  const binary = state.projectBinary;
78585
81652
  const conflict2 = binary?.conflict;
78586
81653
  if (binary === void 0 || conflict2 === void 0) continue;
78587
- const destination = join18(workspace.root, binary.path);
81654
+ const destination = join19(workspace.root, binary.path);
78588
81655
  if (side === "theirs") {
78589
81656
  if (conflict2.remoteSha256 !== null && conflict2.artifactPath !== void 0) {
78590
81657
  writeVerifiedBinaryDownloadV4(
78591
81658
  destination,
78592
- readFileSync14(join18(workspace.root, conflict2.artifactPath)),
81659
+ readFileSync15(join19(workspace.root, conflict2.artifactPath)),
78593
81660
  conflict2.remoteSha256
78594
81661
  );
78595
81662
  binary.sha256 = conflict2.remoteSha256;
@@ -78599,7 +81666,7 @@ function runResolve(workspace, side) {
78599
81666
  }
78600
81667
  }
78601
81668
  if (conflict2.artifactPath !== void 0) {
78602
- rmSync7(join18(workspace.root, conflict2.artifactPath), { force: true });
81669
+ rmSync7(join19(workspace.root, conflict2.artifactPath), { force: true });
78603
81670
  }
78604
81671
  delete binary.conflict;
78605
81672
  resolvedBinaries += 1;
@@ -78652,7 +81719,7 @@ function resolveMarkers(source, side) {
78652
81719
  return output.join("\n");
78653
81720
  }
78654
81721
  function workspaceFilePath(workspace, file) {
78655
- return join18(workspace.root, file);
81722
+ return join19(workspace.root, file);
78656
81723
  }
78657
81724
  var init_resolve = __esm({
78658
81725
  "src/commands/resolve.ts"() {