@neocompose/cli 0.17.0 → 0.18.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 +2657 -60
  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(
@@ -7529,6 +8674,7 @@ var init_strict_resolver = __esm({
7529
8674
  }
7530
8675
  }
7531
8676
  }
8677
+ mergeBranchInvalidations(scope, branchExitOwnershipScopes);
7532
8678
  mergeBranchOwnership(scope, branchExitOwnershipScopes);
7533
8679
  return {
7534
8680
  type: "if" /* If */,
@@ -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,6 +9378,13 @@ 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
9389
  const target = local && statement.op === "=" ? { ...resolvedTarget, type: local.type } : resolvedTarget;
7631
9390
  const writability = local?.writability ?? target.writability;
@@ -8120,7 +9879,8 @@ var init_strict_resolver = __esm({
8120
9879
  variableId: entry.variableId
8121
9880
  },
8122
9881
  type: scope.lookupNarrowedType(name, entry) ?? entry.type,
8123
- ...ownership ? { writability: ownership } : {}
9882
+ ...ownership ? { writability: ownership } : {},
9883
+ ...entry.writeRoot ? { writeRoot: entry.writeRoot } : {}
8124
9884
  };
8125
9885
  }
8126
9886
  const global = this.context.project.globals.find(
@@ -8485,6 +10245,7 @@ var init_strict_resolver = __esm({
8485
10245
  };
8486
10246
  }
8487
10247
  const writability = this.memberWritability(member, receiver);
10248
+ const entryWritability = member.lookup?.multiselect === true ? this.lookupEntryWritability(member, receiver) : void 0;
8488
10249
  const lookupWire = member.lookup?.multiselect === true && memberType2.kind === "set" ? {
8489
10250
  type: 9 /* Lookup */,
8490
10251
  required: !isNullable(memberType2),
@@ -8505,7 +10266,8 @@ var init_strict_resolver = __esm({
8505
10266
  ),
8506
10267
  symbol: member,
8507
10268
  ...writability ? { writability } : {},
8508
- ...lookupWire ? { wireType: lookupWire } : {}
10269
+ ...lookupWire ? { wireType: lookupWire } : {},
10270
+ ...entryWritability ? { entryWritability } : {}
8509
10271
  };
8510
10272
  }
8511
10273
  throw new CompileError(
@@ -8583,6 +10345,10 @@ var init_strict_resolver = __esm({
8583
10345
  if (member.lookup.multiselect) {
8584
10346
  return member.writability ? toWritability(member.writability) : receiver.writability ?? "save" /* Save */;
8585
10347
  }
10348
+ return this.lookupEntryWritability(member, receiver);
10349
+ }
10350
+ lookupEntryWritability(member, receiver) {
10351
+ if (!member.lookup) return "readOnly" /* ReadOnly */;
8586
10352
  const target = this.project.symbolById.get(
8587
10353
  member.lookup.collectionMemberId
8588
10354
  );
@@ -9380,11 +11146,18 @@ var init_strict_resolver = __esm({
9380
11146
  pos
9381
11147
  );
9382
11148
  const filteredType = receiver.type.kind === "list" || receiver.type.kind === "dictionary" || receiver.type.kind === "set" ? { ...receiver.type, readOnly: true } : receiver.type;
9383
- return intrinsic(
11149
+ const filtered = intrinsic(
9384
11150
  "where" /* Where */,
9385
11151
  { collectionPointer: receiver.pointer, function: fn },
9386
11152
  filteredType
9387
11153
  );
11154
+ return {
11155
+ ...filtered,
11156
+ ...receiver.writability ? { writability: receiver.writability } : {},
11157
+ ...receiver.wireType ? { wireType: receiver.wireType } : {},
11158
+ writeRoot: receiver.writeRoot ?? this.writeThroughRoot(receiver.pointer, scope),
11159
+ ...receiver.entryWritability ? { entryWritability: receiver.entryWritability } : {}
11160
+ };
9388
11161
  }
9389
11162
  if (name === "First" || name === "FirstOrDefault" || name === "Find") {
9390
11163
  if (args.length > 1) {
@@ -9401,7 +11174,7 @@ var init_strict_resolver = __esm({
9401
11174
  pos
9402
11175
  ) : void 0;
9403
11176
  const result = name === "FirstOrDefault" || name === "Find" ? { ...collection.valueType, nullable: true } : collection.valueType;
9404
- return intrinsic(
11177
+ const first = intrinsic(
9405
11178
  name === "First" ? "first" /* First */ : "firstOrDefault" /* FirstOrDefault */,
9406
11179
  {
9407
11180
  collectionPointer: receiver.pointer,
@@ -9409,6 +11182,12 @@ var init_strict_resolver = __esm({
9409
11182
  },
9410
11183
  result
9411
11184
  );
11185
+ const entryWritability = receiver.entryWritability ?? receiver.writability ?? "runtime" /* Runtime */;
11186
+ return {
11187
+ ...first,
11188
+ writability: entryWritability,
11189
+ writeRoot: this.expressionWriteRoot(receiver, scope)
11190
+ };
9412
11191
  }
9413
11192
  if (name === "Select") {
9414
11193
  requireArgCount(name, args, 1, pos);
@@ -9416,7 +11195,13 @@ var init_strict_resolver = __esm({
9416
11195
  if (lambda.kind !== "lambda") {
9417
11196
  throw new CompileError("Select requires a lambda argument.", pos);
9418
11197
  }
9419
- const resultType = this.inferLambdaReturnType(lambda, collection, scope);
11198
+ const inferredReturn = this.inferLambdaReturn(
11199
+ lambda,
11200
+ collection,
11201
+ receiver,
11202
+ scope
11203
+ );
11204
+ const resultType = inferredReturn.type;
9420
11205
  const fn = this.resolveCollectionLambda(
9421
11206
  lambda,
9422
11207
  collection,
@@ -9424,11 +11209,16 @@ var init_strict_resolver = __esm({
9424
11209
  scope,
9425
11210
  pos
9426
11211
  );
9427
- return intrinsic(
11212
+ const selected2 = intrinsic(
9428
11213
  "select" /* Select */,
9429
11214
  { collectionPointer: receiver.pointer, function: fn },
9430
11215
  { kind: "list", elementType: resultType, readOnly: true }
9431
11216
  );
11217
+ return {
11218
+ ...selected2,
11219
+ entryWritability: inferredReturn.writability,
11220
+ writeRoot: inferredReturn.writeRoot
11221
+ };
9432
11222
  }
9433
11223
  return null;
9434
11224
  }
@@ -9594,6 +11384,7 @@ var init_strict_resolver = __esm({
9594
11384
  const scope = new Scope(outerScope);
9595
11385
  const parameterTypes = collection.kind === "dictionary" ? [collection.keyType, collection.valueType] : [collection.valueType];
9596
11386
  const parameters = ast.params.map((parameter3, index) => {
11387
+ this.assertLocalNameAvailable(parameter3.name, scope, parameter3.pos);
9597
11388
  const inferred = requiredAt(parameterTypes, index);
9598
11389
  const declared = parameter3.type ? this.resolveType(parameter3.type) : inferred;
9599
11390
  if (parameter3.type)
@@ -9615,6 +11406,11 @@ var init_strict_resolver = __esm({
9615
11406
  );
9616
11407
  return item;
9617
11408
  });
11409
+ this.functionControlBoundaries.push({
11410
+ controlDepth: this.controlContexts.length,
11411
+ loopFlowDepth: this.loopFlowContexts.length,
11412
+ catchDepth: this.catchContexts.length
11413
+ });
9618
11414
  this.lambdaDepth++;
9619
11415
  this.expectedReturnStack.push(returnType);
9620
11416
  try {
@@ -9634,31 +11430,42 @@ var init_strict_resolver = __esm({
9634
11430
  } finally {
9635
11431
  this.expectedReturnStack.pop();
9636
11432
  this.lambdaDepth--;
11433
+ this.functionControlBoundaries.pop();
9637
11434
  }
9638
11435
  }
9639
- inferLambdaReturnType(lambda, collection, outerScope) {
11436
+ inferLambdaReturn(lambda, collection, source, outerScope) {
9640
11437
  const returns = collectReturns(lambda.body);
9641
11438
  if (returns.length === 0) {
9642
11439
  throw new CompileError("Select lambda must return a value.", lambda.pos);
9643
11440
  }
9644
11441
  const scope = new Scope(outerScope);
9645
11442
  const parameterTypes = collection.kind === "dictionary" ? [collection.keyType, collection.valueType] : [collection.valueType];
11443
+ const valueParameterIndex = collection.kind === "dictionary" ? 1 : 0;
11444
+ const sourceWritability = source.entryWritability ?? source.writability ?? "runtime" /* Runtime */;
11445
+ const sourceWriteRoot = this.expressionWriteRoot(source, outerScope);
9646
11446
  lambda.params.forEach((parameter3, index) => {
11447
+ this.assertLocalNameAvailable(parameter3.name, scope, parameter3.pos);
9647
11448
  const type = parameter3.type ? this.resolveType(parameter3.type) : requiredAt(parameterTypes, index);
11449
+ const isValueParameter = index === valueParameterIndex;
9648
11450
  scope.define(
9649
11451
  scopeVariable(
9650
11452
  parameter3.name,
9651
11453
  variable(parameter3.name, type, void 0, this.project),
9652
11454
  type,
9653
- void 0,
9654
- "runtime" /* Runtime */
11455
+ isValueParameter ? sourceWritability : "runtime" /* Runtime */,
11456
+ isValueParameter ? sourceWritability : "runtime" /* Runtime */,
11457
+ isValueParameter ? sourceWriteRoot : "local"
9655
11458
  )
9656
11459
  );
9657
11460
  });
9658
11461
  let inferred = null;
11462
+ let inferredWritability = null;
11463
+ let inferredWriteRoot = null;
9659
11464
  for (const returned of returns) {
9660
11465
  if (!returned.expr) continue;
9661
11466
  const current = this.resolveExpression(returned.expr, scope);
11467
+ const currentWritability = current.writability ?? "runtime" /* Runtime */;
11468
+ const currentWriteRoot = this.expressionWriteRoot(current, scope);
9662
11469
  if (!inferred) inferred = current.type;
9663
11470
  else {
9664
11471
  const common = commonNeoScriptAssignableType(
@@ -9674,8 +11481,14 @@ var init_strict_resolver = __esm({
9674
11481
  }
9675
11482
  inferred = common;
9676
11483
  }
11484
+ inferredWritability = inferredWritability === null ? currentWritability : inferredWritability === currentWritability ? inferredWritability : "runtime" /* Runtime */;
11485
+ inferredWriteRoot = inferredWriteRoot === null ? currentWriteRoot : inferredWriteRoot === currentWriteRoot ? inferredWriteRoot : "unknown";
9677
11486
  }
9678
- return inferred ?? { kind: "primitive", name: "null", nullable: true };
11487
+ return {
11488
+ type: inferred ?? { kind: "primitive", name: "null", nullable: true },
11489
+ writability: inferredWritability ?? "runtime" /* Runtime */,
11490
+ writeRoot: inferredWriteRoot ?? "unknown"
11491
+ };
9679
11492
  }
9680
11493
  resolveFunctionStatement(expression, scope) {
9681
11494
  if (expression.kind !== "call" || expression.callee.kind !== "member")
@@ -10129,6 +11942,14 @@ var init_strict_resolver = __esm({
10129
11942
  }
10130
11943
  return this.writeTargetRoot(pointer, scope);
10131
11944
  }
11945
+ expressionWriteRoot(expression, scope) {
11946
+ if (expression.writeRoot) return expression.writeRoot;
11947
+ if (expression.pointer.type === "value" /* Value */) return "local";
11948
+ if (expression.pointer.type === "function" /* Function */ && CONSTRUCTING_FUNCTION_KINDS.has(expression.pointer.function.type)) {
11949
+ return "local";
11950
+ }
11951
+ return this.writeThroughRoot(expression.pointer, scope);
11952
+ }
10132
11953
  /**
10133
11954
  * P43 §7.1. The write-target root a newly declared local inherits.
10134
11955
  *
@@ -10478,10 +12299,17 @@ function formatNeoScript(text, options = {}) {
10478
12299
  const tabSize = options.tabSize ?? 2;
10479
12300
  const indentUnit = options.insertSpaces === false ? " " : " ".repeat(tabSize);
10480
12301
  const lines = text.replace(/\r\n/g, "\n").split("\n");
12302
+ const normalizedText = lines.join("\n");
10481
12303
  const syntax = analyzeNeoScriptSyntax(
10482
- lines.join("\n"),
12304
+ normalizedText,
10483
12305
  options.kind ?? inferDocumentKind(text)
10484
12306
  );
12307
+ const splitCatchText = splitSameLineCatchClauses(
12308
+ normalizedText,
12309
+ syntax.lexed.tokens
12310
+ );
12311
+ if (splitCatchText !== normalizedText)
12312
+ return formatNeoScript(splitCatchText, options);
10485
12313
  const delimiterTokensByLine = /* @__PURE__ */ new Map();
10486
12314
  for (const token of syntax.lexed.tokens) {
10487
12315
  if (token.kind !== "punctuation" || !isDelimiter(token.text)) continue;
@@ -10505,8 +12333,14 @@ function formatNeoScript(text, options = {}) {
10505
12333
  lineTokens,
10506
12334
  firstContentCharacter
10507
12335
  );
10508
- const lineDepth = Math.max(0, depth - leadingClosers);
10509
- formatted.push(`${indentUnit.repeat(lineDepth)}${content}`);
12336
+ const contentOffset = syntax.lexed.source.offsetAt({
12337
+ line: lineIndex,
12338
+ character: firstContentCharacter
12339
+ });
12340
+ const sectionDepth = switchSectionIndentDepth(syntax.parsed, contentOffset);
12341
+ const lineDepth = Math.max(0, depth - leadingClosers) + sectionDepth;
12342
+ const normalized = normalizeControlHeaderSpacing(content);
12343
+ formatted.push(`${indentUnit.repeat(lineDepth)}${normalized}`);
10510
12344
  for (const token of lineTokens) {
10511
12345
  depth = isOpeningDelimiter(token.text) ? depth + 1 : Math.max(0, depth - 1);
10512
12346
  }
@@ -10523,6 +12357,53 @@ function formattingEdit(text, options) {
10523
12357
  const source = new SourceText(text);
10524
12358
  return [{ range: source.fullRange(), newText: formatted }];
10525
12359
  }
12360
+ function normalizeControlHeaderSpacing(content) {
12361
+ const withKeywordSpace = content.replace(
12362
+ /^(for|foreach|switch|catch)\s*\(/,
12363
+ "$1 ("
12364
+ );
12365
+ const withCatchFilterSpace = withKeywordSpace.replace(
12366
+ /\)\s*when\s*\(/,
12367
+ ") when ("
12368
+ );
12369
+ if (!withCatchFilterSpace.startsWith("foreach ("))
12370
+ return withCatchFilterSpace;
12371
+ return withCatchFilterSpace.replace(/\s+in\s+/, " in ");
12372
+ }
12373
+ function splitSameLineCatchClauses(text, tokens) {
12374
+ const boundaries = [];
12375
+ for (let index = 1; index < tokens.length; index++) {
12376
+ const catchKeyword = tokens[index];
12377
+ const preceding = tokens[index - 1];
12378
+ if (catchKeyword?.kind !== "keyword" || catchKeyword.text !== "catch" || preceding?.kind !== "punctuation" || preceding.text !== "}" || preceding.range.end.line !== catchKeyword.range.start.line) {
12379
+ continue;
12380
+ }
12381
+ if (!/^\s*$/.test(text.slice(preceding.end, catchKeyword.start))) continue;
12382
+ boundaries.push({ start: preceding.end, end: catchKeyword.start });
12383
+ }
12384
+ if (boundaries.length === 0) return text;
12385
+ let result = "";
12386
+ let cursor = 0;
12387
+ for (const boundary of boundaries) {
12388
+ result += `${text.slice(cursor, boundary.start)}
12389
+ `;
12390
+ cursor = boundary.end;
12391
+ }
12392
+ return result + text.slice(cursor);
12393
+ }
12394
+ function switchSectionIndentDepth(parsed, contentOffset) {
12395
+ let depth = 0;
12396
+ for (const statement of parsed.switches) {
12397
+ for (const section of statement.sections) {
12398
+ const lastLabel = section.labels.at(-1);
12399
+ if (!lastLabel) continue;
12400
+ if (contentOffset > lastLabel.colonEnd && contentOffset < section.bodyEnd) {
12401
+ depth++;
12402
+ }
12403
+ }
12404
+ }
12405
+ return depth;
12406
+ }
10526
12407
  function countLeadingClosers(lineTokens, firstContentCharacter) {
10527
12408
  let count = 0;
10528
12409
  let expectedCharacter = firstContentCharacter;
@@ -16398,11 +18279,55 @@ function collectIdentifierReads(expression, names) {
16398
18279
  for (const inner of statement.elseBody ?? [])
16399
18280
  walkStatement(inner, visible);
16400
18281
  return;
18282
+ case "for": {
18283
+ walk(statement.initializer.init, visible);
18284
+ const inner = new Set(visible);
18285
+ inner.delete(statement.initializer.name);
18286
+ walk(statement.condition, inner);
18287
+ walkStatement(statement.iterator, inner);
18288
+ for (const bodyStatement of statement.body) {
18289
+ walkStatement(bodyStatement, inner);
18290
+ }
18291
+ return;
18292
+ }
18293
+ case "forEach": {
18294
+ walk(statement.collection, visible);
18295
+ const inner = new Set(visible);
18296
+ inner.delete(statement.name);
18297
+ for (const bodyStatement of statement.body) {
18298
+ walkStatement(bodyStatement, inner);
18299
+ }
18300
+ return;
18301
+ }
18302
+ case "switch":
18303
+ walk(statement.selector, visible);
18304
+ for (const section of statement.sections) {
18305
+ for (const label of section.labels) {
18306
+ if (label.kind === "case") walk(label.expression, visible);
18307
+ }
18308
+ for (const bodyStatement of section.body) {
18309
+ walkStatement(bodyStatement, visible);
18310
+ }
18311
+ }
18312
+ return;
18313
+ case "try":
18314
+ for (const bodyStatement of statement.body) {
18315
+ walkStatement(bodyStatement, visible);
18316
+ }
18317
+ for (const clause of statement.catches) {
18318
+ const catchScope = new Set(visible);
18319
+ catchScope.delete(clause.name);
18320
+ if (clause.filter) walk(clause.filter, catchScope);
18321
+ for (const bodyStatement of clause.body) {
18322
+ walkStatement(bodyStatement, catchScope);
18323
+ }
18324
+ }
18325
+ return;
16401
18326
  case "return":
16402
18327
  if (statement.expr) walk(statement.expr, visible);
16403
18328
  return;
16404
18329
  case "throw":
16405
- walk(statement.expr, visible);
18330
+ if (statement.expr) walk(statement.expr, visible);
16406
18331
  return;
16407
18332
  case "assign":
16408
18333
  walk(statement.target, visible);
@@ -16411,6 +18336,9 @@ function collectIdentifierReads(expression, names) {
16411
18336
  case "exprStmt":
16412
18337
  walk(statement.expr, visible);
16413
18338
  return;
18339
+ case "break":
18340
+ case "continue":
18341
+ return;
16414
18342
  }
16415
18343
  };
16416
18344
  walk(expression, names);
@@ -17098,7 +19026,7 @@ function validateStatements(statements, returnType, scope, environment, uri, ran
17098
19026
  range2,
17099
19027
  diagnostics
17100
19028
  );
17101
- } else if (statement.kind === "throw") {
19029
+ } else if (statement.kind === "throw" && statement.expr) {
17102
19030
  validateExpression(
17103
19031
  statement.expr,
17104
19032
  primitiveType("string"),
@@ -17108,6 +19036,40 @@ function validateStatements(statements, returnType, scope, environment, uri, ran
17108
19036
  range2,
17109
19037
  diagnostics
17110
19038
  );
19039
+ } else if (statement.kind === "try") {
19040
+ validateStatements(
19041
+ statement.body,
19042
+ returnType,
19043
+ new Map(scope),
19044
+ environment,
19045
+ uri,
19046
+ range2,
19047
+ diagnostics
19048
+ );
19049
+ for (const clause of statement.catches) {
19050
+ const catchScope = new Map(scope);
19051
+ catchScope.set(clause.name, primitiveType("string"));
19052
+ if (clause.filter) {
19053
+ validateExpression(
19054
+ clause.filter,
19055
+ primitiveType("bool"),
19056
+ catchScope,
19057
+ environment,
19058
+ uri,
19059
+ range2,
19060
+ diagnostics
19061
+ );
19062
+ }
19063
+ validateStatements(
19064
+ clause.body,
19065
+ returnType,
19066
+ catchScope,
19067
+ environment,
19068
+ uri,
19069
+ range2,
19070
+ diagnostics
19071
+ );
19072
+ }
17111
19073
  } else if (statement.kind === "if") {
17112
19074
  for (const branch of statement.branches) {
17113
19075
  validateExpression(
@@ -33513,6 +35475,9 @@ function isNSTypeInfoCollectionInternal(value, ancestors) {
33513
35475
  }
33514
35476
  return true;
33515
35477
  }
35478
+ function isNSTypeInfoLookup(value) {
35479
+ return isNSTypeInfoLookupInternal(value, /* @__PURE__ */ new Set());
35480
+ }
33516
35481
  function isNSTypeInfoLookupInternal(value, ancestors) {
33517
35482
  if (!isNSTypeInfoBase(value)) return false;
33518
35483
  if (value.type !== 9 /* Lookup */) return false;
@@ -33781,6 +35746,20 @@ function isNSFunctionWithReturnType(value) {
33781
35746
  if (!Array.isArray(v.parameters)) return false;
33782
35747
  if (!v.parameters.every(isNSVariable)) return false;
33783
35748
  if (!isNSInstructions(v.instructions)) return false;
35749
+ if ([
35750
+ "for" /* for */,
35751
+ "forEach" /* forEach */,
35752
+ "break" /* break */,
35753
+ "continue" /* continue */
35754
+ ].some((type) => nsInstructionsContainType(v.instructions ?? [], type)) && (v.compilerRevision ?? 1) < 4) {
35755
+ return false;
35756
+ }
35757
+ if (nsInstructionsContainType(v.instructions, "switch" /* switch */) && (v.compilerRevision ?? 1) < 5) {
35758
+ return false;
35759
+ }
35760
+ if (nsInstructionsContainType(v.instructions, "try" /* try */) && (v.compilerRevision ?? 1) < 6) {
35761
+ return false;
35762
+ }
33784
35763
  if (!isNSTypeInfo(v.typeInfo)) return false;
33785
35764
  return true;
33786
35765
  }
@@ -34038,8 +36017,159 @@ function isNSInstructionFunctionCall(value) {
34038
36017
  if (v?.type !== "functionCall" /* functionCall */) return false;
34039
36018
  return isNSPointerCallFunction(v.call);
34040
36019
  }
36020
+ function isNSLoopBinding(value) {
36021
+ const binding = value;
36022
+ if (typeof binding !== "object" || binding === null) return false;
36023
+ if (typeof binding.id !== "string" || binding.id.length === 0) return false;
36024
+ if (!isNSTypeInfo(binding.typeInfo)) return false;
36025
+ if (binding.readonly !== true) return false;
36026
+ if (binding.writability === void 0) return true;
36027
+ return Object.values(NSWritability).includes(binding.writability);
36028
+ }
36029
+ function isNSInstructionFor(value) {
36030
+ const instruction = value;
36031
+ if (instruction?.type !== "for" /* for */) return false;
36032
+ if (!isNSVariable(instruction.initializer)) return false;
36033
+ if (!isNSBooleanExpression(instruction.condition)) return false;
36034
+ if (!isNSInstructionAssign(instruction.iterator)) return false;
36035
+ return isNSInstructions(instruction.instructions);
36036
+ }
36037
+ function isNSInstructionForEach(value) {
36038
+ const instruction = value;
36039
+ if (instruction?.type !== "forEach" /* forEach */) return false;
36040
+ if (!isNSLoopBinding(instruction.binding)) return false;
36041
+ if (!isNSPointer(instruction.collectionPointer)) return false;
36042
+ if (!isNSTypeInfoCollection(instruction.collectionTypeInfo) && !isNSTypeInfoLookup(instruction.collectionTypeInfo)) {
36043
+ return false;
36044
+ }
36045
+ if (instruction.collectionTypeInfo.required !== true) return false;
36046
+ return isNSInstructions(instruction.instructions);
36047
+ }
36048
+ function isNSInstructionBreak(value) {
36049
+ const instruction = value;
36050
+ return instruction?.type === "break" /* break */;
36051
+ }
36052
+ function isNSInstructionContinue(value) {
36053
+ const instruction = value;
36054
+ return instruction?.type === "continue" /* continue */;
36055
+ }
36056
+ function isNSSwitchSelectorType(value) {
36057
+ if (!isNSTypeInfo(value)) return false;
36058
+ return value.type === 2 /* Int */ || value.type === 3 /* String */ || value.type === 1 /* Bool */ || value.type === 8 /* Enum */;
36059
+ }
36060
+ function nsSwitchLabelKey(value, selectorTypeInfo) {
36061
+ if (!isNSValue(value)) return null;
36062
+ if (value.typeInfo.required !== true) return null;
36063
+ if (value.typeInfo.type === 0 /* Null */) {
36064
+ return value.value === null && selectorTypeInfo.required === false ? "null" : null;
36065
+ }
36066
+ if (value.typeInfo.type !== selectorTypeInfo.type) return null;
36067
+ switch (value.typeInfo.type) {
36068
+ case 2 /* Int */:
36069
+ return typeof value.value === "number" && Number.isSafeInteger(value.value) ? `int:${String(value.value)}` : null;
36070
+ case 3 /* String */:
36071
+ return typeof value.value === "string" ? `string:${JSON.stringify(value.value)}` : null;
36072
+ case 1 /* Bool */:
36073
+ return typeof value.value === "boolean" ? `bool:${String(value.value)}` : null;
36074
+ case 8 /* Enum */:
36075
+ 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) {
36076
+ return null;
36077
+ }
36078
+ return `enum:${value.typeInfo.enumId}:${JSON.stringify(value.value[0])}`;
36079
+ default:
36080
+ return null;
36081
+ }
36082
+ }
36083
+ function isNSSwitchSection(value, selectorTypeInfo, seenLabels) {
36084
+ const section = value;
36085
+ if (typeof section !== "object" || section === null) return false;
36086
+ if (!Array.isArray(section.labels) || section.labels.length === 0) {
36087
+ return false;
36088
+ }
36089
+ for (const label of section.labels) {
36090
+ const key = nsSwitchLabelKey(label, selectorTypeInfo);
36091
+ if (key === null || seenLabels.has(key)) return false;
36092
+ seenLabels.add(key);
36093
+ }
36094
+ return isNSInstructions(section.instructions);
36095
+ }
36096
+ function isNSInstructionSwitch(value) {
36097
+ const instruction = value;
36098
+ if (instruction?.type !== "switch" /* switch */) return false;
36099
+ if (!isNSPointer(instruction.selector)) return false;
36100
+ if (!isNSSwitchSelectorType(instruction.selectorTypeInfo)) return false;
36101
+ if (!Array.isArray(instruction.sections)) return false;
36102
+ const selectorTypeInfo = instruction.selectorTypeInfo;
36103
+ const seenLabels = /* @__PURE__ */ new Set();
36104
+ if (!instruction.sections.every(
36105
+ (section) => isNSSwitchSection(section, selectorTypeInfo, seenLabels)
36106
+ )) {
36107
+ return false;
36108
+ }
36109
+ return instruction.defaultInstructions === void 0 || instruction.defaultInstructions === null || isNSInstructions(instruction.defaultInstructions);
36110
+ }
36111
+ function isNSCatchClause(value, bindingIds, isFinal) {
36112
+ const clause = value;
36113
+ if (typeof clause !== "object" || clause === null) return false;
36114
+ const binding = clause.binding;
36115
+ if (typeof binding !== "object" || binding === null) return false;
36116
+ if (typeof binding.id !== "string" || binding.id.length === 0) return false;
36117
+ if (bindingIds.has(binding.id)) return false;
36118
+ bindingIds.add(binding.id);
36119
+ if (!isNSTypeInfo(binding.typeInfo)) return false;
36120
+ if (binding.typeInfo.type !== 3 /* String */) return false;
36121
+ if (binding.typeInfo.required !== true) return false;
36122
+ if (binding.readonly !== true) return false;
36123
+ const hasFilter = clause.filter !== void 0 && clause.filter !== null;
36124
+ if (!hasFilter && !isFinal) return false;
36125
+ if (hasFilter && !isNSBooleanExpression(clause.filter)) return false;
36126
+ return isNSInstructions(clause.instructions);
36127
+ }
36128
+ function isNSInstructionTry(value) {
36129
+ const instruction = value;
36130
+ if (instruction?.type !== "try" /* try */) return false;
36131
+ if (!isNSInstructions(instruction.instructions)) return false;
36132
+ if (!Array.isArray(instruction.catches)) return false;
36133
+ if (instruction.catches.length === 0) return false;
36134
+ const bindingIds = /* @__PURE__ */ new Set();
36135
+ for (let index = 0; index < instruction.catches.length; index += 1) {
36136
+ const clause = instruction.catches[index];
36137
+ if (!isNSCatchClause(
36138
+ clause,
36139
+ bindingIds,
36140
+ index === instruction.catches.length - 1
36141
+ )) {
36142
+ return false;
36143
+ }
36144
+ }
36145
+ return true;
36146
+ }
34041
36147
  function isNSInstruction(value) {
34042
- return isNSInstructionVariable(value) || isNSInstructionIfBranch(value) || isNSInstructionReturn(value) || isNSInstructionThrow(value) || isNSInstructionAssign(value) || isNSInstructionCollectionCall(value) || isNSInstructionFunctionCall(value);
36148
+ 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);
36149
+ }
36150
+ function nsInstructionsContainType(instructions, type) {
36151
+ return instructions.some((instruction) => {
36152
+ if (instruction.type === type) return true;
36153
+ if (instruction.type === "if" /* if */) {
36154
+ return instruction.branches.some(
36155
+ (branch) => nsInstructionsContainType(branch.instructions, type)
36156
+ ) || instruction.else !== null && instruction.else !== void 0 && nsInstructionsContainType(instruction.else, type);
36157
+ }
36158
+ if (instruction.type === "for" /* for */ || instruction.type === "forEach" /* forEach */) {
36159
+ return nsInstructionsContainType(instruction.instructions, type);
36160
+ }
36161
+ if (instruction.type === "switch" /* switch */) {
36162
+ return instruction.sections.some(
36163
+ (section) => nsInstructionsContainType(section.instructions, type)
36164
+ ) || instruction.defaultInstructions !== null && instruction.defaultInstructions !== void 0 && nsInstructionsContainType(instruction.defaultInstructions, type);
36165
+ }
36166
+ if (instruction.type === "try" /* try */) {
36167
+ return nsInstructionsContainType(instruction.instructions, type) || instruction.catches.some(
36168
+ (clause) => nsInstructionsContainType(clause.instructions, type)
36169
+ );
36170
+ }
36171
+ return false;
36172
+ });
34043
36173
  }
34044
36174
  function isNSInstructions(value) {
34045
36175
  return Array.isArray(value) && value.every(isNSInstruction);
@@ -63744,6 +65874,9 @@ var init_decimal = __esm({
63744
65874
  });
63745
65875
 
63746
65876
  // ../src/view-models/neoscript-evaluator/evaluateNSGetter.ts
65877
+ function isCatchableNSRuntimeError(error) {
65878
+ return error instanceof NSGetterRuntimeError && !(error instanceof NonCatchableNSGetterRuntimeError);
65879
+ }
63747
65880
  function makeEvaluatorLookups(members, values) {
63748
65881
  const attrMap = /* @__PURE__ */ new Map();
63749
65882
  for (const a of members) attrMap.set(a.id, a);
@@ -63958,6 +66091,7 @@ function evaluateNSGetterWithEffects(getter, ctx, argumentValues = []) {
63958
66091
  );
63959
66092
  return createdSessionValues.length === 0 ? { value: result.value, writes } : { value: result.value, writes, createdSessionValues };
63960
66093
  }
66094
+ rejectEscapedLoopTransfer(result, "NeoScript getter");
63961
66095
  } catch (error) {
63962
66096
  finalizeConstructorAllocations(runtimeCtx, void 0, ownsInvocationState);
63963
66097
  throw error;
@@ -63983,6 +66117,7 @@ function evaluateNSAction(action, ctx) {
63983
66117
  );
63984
66118
  }
63985
66119
  }
66120
+ rejectEscapedLoopTransfer(result, "NeoScript action");
63986
66121
  finalizeConstructorAllocations(runtimeCtx, void 0, ownsInvocationState);
63987
66122
  } catch (error) {
63988
66123
  finalizeConstructorAllocations(runtimeCtx, void 0, ownsInvocationState);
@@ -64052,7 +66187,8 @@ function withEvaluationRuntime(ctx, writes) {
64052
66187
  functionStack: [],
64053
66188
  constructorGroups: /* @__PURE__ */ new Map(),
64054
66189
  ownedValueAttachments: /* @__PURE__ */ new Map(),
64055
- constructionStack: []
66190
+ constructionStack: [],
66191
+ loopIterations: 0
64056
66192
  },
64057
66193
  __indexes: ctx.__executionState === void 0 || ctx.__valueOverlay === void 0 ? void 0 : ctx.__indexes
64058
66194
  };
@@ -64302,6 +66438,7 @@ function evaluateNSSetter(setter, ctx, value, writes) {
64302
66438
  `Setter returned a non-null value: ${String(result.value)}`
64303
66439
  );
64304
66440
  }
66441
+ rejectEscapedLoopTransfer(result, "NeoScript setter");
64305
66442
  }
64306
66443
  function executeCompiledFunction(action, ctx, args, allowFallthroughReturn, runtimeSignature) {
64307
66444
  if (action.parameters.length < 2) {
@@ -64345,6 +66482,7 @@ function executeCompiledFunction(action, ctx, args, allowFallthroughReturn, runt
64345
66482
  "NSFunction ended without returning its declared value."
64346
66483
  );
64347
66484
  }
66485
+ rejectEscapedLoopTransfer(result, "NeoScript function");
64348
66486
  let value = result.value;
64349
66487
  const runtimeReturnType = runtimeSignature?.returnTypeInfo ?? action.typeInfo;
64350
66488
  if (runtimeReturnType.type === NS_TYPE_VOID) {
@@ -64365,6 +66503,40 @@ function executeCompiledFunction(action, ctx, args, allowFallthroughReturn, runt
64365
66503
  }
64366
66504
  return value;
64367
66505
  }
66506
+ function createChildScope(parent) {
66507
+ const child = new Map(parent);
66508
+ const inherited = readonlyBindingErrorsByScope.get(parent);
66509
+ if (inherited !== void 0 && inherited.size > 0) {
66510
+ readonlyBindingErrorsByScope.set(child, new Map(inherited));
66511
+ }
66512
+ return child;
66513
+ }
66514
+ function markReadonlyBinding(scope, bindingId, errorMessage3) {
66515
+ const existing = readonlyBindingErrorsByScope.get(scope);
66516
+ if (existing === void 0) {
66517
+ readonlyBindingErrorsByScope.set(
66518
+ scope,
66519
+ /* @__PURE__ */ new Map([[bindingId, errorMessage3]])
66520
+ );
66521
+ return;
66522
+ }
66523
+ readonlyBindingErrorsByScope.set(
66524
+ scope,
66525
+ new Map([...existing, [bindingId, errorMessage3]])
66526
+ );
66527
+ }
66528
+ function assertVariableBindingWritable(scope, bindingId) {
66529
+ const errorMessage3 = readonlyBindingErrorsByScope.get(scope)?.get(bindingId);
66530
+ if (errorMessage3 === void 0) return;
66531
+ throw new NSGetterRuntimeError(errorMessage3);
66532
+ }
66533
+ function rejectEscapedLoopTransfer(result, bodyLabel) {
66534
+ if (result.kind === "break" || result.kind === "continue") {
66535
+ throw new NSGetterRuntimeError(
66536
+ `Corrupt ${bodyLabel} IR: '${result.kind}' escaped the nearest loop.`
66537
+ );
66538
+ }
66539
+ }
64368
66540
  function evaluationOptions(ctx, allowFallthroughReturn) {
64369
66541
  const writes = ctx.__executionState?.writes;
64370
66542
  if (writes === void 0) {
@@ -64387,6 +66559,186 @@ function createTopLevelScope(ctx) {
64387
66559
  );
64388
66560
  return scope;
64389
66561
  }
66562
+ function consumeLoopIteration(ctx) {
66563
+ const state = ctx.__executionState;
66564
+ if (state === void 0) {
66565
+ throw new NSGetterRuntimeError(
66566
+ "NeoScript loop executed without a shared execution state."
66567
+ );
66568
+ }
66569
+ if (state.loopIterations >= MAX_LOOP_ITERATIONS) {
66570
+ throw new NSGetterRuntimeError(
66571
+ `NeoScript loop iteration limit of ${MAX_LOOP_ITERATIONS} exceeded.`
66572
+ );
66573
+ }
66574
+ state.loopIterations += 1;
66575
+ }
66576
+ function synchronizeExistingBindings(parent, child, bindingIds) {
66577
+ for (const bindingId of bindingIds) {
66578
+ parent.set(bindingId, child.get(bindingId));
66579
+ }
66580
+ }
66581
+ function evalInstructionsInChildScope(instructions, parent, ctx, options) {
66582
+ const parentBindingIds = [...parent.keys()];
66583
+ const child = createChildScope(parent);
66584
+ try {
66585
+ return evalInstructions(instructions, child, ctx, options);
66586
+ } finally {
66587
+ synchronizeExistingBindings(parent, child, parentBindingIds);
66588
+ }
66589
+ }
66590
+ function switchSelectorTypeSupported(typeInfo) {
66591
+ return typeInfo.type === 2 /* Int */ || typeInfo.type === 3 /* String */ || typeInfo.type === 1 /* Bool */ || typeInfo.type === 8 /* Enum */;
66592
+ }
66593
+ function switchLabelKeyOrThrow(label, selectorTypeInfo) {
66594
+ if (label.typeInfo.required !== true) {
66595
+ throw new CorruptNeoScriptIRError(
66596
+ "Corrupt NeoScript switch IR: case labels must use canonical required type information."
66597
+ );
66598
+ }
66599
+ if (label.typeInfo.type === 0 /* Null */) {
66600
+ if (label.value !== null || selectorTypeInfo.required !== false) {
66601
+ throw new CorruptNeoScriptIRError(
66602
+ "Corrupt NeoScript switch IR: a null case label requires an optional selector."
66603
+ );
66604
+ }
66605
+ return "null";
66606
+ }
66607
+ if (label.typeInfo.type !== selectorTypeInfo.type) {
66608
+ throw new CorruptNeoScriptIRError(
66609
+ "Corrupt NeoScript switch IR: case label type does not match the selector type."
66610
+ );
66611
+ }
66612
+ switch (label.typeInfo.type) {
66613
+ case 2 /* Int */:
66614
+ if (typeof label.value !== "number" || !Number.isSafeInteger(label.value)) {
66615
+ throw new CorruptNeoScriptIRError(
66616
+ "Corrupt NeoScript switch IR: int case label is not a safe integer."
66617
+ );
66618
+ }
66619
+ return `int:${String(label.value)}`;
66620
+ case 3 /* String */:
66621
+ if (typeof label.value !== "string") {
66622
+ throw new CorruptNeoScriptIRError(
66623
+ "Corrupt NeoScript switch IR: string case label has a non-string value."
66624
+ );
66625
+ }
66626
+ return `string:${JSON.stringify(label.value)}`;
66627
+ case 1 /* Bool */:
66628
+ if (typeof label.value !== "boolean") {
66629
+ throw new CorruptNeoScriptIRError(
66630
+ "Corrupt NeoScript switch IR: bool case label has a non-boolean value."
66631
+ );
66632
+ }
66633
+ return `bool:${String(label.value)}`;
66634
+ case 8 /* Enum */:
66635
+ 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) {
66636
+ throw new CorruptNeoScriptIRError(
66637
+ "Corrupt NeoScript switch IR: enum case label does not match the selector enum."
66638
+ );
66639
+ }
66640
+ return `enum:${label.typeInfo.enumId}:${JSON.stringify(label.value[0])}`;
66641
+ default:
66642
+ throw new CorruptNeoScriptIRError(
66643
+ "Corrupt NeoScript switch IR: unsupported case label type."
66644
+ );
66645
+ }
66646
+ }
66647
+ function validateSwitchInstruction(instruction) {
66648
+ if (!switchSelectorTypeSupported(instruction.selectorTypeInfo)) {
66649
+ throw new CorruptNeoScriptIRError(
66650
+ "Corrupt NeoScript switch IR: selector type must be int, string, bool, or enum."
66651
+ );
66652
+ }
66653
+ const labels = /* @__PURE__ */ new Set();
66654
+ for (const section of instruction.sections) {
66655
+ if (section.labels.length === 0) {
66656
+ throw new CorruptNeoScriptIRError(
66657
+ "Corrupt NeoScript switch IR: case section has no labels."
66658
+ );
66659
+ }
66660
+ for (const label of section.labels) {
66661
+ const key = switchLabelKeyOrThrow(label, instruction.selectorTypeInfo);
66662
+ if (labels.has(key)) {
66663
+ throw new CorruptNeoScriptIRError(
66664
+ "Corrupt NeoScript switch IR: duplicate case label."
66665
+ );
66666
+ }
66667
+ labels.add(key);
66668
+ }
66669
+ }
66670
+ }
66671
+ function assertSwitchSelectorValue(value, typeInfo, ctx) {
66672
+ const matchesType = runtimeValueMatchesType(value, typeInfo, ctx) && (typeInfo.type !== 2 /* Int */ || value === null || typeof value === "number" && Number.isSafeInteger(value));
66673
+ if (!matchesType) {
66674
+ throw new NSGetterRuntimeError(
66675
+ "NeoScript switch selector does not match its compiled type."
66676
+ );
66677
+ }
66678
+ }
66679
+ function validateTryInstruction(instruction) {
66680
+ if (!Array.isArray(instruction.catches)) {
66681
+ throw new CorruptNeoScriptIRError(
66682
+ "Corrupt NeoScript try IR: catch clause list is missing."
66683
+ );
66684
+ }
66685
+ if (instruction.catches.length === 0) {
66686
+ throw new CorruptNeoScriptIRError(
66687
+ "Corrupt NeoScript try IR: at least one catch clause is required."
66688
+ );
66689
+ }
66690
+ const bindingIds = /* @__PURE__ */ new Set();
66691
+ for (let index = 0; index < instruction.catches.length; index += 1) {
66692
+ const clause = instruction.catches[index];
66693
+ if (typeof clause?.binding !== "object" || clause.binding === null) {
66694
+ throw new CorruptNeoScriptIRError(
66695
+ "Corrupt NeoScript try IR: catch clause is missing its binding."
66696
+ );
66697
+ }
66698
+ if (typeof clause.binding.id !== "string") {
66699
+ throw new CorruptNeoScriptIRError(
66700
+ "Corrupt NeoScript try IR: catch binding id must be a string."
66701
+ );
66702
+ }
66703
+ if (clause.binding.id.length === 0) {
66704
+ throw new CorruptNeoScriptIRError(
66705
+ "Corrupt NeoScript try IR: catch binding id must be non-empty."
66706
+ );
66707
+ }
66708
+ if (bindingIds.has(clause.binding.id)) {
66709
+ throw new CorruptNeoScriptIRError(
66710
+ "Corrupt NeoScript try IR: catch binding ids must be unique."
66711
+ );
66712
+ }
66713
+ bindingIds.add(clause.binding.id);
66714
+ if (clause.binding.typeInfo?.type !== 3 /* String */) {
66715
+ throw new CorruptNeoScriptIRError(
66716
+ "Corrupt NeoScript try IR: catch binding type must be string."
66717
+ );
66718
+ }
66719
+ if (clause.binding.typeInfo.required !== true) {
66720
+ throw new CorruptNeoScriptIRError(
66721
+ "Corrupt NeoScript try IR: catch binding string must be required."
66722
+ );
66723
+ }
66724
+ if (clause.binding.readonly !== true) {
66725
+ throw new CorruptNeoScriptIRError(
66726
+ "Corrupt NeoScript try IR: catch binding must be read-only."
66727
+ );
66728
+ }
66729
+ const unfiltered = clause.filter === void 0 || clause.filter === null;
66730
+ if (unfiltered && index !== instruction.catches.length - 1) {
66731
+ throw new CorruptNeoScriptIRError(
66732
+ "Corrupt NeoScript try IR: an unfiltered catch must be the final clause."
66733
+ );
66734
+ }
66735
+ }
66736
+ if (!isNSInstructionTry(instruction)) {
66737
+ throw new CorruptNeoScriptIRError(
66738
+ "Corrupt NeoScript try IR: catch filter or child instructions are malformed."
66739
+ );
66740
+ }
66741
+ }
64390
66742
  function evalInstructions(instructions, scope, ctx, options) {
64391
66743
  for (const ins of instructions) {
64392
66744
  switch (ins.type) {
@@ -64409,13 +66761,13 @@ function evalInstructions(instructions, scope, ctx, options) {
64409
66761
  ctx,
64410
66762
  options
64411
66763
  );
64412
- if (r.kind === "return") return r;
66764
+ if (r.kind !== "fallthrough") return r;
64413
66765
  break;
64414
66766
  }
64415
66767
  }
64416
66768
  if (!matched && ins.else) {
64417
66769
  const r = evalInstructions(ins.else, scope, ctx, options);
64418
- if (r.kind === "return") return r;
66770
+ if (r.kind !== "fallthrough") return r;
64419
66771
  }
64420
66772
  break;
64421
66773
  }
@@ -64433,6 +66785,7 @@ function evalInstructions(instructions, scope, ctx, options) {
64433
66785
  value = coerceDecimalOperand(value, "assignment");
64434
66786
  }
64435
66787
  if (ins.target.pointer.type === "variable" /* variable */) {
66788
+ assertVariableBindingWritable(scope, ins.target.pointer.variableId);
64436
66789
  scope.set(ins.target.pointer.variableId, value);
64437
66790
  break;
64438
66791
  }
@@ -64473,6 +66826,174 @@ function evalInstructions(instructions, scope, ctx, options) {
64473
66826
  evalPointer(ins.call, scope, ctx);
64474
66827
  break;
64475
66828
  }
66829
+ case "for" /* for */: {
66830
+ const parentBindingIds = [...scope.keys()];
66831
+ const loopScope = createChildScope(scope);
66832
+ try {
66833
+ const initialValue = coerceAssignmentValue(
66834
+ evalPointer(ins.initializer.pointer, loopScope, ctx),
66835
+ ins.initializer.typeInfo,
66836
+ "variable initialization"
66837
+ );
66838
+ loopScope.set(ins.initializer.id, initialValue);
66839
+ while (evalBooleanExpression(ins.condition, loopScope, ctx)) {
66840
+ consumeLoopIteration(ctx);
66841
+ const result = evalInstructionsInChildScope(
66842
+ ins.instructions,
66843
+ loopScope,
66844
+ ctx,
66845
+ options
66846
+ );
66847
+ if (result.kind === "return") return result;
66848
+ if (result.kind === "break") break;
66849
+ evalInstructions([ins.iterator], loopScope, ctx, options);
66850
+ }
66851
+ } finally {
66852
+ synchronizeExistingBindings(scope, loopScope, parentBindingIds);
66853
+ }
66854
+ break;
66855
+ }
66856
+ case "forEach" /* forEach */: {
66857
+ const collection = evalPointer(ins.collectionPointer, scope, ctx);
66858
+ const membership = snapshotCollectionMembership(collection);
66859
+ const parentBindingIds = [...scope.keys()];
66860
+ const loopScope = createChildScope(scope);
66861
+ markReadonlyBinding(
66862
+ loopScope,
66863
+ ins.binding.id,
66864
+ READONLY_FOREACH_BINDING_ERROR
66865
+ );
66866
+ try {
66867
+ for (const rawEntry of membership) {
66868
+ consumeLoopIteration(ctx);
66869
+ loopScope.set(
66870
+ ins.binding.id,
66871
+ coerceAssignmentValue(
66872
+ resolveValueIfId(rawEntry, ctx),
66873
+ ins.binding.typeInfo,
66874
+ "foreach binding"
66875
+ )
66876
+ );
66877
+ const result = evalInstructionsInChildScope(
66878
+ ins.instructions,
66879
+ loopScope,
66880
+ ctx,
66881
+ options
66882
+ );
66883
+ if (result.kind === "return") return result;
66884
+ if (result.kind === "break") break;
66885
+ }
66886
+ } finally {
66887
+ synchronizeExistingBindings(scope, loopScope, parentBindingIds);
66888
+ }
66889
+ break;
66890
+ }
66891
+ case "switch" /* switch */: {
66892
+ validateSwitchInstruction(ins);
66893
+ const selector = evalPointer(ins.selector, scope, ctx);
66894
+ assertSwitchSelectorValue(selector, ins.selectorTypeInfo, ctx);
66895
+ const matchedSection = ins.sections.find(
66896
+ (section) => section.labels.some((label) => jsEqual(selector, label.value))
66897
+ );
66898
+ const selectedInstructions = matchedSection?.instructions ?? ins.defaultInstructions;
66899
+ if (selectedInstructions === void 0 || selectedInstructions === null) {
66900
+ break;
66901
+ }
66902
+ const parentBindingIds = [...scope.keys()];
66903
+ const sectionScope = createChildScope(scope);
66904
+ const result = (() => {
66905
+ try {
66906
+ return evalInstructions(
66907
+ selectedInstructions,
66908
+ sectionScope,
66909
+ ctx,
66910
+ options
66911
+ );
66912
+ } finally {
66913
+ synchronizeExistingBindings(scope, sectionScope, parentBindingIds);
66914
+ }
66915
+ })();
66916
+ if (result.kind === "break") break;
66917
+ if (result.kind === "fallthrough") {
66918
+ throw new CorruptNeoScriptIRError(
66919
+ "Corrupt NeoScript switch IR: selected section reached its end."
66920
+ );
66921
+ }
66922
+ return result;
66923
+ }
66924
+ case "try" /* try */: {
66925
+ validateTryInstruction(ins);
66926
+ const parentBindingIds = [...scope.keys()];
66927
+ const tryScope = createChildScope(scope);
66928
+ let tryResult;
66929
+ try {
66930
+ try {
66931
+ tryResult = evalInstructions(
66932
+ ins.instructions,
66933
+ tryScope,
66934
+ ctx,
66935
+ options
66936
+ );
66937
+ } finally {
66938
+ synchronizeExistingBindings(scope, tryScope, parentBindingIds);
66939
+ }
66940
+ } catch (error) {
66941
+ if (!isCatchableNSRuntimeError(error)) throw error;
66942
+ let matched = false;
66943
+ for (const clause of ins.catches) {
66944
+ const catchScope = createChildScope(scope);
66945
+ catchScope.set(clause.binding.id, error.message);
66946
+ markReadonlyBinding(
66947
+ catchScope,
66948
+ clause.binding.id,
66949
+ READONLY_CATCH_BINDING_ERROR
66950
+ );
66951
+ const filter = clause.filter;
66952
+ let filterMatches = filter === null || filter === void 0;
66953
+ if (filter !== null && filter !== void 0) {
66954
+ try {
66955
+ filterMatches = evalBooleanExpression(filter, catchScope, ctx);
66956
+ } catch (filterError) {
66957
+ if (!isCatchableNSRuntimeError(filterError)) {
66958
+ synchronizeExistingBindings(
66959
+ scope,
66960
+ catchScope,
66961
+ parentBindingIds
66962
+ );
66963
+ throw filterError;
66964
+ }
66965
+ filterMatches = false;
66966
+ }
66967
+ }
66968
+ if (!filterMatches) {
66969
+ synchronizeExistingBindings(scope, catchScope, parentBindingIds);
66970
+ continue;
66971
+ }
66972
+ matched = true;
66973
+ let catchResult;
66974
+ try {
66975
+ catchResult = evalInstructions(
66976
+ clause.instructions,
66977
+ catchScope,
66978
+ ctx,
66979
+ options
66980
+ );
66981
+ } finally {
66982
+ synchronizeExistingBindings(scope, catchScope, parentBindingIds);
66983
+ }
66984
+ if (catchResult.kind !== "fallthrough") return catchResult;
66985
+ break;
66986
+ }
66987
+ if (!matched) throw error;
66988
+ break;
66989
+ }
66990
+ if (tryResult.kind !== "fallthrough") return tryResult;
66991
+ break;
66992
+ }
66993
+ case "break" /* break */:
66994
+ return { kind: "break" };
66995
+ case "continue" /* continue */:
66996
+ return { kind: "continue" };
64476
66997
  }
64477
66998
  void touchUnknown(ins);
64478
66999
  }
@@ -66226,6 +68747,9 @@ function applyArithmetic(op, operands, decimal2) {
66226
68747
  });
66227
68748
  }
66228
68749
  }
68750
+ function coerceAssignmentValue(value, targetTypeInfo, context) {
68751
+ return targetTypeInfo.type === 20 /* Decimal */ && typeof value === "number" ? coerceDecimalOperand(value, context) : value;
68752
+ }
66229
68753
  function coerceDecimalOperand(value, context) {
66230
68754
  if (typeof value === "string") {
66231
68755
  if (!isDecimalString(value)) {
@@ -68488,33 +71012,55 @@ function collectionLength(c) {
68488
71012
  `Cannot Count() ${typeof c}; expected list, dictionary, or string`
68489
71013
  );
68490
71014
  }
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 [];
71015
+ function snapshotCollectionMembership(collection) {
71016
+ const membership = [];
71017
+ const valid = forEachRawCollectionEntry(collection, ({ raw }) => {
71018
+ membership.push(raw);
71019
+ });
71020
+ if (valid) return membership;
71021
+ throw new NSGetterRuntimeError(
71022
+ "NeoScript foreach requires a present collection."
71023
+ );
68499
71024
  }
68500
- function iterateCollection(c, ctx, callback) {
71025
+ function forEachRawCollectionEntry(c, callback) {
68501
71026
  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);
71027
+ for (let key = 0; key < c.length; key += 1) {
71028
+ const raw = c[key];
71029
+ callback({
71030
+ raw,
71031
+ key,
71032
+ valueId: typeof raw === "string" ? raw : null
71033
+ });
68506
71034
  }
68507
- return;
71035
+ return true;
68508
71036
  }
68509
71037
  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);
71038
+ for (const [key, raw] of Object.entries(c)) {
71039
+ callback({
71040
+ raw,
71041
+ key,
71042
+ valueId: typeof raw === "string" ? raw : null
71043
+ });
68513
71044
  }
71045
+ return true;
68514
71046
  }
71047
+ return false;
71048
+ }
71049
+ function collectionEntries(c, ctx) {
71050
+ const entries = [];
71051
+ forEachRawCollectionEntry(c, ({ raw }) => {
71052
+ entries.push(resolveValueIfId(raw, ctx));
71053
+ });
71054
+ return entries;
71055
+ }
71056
+ function iterateCollection(c, ctx, callback) {
71057
+ forEachRawCollectionEntry(c, ({ raw, key, valueId }) => {
71058
+ const entry = resolveValueIfId(raw, ctx);
71059
+ callback(entry, key, valueId);
71060
+ });
68515
71061
  }
68516
71062
  function pushParams(parent, parameters, positional, isList) {
68517
- const child = new Map(parent);
71063
+ const child = createChildScope(parent);
68518
71064
  if (parameters.length === 1) {
68519
71065
  child.set(parameters[0].id, positional[1]);
68520
71066
  } else if (parameters.length === 2) {
@@ -68524,7 +71070,7 @@ function pushParams(parent, parameters, positional, isList) {
68524
71070
  }
68525
71071
  return child;
68526
71072
  }
68527
- var NativeFunctionDelegateUnavailableError, liveListIndexesByProject, MAX_CONSTRUCTION_DEPTH, LazyValueOverlay;
71073
+ var NonCatchableNSGetterRuntimeError, NativeFunctionDelegateUnavailableError, CorruptNeoScriptIRError, liveListIndexesByProject, MAX_CONSTRUCTION_DEPTH, MAX_LOOP_ITERATIONS, LazyValueOverlay, readonlyBindingErrorsByScope, READONLY_FOREACH_BINDING_ERROR, READONLY_CATCH_BINDING_ERROR;
68528
71074
  var init_evaluateNSGetter = __esm({
68529
71075
  "../src/view-models/neoscript-evaluator/evaluateNSGetter.ts"() {
68530
71076
  "use strict";
@@ -68540,10 +71086,15 @@ var init_evaluateNSGetter = __esm({
68540
71086
  init_core();
68541
71087
  init_member_storage_key();
68542
71088
  init_project2();
68543
- NativeFunctionDelegateUnavailableError = class extends NSGetterRuntimeError {
71089
+ NonCatchableNSGetterRuntimeError = class extends NSGetterRuntimeError {
71090
+ };
71091
+ NativeFunctionDelegateUnavailableError = class extends NonCatchableNSGetterRuntimeError {
71092
+ };
71093
+ CorruptNeoScriptIRError = class extends NonCatchableNSGetterRuntimeError {
68544
71094
  };
68545
71095
  liveListIndexesByProject = /* @__PURE__ */ new WeakMap();
68546
71096
  MAX_CONSTRUCTION_DEPTH = 64;
71097
+ MAX_LOOP_ITERATIONS = 1e4;
68547
71098
  LazyValueOverlay = class extends Map {
68548
71099
  constructor(vm) {
68549
71100
  super();
@@ -68603,6 +71154,9 @@ var init_evaluateNSGetter = __esm({
68603
71154
  return indexes;
68604
71155
  }
68605
71156
  };
71157
+ readonlyBindingErrorsByScope = /* @__PURE__ */ new WeakMap();
71158
+ READONLY_FOREACH_BINDING_ERROR = "Cannot assign to a read-only foreach iterator binding.";
71159
+ READONLY_CATCH_BINDING_ERROR = "Cannot assign to a read-only catch message binding.";
68606
71160
  }
68607
71161
  });
68608
71162
 
@@ -69354,6 +71908,49 @@ function stringifyReturnInstructions(instructions, sourceType) {
69354
71908
  pointer: stringifyTextVariablePointer(instruction.pointer, sourceType)
69355
71909
  };
69356
71910
  }
71911
+ if (instruction.type === "switch" /* switch */) {
71912
+ return {
71913
+ ...instruction,
71914
+ sections: instruction.sections.map((section) => ({
71915
+ ...section,
71916
+ instructions: stringifyReturnInstructions(
71917
+ section.instructions,
71918
+ sourceType
71919
+ )
71920
+ })),
71921
+ ...instruction.defaultInstructions === void 0 ? {} : {
71922
+ defaultInstructions: instruction.defaultInstructions === null ? null : stringifyReturnInstructions(
71923
+ instruction.defaultInstructions,
71924
+ sourceType
71925
+ )
71926
+ }
71927
+ };
71928
+ }
71929
+ if (instruction.type === "try" /* try */) {
71930
+ return {
71931
+ ...instruction,
71932
+ instructions: stringifyReturnInstructions(
71933
+ instruction.instructions,
71934
+ sourceType
71935
+ ),
71936
+ catches: instruction.catches.map((clause) => ({
71937
+ ...clause,
71938
+ instructions: stringifyReturnInstructions(
71939
+ clause.instructions,
71940
+ sourceType
71941
+ )
71942
+ }))
71943
+ };
71944
+ }
71945
+ if (instruction.type === "for" /* for */ || instruction.type === "forEach" /* forEach */) {
71946
+ return {
71947
+ ...instruction,
71948
+ instructions: stringifyReturnInstructions(
71949
+ instruction.instructions,
71950
+ sourceType
71951
+ )
71952
+ };
71953
+ }
69357
71954
  if (instruction.type !== "if" /* if */) return instruction;
69358
71955
  return {
69359
71956
  ...instruction,