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.
- package/dist/__tests__/dce.test.js +4 -3
- package/dist/__tests__/e2e.test.js +2 -2
- package/dist/__tests__/lowering.test.js +3 -3
- package/dist/__tests__/mc-integration.test.js +16 -16
- package/dist/lowering/index.d.ts +1 -0
- package/dist/lowering/index.js +64 -14
- package/dist/optimizer/dce.js +5 -2
- package/package.json +1 -1
- package/src/__tests__/dce.test.ts +4 -3
- package/src/__tests__/e2e.test.ts +2 -2
- package/src/__tests__/fixtures/array-test.mcrs +1 -1
- package/src/__tests__/fixtures/break-continue-test.mcrs +1 -1
- package/src/__tests__/fixtures/enum-test.mcrs +1 -1
- package/src/__tests__/fixtures/foreach-at-test.mcrs +1 -1
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -13
- package/src/__tests__/fixtures/match-range-test.mcrs +1 -1
- package/src/__tests__/fixtures/struct-test.mcrs +1 -1
- package/src/__tests__/lowering.test.ts +3 -3
- package/src/__tests__/mc-integration.test.ts +17 -16
- package/src/lowering/index.ts +64 -14
- package/src/optimizer/dce.ts +5 -2
|
@@ -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
|
|
49
|
+
it('removes private unused functions (prefixed with _)', () => {
|
|
50
50
|
const source = `
|
|
51
|
-
fn
|
|
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 === '
|
|
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
|
|
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('/
|
|
702
|
-
await mc.command('/scoreboard
|
|
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
|
-
|
|
705
|
-
await mc.command('/
|
|
706
|
-
await mc.command('/summon minecraft:armor_stand 2 65 0');
|
|
707
|
-
await mc.command('/
|
|
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
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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)
|
package/dist/lowering/index.d.ts
CHANGED
package/dist/lowering/index.js
CHANGED
|
@@ -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
|
package/dist/optimizer/dce.js
CHANGED
|
@@ -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
|
-
|
|
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',
|
|
134
|
+
'keep',
|
|
132
135
|
].includes(decorator.name))) {
|
|
133
136
|
entries.add(fn.name);
|
|
134
137
|
}
|
package/package.json
CHANGED
|
@@ -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
|
|
17
|
+
it('removes private unused functions (prefixed with _)', () => {
|
|
18
18
|
const source = `
|
|
19
|
-
fn
|
|
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 === '
|
|
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
|
})
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
scoreboard_set("#foreach_at_count", #rs, 0);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
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", #
|
|
3
|
-
scoreboard_set("#is_check", #
|
|
8
|
+
scoreboard_set("#is_check", #armor_stands, 0);
|
|
9
|
+
scoreboard_set("#is_check", #items, 0);
|
|
4
10
|
|
|
5
|
-
foreach
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
12
|
-
let
|
|
13
|
-
scoreboard_set("#is_check", #
|
|
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
|
+
}
|
|
@@ -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
|
|
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('/
|
|
761
|
-
await mc.command('/scoreboard
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
await mc.command('/summon minecraft:armor_stand
|
|
766
|
-
await mc.command('/
|
|
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
|
|
772
|
-
const
|
|
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(
|
|
777
|
-
expect(
|
|
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('/
|
|
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 () => {
|
package/src/lowering/index.ts
CHANGED
|
@@ -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
|
|
package/src/optimizer/dce.ts
CHANGED
|
@@ -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
|
-
|
|
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',
|
|
153
|
+
'keep',
|
|
151
154
|
].includes(decorator.name))) {
|
|
152
155
|
entries.add(fn.name)
|
|
153
156
|
}
|