redscript-mc 1.2.10 → 1.2.12

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.
@@ -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.12",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -14,17 +14,18 @@ function getFileContent(files: ReturnType<typeof compile>['files'], suffix: stri
14
14
  }
15
15
 
16
16
  describe('AST dead code elimination', () => {
17
- it('removes unused functions reachable from entry points', () => {
17
+ it('removes private unused functions (prefixed with _)', () => {
18
18
  const source = `
19
- fn unused() { say("never called"); }
19
+ fn _unused() { say("never called"); }
20
20
  fn used() { say("called"); }
21
21
  @tick fn main() { used(); }
22
22
  `
23
23
 
24
24
  const result = compile(source, { namespace: 'test' })
25
25
 
26
+ // _unused is removed because it starts with _ (private) and is not called
26
27
  expect(result.ast.declarations.map(fn => fn.name)).toEqual(['used', 'main'])
27
- expect(result.ir.functions.some(fn => fn.name === 'unused')).toBe(false)
28
+ expect(result.ir.functions.some(fn => fn.name === '_unused')).toBe(false)
28
29
  })
29
30
 
30
31
  it('removes unused local variables from the AST body', () => {
@@ -196,8 +196,8 @@ fn main() {
196
196
  const thenFiles = files.filter(file => file.path.includes('/main/then_') && file.content.includes('kill @s'))
197
197
 
198
198
  expect(mainFn).toContain('execute as @e run function test:main/foreach_0')
199
- expect(foreachFn).toContain('execute if entity @s[type=player] run function test:main/then_')
200
- expect(foreachFn).toContain('execute if entity @s[type=zombie] run function test:main/then_')
199
+ expect(foreachFn).toContain('execute if entity @s[type=minecraft:player] run function test:main/then_')
200
+ expect(foreachFn).toContain('execute if entity @s[type=minecraft:zombie] run function test:main/then_')
201
201
  expect(thenFiles).toHaveLength(2)
202
202
  })
203
203
  })
@@ -0,0 +1,30 @@
1
+ // Array operations test
2
+
3
+ @tick 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
+ 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
+ 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
+ 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
+ }
@@ -1,20 +1,27 @@
1
+ @load fn setup() {
2
+ scoreboard_add("armor_stands");
3
+ scoreboard_add("items");
4
+ }
5
+
6
+ // Test is-check type narrowing using armor_stands (don't despawn without players)
1
7
  fn check_types() {
2
- scoreboard_set("#is_check", #players, 0);
3
- scoreboard_set("#is_check", #zombies, 0);
8
+ scoreboard_set("#is_check", #armor_stands, 0);
9
+ scoreboard_set("#is_check", #items, 0);
4
10
 
5
- foreach (e in @e[type=zombie,tag=is_check_target]) {
6
- if (e is Player) {
7
- let players: int = scoreboard_get("#is_check", #players);
8
- scoreboard_set("#is_check", #players, players + 1);
11
+ // Test foreach with is-check on armor_stands
12
+ foreach (e in @e[tag=is_check_target]) {
13
+ if (e is ArmorStand) {
14
+ let count: int = scoreboard_get("#is_check", #armor_stands);
15
+ scoreboard_set("#is_check", #armor_stands, count + 1);
9
16
  }
10
17
 
11
- if (e is Zombie) {
12
- let players: int = scoreboard_get("#is_check", #players);
13
- scoreboard_set("#is_check", #players, players);
18
+ if (e is Item) {
19
+ let count: int = scoreboard_get("#is_check", #items);
20
+ scoreboard_set("#is_check", #items, count + 1);
14
21
  }
15
-
16
- let zombies: int = scoreboard_get("#is_check", #zombies);
17
- scoreboard_set("#is_check", #zombies, zombies + 1);
18
- kill(e);
19
22
  }
20
23
  }
24
+
25
+ fn cleanup() {
26
+ raw("kill @e[tag=is_check_target]");
27
+ }
@@ -0,0 +1,45 @@
1
+ // Match with range patterns test
2
+
3
+ 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
+ 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
+ }
@@ -285,7 +285,7 @@ fn scan() {
285
285
  `)
286
286
  const foreachFn = ir.functions.find(fn => fn.name.includes('scan/foreach'))!
287
287
  const rawCmds = getRawCommands(foreachFn)
288
- const isCheckCmd = rawCmds.find(cmd => cmd.startsWith('execute if entity @s[type=player] run function test:scan/then_'))
288
+ const isCheckCmd = rawCmds.find(cmd => cmd.startsWith('execute if entity @s[type=minecraft:player] run function test:scan/then_'))
289
289
  expect(isCheckCmd).toBeDefined()
290
290
 
291
291
  const thenFn = ir.functions.find(fn => fn.name.startsWith('scan/then_'))!
@@ -360,8 +360,8 @@ fn test() {
360
360
 
361
361
  expect(getRawCommands(mainFn)).toContain('execute as @e run function test:test/foreach_0')
362
362
  expect(thenFns).toHaveLength(2)
363
- expect(rawCmds).toContain(`execute if entity @s[type=player] run function test:${playerThenFn.name}`)
364
- expect(rawCmds).toContain(`execute if entity @s[type=zombie] run function test:${zombieThenFn.name}`)
363
+ expect(rawCmds).toContain(`execute if entity @s[type=minecraft:player] run function test:${playerThenFn.name}`)
364
+ expect(rawCmds).toContain(`execute if entity @s[type=minecraft:zombie] run function test:${zombieThenFn.name}`)
365
365
  expect(getRawCommands(playerThenFn).some(cmd => cmd.includes('give @s diamond 1'))).toBe(true)
366
366
  expect(getRawCommands(zombieThenFn)).toContain('kill @s')
367
367
  })