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.
- 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 +101 -16
- package/dist/ast/types.d.ts +78 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lowering/index.d.ts +1 -0
- package/dist/lowering/index.js +123 -18
- package/dist/optimizer/dce.js +5 -2
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +159 -18
- 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 +30 -0
- package/src/__tests__/fixtures/break-continue-test.mcrs +46 -0
- package/src/__tests__/fixtures/enum-test.mcrs +37 -0
- package/src/__tests__/fixtures/foreach-at-test.mcrs +33 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -13
- package/src/__tests__/fixtures/match-range-test.mcrs +45 -0
- package/src/__tests__/fixtures/struct-test.mcrs +34 -0
- package/src/__tests__/lowering.test.ts +3 -3
- package/src/__tests__/mc-integration.test.ts +114 -16
- package/src/ast/types.ts +22 -1
- package/src/index.ts +1 -1
- package/src/lowering/index.ts +123 -18
- package/src/optimizer/dce.ts +5 -2
- package/src/parser/index.ts +145 -18
|
@@ -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 () => {
|
|
@@ -797,3 +798,100 @@ describe('MC Integration - New Features', () => {
|
|
|
797
798
|
expect(tickResult.ok).toBe(true)
|
|
798
799
|
})
|
|
799
800
|
})
|
|
801
|
+
|
|
802
|
+
describe('MC Integration - Extended Coverage', () => {
|
|
803
|
+
test('struct-test.mcrs: struct instantiation and field access', async () => {
|
|
804
|
+
if (!serverOnline) return
|
|
805
|
+
|
|
806
|
+
writeFixtureFile('struct-test.mcrs', 'struct_test')
|
|
807
|
+
await mc.reload()
|
|
808
|
+
await mc.command('/function struct_test:__load').catch(() => {})
|
|
809
|
+
await mc.command('/function struct_test:test_struct')
|
|
810
|
+
await mc.ticks(5)
|
|
811
|
+
|
|
812
|
+
expect(await mc.scoreboard('#struct_x', 'rs')).toBe(10)
|
|
813
|
+
expect(await mc.scoreboard('#struct_y', 'rs')).toBe(64)
|
|
814
|
+
expect(await mc.scoreboard('#struct_z', 'rs')).toBe(-5)
|
|
815
|
+
expect(await mc.scoreboard('#struct_x2', 'rs')).toBe(15) // 10+5
|
|
816
|
+
expect(await mc.scoreboard('#struct_z2', 'rs')).toBe(-10) // -5*2
|
|
817
|
+
expect(await mc.scoreboard('#struct_alive', 'rs')).toBe(1)
|
|
818
|
+
expect(await mc.scoreboard('#struct_score', 'rs')).toBe(100)
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
test('enum-test.mcrs: enum values and match', async () => {
|
|
822
|
+
if (!serverOnline) return
|
|
823
|
+
|
|
824
|
+
writeFixtureFile('enum-test.mcrs', 'enum_test')
|
|
825
|
+
await mc.reload()
|
|
826
|
+
await mc.command('/function enum_test:__load').catch(() => {})
|
|
827
|
+
await mc.command('/function enum_test:test_enum')
|
|
828
|
+
await mc.ticks(5)
|
|
829
|
+
|
|
830
|
+
expect(await mc.scoreboard('#enum_phase', 'rs')).toBe(2) // Playing=2
|
|
831
|
+
expect(await mc.scoreboard('#enum_match', 'rs')).toBe(2) // matched Playing
|
|
832
|
+
expect(await mc.scoreboard('#enum_rank', 'rs')).toBe(10) // Diamond=10
|
|
833
|
+
expect(await mc.scoreboard('#enum_high', 'rs')).toBe(1) // Diamond > Gold
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
test('array-test.mcrs: array operations', async () => {
|
|
837
|
+
if (!serverOnline) return
|
|
838
|
+
|
|
839
|
+
writeFixtureFile('array-test.mcrs', 'array_test')
|
|
840
|
+
await mc.reload()
|
|
841
|
+
await mc.command('/function array_test:__load').catch(() => {})
|
|
842
|
+
await mc.command('/function array_test:test_array')
|
|
843
|
+
await mc.ticks(5)
|
|
844
|
+
|
|
845
|
+
expect(await mc.scoreboard('#arr_0', 'rs')).toBe(10)
|
|
846
|
+
expect(await mc.scoreboard('#arr_2', 'rs')).toBe(30)
|
|
847
|
+
expect(await mc.scoreboard('#arr_4', 'rs')).toBe(50)
|
|
848
|
+
expect(await mc.scoreboard('#arr_len', 'rs')).toBe(5)
|
|
849
|
+
expect(await mc.scoreboard('#arr_sum', 'rs')).toBe(150) // 10+20+30+40+50
|
|
850
|
+
expect(await mc.scoreboard('#arr_push', 'rs')).toBe(4) // [1,2,3,4].len
|
|
851
|
+
expect(await mc.scoreboard('#arr_pop', 'rs')).toBe(4) // popped value
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
test('break-continue-test.mcrs: break and continue statements', async () => {
|
|
855
|
+
if (!serverOnline) return
|
|
856
|
+
|
|
857
|
+
writeFixtureFile('break-continue-test.mcrs', 'break_continue_test')
|
|
858
|
+
await mc.reload()
|
|
859
|
+
await mc.command('/function break_continue_test:__load').catch(() => {})
|
|
860
|
+
await mc.command('/function break_continue_test:test_break_continue')
|
|
861
|
+
await mc.ticks(10)
|
|
862
|
+
|
|
863
|
+
expect(await mc.scoreboard('#break_at', 'rs')).toBe(5)
|
|
864
|
+
expect(await mc.scoreboard('#sum_evens', 'rs')).toBe(20) // 0+2+4+6+8
|
|
865
|
+
expect(await mc.scoreboard('#while_break', 'rs')).toBe(7)
|
|
866
|
+
expect(await mc.scoreboard('#nested_break', 'rs')).toBe(3) // outer completes 3 times
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
test('match-range-test.mcrs: match with range patterns', async () => {
|
|
870
|
+
if (!serverOnline) return
|
|
871
|
+
|
|
872
|
+
writeFixtureFile('match-range-test.mcrs', 'match_range_test')
|
|
873
|
+
await mc.reload()
|
|
874
|
+
await mc.command('/function match_range_test:__load').catch(() => {})
|
|
875
|
+
await mc.command('/function match_range_test:test_match_range')
|
|
876
|
+
await mc.ticks(5)
|
|
877
|
+
|
|
878
|
+
expect(await mc.scoreboard('#grade', 'rs')).toBe(4) // score=85 → B
|
|
879
|
+
expect(await mc.scoreboard('#boundary_59', 'rs')).toBe(1) // 59 matches 0..59
|
|
880
|
+
expect(await mc.scoreboard('#boundary_60', 'rs')).toBe(2) // 60 matches 60..100
|
|
881
|
+
expect(await mc.scoreboard('#neg_range', 'rs')).toBe(1) // -5 matches ..0
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
test('foreach-at-test.mcrs: foreach with at @s context', async () => {
|
|
885
|
+
if (!serverOnline) return
|
|
886
|
+
|
|
887
|
+
writeFixtureFile('foreach-at-test.mcrs', 'foreach_at_test')
|
|
888
|
+
await mc.reload()
|
|
889
|
+
await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false })
|
|
890
|
+
await mc.command('/function foreach_at_test:setup').catch(() => {})
|
|
891
|
+
await mc.command('/function foreach_at_test:test_foreach_at')
|
|
892
|
+
await mc.ticks(10)
|
|
893
|
+
|
|
894
|
+
expect(await mc.scoreboard('#foreach_count', 'rs')).toBe(3)
|
|
895
|
+
expect(await mc.scoreboard('#foreach_at_count', 'rs')).toBe(3)
|
|
896
|
+
})
|
|
897
|
+
})
|
package/src/ast/types.ts
CHANGED
|
@@ -179,11 +179,32 @@ export type LiteralExpr =
|
|
|
179
179
|
// ---------------------------------------------------------------------------
|
|
180
180
|
|
|
181
181
|
export type ExecuteSubcommand =
|
|
182
|
+
// Context modifiers
|
|
182
183
|
| { kind: 'as'; selector: EntitySelector }
|
|
183
184
|
| { kind: 'at'; selector: EntitySelector }
|
|
185
|
+
| { kind: 'positioned'; x: string; y: string; z: string }
|
|
186
|
+
| { kind: 'positioned_as'; selector: EntitySelector }
|
|
187
|
+
| { kind: 'rotated'; yaw: string; pitch: string }
|
|
188
|
+
| { kind: 'rotated_as'; selector: EntitySelector }
|
|
189
|
+
| { kind: 'facing'; x: string; y: string; z: string }
|
|
190
|
+
| { kind: 'facing_entity'; selector: EntitySelector; anchor: 'eyes' | 'feet' }
|
|
191
|
+
| { kind: 'anchored'; anchor: 'eyes' | 'feet' }
|
|
192
|
+
| { kind: 'align'; axes: string }
|
|
193
|
+
| { kind: 'in'; dimension: string }
|
|
194
|
+
| { kind: 'on'; relation: string }
|
|
195
|
+
| { kind: 'summon'; entity: string }
|
|
196
|
+
// Conditions
|
|
184
197
|
| { kind: 'if_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
|
|
185
198
|
| { kind: 'unless_entity'; selector?: EntitySelector; varName?: string; filters?: SelectorFilter }
|
|
186
|
-
| { kind: '
|
|
199
|
+
| { kind: 'if_block'; pos: [string, string, string]; block: string }
|
|
200
|
+
| { kind: 'unless_block'; pos: [string, string, string]; block: string }
|
|
201
|
+
| { kind: 'if_score'; target: string; targetObj: string; op: string; source: string; sourceObj: string }
|
|
202
|
+
| { kind: 'unless_score'; target: string; targetObj: string; op: string; source: string; sourceObj: string }
|
|
203
|
+
| { kind: 'if_score_range'; target: string; targetObj: string; range: string }
|
|
204
|
+
| { kind: 'unless_score_range'; target: string; targetObj: string; range: string }
|
|
205
|
+
// Store
|
|
206
|
+
| { kind: 'store_result'; target: string; targetObj: string }
|
|
207
|
+
| { kind: 'store_success'; target: string; targetObj: string }
|
|
187
208
|
|
|
188
209
|
export type Stmt =
|
|
189
210
|
| { kind: 'let'; name: string; type?: TypeNode; init: Expr; span?: Span }
|
package/src/index.ts
CHANGED
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 {
|
|
@@ -1200,17 +1234,51 @@ export class Lowering {
|
|
|
1200
1234
|
const parts: string[] = ['execute']
|
|
1201
1235
|
for (const sub of stmt.subcommands) {
|
|
1202
1236
|
switch (sub.kind) {
|
|
1237
|
+
// Context modifiers
|
|
1203
1238
|
case 'as':
|
|
1204
1239
|
parts.push(`as ${this.selectorToString(sub.selector)}`)
|
|
1205
1240
|
break
|
|
1206
1241
|
case 'at':
|
|
1207
1242
|
parts.push(`at ${this.selectorToString(sub.selector)}`)
|
|
1208
1243
|
break
|
|
1244
|
+
case 'positioned':
|
|
1245
|
+
parts.push(`positioned ${sub.x} ${sub.y} ${sub.z}`)
|
|
1246
|
+
break
|
|
1247
|
+
case 'positioned_as':
|
|
1248
|
+
parts.push(`positioned as ${this.selectorToString(sub.selector)}`)
|
|
1249
|
+
break
|
|
1250
|
+
case 'rotated':
|
|
1251
|
+
parts.push(`rotated ${sub.yaw} ${sub.pitch}`)
|
|
1252
|
+
break
|
|
1253
|
+
case 'rotated_as':
|
|
1254
|
+
parts.push(`rotated as ${this.selectorToString(sub.selector)}`)
|
|
1255
|
+
break
|
|
1256
|
+
case 'facing':
|
|
1257
|
+
parts.push(`facing ${sub.x} ${sub.y} ${sub.z}`)
|
|
1258
|
+
break
|
|
1259
|
+
case 'facing_entity':
|
|
1260
|
+
parts.push(`facing entity ${this.selectorToString(sub.selector)} ${sub.anchor}`)
|
|
1261
|
+
break
|
|
1262
|
+
case 'anchored':
|
|
1263
|
+
parts.push(`anchored ${sub.anchor}`)
|
|
1264
|
+
break
|
|
1265
|
+
case 'align':
|
|
1266
|
+
parts.push(`align ${sub.axes}`)
|
|
1267
|
+
break
|
|
1268
|
+
case 'in':
|
|
1269
|
+
parts.push(`in ${sub.dimension}`)
|
|
1270
|
+
break
|
|
1271
|
+
case 'on':
|
|
1272
|
+
parts.push(`on ${sub.relation}`)
|
|
1273
|
+
break
|
|
1274
|
+
case 'summon':
|
|
1275
|
+
parts.push(`summon ${sub.entity}`)
|
|
1276
|
+
break
|
|
1277
|
+
// Conditions
|
|
1209
1278
|
case 'if_entity':
|
|
1210
1279
|
if (sub.selector) {
|
|
1211
1280
|
parts.push(`if entity ${this.selectorToString(sub.selector)}`)
|
|
1212
1281
|
} else if (sub.varName) {
|
|
1213
|
-
// Variable with filters - substitute with @s and apply filters
|
|
1214
1282
|
const sel: EntitySelector = { kind: '@s', filters: sub.filters }
|
|
1215
1283
|
parts.push(`if entity ${this.selectorToString(sel)}`)
|
|
1216
1284
|
}
|
|
@@ -1219,13 +1287,34 @@ export class Lowering {
|
|
|
1219
1287
|
if (sub.selector) {
|
|
1220
1288
|
parts.push(`unless entity ${this.selectorToString(sub.selector)}`)
|
|
1221
1289
|
} else if (sub.varName) {
|
|
1222
|
-
// Variable with filters - substitute with @s and apply filters
|
|
1223
1290
|
const sel: EntitySelector = { kind: '@s', filters: sub.filters }
|
|
1224
1291
|
parts.push(`unless entity ${this.selectorToString(sel)}`)
|
|
1225
1292
|
}
|
|
1226
1293
|
break
|
|
1227
|
-
case '
|
|
1228
|
-
parts.push(`
|
|
1294
|
+
case 'if_block':
|
|
1295
|
+
parts.push(`if block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`)
|
|
1296
|
+
break
|
|
1297
|
+
case 'unless_block':
|
|
1298
|
+
parts.push(`unless block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`)
|
|
1299
|
+
break
|
|
1300
|
+
case 'if_score':
|
|
1301
|
+
parts.push(`if score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`)
|
|
1302
|
+
break
|
|
1303
|
+
case 'unless_score':
|
|
1304
|
+
parts.push(`unless score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`)
|
|
1305
|
+
break
|
|
1306
|
+
case 'if_score_range':
|
|
1307
|
+
parts.push(`if score ${sub.target} ${sub.targetObj} matches ${sub.range}`)
|
|
1308
|
+
break
|
|
1309
|
+
case 'unless_score_range':
|
|
1310
|
+
parts.push(`unless score ${sub.target} ${sub.targetObj} matches ${sub.range}`)
|
|
1311
|
+
break
|
|
1312
|
+
// Store
|
|
1313
|
+
case 'store_result':
|
|
1314
|
+
parts.push(`store result score ${sub.target} ${sub.targetObj}`)
|
|
1315
|
+
break
|
|
1316
|
+
case 'store_success':
|
|
1317
|
+
parts.push(`store success score ${sub.target} ${sub.targetObj}`)
|
|
1229
1318
|
break
|
|
1230
1319
|
}
|
|
1231
1320
|
}
|
|
@@ -1706,6 +1795,22 @@ export class Lowering {
|
|
|
1706
1795
|
|
|
1707
1796
|
const implMethod = this.resolveInstanceMethod(expr)
|
|
1708
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
|
+
}
|
|
1709
1814
|
return this.emitMethodCall(implMethod.loweredName, implMethod.fn, expr.args)
|
|
1710
1815
|
}
|
|
1711
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
|
}
|
package/src/parser/index.ts
CHANGED
|
@@ -660,11 +660,11 @@ export class Parser {
|
|
|
660
660
|
const iterable = this.parseExpr()
|
|
661
661
|
this.expect(')')
|
|
662
662
|
|
|
663
|
-
// Parse optional execute context modifiers (at, positioned, rotated, facing, etc.)
|
|
663
|
+
// Parse optional execute context modifiers (as, at, positioned, rotated, facing, etc.)
|
|
664
664
|
let executeContext: string | undefined
|
|
665
|
-
// Check for
|
|
666
|
-
const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align']
|
|
667
|
-
if (this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
|
|
665
|
+
// Check for execute subcommand keywords
|
|
666
|
+
const execIdentKeywords = ['positioned', 'rotated', 'facing', 'anchored', 'align', 'on', 'summon']
|
|
667
|
+
if (this.check('as') || this.check('at') || this.check('in') || (this.check('ident') && execIdentKeywords.includes(this.peek().value))) {
|
|
668
668
|
// Collect everything until we hit '{'
|
|
669
669
|
let context = ''
|
|
670
670
|
while (!this.check('{') && !this.check('eof')) {
|
|
@@ -738,25 +738,84 @@ export class Parser {
|
|
|
738
738
|
} else if (this.match('at')) {
|
|
739
739
|
const selector = this.parseSelector()
|
|
740
740
|
subcommands.push({ kind: 'at', selector })
|
|
741
|
-
} else if (this.
|
|
742
|
-
|
|
743
|
-
if (this.
|
|
744
|
-
this.
|
|
741
|
+
} else if (this.checkIdent('positioned')) {
|
|
742
|
+
this.advance()
|
|
743
|
+
if (this.match('as')) {
|
|
744
|
+
const selector = this.parseSelector()
|
|
745
|
+
subcommands.push({ kind: 'positioned_as', selector })
|
|
746
|
+
} else {
|
|
747
|
+
const x = this.parseCoordToken()
|
|
748
|
+
const y = this.parseCoordToken()
|
|
749
|
+
const z = this.parseCoordToken()
|
|
750
|
+
subcommands.push({ kind: 'positioned', x, y, z })
|
|
745
751
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
+
} else if (this.checkIdent('rotated')) {
|
|
753
|
+
this.advance()
|
|
754
|
+
if (this.match('as')) {
|
|
755
|
+
const selector = this.parseSelector()
|
|
756
|
+
subcommands.push({ kind: 'rotated_as', selector })
|
|
757
|
+
} else {
|
|
758
|
+
const yaw = this.parseCoordToken()
|
|
759
|
+
const pitch = this.parseCoordToken()
|
|
760
|
+
subcommands.push({ kind: 'rotated', yaw, pitch })
|
|
752
761
|
}
|
|
753
|
-
|
|
754
|
-
|
|
762
|
+
} else if (this.checkIdent('facing')) {
|
|
763
|
+
this.advance()
|
|
764
|
+
if (this.checkIdent('entity')) {
|
|
765
|
+
this.advance()
|
|
766
|
+
const selector = this.parseSelector()
|
|
767
|
+
const anchor = this.checkIdent('eyes') || this.checkIdent('feet') ? this.advance().value as 'eyes' | 'feet' : 'feet'
|
|
768
|
+
subcommands.push({ kind: 'facing_entity', selector, anchor })
|
|
769
|
+
} else {
|
|
770
|
+
const x = this.parseCoordToken()
|
|
771
|
+
const y = this.parseCoordToken()
|
|
772
|
+
const z = this.parseCoordToken()
|
|
773
|
+
subcommands.push({ kind: 'facing', x, y, z })
|
|
774
|
+
}
|
|
775
|
+
} else if (this.checkIdent('anchored')) {
|
|
776
|
+
this.advance()
|
|
777
|
+
const anchor = this.advance().value as 'eyes' | 'feet'
|
|
778
|
+
subcommands.push({ kind: 'anchored', anchor })
|
|
779
|
+
} else if (this.checkIdent('align')) {
|
|
780
|
+
this.advance()
|
|
781
|
+
const axes = this.advance().value
|
|
782
|
+
subcommands.push({ kind: 'align', axes })
|
|
783
|
+
} else if (this.checkIdent('on')) {
|
|
784
|
+
this.advance()
|
|
785
|
+
const relation = this.advance().value
|
|
786
|
+
subcommands.push({ kind: 'on', relation })
|
|
787
|
+
} else if (this.checkIdent('summon')) {
|
|
788
|
+
this.advance()
|
|
789
|
+
const entity = this.advance().value
|
|
790
|
+
subcommands.push({ kind: 'summon', entity })
|
|
791
|
+
} else if (this.checkIdent('store')) {
|
|
792
|
+
this.advance()
|
|
793
|
+
const storeType = this.advance().value // 'result' or 'success'
|
|
794
|
+
if (this.checkIdent('score')) {
|
|
795
|
+
this.advance()
|
|
796
|
+
const target = this.advance().value
|
|
797
|
+
const targetObj = this.advance().value
|
|
798
|
+
if (storeType === 'result') {
|
|
799
|
+
subcommands.push({ kind: 'store_result', target, targetObj })
|
|
800
|
+
} else {
|
|
801
|
+
subcommands.push({ kind: 'store_success', target, targetObj })
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
this.error('store currently only supports score target')
|
|
805
|
+
}
|
|
806
|
+
} else if (this.match('if')) {
|
|
807
|
+
this.parseExecuteCondition(subcommands, 'if')
|
|
808
|
+
} else if (this.match('unless')) {
|
|
809
|
+
this.parseExecuteCondition(subcommands, 'unless')
|
|
755
810
|
} else if (this.match('in')) {
|
|
756
|
-
|
|
811
|
+
// Dimension can be namespaced: minecraft:the_nether
|
|
812
|
+
let dim = this.advance().value
|
|
813
|
+
if (this.match(':')) {
|
|
814
|
+
dim += ':' + this.advance().value
|
|
815
|
+
}
|
|
757
816
|
subcommands.push({ kind: 'in', dimension: dim })
|
|
758
817
|
} else {
|
|
759
|
-
this.error(`Unexpected token in execute statement: ${this.peek().kind}`)
|
|
818
|
+
this.error(`Unexpected token in execute statement: ${this.peek().kind} (${this.peek().value})`)
|
|
760
819
|
}
|
|
761
820
|
}
|
|
762
821
|
|
|
@@ -766,6 +825,74 @@ export class Parser {
|
|
|
766
825
|
return this.withLoc({ kind: 'execute', subcommands, body }, executeToken)
|
|
767
826
|
}
|
|
768
827
|
|
|
828
|
+
private parseExecuteCondition(subcommands: ExecuteSubcommand[], type: 'if' | 'unless'): void {
|
|
829
|
+
if (this.checkIdent('entity') || this.check('selector')) {
|
|
830
|
+
if (this.checkIdent('entity')) this.advance()
|
|
831
|
+
const selectorOrVar = this.parseSelectorOrVarSelector()
|
|
832
|
+
subcommands.push({ kind: type === 'if' ? 'if_entity' : 'unless_entity', ...selectorOrVar })
|
|
833
|
+
} else if (this.checkIdent('block')) {
|
|
834
|
+
this.advance()
|
|
835
|
+
const x = this.parseCoordToken()
|
|
836
|
+
const y = this.parseCoordToken()
|
|
837
|
+
const z = this.parseCoordToken()
|
|
838
|
+
const block = this.parseBlockId()
|
|
839
|
+
subcommands.push({ kind: type === 'if' ? 'if_block' : 'unless_block', pos: [x, y, z], block })
|
|
840
|
+
} else if (this.checkIdent('score')) {
|
|
841
|
+
this.advance()
|
|
842
|
+
const target = this.advance().value
|
|
843
|
+
const targetObj = this.advance().value
|
|
844
|
+
// Check for range or comparison
|
|
845
|
+
if (this.checkIdent('matches')) {
|
|
846
|
+
this.advance()
|
|
847
|
+
const range = this.advance().value
|
|
848
|
+
subcommands.push({ kind: type === 'if' ? 'if_score_range' : 'unless_score_range', target, targetObj, range })
|
|
849
|
+
} else {
|
|
850
|
+
const op = this.advance().value // <, <=, =, >=, >
|
|
851
|
+
const source = this.advance().value
|
|
852
|
+
const sourceObj = this.advance().value
|
|
853
|
+
subcommands.push({
|
|
854
|
+
kind: type === 'if' ? 'if_score' : 'unless_score',
|
|
855
|
+
target, targetObj, op, source, sourceObj
|
|
856
|
+
})
|
|
857
|
+
}
|
|
858
|
+
} else {
|
|
859
|
+
this.error(`Unknown condition type after ${type}`)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private parseCoordToken(): string {
|
|
864
|
+
// Handle ~, ^, numbers, relative coords like ~5, ^-3
|
|
865
|
+
const token = this.peek()
|
|
866
|
+
if (token.kind === 'rel_coord' || token.kind === 'local_coord' ||
|
|
867
|
+
token.kind === 'int_lit' || token.kind === 'float_lit' ||
|
|
868
|
+
token.kind === '-' || token.kind === 'ident') {
|
|
869
|
+
return this.advance().value
|
|
870
|
+
}
|
|
871
|
+
this.error(`Expected coordinate, got ${token.kind}`)
|
|
872
|
+
return '~'
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private parseBlockId(): string {
|
|
876
|
+
// Parse block ID like minecraft:stone or stone
|
|
877
|
+
let id = this.advance().value
|
|
878
|
+
if (this.match(':')) {
|
|
879
|
+
id += ':' + this.advance().value
|
|
880
|
+
}
|
|
881
|
+
// Handle block states [facing=north]
|
|
882
|
+
if (this.check('[')) {
|
|
883
|
+
id += this.advance().value // [
|
|
884
|
+
while (!this.check(']') && !this.check('eof')) {
|
|
885
|
+
id += this.advance().value
|
|
886
|
+
}
|
|
887
|
+
id += this.advance().value // ]
|
|
888
|
+
}
|
|
889
|
+
return id
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
private checkIdent(value: string): boolean {
|
|
893
|
+
return this.check('ident') && this.peek().value === value
|
|
894
|
+
}
|
|
895
|
+
|
|
769
896
|
private parseExprStmt(): Stmt {
|
|
770
897
|
const expr = this.parseExpr()
|
|
771
898
|
this.expect(';')
|