redscript-mc 1.2.10 → 1.2.11

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.
@@ -731,4 +731,89 @@ describe('MC Integration - New Features', () => {
731
731
  expect(tickResult.ok).toBe(true);
732
732
  });
733
733
  });
734
+ describe('MC Integration - Extended Coverage', () => {
735
+ test('struct-test.mcrs: struct instantiation and field access', async () => {
736
+ if (!serverOnline)
737
+ return;
738
+ writeFixtureFile('struct-test.mcrs', 'struct_test');
739
+ await mc.reload();
740
+ await mc.command('/function struct_test:__load').catch(() => { });
741
+ await mc.command('/function struct_test:test_struct');
742
+ await mc.ticks(5);
743
+ expect(await mc.scoreboard('#struct_x', 'rs')).toBe(10);
744
+ expect(await mc.scoreboard('#struct_y', 'rs')).toBe(64);
745
+ expect(await mc.scoreboard('#struct_z', 'rs')).toBe(-5);
746
+ expect(await mc.scoreboard('#struct_x2', 'rs')).toBe(15); // 10+5
747
+ expect(await mc.scoreboard('#struct_z2', 'rs')).toBe(-10); // -5*2
748
+ expect(await mc.scoreboard('#struct_alive', 'rs')).toBe(1);
749
+ expect(await mc.scoreboard('#struct_score', 'rs')).toBe(100);
750
+ });
751
+ test('enum-test.mcrs: enum values and match', async () => {
752
+ if (!serverOnline)
753
+ return;
754
+ writeFixtureFile('enum-test.mcrs', 'enum_test');
755
+ await mc.reload();
756
+ await mc.command('/function enum_test:__load').catch(() => { });
757
+ await mc.command('/function enum_test:test_enum');
758
+ await mc.ticks(5);
759
+ expect(await mc.scoreboard('#enum_phase', 'rs')).toBe(2); // Playing=2
760
+ expect(await mc.scoreboard('#enum_match', 'rs')).toBe(2); // matched Playing
761
+ expect(await mc.scoreboard('#enum_rank', 'rs')).toBe(10); // Diamond=10
762
+ expect(await mc.scoreboard('#enum_high', 'rs')).toBe(1); // Diamond > Gold
763
+ });
764
+ test('array-test.mcrs: array operations', async () => {
765
+ if (!serverOnline)
766
+ return;
767
+ writeFixtureFile('array-test.mcrs', 'array_test');
768
+ await mc.reload();
769
+ await mc.command('/function array_test:__load').catch(() => { });
770
+ await mc.command('/function array_test:test_array');
771
+ await mc.ticks(5);
772
+ expect(await mc.scoreboard('#arr_0', 'rs')).toBe(10);
773
+ expect(await mc.scoreboard('#arr_2', 'rs')).toBe(30);
774
+ expect(await mc.scoreboard('#arr_4', 'rs')).toBe(50);
775
+ expect(await mc.scoreboard('#arr_len', 'rs')).toBe(5);
776
+ expect(await mc.scoreboard('#arr_sum', 'rs')).toBe(150); // 10+20+30+40+50
777
+ expect(await mc.scoreboard('#arr_push', 'rs')).toBe(4); // [1,2,3,4].len
778
+ expect(await mc.scoreboard('#arr_pop', 'rs')).toBe(4); // popped value
779
+ });
780
+ test('break-continue-test.mcrs: break and continue statements', async () => {
781
+ if (!serverOnline)
782
+ return;
783
+ writeFixtureFile('break-continue-test.mcrs', 'break_continue_test');
784
+ await mc.reload();
785
+ await mc.command('/function break_continue_test:__load').catch(() => { });
786
+ await mc.command('/function break_continue_test:test_break_continue');
787
+ await mc.ticks(10);
788
+ expect(await mc.scoreboard('#break_at', 'rs')).toBe(5);
789
+ expect(await mc.scoreboard('#sum_evens', 'rs')).toBe(20); // 0+2+4+6+8
790
+ expect(await mc.scoreboard('#while_break', 'rs')).toBe(7);
791
+ expect(await mc.scoreboard('#nested_break', 'rs')).toBe(3); // outer completes 3 times
792
+ });
793
+ test('match-range-test.mcrs: match with range patterns', async () => {
794
+ if (!serverOnline)
795
+ return;
796
+ writeFixtureFile('match-range-test.mcrs', 'match_range_test');
797
+ await mc.reload();
798
+ await mc.command('/function match_range_test:__load').catch(() => { });
799
+ await mc.command('/function match_range_test:test_match_range');
800
+ await mc.ticks(5);
801
+ expect(await mc.scoreboard('#grade', 'rs')).toBe(4); // score=85 → B
802
+ expect(await mc.scoreboard('#boundary_59', 'rs')).toBe(1); // 59 matches 0..59
803
+ expect(await mc.scoreboard('#boundary_60', 'rs')).toBe(2); // 60 matches 60..100
804
+ expect(await mc.scoreboard('#neg_range', 'rs')).toBe(1); // -5 matches ..0
805
+ });
806
+ test('foreach-at-test.mcrs: foreach with at @s context', async () => {
807
+ if (!serverOnline)
808
+ return;
809
+ writeFixtureFile('foreach-at-test.mcrs', 'foreach_at_test');
810
+ await mc.reload();
811
+ await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false });
812
+ await mc.command('/function foreach_at_test:setup').catch(() => { });
813
+ await mc.command('/function foreach_at_test:test_foreach_at');
814
+ await mc.ticks(10);
815
+ expect(await mc.scoreboard('#foreach_count', 'rs')).toBe(3);
816
+ expect(await mc.scoreboard('#foreach_at_count', 'rs')).toBe(3);
817
+ });
818
+ });
734
819
  //# sourceMappingURL=mc-integration.test.js.map
