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.
@@ -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)
@@ -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");
@@ -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
  }
@@ -1006,18 +1040,52 @@ class Lowering {
1006
1040
  const parts = ['execute'];
1007
1041
  for (const sub of stmt.subcommands) {
1008
1042
  switch (sub.kind) {
1043
+ // Context modifiers
1009
1044
  case 'as':
1010
1045
  parts.push(`as ${this.selectorToString(sub.selector)}`);
1011
1046
  break;
1012
1047
  case 'at':
1013
1048
  parts.push(`at ${this.selectorToString(sub.selector)}`);
1014
1049
  break;
1050
+ case 'positioned':
1051
+ parts.push(`positioned ${sub.x} ${sub.y} ${sub.z}`);
1052
+ break;
1053
+ case 'positioned_as':
1054
+ parts.push(`positioned as ${this.selectorToString(sub.selector)}`);
1055
+ break;
1056
+ case 'rotated':
1057
+ parts.push(`rotated ${sub.yaw} ${sub.pitch}`);
1058
+ break;
1059
+ case 'rotated_as':
1060
+ parts.push(`rotated as ${this.selectorToString(sub.selector)}`);
1061
+ break;
1062
+ case 'facing':
1063
+ parts.push(`facing ${sub.x} ${sub.y} ${sub.z}`);
1064
+ break;
1065
+ case 'facing_entity':
1066
+ parts.push(`facing entity ${this.selectorToString(sub.selector)} ${sub.anchor}`);
1067
+ break;
1068
+ case 'anchored':
1069
+ parts.push(`anchored ${sub.anchor}`);
1070
+ break;
1071
+ case 'align':
1072
+ parts.push(`align ${sub.axes}`);
1073
+ break;
1074
+ case 'in':
1075
+ parts.push(`in ${sub.dimension}`);
1076
+ break;
1077
+ case 'on':
1078
+ parts.push(`on ${sub.relation}`);
1079
+ break;
1080
+ case 'summon':
1081
+ parts.push(`summon ${sub.entity}`);
1082
+ break;
1083
+ // Conditions
1015
1084
  case 'if_entity':
1016
1085
  if (sub.selector) {
1017
1086
  parts.push(`if entity ${this.selectorToString(sub.selector)}`);
1018
1087
  }
1019
1088
  else if (sub.varName) {
1020
- // Variable with filters - substitute with @s and apply filters
1021
1089
  const sel = { kind: '@s', filters: sub.filters };
1022
1090
  parts.push(`if entity ${this.selectorToString(sel)}`);
1023
1091
  }
@@ -1027,13 +1095,34 @@ class Lowering {
1027
1095
  parts.push(`unless entity ${this.selectorToString(sub.selector)}`);
1028
1096
  }
1029
1097
  else if (sub.varName) {
1030
- // Variable with filters - substitute with @s and apply filters
1031
1098
  const sel = { kind: '@s', filters: sub.filters };
1032
1099
  parts.push(`unless entity ${this.selectorToString(sel)}`);
1033
1100
  }
1034
1101
  break;
1035
- case 'in':
1036
- parts.push(`in ${sub.dimension}`);
1102
+ case 'if_block':
1103
+ parts.push(`if block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`);
1104
+ break;
1105
+ case 'unless_block':
1106
+ parts.push(`unless block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`);
1107
+ break;
1108
+ case 'if_score':
1109
+ parts.push(`if score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`);
1110
+ break;
1111
+ case 'unless_score':
1112
+ parts.push(`unless score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`);
1113
+ break;
1114
+ case 'if_score_range':
1115
+ parts.push(`if score ${sub.target} ${sub.targetObj} matches ${sub.range}`);
1116
+ break;
1117
+ case 'unless_score_range':
1118
+ parts.push(`unless score ${sub.target} ${sub.targetObj} matches ${sub.range}`);
1119
+ break;
1120
+ // Store
1121
+ case 'store_result':
1122
+ parts.push(`store result score ${sub.target} ${sub.targetObj}`);
1123
+ break;
1124
+ case 'store_success':
1125
+ parts.push(`store success score ${sub.target} ${sub.targetObj}`);
1037
1126
  break;
1038
1127
  }
1039
1128
  }
@@ -1451,6 +1540,22 @@ class Lowering {
1451
1540
  }
1452
1541
  const implMethod = this.resolveInstanceMethod(expr);
1453
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
+ }
1454
1559
  return this.emitMethodCall(implMethod.loweredName, implMethod.fn, expr.args);
1455
1560
  }
1456
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
  }
@@ -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;