redscript-mc 1.2.11 → 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.
@@ -46,15 +46,16 @@ function getFileContent(files, suffix) {
46
46
  return file.content;
47
47
  }
48
48
  describe('AST dead code elimination', () => {
49
- it('removes unused functions reachable from entry points', () => {
49
+ it('removes private unused functions (prefixed with _)', () => {
50
50
  const source = `
51
- fn unused() { say("never called"); }
51
+ fn _unused() { say("never called"); }
52
52
  fn used() { say("called"); }
53
53
  @tick fn main() { used(); }
54
54
  `;
55
55
  const result = (0, index_1.compile)(source, { namespace: 'test' });
56
+ // _unused is removed because it starts with _ (private) and is not called
56
57
  expect(result.ast.declarations.map(fn => fn.name)).toEqual(['used', 'main']);
57
- expect(result.ir.functions.some(fn => fn.name === 'unused')).toBe(false);
58
+ expect(result.ir.functions.some(fn => fn.name === '_unused')).toBe(false);
58
59
  });
59
60
  it('removes unused local variables from the AST body', () => {
60
61
  const source = `
@@ -171,8 +171,8 @@ fn main() {
171
171
  const foreachFn = getSubFunction(files, 'main', 'foreach_0');
172
172
  const thenFiles = files.filter(file => file.path.includes('/main/then_') && file.content.includes('kill @s'));
173
173
  expect(mainFn).toContain('execute as @e run function test:main/foreach_0');
174
- expect(foreachFn).toContain('execute if entity @s[type=player] run function test:main/then_');
175
- expect(foreachFn).toContain('execute if entity @s[type=zombie] run function test:main/then_');
174
+ expect(foreachFn).toContain('execute if entity @s[type=minecraft:player] run function test:main/then_');
175
+ expect(foreachFn).toContain('execute if entity @s[type=minecraft:zombie] run function test:main/then_');
176
176
  expect(thenFiles).toHaveLength(2);
177
177
  });
178
178
  });
@@ -251,7 +251,7 @@ fn scan() {
251
251
  `);
252
252
  const foreachFn = ir.functions.find(fn => fn.name.includes('scan/foreach'));
253
253
  const rawCmds = getRawCommands(foreachFn);
254
- const isCheckCmd = rawCmds.find(cmd => cmd.startsWith('execute if entity @s[type=player] run function test:scan/then_'));
254
+ const isCheckCmd = rawCmds.find(cmd => cmd.startsWith('execute if entity @s[type=minecraft:player] run function test:scan/then_'));
255
255
  expect(isCheckCmd).toBeDefined();
256
256
  const thenFn = ir.functions.find(fn => fn.name.startsWith('scan/then_'));
257
257
  expect(getRawCommands(thenFn)).toContain('kill @s');
@@ -315,8 +315,8 @@ fn test() {
315
315
  const [playerThenFn, zombieThenFn] = thenFns;
316
316
  expect(getRawCommands(mainFn)).toContain('execute as @e run function test:test/foreach_0');
317
317
  expect(thenFns).toHaveLength(2);
318
- expect(rawCmds).toContain(`execute if entity @s[type=player] run function test:${playerThenFn.name}`);
319
- expect(rawCmds).toContain(`execute if entity @s[type=zombie] run function test:${zombieThenFn.name}`);
318
+ expect(rawCmds).toContain(`execute if entity @s[type=minecraft:player] run function test:${playerThenFn.name}`);
319
+ expect(rawCmds).toContain(`execute if entity @s[type=minecraft:zombie] run function test:${zombieThenFn.name}`);
320
320
  expect(getRawCommands(playerThenFn).some(cmd => cmd.includes('give @s diamond 1'))).toBe(true);
321
321
  expect(getRawCommands(zombieThenFn)).toContain('kill @s');
322
322
  });
@@ -694,28 +694,28 @@ describe('MC Integration - New Features', () => {
694
694
  expect(count).toBeGreaterThanOrEqual(3);
695
695
  expect(count).toBeLessThanOrEqual(3);
696
696
  });
697
- test('is-check-test.mcrs: foreach is-narrowing only matches zombie entities', async () => {
697
+ test('is-check-test.mcrs: foreach is-narrowing correctly matches entity types', async () => {
698
698
  if (!serverOnline)
699
699
  return;
700
700
  await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false });
701
- await mc.command('/scoreboard players set #is_check players 0');
702
- await mc.command('/scoreboard players set #is_check zombies 0');
701
+ await mc.command('/forceload add 0 0').catch(() => { }); // Ensure chunk is loaded
702
+ await mc.command('/scoreboard objectives add armor_stands dummy').catch(() => { });
703
+ await mc.command('/scoreboard objectives add items dummy').catch(() => { });
704
+ await mc.command('/scoreboard players set #is_check armor_stands 0');
705
+ await mc.command('/scoreboard players set #is_check items 0');
703
706
  await mc.command('/function is_check_test:__load').catch(() => { });
704
- await mc.command('/summon minecraft:zombie 0 65 0');
705
- await mc.command('/tag @e[type=minecraft:zombie,sort=nearest,limit=1] add is_check_target');
706
- await mc.command('/summon minecraft:armor_stand 2 65 0');
707
- await mc.command('/tag @e[type=minecraft:armor_stand,sort=nearest,limit=1] add is_check_target');
707
+ // Spawn 2 armor_stands and 1 item (all persist without players)
708
+ await mc.command('/summon minecraft:armor_stand 0 65 0 {Tags:["is_check_target"],NoGravity:1b}');
709
+ await mc.command('/summon minecraft:armor_stand 2 65 0 {Tags:["is_check_target"],NoGravity:1b}');
710
+ await mc.command('/summon minecraft:item 4 65 0 {Tags:["is_check_target"],Item:{id:"minecraft:stone",count:1},Age:-32768}');
711
+ await mc.ticks(5);
708
712
  await mc.command('/function is_check_test:check_types');
709
713
  await mc.ticks(5);
710
- const zombies = await mc.scoreboard('#is_check', 'zombies');
711
- const players = await mc.scoreboard('#is_check', 'players');
712
- const zombieEntities = await mc.entities('@e[type=minecraft:zombie,tag=is_check_target]');
713
- const standEntities = await mc.entities('@e[type=minecraft:armor_stand,tag=is_check_target]');
714
- expect(zombies).toBe(1);
715
- expect(players).toBe(0);
716
- expect(zombieEntities).toHaveLength(0);
717
- expect(standEntities).toHaveLength(1);
718
- await mc.command('/kill @e[tag=is_check_target]').catch(() => { });
714
+ const armorStands = await mc.scoreboard('#is_check', 'armor_stands');
715
+ const items = await mc.scoreboard('#is_check', 'items');
716
+ expect(armorStands).toBe(2); // 2 armor_stands matched
717
+ expect(items).toBe(1); // 1 item matched
718
+ await mc.command('/function is_check_test:cleanup').catch(() => { });
719
719
  });
720
720
  test('event-test.mcrs: @on(PlayerDeath) compiles and loads', async () => {
721
721
  if (!serverOnline)
@@ -38,6 +38,7 @@ export declare class Lowering {
38
38
  private currentContext;
39
39
  private blockPosVars;
40
40
  private structDefs;
41
+ private structDecls;
41
42
  private enumDefs;
42
43
  private functionDefaults;
43
44
  private constValues;
@@ -126,20 +126,20 @@ function getSpan(node) {
126
126
  const NAMESPACED_ENTITY_TYPE_RE = /^[a-z0-9_.-]+:[a-z0-9_./-]+$/;
127
127
  const BARE_ENTITY_TYPE_RE = /^[a-z0-9_./-]+$/;
128
128
  const ENTITY_TO_MC_TYPE = {
129
- Player: 'player',
130
- Zombie: 'zombie',
131
- Skeleton: 'skeleton',
132
- Creeper: 'creeper',
133
- Spider: 'spider',
134
- Enderman: 'enderman',
135
- Pig: 'pig',
136
- Cow: 'cow',
137
- Sheep: 'sheep',
138
- Chicken: 'chicken',
139
- Villager: 'villager',
140
- ArmorStand: 'armor_stand',
141
- Item: 'item',
142
- Arrow: 'arrow',
129
+ Player: 'minecraft:player',
130
+ Zombie: 'minecraft:zombie',
131
+ Skeleton: 'minecraft:skeleton',
132
+ Creeper: 'minecraft:creeper',
133
+ Spider: 'minecraft:spider',
134
+ Enderman: 'minecraft:enderman',
135
+ Pig: 'minecraft:pig',
136
+ Cow: 'minecraft:cow',
137
+ Sheep: 'minecraft:sheep',
138
+ Chicken: 'minecraft:chicken',
139
+ Villager: 'minecraft:villager',
140
+ ArmorStand: 'minecraft:armor_stand',
141
+ Item: 'minecraft:item',
142
+ Arrow: 'minecraft:arrow',
143
143
  };
144
144
  function normalizeSelector(selector, warnings) {
145
145
  return selector.replace(/type=([^,\]]+)/g, (match, entityType) => {
@@ -199,6 +199,8 @@ class Lowering {
199
199
  this.blockPosVars = new Map();
200
200
  // Struct definitions: name → { fieldName: TypeNode }
201
201
  this.structDefs = new Map();
202
+ // Full struct declarations for field iteration
203
+ this.structDecls = new Map();
202
204
  this.enumDefs = new Map();
203
205
  this.functionDefaults = new Map();
204
206
  this.constValues = new Map();
@@ -224,6 +226,7 @@ class Lowering {
224
226
  fields.set(field.name, field.type);
225
227
  }
226
228
  this.structDefs.set(struct.name, fields);
229
+ this.structDecls.set(struct.name, struct);
227
230
  }
228
231
  for (const enumDecl of program.enums ?? []) {
229
232
  const variants = new Map();
@@ -551,6 +554,22 @@ class Lowering {
551
554
  }
552
555
  return;
553
556
  }
557
+ // Handle struct initialization from function call (copy from __ret_struct)
558
+ if ((stmt.init.kind === 'call' || stmt.init.kind === 'static_call') && stmt.type?.kind === 'struct') {
559
+ // First, execute the function call
560
+ this.lowerExpr(stmt.init);
561
+ // Then copy all fields from __ret_struct to the variable's storage
562
+ const structDecl = this.structDecls.get(stmt.type.name);
563
+ if (structDecl) {
564
+ const structName = stmt.type.name.toLowerCase();
565
+ for (const field of structDecl.fields) {
566
+ const srcPath = `rs:heap __ret_struct.${field.name}`;
567
+ const dstPath = `rs:heap ${structName}_${stmt.name}.${field.name}`;
568
+ this.builder.emitRaw(`data modify storage ${dstPath} set from storage ${srcPath}`);
569
+ }
570
+ }
571
+ return;
572
+ }
554
573
  // Handle array literal initialization
555
574
  if (stmt.init.kind === 'array_lit') {
556
575
  // Initialize empty NBT array
@@ -600,6 +619,21 @@ class Lowering {
600
619
  }
601
620
  lowerReturnStmt(stmt) {
602
621
  if (stmt.value) {
622
+ // Handle struct literal return: store fields to __ret_struct storage
623
+ if (stmt.value.kind === 'struct_lit') {
624
+ for (const field of stmt.value.fields) {
625
+ const path = `rs:heap __ret_struct.${field.name}`;
626
+ const fieldValue = this.lowerExpr(field.value);
627
+ if (fieldValue.kind === 'const') {
628
+ this.builder.emitRaw(`data modify storage ${path} set value ${fieldValue.value}`);
629
+ }
630
+ else if (fieldValue.kind === 'var') {
631
+ this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${fieldValue.name} rs`);
632
+ }
633
+ }
634
+ this.builder.emitReturn({ kind: 'const', value: 0 });
635
+ return;
636
+ }
603
637
  const value = this.lowerExpr(stmt.value);
604
638
  this.builder.emitReturn(value);
605
639
  }
@@ -1506,6 +1540,22 @@ class Lowering {
1506
1540
  }
1507
1541
  const implMethod = this.resolveInstanceMethod(expr);
1508
1542
  if (implMethod) {
1543
+ // Copy struct fields from instance to 'self' storage before calling
1544
+ const receiver = expr.args[0];
1545
+ if (receiver?.kind === 'ident') {
1546
+ const receiverType = this.inferExprType(receiver);
1547
+ if (receiverType?.kind === 'struct') {
1548
+ const structDecl = this.structDecls.get(receiverType.name);
1549
+ const structName = receiverType.name.toLowerCase();
1550
+ if (structDecl) {
1551
+ for (const field of structDecl.fields) {
1552
+ const srcPath = `rs:heap ${structName}_${receiver.name}.${field.name}`;
1553
+ const dstPath = `rs:heap ${structName}_self.${field.name}`;
1554
+ this.builder.emitRaw(`data modify storage ${dstPath} set from storage ${srcPath}`);
1555
+ }
1556
+ }
1557
+ }
1558
+ }
1509
1559
  return this.emitMethodCall(implMethod.loweredName, implMethod.fn, expr.args);
1510
1560
  }
1511
1561
  // Regular function call
@@ -115,9 +115,12 @@ class DeadCodeEliminator {
115
115
  findEntryPoints(program) {
116
116
  const entries = new Set();
117
117
  for (const fn of program.declarations) {
118
- if (fn.name === 'main') {
118
+ // All top-level functions are entry points (callable via /function)
119
+ // Exception: functions starting with _ are considered private/internal
120
+ if (!fn.name.startsWith('_')) {
119
121
  entries.add(fn.name);
120
122
  }
123
+ // Decorated functions are always entry points (even if prefixed with _)
121
124
  if (fn.decorators.some(decorator => [
122
125
  'tick',
123
126
  'load',
@@ -128,7 +131,7 @@ class DeadCodeEliminator {
128
131
  'on_death',
129
132
  'on_login',
130
133
  'on_join_team',
131
- 'keep', // Prevent DCE from removing this function
134
+ 'keep',
132
135
  ].includes(decorator.name))) {
133
136
  entries.add(fn.name);
134
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "1.2.11",
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
  })
@@ -1,6 +1,6 @@
1
1
  // Array operations test
2
2
 
3
- @keep fn test_array() {
3
+ @tick fn test_array() {
4
4
  // Array initialization
5
5
  let nums: int[] = [10, 20, 30, 40, 50];
6
6
 
@@ -1,6 +1,6 @@
1
1
  // Break and continue statements test
2
2
 
3
- @keep fn test_break_continue() {
3
+ fn test_break_continue() {
4
4
  // Test break - should stop at i=5
5
5
  let break_at: int = -1;
6
6
  for (let i: int = 0; i < 10; i = i + 1) {
@@ -14,7 +14,7 @@ enum Rank {
14
14
  Diamond = 10
15
15
  }
16
16
 
17
- @keep fn test_enum() {
17
+ fn test_enum() {
18
18
  // Basic enum value (use . not ::)
19
19
  let phase: int = GamePhase.Playing;
20
20
  scoreboard_set("#enum_phase", #rs, phase);
@@ -6,7 +6,7 @@
6
6
  scoreboard_set("#foreach_at_count", #rs, 0);
7
7
  }
8
8
 
9
- @keep fn test_foreach_at() {
9
+ fn test_foreach_at() {
10
10
  // Spawn test entities
11
11
  raw("summon minecraft:armor_stand ~ ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
12
12
  raw("summon minecraft:armor_stand ~2 ~ ~ {Tags:[\"test_foreach\"],NoGravity:1b}");
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  // Match with range patterns test
2
2
 
3
- @keep fn test_match_range() {
3
+ fn test_match_range() {
4
4
  // Test score grading
5
5
  let score: int = 85;
6
6
  let grade: int = 0;
@@ -11,7 +11,7 @@ struct Player {
11
11
  alive: bool
12
12
  }
13
13
 
14
- @keep fn test_struct() {
14
+ fn test_struct() {
15
15
  // Create struct instance
16
16
  let p: Point = { x: 10, y: 64, z: -5 };
17
17
 
@@ -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
  })
@@ -753,32 +753,33 @@ describe('MC Integration - New Features', () => {
753
753
  expect(count).toBeLessThanOrEqual(3)
754
754
  })
755
755
 
756
- test('is-check-test.mcrs: foreach is-narrowing only matches zombie entities', async () => {
756
+ test('is-check-test.mcrs: foreach is-narrowing correctly matches entity types', async () => {
757
757
  if (!serverOnline) return
758
758
 
759
759
  await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false })
760
- await mc.command('/scoreboard players set #is_check players 0')
761
- await mc.command('/scoreboard players set #is_check zombies 0')
760
+ await mc.command('/forceload add 0 0').catch(() => {}) // Ensure chunk is loaded
761
+ await mc.command('/scoreboard objectives add armor_stands dummy').catch(() => {})
762
+ await mc.command('/scoreboard objectives add items dummy').catch(() => {})
763
+ await mc.command('/scoreboard players set #is_check armor_stands 0')
764
+ await mc.command('/scoreboard players set #is_check items 0')
762
765
  await mc.command('/function is_check_test:__load').catch(() => {})
763
- await mc.command('/summon minecraft:zombie 0 65 0')
764
- await mc.command('/tag @e[type=minecraft:zombie,sort=nearest,limit=1] add is_check_target')
765
- await mc.command('/summon minecraft:armor_stand 2 65 0')
766
- await mc.command('/tag @e[type=minecraft:armor_stand,sort=nearest,limit=1] add is_check_target')
766
+
767
+ // Spawn 2 armor_stands and 1 item (all persist without players)
768
+ await mc.command('/summon minecraft:armor_stand 0 65 0 {Tags:["is_check_target"],NoGravity:1b}')
769
+ await mc.command('/summon minecraft:armor_stand 2 65 0 {Tags:["is_check_target"],NoGravity:1b}')
770
+ await mc.command('/summon minecraft:item 4 65 0 {Tags:["is_check_target"],Item:{id:"minecraft:stone",count:1},Age:-32768}')
771
+ await mc.ticks(5)
767
772
 
768
773
  await mc.command('/function is_check_test:check_types')
769
774
  await mc.ticks(5)
770
775
 
771
- const zombies = await mc.scoreboard('#is_check', 'zombies')
772
- const players = await mc.scoreboard('#is_check', 'players')
773
- const zombieEntities = await mc.entities('@e[type=minecraft:zombie,tag=is_check_target]')
774
- const standEntities = await mc.entities('@e[type=minecraft:armor_stand,tag=is_check_target]')
776
+ const armorStands = await mc.scoreboard('#is_check', 'armor_stands')
777
+ const items = await mc.scoreboard('#is_check', 'items')
775
778
 
776
- expect(zombies).toBe(1)
777
- expect(players).toBe(0)
778
- expect(zombieEntities).toHaveLength(0)
779
- expect(standEntities).toHaveLength(1)
779
+ expect(armorStands).toBe(2) // 2 armor_stands matched
780
+ expect(items).toBe(1) // 1 item matched
780
781
 
781
- await mc.command('/kill @e[tag=is_check_target]').catch(() => {})
782
+ await mc.command('/function is_check_test:cleanup').catch(() => {})
782
783
  })
783
784
 
784
785
  test('event-test.mcrs: @on(PlayerDeath) compiles and loads', async () => {
@@ -118,20 +118,20 @@ const NAMESPACED_ENTITY_TYPE_RE = /^[a-z0-9_.-]+:[a-z0-9_./-]+$/
118
118
  const BARE_ENTITY_TYPE_RE = /^[a-z0-9_./-]+$/
119
119
 
120
120
  const ENTITY_TO_MC_TYPE: Partial<Record<EntityTypeName, string>> = {
121
- Player: 'player',
122
- Zombie: 'zombie',
123
- Skeleton: 'skeleton',
124
- Creeper: 'creeper',
125
- Spider: 'spider',
126
- Enderman: 'enderman',
127
- Pig: 'pig',
128
- Cow: 'cow',
129
- Sheep: 'sheep',
130
- Chicken: 'chicken',
131
- Villager: 'villager',
132
- ArmorStand: 'armor_stand',
133
- Item: 'item',
134
- Arrow: 'arrow',
121
+ Player: 'minecraft:player',
122
+ Zombie: 'minecraft:zombie',
123
+ Skeleton: 'minecraft:skeleton',
124
+ Creeper: 'minecraft:creeper',
125
+ Spider: 'minecraft:spider',
126
+ Enderman: 'minecraft:enderman',
127
+ Pig: 'minecraft:pig',
128
+ Cow: 'minecraft:cow',
129
+ Sheep: 'minecraft:sheep',
130
+ Chicken: 'minecraft:chicken',
131
+ Villager: 'minecraft:villager',
132
+ ArmorStand: 'minecraft:armor_stand',
133
+ Item: 'minecraft:item',
134
+ Arrow: 'minecraft:arrow',
135
135
  }
136
136
 
137
137
  function normalizeSelector(selector: string, warnings: Warning[]): string {
@@ -213,6 +213,8 @@ export class Lowering {
213
213
 
214
214
  // Struct definitions: name → { fieldName: TypeNode }
215
215
  private structDefs: Map<string, Map<string, TypeNode>> = new Map()
216
+ // Full struct declarations for field iteration
217
+ private structDecls: Map<string, StructDecl> = new Map()
216
218
  private enumDefs: Map<string, Map<string, number>> = new Map()
217
219
  private functionDefaults: Map<string, Array<Expr | undefined>> = new Map()
218
220
  private constValues: Map<string, ConstDecl['value']> = new Map()
@@ -243,6 +245,7 @@ export class Lowering {
243
245
  fields.set(field.name, field.type)
244
246
  }
245
247
  this.structDefs.set(struct.name, fields)
248
+ this.structDecls.set(struct.name, struct)
246
249
  }
247
250
 
248
251
  for (const enumDecl of program.enums ?? []) {
@@ -630,6 +633,23 @@ export class Lowering {
630
633
  return
631
634
  }
632
635
 
636
+ // Handle struct initialization from function call (copy from __ret_struct)
637
+ if ((stmt.init.kind === 'call' || stmt.init.kind === 'static_call') && stmt.type?.kind === 'struct') {
638
+ // First, execute the function call
639
+ this.lowerExpr(stmt.init)
640
+ // Then copy all fields from __ret_struct to the variable's storage
641
+ const structDecl = this.structDecls.get(stmt.type.name)
642
+ if (structDecl) {
643
+ const structName = stmt.type.name.toLowerCase()
644
+ for (const field of structDecl.fields) {
645
+ const srcPath = `rs:heap __ret_struct.${field.name}`
646
+ const dstPath = `rs:heap ${structName}_${stmt.name}.${field.name}`
647
+ this.builder.emitRaw(`data modify storage ${dstPath} set from storage ${srcPath}`)
648
+ }
649
+ }
650
+ return
651
+ }
652
+
633
653
  // Handle array literal initialization
634
654
  if (stmt.init.kind === 'array_lit') {
635
655
  // Initialize empty NBT array
@@ -684,6 +704,20 @@ export class Lowering {
684
704
 
685
705
  private lowerReturnStmt(stmt: Extract<Stmt, { kind: 'return' }>): void {
686
706
  if (stmt.value) {
707
+ // Handle struct literal return: store fields to __ret_struct storage
708
+ if (stmt.value.kind === 'struct_lit') {
709
+ for (const field of stmt.value.fields) {
710
+ const path = `rs:heap __ret_struct.${field.name}`
711
+ const fieldValue = this.lowerExpr(field.value)
712
+ if (fieldValue.kind === 'const') {
713
+ this.builder.emitRaw(`data modify storage ${path} set value ${fieldValue.value}`)
714
+ } else if (fieldValue.kind === 'var') {
715
+ this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${fieldValue.name} rs`)
716
+ }
717
+ }
718
+ this.builder.emitReturn({ kind: 'const', value: 0 })
719
+ return
720
+ }
687
721
  const value = this.lowerExpr(stmt.value)
688
722
  this.builder.emitReturn(value)
689
723
  } else {
@@ -1761,6 +1795,22 @@ export class Lowering {
1761
1795
 
1762
1796
  const implMethod = this.resolveInstanceMethod(expr)
1763
1797
  if (implMethod) {
1798
+ // Copy struct fields from instance to 'self' storage before calling
1799
+ const receiver = expr.args[0]
1800
+ if (receiver?.kind === 'ident') {
1801
+ const receiverType = this.inferExprType(receiver)
1802
+ if (receiverType?.kind === 'struct') {
1803
+ const structDecl = this.structDecls.get(receiverType.name)
1804
+ const structName = receiverType.name.toLowerCase()
1805
+ if (structDecl) {
1806
+ for (const field of structDecl.fields) {
1807
+ const srcPath = `rs:heap ${structName}_${receiver.name}.${field.name}`
1808
+ const dstPath = `rs:heap ${structName}_self.${field.name}`
1809
+ this.builder.emitRaw(`data modify storage ${dstPath} set from storage ${srcPath}`)
1810
+ }
1811
+ }
1812
+ }
1813
+ }
1764
1814
  return this.emitMethodCall(implMethod.loweredName, implMethod.fn, expr.args)
1765
1815
  }
1766
1816
 
@@ -133,10 +133,13 @@ export class DeadCodeEliminator {
133
133
  const entries = new Set<string>()
134
134
 
135
135
  for (const fn of program.declarations) {
136
- if (fn.name === 'main') {
136
+ // All top-level functions are entry points (callable via /function)
137
+ // Exception: functions starting with _ are considered private/internal
138
+ if (!fn.name.startsWith('_')) {
137
139
  entries.add(fn.name)
138
140
  }
139
141
 
142
+ // Decorated functions are always entry points (even if prefixed with _)
140
143
  if (fn.decorators.some(decorator => [
141
144
  'tick',
142
145
  'load',
@@ -147,7 +150,7 @@ export class DeadCodeEliminator {
147
150
  'on_death',
148
151
  'on_login',
149
152
  'on_join_team',
150
- 'keep', // Prevent DCE from removing this function
153
+ 'keep',
151
154
  ].includes(decorator.name))) {
152
155
  entries.add(fn.name)
153
156
  }