@@ -247,6 +247,45 @@ export type ExecuteSubcommand = {
247
247
  } | {
248
248
  kind: 'at';
249
249
  selector: EntitySelector;
250
+ } | {
251
+ kind: 'positioned';
252
+ x: string;
253
+ y: string;
254
+ z: string;
255
+ } | {
256
+ kind: 'positioned_as';
257
+ selector: EntitySelector;
258
+ } | {
259
+ kind: 'rotated';
260
+ yaw: string;
261
+ pitch: string;
262
+ } | {
263
+ kind: 'rotated_as';
264
+ selector: EntitySelector;
265
+ } | {
266
+ kind: 'facing';
267
+ x: string;
268
+ y: string;
269
+ z: string;
270
+ } | {
271
+ kind: 'facing_entity';
272
+ selector: EntitySelector;
273
+ anchor: 'eyes' | 'feet';
274
+ } | {
275
+ kind: 'anchored';
276
+ anchor: 'eyes' | 'feet';
277
+ } | {
278
+ kind: 'align';
279
+ axes: string;
280
+ } | {
281
+ kind: 'in';
282
+ dimension: string;
283
+ } | {
284
+ kind: 'on';
285
+ relation: string;
286
+ } | {
287
+ kind: 'summon';
288
+ entity: string;
250
289
  } | {
251
290
  kind: 'if_entity';
252
291
  selector?: EntitySelector;
@@ -258,8 +297,45 @@ export type ExecuteSubcommand = {
258
297
  varName?: string;
259
298
  filters?: SelectorFilter;
260
299
  } | {
261
- kind: 'in';
262
- dimension: string;
300
+ kind: 'if_block';
301
+ pos: [string, string, string];
302
+ block: string;
303
+ } | {
304
+ kind: 'unless_block';
305
+ pos: [string, string, string];
306
+ block: string;
307
+ } | {
308
+ kind: 'if_score';
309
+ target: string;
310
+ targetObj: string;
311
+ op: string;
312
+ source: string;
313
+ sourceObj: string;
314
+ } | {
315
+ kind: 'unless_score';
316
+ target: string;
317
+ targetObj: string;
318
+ op: string;
319
+ source: string;
320
+ sourceObj: string;
321
+ } | {
322
+ kind: 'if_score_range';
323
+ target: string;
324
+ targetObj: string;
325
+ range: string;
326
+ } | {
327
+ kind: 'unless_score_range';
328
+ target: string;
329
+ targetObj: string;
330
+ range: string;
331
+ } | {
332
+ kind: 'store_result';
333
+ target: string;
334
+ targetObj: string;
335
+ } | {
336
+ kind: 'store_success';
337
+ target: string;
338
+ targetObj: string;
263
339
  };
264
340
  export type Stmt = {
265
341
  kind: 'let';
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Main entry point for programmatic usage.
5
5
  */
6
- export declare const version = "1.2.10";
6
+ export declare const version = "1.2.11";
7
7
  import type { Warning } from './lowering';
8
8
  import { DatapackFile } from './codegen/mcfunction';
9
9
  import type { IRModule } from './ir/types';
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ exports.MCCommandValidator = exports.generateDatapack = exports.optimize = expor
9
9
  exports.compile = compile;
10
10
  exports.check = check;
11
11
  // eslint-disable-next-line @typescript-eslint/no-var-requires
12
- exports.version = '1.2.10';
12
+ exports.version = '1.2.11';
13
13
  const lexer_1 = require("./lexer");
14
14
  const parser_1 = require("./parser");
15
15
  const typechecker_1 = require("./typechecker");
@@ -1006,18 +1006,52 @@ class Lowering {
1006
1006
  const parts = ['execute'];
1007
1007
  for (const sub of stmt.subcommands) {
1008
1008
  switch (sub.kind) {
1009
+ // Context modifiers
1009
1010
  case 'as':
1010
1011
  parts.push(`as ${this.selectorToString(sub.selector)}`);
1011
1012
  break;
1012
1013
  case 'at':
1013
1014
  parts.push(`at ${this.selectorToString(sub.selector)}`);
1014
1015
  break;
1016
+ case 'positioned':
1017
+ parts.push(`positioned ${sub.x} ${sub.y} ${sub.z}`);
1018
+ break;
1019
+ case 'positioned_as':
1020
+ parts.push(`positioned as ${this.selectorToString(sub.selector)}`);
1021
+ break;
1022
+ case 'rotated':
1023
+ parts.push(`rotated ${sub.yaw} ${sub.pitch}`);
1024
+ break;
1025
+ case 'rotated_as':
1026
+ parts.push(`rotated as ${this.selectorToString(sub.selector)}`);
1027
+ break;
1028
+ case 'facing':
1029
+ parts.push(`facing ${sub.x} ${sub.y} ${sub.z}`);
1030
+ break;
1031
+ case 'facing_entity':
1032
+ parts.push(`facing entity ${this.selectorToString(sub.selector)} ${sub.anchor}`);
1033
+ break;
1034
+ case 'anchored':
1035
+ parts.push(`anchored ${sub.anchor}`);
1036
+ break;
1037
+ case 'align':
1038
+ parts.push(`align ${sub.axes}`);
1039
+ break;
1040
+ case 'in':
1041
+ parts.push(`in ${sub.dimension}`);
1042
+ break;
1043
+ case 'on':
1044
+ parts.push(`on ${sub.relation}`);
1045
+ break;
1046
+ case 'summon':
1047
+ parts.push(`summon ${sub.entity}`);
1048
+ break;
1049
+ // Conditions
1015
1050
  case 'if_entity':
1016
1051
  if (sub.selector) {
1017
1052
  parts.push(`if entity ${this.selectorToString(sub.selector)}`);
1018
1053
  }
1019
1054
  else if (sub.varName) {
1020
- // Variable with filters - substitute with @s and apply filters
1021
1055
  const sel = { kind: '@s', filters: sub.filters };
1022
1056
  parts.push(`if entity ${this.selectorToString(sel)}`);
1023
1057
  }
@@ -1027,13 +1061,34 @@ class Lowering {
1027
1061
  parts.push(`unless entity ${this.selectorToString(sub.selector)}`);
1028
1062
  }
1029
1063
  else if (sub.varName) {
1030
- // Variable with filters - substitute with @s and apply filters
1031
1064
  const sel = { kind: '@s', filters: sub.filters };
1032
1065
  parts.push(`unless entity ${this.selectorToString(sel)}`);
1033
1066
  }
1034
1067
  break;
1035
- case 'in':
1036
- parts.push(`in ${sub.dimension}`);
1068
+ case 'if_block':
1069
+ parts.push(`if block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`);
1070
+ break;
1071
+ case 'unless_block':
1072
+ parts.push(`unless block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`);
1073
+ break;
1074
+ case 'if_score':
1075
+ parts.push(`if score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`);
1076
+ break;
1077
+ case 'unless_score':
1078
+ parts.push(`unless score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`);
1079
+ break;
1080
+ case 'if_score_range':
1081
+ parts.push(`if score ${sub.target} ${sub.targetObj} matches ${sub.range}`);
1082
+ break;
1083
+ case 'unless_score_range':
1084
+ parts.push(`unless score ${sub.target} ${sub.targetObj} matches ${sub.range}`);
1085
+ break;
1086
+ // Store
1087
+ case 'store_result':
1088
+ parts.push(`store result score ${sub.target} ${sub.targetObj}`);
1089
+ break;
1090
+ case 'store_success':
1091
+ parts.push(`store success score ${sub.target} ${sub.targetObj}`);
1037
1092
  break;
1038
1093
  }
1039
1094
  }
@@ -45,6 +45,10 @@ export declare class Parser {
45
45
  private parseAsStmt;
46
46
  private parseAtStmt;
47
47
  private parseExecuteStmt;
48
+ private parseExecuteCondition;
49
+ private parseCoordToken;
50
+ private parseBlockId;
51
+ private checkIdent;
48
52
  private parseExprStmt;
49
53
  private parseExpr;
50
54
  private parseAssignment;
@@ -546,11 +546,11 @@ class Parser {
546
546
  this.expect('in');
547
547
  const iterable = this.parseExpr();
548
548
  this.expect(')');
549
- // Parse optional execute context modifiers (at, positioned, rotated, facing, etc.)
549
+ // Parse optional execute context modifiers (as, at, positioned, rotated, facing, etc.)
550
550
  let executeContext;
551
- // Check for 'at' keyword or identifiers like 'positioned', 'rotated', 'facing', 'anchored', 'align'
552
- const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align'];
553
- if (this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
551
+ // Check for execute subcommand keywords
552
+ const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align', 'on', 'summon'];
553
+ if (this.check('as') || this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
554
554
  // Collect everything until we hit '{'
555
555
  let context = '';
556
556
  while (!this.check('{') && !this.check('eof')) {
@@ -615,34 +615,175 @@ class Parser {
615
615
  const selector = this.parseSelector();
616
616
  subcommands.push({ kind: 'at', selector });
617
617
  }
618
- else if (this.match('if')) {
619
- // Expect 'entity' keyword (as ident) or just parse selector directly
620
- if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
621
- this.advance(); // consume 'entity'
618
+ else if (this.checkIdent('positioned')) {
619
+ this.advance();
620
+ if (this.match('as')) {
621
+ const selector = this.parseSelector();
622
+ subcommands.push({ kind: 'positioned_as', selector });
623
+ }
624
+ else {
625
+ const x = this.parseCoordToken();
626
+ const y = this.parseCoordToken();
627
+ const z = this.parseCoordToken();
628
+ subcommands.push({ kind: 'positioned', x, y, z });
622
629
  }
623
- const selectorOrVar = this.parseSelectorOrVarSelector();
624
- subcommands.push({ kind: 'if_entity', ...selectorOrVar });
625
630
  }
626
- else if (this.match('unless')) {
627
- // Expect 'entity' keyword (as ident) or just parse selector directly
628
- if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
629
- this.advance(); // consume 'entity'
631
+ else if (this.checkIdent('rotated')) {
632
+ this.advance();
633
+ if (this.match('as')) {
634
+ const selector = this.parseSelector();
635
+ subcommands.push({ kind: 'rotated_as', selector });
636
+ }
637
+ else {
638
+ const yaw = this.parseCoordToken();
639
+ const pitch = this.parseCoordToken();
640
+ subcommands.push({ kind: 'rotated', yaw, pitch });
641
+ }
642
+ }
643
+ else if (this.checkIdent('facing')) {
644
+ this.advance();
645
+ if (this.checkIdent('entity')) {
646
+ this.advance();
647
+ const selector = this.parseSelector();
648
+ const anchor = this.checkIdent('eyes') || this.checkIdent('feet') ? this.advance().value : 'feet';
649
+ subcommands.push({ kind: 'facing_entity', selector, anchor });
650
+ }
651
+ else {
652
+ const x = this.parseCoordToken();
653
+ const y = this.parseCoordToken();
654
+ const z = this.parseCoordToken();
655
+ subcommands.push({ kind: 'facing', x, y, z });
656
+ }
657
+ }
658
+ else if (this.checkIdent('anchored')) {
659
+ this.advance();
660
+ const anchor = this.advance().value;
661
+ subcommands.push({ kind: 'anchored', anchor });
662
+ }
663
+ else if (this.checkIdent('align')) {
664
+ this.advance();
665
+ const axes = this.advance().value;
666
+ subcommands.push({ kind: 'align', axes });
667
+ }
668
+ else if (this.checkIdent('on')) {
669
+ this.advance();
670
+ const relation = this.advance().value;
671
+ subcommands.push({ kind: 'on', relation });
672
+ }
673
+ else if (this.checkIdent('summon')) {
674
+ this.advance();
675
+ const entity = this.advance().value;
676
+ subcommands.push({ kind: 'summon', entity });
677
+ }
678
+ else if (this.checkIdent('store')) {
679
+ this.advance();
680
+ const storeType = this.advance().value; // 'result' or 'success'
681
+ if (this.checkIdent('score')) {
682
+ this.advance();
683
+ const target = this.advance().value;
684
+ const targetObj = this.advance().value;
685
+ if (storeType === 'result') {
686
+ subcommands.push({ kind: 'store_result', target, targetObj });
687
+ }
688
+ else {
689
+ subcommands.push({ kind: 'store_success', target, targetObj });
690
+ }
691
+ }
692
+ else {
693
+ this.error('store currently only supports score target');
630
694
  }
631
- const selectorOrVar = this.parseSelectorOrVarSelector();
632
- subcommands.push({ kind: 'unless_entity', ...selectorOrVar });
695
+ }
696
+ else if (this.match('if')) {
697
+ this.parseExecuteCondition(subcommands, 'if');
698
+ }
699
+ else if (this.match('unless')) {
700
+ this.parseExecuteCondition(subcommands, 'unless');
633
701
  }
634
702
  else if (this.match('in')) {
635
- const dim = this.expect('ident').value;
703
+ // Dimension can be namespaced: minecraft:the_nether
704
+ let dim = this.advance().value;
705
+ if (this.match(':')) {
706
+ dim += ':' + this.advance().value;
707
+ }
636
708
  subcommands.push({ kind: 'in', dimension: dim });
637
709
  }
638
710
  else {
639
- this.error(`Unexpected token in execute statement: ${this.peek().kind}`);
711
+ this.error(`Unexpected token in execute statement: ${this.peek().kind} (${this.peek().value})`);
640
712
  }
641
713
  }
642
714
  this.expect('run');
643
715
  const body = this.parseBlock();
644
716
  return this.withLoc({ kind: 'execute', subcommands, body }, executeToken);
645
717
  }
718
+ parseExecuteCondition(subcommands, type) {
719
+ if (this.checkIdent('entity') || this.check('selector')) {
720
+ if (this.checkIdent('entity'))
721
+ this.advance();
722
+ const selectorOrVar = this.parseSelectorOrVarSelector();
723
+ subcommands.push({ kind: type === 'if' ? 'if_entity' : 'unless_entity', ...selectorOrVar });
724
+ }
725
+ else if (this.checkIdent('block')) {
726
+ this.advance();
727
+ const x = this.parseCoordToken();
728
+ const y = this.parseCoordToken();
729
+ const z = this.parseCoordToken();
730
+ const block = this.parseBlockId();
731
+ subcommands.push({ kind: type === 'if' ? 'if_block' : 'unless_block', pos: [x, y, z], block });
732
+ }
733
+ else if (this.checkIdent('score')) {
734
+ this.advance();
735
+ const target = this.advance().value;
736
+ const targetObj = this.advance().value;
737
+ // Check for range or comparison
738
+ if (this.checkIdent('matches')) {
739
+ this.advance();
740
+ const range = this.advance().value;
741
+ subcommands.push({ kind: type === 'if' ? 'if_score_range' : 'unless_score_range', target, targetObj, range });
742
+ }
743
+ else {
744
+ const op = this.advance().value; // <, <=, =, >=, >
745
+ const source = this.advance().value;
746
+ const sourceObj = this.advance().value;
747
+ subcommands.push({
748
+ kind: type === 'if' ? 'if_score' : 'unless_score',
749
+ target, targetObj, op, source, sourceObj
750
+ });
751
+ }
752
+ }
753
+ else {
754
+ this.error(`Unknown condition type after ${type}`);
755
+ }
756
+ }
757
+ parseCoordToken() {
758
+ // Handle ~, ^, numbers, relative coords like ~5, ^-3
759
+ const token = this.peek();
760
+ if (token.kind === 'rel_coord' || token.kind === 'local_coord' ||
761
+ token.kind === 'int_lit' || token.kind === 'float_lit' ||
762
+ token.kind === '-' || token.kind === 'ident') {
763
+ return this.advance().value;
764
+ }
765
+ this.error(`Expected coordinate, got ${token.kind}`);
766
+ return '~';
767
+ }
768
+ parseBlockId() {
769
+ // Parse block ID like minecraft:stone or stone
770
+ let id = this.advance().value;
771
+ if (this.match(':')) {
772
+ id += ':' + this.advance().value;
773
+ }
774
+ // Handle block states [facing=north]
775
+ if (this.check('[')) {
776
+ id += this.advance().value; // [
777
+ while (!this.check(']') && !this.check('eof')) {
778
+ id += this.advance().value;
779
+ }
780
+ id += this.advance().value; // ]
781
+ }
782
+ return id;
783
+ }
784
+ checkIdent(value) {
785
+ return this.check('ident') && this.peek().value === value;
786
+ }
646
787
  parseExprStmt() {
647
788
  const expr = this.parseExpr();
648
789
  this.expect(';');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "1.2.10",
3
+ "version": "1.2.11",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,30 @@
1
+ // Array operations test
2
+
3
+ @keep fn test_array() {
4
+ // Array initialization
5
+ let nums: int[] = [10, 20, 30, 40, 50];
6
+
7
+ // Array access
8
+ scoreboard_set("#arr_0", #rs, nums[0]);
9
+ scoreboard_set("#arr_2", #rs, nums[2]);
10
+ scoreboard_set("#arr_4", #rs, nums[4]);
11
+
12
+ // Array length via .len property
13
+ scoreboard_set("#arr_len", #rs, nums.len);
14
+
15
+ // Sum via foreach
16
+ let sum: int = 0;
17
+ foreach (n in nums) {
18
+ sum = sum + n;
19
+ }
20
+ scoreboard_set("#arr_sum", #rs, sum);
21
+
22
+ // Push operation
23
+ let arr2: int[] = [1, 2, 3];
24
+ arr2.push(4);
25
+ scoreboard_set("#arr_push", #rs, arr2.len);
26
+
27
+ // Pop operation
28
+ let popped: int = arr2.pop();
29
+ scoreboard_set("#arr_pop", #rs, popped);
30
+ }
@@ -0,0 +1,46 @@
1
+ // Break and continue statements test
2
+
3
+ @keep fn test_break_continue() {
4
+ // Test break - should stop at i=5
5
+ let break_at: int = -1;
6
+ for (let i: int = 0; i < 10; i = i + 1) {
7
+ if (i == 5) {
8
+ break_at = i;
9
+ break;
10
+ }
11
+ }
12
+ scoreboard_set("#break_at", #rs, break_at);
13
+
14
+ // Test continue - sum only even numbers
15
+ let sum_evens: int = 0;
16
+ for (let i: int = 0; i < 10; i = i + 1) {
17
+ if (i % 2 != 0) {
18
+ continue;
19
+ }
20
+ sum_evens = sum_evens + i;
21
+ }
22
+ // 0+2+4+6+8 = 20
23
+ scoreboard_set("#sum_evens", #rs, sum_evens);
24
+
25
+ // While with break
26
+ let count: int = 0;
27
+ while (true) {
28
+ count = count + 1;
29
+ if (count >= 7) {
30
+ break;
31
+ }
32
+ }
33
+ scoreboard_set("#while_break", #rs, count);
34
+
35
+ // Nested loop break (breaks inner only)
36
+ let outer_count: int = 0;
37
+ for (let a: int = 0; a < 3; a = a + 1) {
38
+ for (let b: int = 0; b < 10; b = b + 1) {
39
+ if (b == 2) {
40
+ break;
41
+ }
42
+ }
43
+ outer_count = outer_count + 1;
44
+ }
45
+ scoreboard_set("#nested_break", #rs, outer_count);
46
+ }
@@ -0,0 +1,37 @@
1
+ // Enum definition and matching test
2
+
3
+ enum GamePhase {
4
+ Lobby, // 0
5
+ Starting, // 1
6
+ Playing, // 2
7
+ Ended // 3
8
+ }
9
+
10
+ enum Rank {
11
+ Bronze = 1,
12
+ Silver = 2,
13
+ Gold = 3,
14
+ Diamond = 10
15
+ }
16
+
17
+ @keep fn test_enum() {
18
+ // Basic enum value (use . not ::)
19
+ let phase: int = GamePhase.Playing;
20
+ scoreboard_set("#enum_phase", #rs, phase);
21
+
22
+ // Match on enum
23
+ match (phase) {
24
+ GamePhase.Lobby => { scoreboard_set("#enum_match", #rs, 0); }
25
+ GamePhase.Playing => { scoreboard_set("#enum_match", #rs, 2); }
26
+ _ => { scoreboard_set("#enum_match", #rs, -1); }
27
+ }
28
+
29
+ // Custom values
30
+ let rank: int = Rank.Diamond;
31
+ scoreboard_set("#enum_rank", #rs, rank);
32
+
33
+ // Comparison
34
+ if (rank > Rank.Gold) {
35
+ scoreboard_set("#enum_high", #rs, 1);
36
+ }
37
+ }
@@ -0,0 +1,33 @@
1
+ // Foreach with execute context modifiers test
2
+
3
+ @load fn setup() {
4
+ scoreboard_add("rs");
5
+ scoreboard_set("#foreach_count", #rs, 0);
6
+ scoreboard_set("#foreach_at_count", #rs, 0);
7
+ }
8
+
9
+ @keep fn test_foreach_at() {
10
+ // Spawn test entities
11
+ raw("summon minecraft:armor_stand ~ ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
12
+ raw("summon minecraft:armor_stand ~2 ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
13
+ raw("summon minecraft:armor_stand ~4 ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
14
+
15
+ // Basic foreach
16
+ let count: int = 0;
17
+ foreach (e in @e[type=armor_stand,tag=test_foreach]) {
18
+ count = count + 1;
19
+ }
20
+ scoreboard_set("#foreach_count", #rs, count);
21
+
22
+ // Foreach with at @s (execute at entity position)
23
+ let at_count: int = 0;
24
+ foreach (e in @e[type=armor_stand,tag=test_foreach]) at @s {
25
+ // This runs at each entity's position
26
+ at_count = at_count + 1;
27
+ raw("particle minecraft:heart ~ ~1 ~ 0 0 0 0 1");
28
+ }
29
+ scoreboard_set("#foreach_at_count", #rs, at_count);
30
+
31
+ // Cleanup
32
+ raw("kill @e[type=armor_stand,tag=test_foreach]");
33
+ }
@@ -0,0 +1,45 @@
1
+ // Match with range patterns test
2
+
3
+ @keep fn test_match_range() {
4
+ // Test score grading
5
+ let score: int = 85;
6
+ let grade: int = 0;
7
+
8
+ match (score) {
9
+ 0..59 => { grade = 1; } // F
10
+ 60..69 => { grade = 2; } // D
11
+ 70..79 => { grade = 3; } // C
12
+ 80..89 => { grade = 4; } // B
13
+ 90..100 => { grade = 5; } // A
14
+ _ => { grade = 0; }
15
+ }
16
+ scoreboard_set("#grade", #rs, grade);
17
+
18
+ // Test boundary values
19
+ let val1: int = 59;
20
+ let result1: int = 0;
21
+ match (val1) {
22
+ 0..59 => { result1 = 1; }
23
+ 60..100 => { result1 = 2; }
24
+ _ => { result1 = 0; }
25
+ }
26
+ scoreboard_set("#boundary_59", #rs, result1);
27
+
28
+ let val2: int = 60;
29
+ let result2: int = 0;
30
+ match (val2) {
31
+ 0..59 => { result2 = 1; }
32
+ 60..100 => { result2 = 2; }
33
+ _ => { result2 = 0; }
34
+ }
35
+ scoreboard_set("#boundary_60", #rs, result2);
36
+
37
+ // Open-ended ranges
38
+ let neg: int = -5;
39
+ let neg_result: int = 0;
40
+ match (neg) {
41
+ ..0 => { neg_result = 1; }
42
+ 0.. => { neg_result = 2; }
43
+ }
44
+ scoreboard_set("#neg_range", #rs, neg_result);
45
+ }
@@ -0,0 +1,34 @@
1
+ // Struct instantiation and field access test
2
+
3
+ struct Point {
4
+ x: int,
5
+ y: int,
6
+ z: int
7
+ }
8
+
9
+ struct Player {
10
+ score: int,
11
+ alive: bool
12
+ }
13
+
14
+ @keep fn test_struct() {
15
+ // Create struct instance
16
+ let p: Point = { x: 10, y: 64, z: -5 };
17
+
18
+ // Access fields
19
+ scoreboard_set("#struct_x", #rs, p.x);
20
+ scoreboard_set("#struct_y", #rs, p.y);
21
+ scoreboard_set("#struct_z", #rs, p.z);
22
+
23
+ // Modify via new instance
24
+ let p2: Point = { x: p.x + 5, y: p.y, z: p.z * 2 };
25
+ scoreboard_set("#struct_x2", #rs, p2.x);
26
+ scoreboard_set("#struct_z2", #rs, p2.z);
27
+
28
+ // Bool field
29
+ let player: Player = { score: 100, alive: true };
30
+ if (player.alive) {
31
+ scoreboard_set("#struct_alive", #rs, 1);
32
+ }
33
+ scoreboard_set("#struct_score", #rs, player.score);
34
+ }
@@ -797,3 +797,100 @@ describe('MC Integration - New Features', () => {
797
797
  expect(tickResult.ok).toBe(true)
798
798
  })
799
799
  })
800
+
801
+ describe('MC Integration - Extended Coverage', () => {
802
+ test('struct-test.mcrs: struct instantiation and field access', async () => {
803
+ if (!serverOnline) return
804
+
805
+ writeFixtureFile('struct-test.mcrs', 'struct_test')
806
+ await mc.reload()
807
+ await mc.command('/function struct_test:__load').catch(() => {})
808
+ await mc.command('/function struct_test:test_struct')
809
+ await mc.ticks(5)
810
+
811
+ expect(await mc.scoreboard('#struct_x', 'rs')).toBe(10)
812
+ expect(await mc.scoreboard('#struct_y', 'rs')).toBe(64)
813
+ expect(await mc.scoreboard('#struct_z', 'rs')).toBe(-5)
814
+ expect(await mc.scoreboard('#struct_x2', 'rs')).toBe(15) // 10+5
815
+ expect(await mc.scoreboard('#struct_z2', 'rs')).toBe(-10) // -5*2
816
+ expect(await mc.scoreboard('#struct_alive', 'rs')).toBe(1)
817
+ expect(await mc.scoreboard('#struct_score', 'rs')).toBe(100)
818
+ })
819
+
820
+ test('enum-test.mcrs: enum values and match', async () => {
821
+ if (!serverOnline) return
822
+
823
+ writeFixtureFile('enum-test.mcrs', 'enum_test')
824
+ await mc.reload()
825
+ await mc.command('/function enum_test:__load').catch(() => {})
826
+ await mc.command('/function enum_test:test_enum')
827
+ await mc.ticks(5)
828
+
829
+ expect(await mc.scoreboard('#enum_phase', 'rs')).toBe(2) // Playing=2
830
+ expect(await mc.scoreboard('#enum_match', 'rs')).toBe(2) // matched Playing
831
+ expect(await mc.scoreboard('#enum_rank', 'rs')).toBe(10) // Diamond=10
832
+ expect(await mc.scoreboard('#enum_high', 'rs')).toBe(1) // Diamond > Gold
833
+ })
834
+
835
+ test('array-test.mcrs: array operations', async () => {
836
+ if (!serverOnline) return
837
+
838
+ writeFixtureFile('array-test.mcrs', 'array_test')
839
+ await mc.reload()
840
+ await mc.command('/function array_test:__load').catch(() => {})
841
+ await mc.command('/function array_test:test_array')
842
+ await mc.ticks(5)
843
+
844
+ expect(await mc.scoreboard('#arr_0', 'rs')).toBe(10)
845
+ expect(await mc.scoreboard('#arr_2', 'rs')).toBe(30)
846
+ expect(await mc.scoreboard('#arr_4', 'rs')).toBe(50)
847
+ expect(await mc.scoreboard('#arr_len', 'rs')).toBe(5)
848
+ expect(await mc.scoreboard('#arr_sum', 'rs')).toBe(150) // 10+20+30+40+50
849
+ expect(await mc.scoreboard('#arr_push', 'rs')).toBe(4) // [1,2,3,4].len
850
+ expect(await mc.scoreboard('#arr_pop', 'rs')).toBe(4) // popped value
851
+ })
852
+
853
+ test('break-continue-test.mcrs: break and continue statements', async () => {
854
+ if (!serverOnline) return
855
+
856
+ writeFixtureFile('break-continue-test.mcrs', 'break_continue_test')
857
+ await mc.reload()
858
+ await mc.command('/function break_continue_test:__load').catch(() => {})
859
+ await mc.command('/function break_continue_test:test_break_continue')
860
+ await mc.ticks(10)
861
+
862
+ expect(await mc.scoreboard('#break_at', 'rs')).toBe(5)
863
+ expect(await mc.scoreboard('#sum_evens', 'rs')).toBe(20) // 0+2+4+6+8
864
+ expect(await mc.scoreboard('#while_break', 'rs')).toBe(7)
865
+ expect(await mc.scoreboard('#nested_break', 'rs')).toBe(3) // outer completes 3 times
866
+ })
867
+
868
+ test('match-range-test.mcrs: match with range patterns', async () => {
869
+ if (!serverOnline) return
870
+
871
+ writeFixtureFile('match-range-test.mcrs', 'match_range_test')
872
+ await mc.reload()
873
+ await mc.command('/function match_range_test:__load').catch(() => {})
874
+ await mc.command('/function match_range_test:test_match_range')
875
+ await mc.ticks(5)
876
+
877
+ expect(await mc.scoreboard('#grade', 'rs')).toBe(4) // score=85 → B
878
+ expect(await mc.scoreboard('#boundary_59', 'rs')).toBe(1) // 59 matches 0..59
879
+ expect(await mc.scoreboard('#boundary_60', 'rs')).toBe(2) // 60 matches 60..100
880
+ expect(await mc.scoreboard('#neg_range', 'rs')).toBe(1) // -5 matches ..0
881
+ })
882
+
883
+ test('foreach-at-test.mcrs: foreach with at @s context', async () => {
884
+ if (!serverOnline) return
885
+
886
+ writeFixtureFile('foreach-at-test.mcrs', 'foreach_at_test')
887
+ await mc.reload()
888
+ await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false })
889
+ await mc.command('/function foreach_at_test:setup').catch(() => {})
890
+ await mc.command('/function foreach_at_test:test_foreach_at')
891
+ await mc.ticks(10)
892
+
893
+ expect(await mc.scoreboard('#foreach_count', 'rs')).toBe(3)
894
+ expect(await mc.scoreboard('#foreach_at_count', 'rs')).toBe(3)
895
+ })
896
+ })
package/src/ast/types.ts CHANGED
@@ -179,11 +179,32 @@ export type LiteralExpr =
179
179
  // ---------------------------------------------------------------------------
180
180
 
181
181
  export type ExecuteSubcommand =
182
+ // Context modifiers
182
183
  | { kind: 'as'; selector: EntitySelector }
183
184
  | { kind: 'at'; selector: EntitySelector }
185
+ | { kind: 'positioned'; x: string; y: string; z: string }
186
+ | { kind: 'positioned_as'; selector: EntitySelector }
187
+ | { kind: 'rotated'; yaw: string; pitch: string }
188
+ | { kind: 'rotated_as'; selector: EntitySelector }
189
+ | { kind: 'facing'; x: string; y: string; z: string }
190
+ | { kind: 'facing_entity'; selector: EntitySelector; anchor: 'eyes' | 'feet' }
191
+ | { kind: 'anchored'; anchor: 'eyes' | 'feet' }
192
+ | { kind: 'align'; axes: string }
193
+ | { kind: 'in'; dimension: string }
194
+ | { kind: 'on'; relation: string }
195
+ | { kind: 'summon'; entity: string }
196
+ // Conditions
184
197
  | { kind: 'if_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
185
198
  | { kind: 'unless_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
186
- | { kind: 'in'; dimension: string }
199
+ | { kind: 'if_block'; pos: [string, string, string]; block: string }
200
+ | { kind: 'unless_block'; pos: [string, string, string]; block: string }
201
+ | { kind: 'if_score'; target: string; targetObj: string; op: string; source: string; sourceObj: string }
202
+ | { kind: 'unless_score'; target: string; targetObj: string; op: string; source: string; sourceObj: string }
203
+ | { kind: 'if_score_range'; target: string; targetObj: string; range: string }
204
+ | { kind: 'unless_score_range'; target: string; targetObj: string; range: string }
205
+ // Store
206
+ | { kind: 'store_result'; target: string; targetObj: string }
207
+ | { kind: 'store_success'; target: string; targetObj: string }
187
208
 
188
209
  export type Stmt =
189
210
  | { kind: 'let'; name: string; type?: TypeNode; init: Expr; span?: Span }
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  // eslint-disable-next-line @typescript-eslint/no-var-requires
8
- export const version = '1.2.10'
8
+ export const version = '1.2.11'
9
9
 
10
10
  import { Lexer } from './lexer'
11
11
  import { Parser } from './parser'
@@ -1200,17 +1200,51 @@ export class Lowering {
1200
1200
  const parts: string[] = ['execute']
1201
1201
  for (const sub of stmt.subcommands) {
1202
1202
  switch (sub.kind) {
1203
+ // Context modifiers
1203
1204
  case 'as':
1204
1205
  parts.push(`as ${this.selectorToString(sub.selector)}`)
1205
1206
  break
1206
1207
  case 'at':
1207
1208
  parts.push(`at ${this.selectorToString(sub.selector)}`)
1208
1209
  break
1210
+ case 'positioned':
1211
+ parts.push(`positioned ${sub.x} ${sub.y} ${sub.z}`)
1212
+ break
1213
+ case 'positioned_as':
1214
+ parts.push(`positioned as ${this.selectorToString(sub.selector)}`)
1215
+ break
1216
+ case 'rotated':
1217
+ parts.push(`rotated ${sub.yaw} ${sub.pitch}`)
1218
+ break
1219
+ case 'rotated_as':
1220
+ parts.push(`rotated as ${this.selectorToString(sub.selector)}`)
1221
+ break
1222
+ case 'facing':
1223
+ parts.push(`facing ${sub.x} ${sub.y} ${sub.z}`)
1224
+ break
1225
+ case 'facing_entity':
1226
+ parts.push(`facing entity ${this.selectorToString(sub.selector)} ${sub.anchor}`)
1227
+ break
1228
+ case 'anchored':
1229
+ parts.push(`anchored ${sub.anchor}`)
1230
+ break
1231
+ case 'align':
1232
+ parts.push(`align ${sub.axes}`)
1233
+ break
1234
+ case 'in':
1235
+ parts.push(`in ${sub.dimension}`)
1236
+ break
1237
+ case 'on':
1238
+ parts.push(`on ${sub.relation}`)
1239
+ break
1240
+ case 'summon':
1241
+ parts.push(`summon ${sub.entity}`)
1242
+ break
1243
+ // Conditions
1209
1244
  case 'if_entity':
1210
1245
  if (sub.selector) {
1211
1246
  parts.push(`if entity ${this.selectorToString(sub.selector)}`)
1212
1247
  } else if (sub.varName) {
1213
- // Variable with filters - substitute with @s and apply filters
1214
1248
  const sel: EntitySelector = { kind: '@s', filters: sub.filters }
1215
1249
  parts.push(`if entity ${this.selectorToString(sel)}`)
1216
1250
  }
@@ -1219,13 +1253,34 @@ export class Lowering {
1219
1253
  if (sub.selector) {
1220
1254
  parts.push(`unless entity ${this.selectorToString(sub.selector)}`)
1221
1255
  } else if (sub.varName) {
1222
- // Variable with filters - substitute with @s and apply filters
1223
1256
  const sel: EntitySelector = { kind: '@s', filters: sub.filters }
1224
1257
  parts.push(`unless entity ${this.selectorToString(sel)}`)
1225
1258
  }
1226
1259
  break
1227
- case 'in':
1228
- parts.push(`in ${sub.dimension}`)
1260
+ case 'if_block':
1261
+ parts.push(`if block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`)
1262
+ break
1263
+ case 'unless_block':
1264
+ parts.push(`unless block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`)
1265
+ break
1266
+ case 'if_score':
1267
+ parts.push(`if score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`)
1268
+ break
1269
+ case 'unless_score':
1270
+ parts.push(`unless score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`)
1271
+ break
1272
+ case 'if_score_range':
1273
+ parts.push(`if score ${sub.target} ${sub.targetObj} matches ${sub.range}`)
1274
+ break
1275
+ case 'unless_score_range':
1276
+ parts.push(`unless score ${sub.target} ${sub.targetObj} matches ${sub.range}`)
1277
+ break
1278
+ // Store
1279
+ case 'store_result':
1280
+ parts.push(`store result score ${sub.target} ${sub.targetObj}`)
1281
+ break
1282
+ case 'store_success':
1283
+ parts.push(`store success score ${sub.target} ${sub.targetObj}`)
1229
1284
  break
1230
1285
  }
1231
1286
  }
@@ -660,11 +660,11 @@ export class Parser {
660
660
  const iterable = this.parseExpr()
661
661
  this.expect(')')
662
662
 
663
- // Parse optional execute context modifiers (at, positioned, rotated, facing, etc.)
663
+ // Parse optional execute context modifiers (as, at, positioned, rotated, facing, etc.)
664
664
  let executeContext: string | undefined
665
- // Check for 'at' keyword or identifiers like 'positioned', 'rotated', 'facing', 'anchored', 'align'
666
- const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align']
667
- if (this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
665
+ // Check for execute subcommand keywords
666
+ const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align', 'on', 'summon']
667
+ if (this.check('as') || this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
668
668
  // Collect everything until we hit '{'
669
669
  let context = ''
670
670
  while (!this.check('{') && !this.check('eof')) {
@@ -738,25 +738,84 @@ export class Parser {
738
738
  } else if (this.match('at')) {
739
739
  const selector = this.parseSelector()
740
740
  subcommands.push({ kind: 'at', selector })
741
- } else if (this.match('if')) {
742
- // Expect 'entity' keyword (as ident) or just parse selector directly
743
- if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
744
- this.advance() // consume 'entity'
741
+ } else if (this.checkIdent('positioned')) {
742
+ this.advance()
743
+ if (this.match('as')) {
744
+ const selector = this.parseSelector()
745
+ subcommands.push({ kind: 'positioned_as', selector })
746
+ } else {
747
+ const x = this.parseCoordToken()
748
+ const y = this.parseCoordToken()
749
+ const z = this.parseCoordToken()
750
+ subcommands.push({ kind: 'positioned', x, y, z })
745
751
  }
746
- const selectorOrVar = this.parseSelectorOrVarSelector()
747
- subcommands.push({ kind: 'if_entity', ...selectorOrVar })
748
- } else if (this.match('unless')) {
749
- // Expect 'entity' keyword (as ident) or just parse selector directly
750
- if (this.peek().kind === 'ident' && this.peek().value === 'entity') {
751
- this.advance() // consume 'entity'
752
+ } else if (this.checkIdent('rotated')) {
753
+ this.advance()
754
+ if (this.match('as')) {
755
+ const selector = this.parseSelector()
756
+ subcommands.push({ kind: 'rotated_as', selector })
757
+ } else {
758
+ const yaw = this.parseCoordToken()
759
+ const pitch = this.parseCoordToken()
760
+ subcommands.push({ kind: 'rotated', yaw, pitch })
752
761
  }
753
- const selectorOrVar = this.parseSelectorOrVarSelector()
754
- subcommands.push({ kind: 'unless_entity', ...selectorOrVar })
762
+ } else if (this.checkIdent('facing')) {
763
+ this.advance()
764
+ if (this.checkIdent('entity')) {
765
+ this.advance()
766
+ const selector = this.parseSelector()
767
+ const anchor = this.checkIdent('eyes') || this.checkIdent('feet') ? this.advance().value as 'eyes' | 'feet' : 'feet'
768
+ subcommands.push({ kind: 'facing_entity', selector, anchor })
769
+ } else {
770
+ const x = this.parseCoordToken()
771
+ const y = this.parseCoordToken()
772
+ const z = this.parseCoordToken()
773
+ subcommands.push({ kind: 'facing', x, y, z })
774
+ }
775
+ } else if (this.checkIdent('anchored')) {
776
+ this.advance()
777
+ const anchor = this.advance().value as 'eyes' | 'feet'
778
+ subcommands.push({ kind: 'anchored', anchor })
779
+ } else if (this.checkIdent('align')) {
780
+ this.advance()
781
+ const axes = this.advance().value
782
+ subcommands.push({ kind: 'align', axes })
783
+ } else if (this.checkIdent('on')) {
784
+ this.advance()
785
+ const relation = this.advance().value
786
+ subcommands.push({ kind: 'on', relation })
787
+ } else if (this.checkIdent('summon')) {
788
+ this.advance()
789
+ const entity = this.advance().value
790
+ subcommands.push({ kind: 'summon', entity })
791
+ } else if (this.checkIdent('store')) {
792
+ this.advance()
793
+ const storeType = this.advance().value // 'result' or 'success'
794
+ if (this.checkIdent('score')) {
795
+ this.advance()
796
+ const target = this.advance().value
797
+ const targetObj = this.advance().value
798
+ if (storeType === 'result') {
799
+ subcommands.push({ kind: 'store_result', target, targetObj })
800
+ } else {
801
+ subcommands.push({ kind: 'store_success', target, targetObj })
802
+ }
803
+ } else {
804
+ this.error('store currently only supports score target')
805
+ }
806
+ } else if (this.match('if')) {
807
+ this.parseExecuteCondition(subcommands, 'if')
808
+ } else if (this.match('unless')) {
809
+ this.parseExecuteCondition(subcommands, 'unless')
755
810
  } else if (this.match('in')) {
756
- const dim = this.expect('ident').value
811
+ // Dimension can be namespaced: minecraft:the_nether
812
+ let dim = this.advance().value
813
+ if (this.match(':')) {
814
+ dim += ':' + this.advance().value
815
+ }
757
816
  subcommands.push({ kind: 'in', dimension: dim })
758
817
  } else {
759
- this.error(`Unexpected token in execute statement: ${this.peek().kind}`)
818
+ this.error(`Unexpected token in execute statement: ${this.peek().kind} (${this.peek().value})`)
760
819
  }
761
820
  }
762
821
 
@@ -766,6 +825,74 @@ export class Parser {
766
825
  return this.withLoc({ kind: 'execute', subcommands, body }, executeToken)
767
826
  }
768
827
 
828
+ private parseExecuteCondition(subcommands: ExecuteSubcommand[], type: 'if' | 'unless'): void {
829
+ if (this.checkIdent('entity') || this.check('selector')) {
830
+ if (this.checkIdent('entity')) this.advance()
831
+ const selectorOrVar = this.parseSelectorOrVarSelector()
832
+ subcommands.push({ kind: type === 'if' ? 'if_entity' : 'unless_entity', ...selectorOrVar })
833
+ } else if (this.checkIdent('block')) {
834
+ this.advance()
835
+ const x = this.parseCoordToken()
836
+ const y = this.parseCoordToken()
837
+ const z = this.parseCoordToken()
838
+ const block = this.parseBlockId()
839
+ subcommands.push({ kind: type === 'if' ? 'if_block' : 'unless_block', pos: [x, y, z], block })
840
+ } else if (this.checkIdent('score')) {
841
+ this.advance()
842
+ const target = this.advance().value
843
+ const targetObj = this.advance().value
844
+ // Check for range or comparison
845
+ if (this.checkIdent('matches')) {
846
+ this.advance()
847
+ const range = this.advance().value
848
+ subcommands.push({ kind: type === 'if' ? 'if_score_range' : 'unless_score_range', target, targetObj, range })
849
+ } else {
850
+ const op = this.advance().value // <, <=, =, >=, >
851
+ const source = this.advance().value
852
+ const sourceObj = this.advance().value
853
+ subcommands.push({
854
+ kind: type === 'if' ? 'if_score' : 'unless_score',
855
+ target, targetObj, op, source, sourceObj
856
+ })
857
+ }
858
+ } else {
859
+ this.error(`Unknown condition type after ${type}`)
860
+ }
861
+ }
862
+
863
+ private parseCoordToken(): string {
864
+ // Handle ~, ^, numbers, relative coords like ~5, ^-3
865
+ const token = this.peek()
866
+ if (token.kind === 'rel_coord' || token.kind === 'local_coord' ||
867
+ token.kind === 'int_lit' || token.kind === 'float_lit' ||
868
+ token.kind === '-' || token.kind === 'ident') {
869
+ return this.advance().value
870
+ }
871
+ this.error(`Expected coordinate, got ${token.kind}`)
872
+ return '~'
873
+ }
874
+
875
+ private parseBlockId(): string {
876
+ // Parse block ID like minecraft:stone or stone
877
+ let id = this.advance().value
878
+ if (this.match(':')) {
879
+ id += ':' + this.advance().value
880
+ }
881
+ // Handle block states [facing=north]
882
+ if (this.check('[')) {
883
+ id += this.advance().value // [
884
+ while (!this.check(']') && !this.check('eof')) {
885
+ id += this.advance().value
886
+ }
887
+ id += this.advance().value // ]
888
+ }
889
+ return id
890
+ }
891
+
892
+ private checkIdent(value: string): boolean {
893
+ return this.check('ident') && this.peek().value === value
894
+ }
895
+
769
896
  private parseExprStmt(): Stmt {
770
897
  const expr = this.parseExpr()
771
898
  this.expect(';')