redscript-mc 1.2.21 → 1.2.25

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.
Files changed (126) hide show
  1. package/.github/workflows/publish-extension-on-ci.yml +100 -0
  2. package/dist/__tests__/entity-types.test.d.ts +1 -0
  3. package/dist/__tests__/entity-types.test.js +203 -0
  4. package/dist/__tests__/var-allocator.test.d.ts +1 -0
  5. package/dist/__tests__/var-allocator.test.js +69 -0
  6. package/dist/ast/types.d.ts +2 -1
  7. package/dist/cli.js +17 -6
  8. package/dist/codegen/mcfunction/index.d.ts +2 -0
  9. package/dist/codegen/mcfunction/index.js +52 -43
  10. package/dist/codegen/structure/index.d.ts +4 -1
  11. package/dist/codegen/structure/index.js +8 -12
  12. package/dist/codegen/var-allocator.d.ts +28 -0
  13. package/dist/codegen/var-allocator.js +78 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +8 -6
  16. package/dist/lowering/index.d.ts +2 -0
  17. package/dist/lowering/index.js +62 -3
  18. package/dist/parser/index.js +22 -1
  19. package/dist/typechecker/index.js +30 -0
  20. package/dist/types/entity-hierarchy.d.ts +29 -0
  21. package/dist/types/entity-hierarchy.js +107 -0
  22. package/editors/vscode/package-lock.json +6 -4
  23. package/editors/vscode/package.json +3 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/entity-types.test.ts +236 -0
  26. package/src/__tests__/var-allocator.test.ts +75 -0
  27. package/src/ast/types.ts +8 -4
  28. package/src/cli.ts +20 -6
  29. package/src/codegen/mcfunction/index.ts +60 -48
  30. package/src/codegen/structure/index.ts +9 -14
  31. package/src/codegen/var-allocator.ts +75 -0
  32. package/src/examples/capture_the_flag.mcrs +34 -34
  33. package/src/examples/hunger_games.mcrs +59 -59
  34. package/src/examples/new_features_demo.mcrs +32 -32
  35. package/src/examples/parkour_race.mcrs +58 -58
  36. package/src/index.ts +10 -6
  37. package/src/lowering/index.ts +73 -8
  38. package/src/parser/index.ts +20 -1
  39. package/src/typechecker/index.ts +30 -0
  40. package/src/types/entity-hierarchy.ts +120 -0
  41. package/dist/data/arena/function/__load.mcfunction +0 -6
  42. package/dist/data/arena/function/__tick.mcfunction +0 -2
  43. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +0 -3
  44. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +0 -1
  45. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +0 -3
  46. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +0 -7
  47. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +0 -1
  48. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +0 -4
  49. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +0 -6
  50. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +0 -1
  51. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +0 -4
  52. package/dist/data/arena/function/announce_leaders.mcfunction +0 -6
  53. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +0 -1
  54. package/dist/data/arena/function/arena_tick/then_0.mcfunction +0 -4
  55. package/dist/data/arena/function/arena_tick.mcfunction +0 -11
  56. package/dist/data/counter/function/__load.mcfunction +0 -5
  57. package/dist/data/counter/function/__tick.mcfunction +0 -2
  58. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +0 -1
  59. package/dist/data/counter/function/counter_tick/then_0.mcfunction +0 -3
  60. package/dist/data/counter/function/counter_tick.mcfunction +0 -11
  61. package/dist/data/minecraft/tags/function/load.json +0 -5
  62. package/dist/data/minecraft/tags/function/tick.json +0 -5
  63. package/dist/data/quiz/function/__load.mcfunction +0 -16
  64. package/dist/data/quiz/function/__tick.mcfunction +0 -6
  65. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +0 -4
  66. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +0 -4
  67. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +0 -4
  68. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +0 -4
  69. package/dist/data/quiz/function/answer_a.mcfunction +0 -4
  70. package/dist/data/quiz/function/answer_b.mcfunction +0 -4
  71. package/dist/data/quiz/function/answer_c.mcfunction +0 -4
  72. package/dist/data/quiz/function/ask_question/else_1.mcfunction +0 -5
  73. package/dist/data/quiz/function/ask_question/else_4.mcfunction +0 -5
  74. package/dist/data/quiz/function/ask_question/else_7.mcfunction +0 -4
  75. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +0 -1
  76. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +0 -2
  77. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +0 -2
  78. package/dist/data/quiz/function/ask_question/then_0.mcfunction +0 -4
  79. package/dist/data/quiz/function/ask_question/then_3.mcfunction +0 -4
  80. package/dist/data/quiz/function/ask_question/then_6.mcfunction +0 -4
  81. package/dist/data/quiz/function/ask_question.mcfunction +0 -7
  82. package/dist/data/quiz/function/finish_quiz.mcfunction +0 -6
  83. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +0 -5
  84. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +0 -3
  85. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +0 -3
  86. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +0 -3
  87. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +0 -5
  88. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +0 -2
  89. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +0 -2
  90. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +0 -2
  91. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +0 -8
  92. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +0 -2
  93. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +0 -2
  94. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +0 -5
  95. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +0 -5
  96. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +0 -6
  97. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +0 -6
  98. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +0 -5
  99. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +0 -6
  100. package/dist/data/quiz/function/handle_answer.mcfunction +0 -11
  101. package/dist/data/quiz/function/start_quiz.mcfunction +0 -5
  102. package/dist/data/shop/function/__load.mcfunction +0 -7
  103. package/dist/data/shop/function/__tick.mcfunction +0 -3
  104. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +0 -4
  105. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +0 -5
  106. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +0 -5
  107. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +0 -3
  108. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +0 -2
  109. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +0 -2
  110. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +0 -2
  111. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +0 -4
  112. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +0 -4
  113. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +0 -4
  114. package/dist/data/shop/function/complete_purchase.mcfunction +0 -7
  115. package/dist/data/shop/function/handle_shop_trigger.mcfunction +0 -3
  116. package/dist/data/turret/function/__load.mcfunction +0 -5
  117. package/dist/data/turret/function/__tick.mcfunction +0 -4
  118. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +0 -4
  119. package/dist/data/turret/function/deploy_turret.mcfunction +0 -8
  120. package/dist/data/turret/function/turret_tick/at_1.mcfunction +0 -2
  121. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +0 -2
  122. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +0 -2
  123. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +0 -3
  124. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +0 -1
  125. package/dist/data/turret/function/turret_tick.mcfunction +0 -5
  126. package/dist/pack.mcmeta +0 -6
@@ -21,11 +21,21 @@ const ENTITY_HIERARCHY = {
21
21
  'Creeper': 'HostileMob',
22
22
  'Spider': 'HostileMob',
23
23
  'Enderman': 'HostileMob',
24
+ 'Blaze': 'HostileMob',
25
+ 'Witch': 'HostileMob',
26
+ 'Slime': 'HostileMob',
27
+ 'ZombieVillager': 'HostileMob',
28
+ 'Husk': 'HostileMob',
29
+ 'Drowned': 'HostileMob',
30
+ 'Stray': 'HostileMob',
31
+ 'WitherSkeleton': 'HostileMob',
32
+ 'CaveSpider': 'HostileMob',
24
33
  'Pig': 'PassiveMob',
25
34
  'Cow': 'PassiveMob',
26
35
  'Sheep': 'PassiveMob',
27
36
  'Chicken': 'PassiveMob',
28
37
  'Villager': 'PassiveMob',
38
+ 'WanderingTrader': 'PassiveMob',
29
39
  'ArmorStand': 'entity',
30
40
  'Item': 'entity',
31
41
  'Arrow': 'entity',
@@ -42,6 +52,24 @@ const MC_TYPE_TO_ENTITY = {
42
52
  'minecraft:spider': 'Spider',
43
53
  'enderman': 'Enderman',
44
54
  'minecraft:enderman': 'Enderman',
55
+ 'blaze': 'Blaze',
56
+ 'minecraft:blaze': 'Blaze',
57
+ 'witch': 'Witch',
58
+ 'minecraft:witch': 'Witch',
59
+ 'slime': 'Slime',
60
+ 'minecraft:slime': 'Slime',
61
+ 'zombie_villager': 'ZombieVillager',
62
+ 'minecraft:zombie_villager': 'ZombieVillager',
63
+ 'husk': 'Husk',
64
+ 'minecraft:husk': 'Husk',
65
+ 'drowned': 'Drowned',
66
+ 'minecraft:drowned': 'Drowned',
67
+ 'stray': 'Stray',
68
+ 'minecraft:stray': 'Stray',
69
+ 'wither_skeleton': 'WitherSkeleton',
70
+ 'minecraft:wither_skeleton': 'WitherSkeleton',
71
+ 'cave_spider': 'CaveSpider',
72
+ 'minecraft:cave_spider': 'CaveSpider',
45
73
  'pig': 'Pig',
46
74
  'minecraft:pig': 'Pig',
47
75
  'cow': 'Cow',
@@ -52,6 +80,8 @@ const MC_TYPE_TO_ENTITY = {
52
80
  'minecraft:chicken': 'Chicken',
53
81
  'villager': 'Villager',
54
82
  'minecraft:villager': 'Villager',
83
+ 'wandering_trader': 'WanderingTrader',
84
+ 'minecraft:wandering_trader': 'WanderingTrader',
55
85
  'armor_stand': 'ArmorStand',
56
86
  'minecraft:armor_stand': 'ArmorStand',
57
87
  'item': 'Item',
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Entity Type Hierarchy
3
+ *
4
+ * Closed inheritance tree mapping to Minecraft's entity registry.
5
+ * Used for type narrowing (is checks), selector<T> annotations,
6
+ * and W_IMPOSSIBLE_AS warnings.
7
+ */
8
+ export interface EntityTypeNode {
9
+ name: string;
10
+ mcId: string | null;
11
+ abstract: boolean;
12
+ parent: string | null;
13
+ }
14
+ export declare const ENTITY_TYPES: EntityTypeNode[];
15
+ /** Map from lowercase name → EntityTypeNode */
16
+ export declare const ENTITY_TYPE_MAP: Map<string, EntityTypeNode>;
17
+ /** Map from mcId (without namespace) → EntityTypeNode */
18
+ export declare const ENTITY_TYPE_BY_MCID: Map<string, EntityTypeNode>;
19
+ /** Check if typeA is a subtype of typeB (recursive ancestry) */
20
+ export declare function isSubtype(typeA: string, typeB: string): boolean;
21
+ /** True if one type is a subtype of the other (in either direction) */
22
+ export declare function areCompatibleTypes(outerType: string, innerType: string): boolean;
23
+ /** Get all non-abstract leaf types under a given node */
24
+ export declare function getConcreteSubtypes(typeName: string): EntityTypeNode[];
25
+ /** Parse "type=zombie" from a selector string, return the entity type name or null */
26
+ export declare function getSelectorEntityType(selector: string): string | null;
27
+ /** Determine the base entity type from a selector kind:
28
+ * @a/@p/@r → "Player", @e → "Entity" or from type filter, @s → null */
29
+ export declare function getBaseSelectorType(selector: string): string | null;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ /**
3
+ * Entity Type Hierarchy
4
+ *
5
+ * Closed inheritance tree mapping to Minecraft's entity registry.
6
+ * Used for type narrowing (is checks), selector<T> annotations,
7
+ * and W_IMPOSSIBLE_AS warnings.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ENTITY_TYPE_BY_MCID = exports.ENTITY_TYPE_MAP = exports.ENTITY_TYPES = void 0;
11
+ exports.isSubtype = isSubtype;
12
+ exports.areCompatibleTypes = areCompatibleTypes;
13
+ exports.getConcreteSubtypes = getConcreteSubtypes;
14
+ exports.getSelectorEntityType = getSelectorEntityType;
15
+ exports.getBaseSelectorType = getBaseSelectorType;
16
+ exports.ENTITY_TYPES = [
17
+ // Root
18
+ { name: 'Entity', mcId: null, abstract: true, parent: null },
19
+ // Direct children of Entity
20
+ { name: 'Player', mcId: 'minecraft:player', abstract: false, parent: 'Entity' },
21
+ { name: 'ArmorStand', mcId: 'minecraft:armor_stand', abstract: false, parent: 'Entity' },
22
+ { name: 'Item', mcId: 'minecraft:item', abstract: false, parent: 'Entity' },
23
+ { name: 'Arrow', mcId: 'minecraft:arrow', abstract: false, parent: 'Entity' },
24
+ // Mob hierarchy
25
+ { name: 'Mob', mcId: null, abstract: true, parent: 'Entity' },
26
+ // Hostile mobs
27
+ { name: 'HostileMob', mcId: null, abstract: true, parent: 'Mob' },
28
+ { name: 'Zombie', mcId: 'minecraft:zombie', abstract: false, parent: 'HostileMob' },
29
+ { name: 'Skeleton', mcId: 'minecraft:skeleton', abstract: false, parent: 'HostileMob' },
30
+ { name: 'Creeper', mcId: 'minecraft:creeper', abstract: false, parent: 'HostileMob' },
31
+ { name: 'Spider', mcId: 'minecraft:spider', abstract: false, parent: 'HostileMob' },
32
+ { name: 'Enderman', mcId: 'minecraft:enderman', abstract: false, parent: 'HostileMob' },
33
+ { name: 'Blaze', mcId: 'minecraft:blaze', abstract: false, parent: 'HostileMob' },
34
+ { name: 'Witch', mcId: 'minecraft:witch', abstract: false, parent: 'HostileMob' },
35
+ { name: 'Slime', mcId: 'minecraft:slime', abstract: false, parent: 'HostileMob' },
36
+ { name: 'ZombieVillager', mcId: 'minecraft:zombie_villager', abstract: false, parent: 'HostileMob' },
37
+ { name: 'Husk', mcId: 'minecraft:husk', abstract: false, parent: 'HostileMob' },
38
+ { name: 'Drowned', mcId: 'minecraft:drowned', abstract: false, parent: 'HostileMob' },
39
+ { name: 'Stray', mcId: 'minecraft:stray', abstract: false, parent: 'HostileMob' },
40
+ { name: 'WitherSkeleton', mcId: 'minecraft:wither_skeleton', abstract: false, parent: 'HostileMob' },
41
+ { name: 'CaveSpider', mcId: 'minecraft:cave_spider', abstract: false, parent: 'HostileMob' },
42
+ // Passive mobs
43
+ { name: 'PassiveMob', mcId: null, abstract: true, parent: 'Mob' },
44
+ { name: 'Pig', mcId: 'minecraft:pig', abstract: false, parent: 'PassiveMob' },
45
+ { name: 'Cow', mcId: 'minecraft:cow', abstract: false, parent: 'PassiveMob' },
46
+ { name: 'Sheep', mcId: 'minecraft:sheep', abstract: false, parent: 'PassiveMob' },
47
+ { name: 'Chicken', mcId: 'minecraft:chicken', abstract: false, parent: 'PassiveMob' },
48
+ { name: 'Villager', mcId: 'minecraft:villager', abstract: false, parent: 'PassiveMob' },
49
+ { name: 'WanderingTrader', mcId: 'minecraft:wandering_trader', abstract: false, parent: 'PassiveMob' },
50
+ ];
51
+ /** Map from lowercase name → EntityTypeNode */
52
+ exports.ENTITY_TYPE_MAP = new Map(exports.ENTITY_TYPES.map(t => [t.name.toLowerCase(), t]));
53
+ /** Map from mcId (without namespace) → EntityTypeNode */
54
+ exports.ENTITY_TYPE_BY_MCID = new Map(exports.ENTITY_TYPES
55
+ .filter(t => t.mcId !== null)
56
+ .map(t => [t.mcId.replace('minecraft:', ''), t]));
57
+ /** Check if typeA is a subtype of typeB (recursive ancestry) */
58
+ function isSubtype(typeA, typeB) {
59
+ if (typeA === typeB)
60
+ return true;
61
+ const node = exports.ENTITY_TYPE_MAP.get(typeA.toLowerCase());
62
+ if (!node || !node.parent)
63
+ return false;
64
+ return isSubtype(node.parent, typeB);
65
+ }
66
+ /** True if one type is a subtype of the other (in either direction) */
67
+ function areCompatibleTypes(outerType, innerType) {
68
+ return isSubtype(outerType, innerType) || isSubtype(innerType, outerType);
69
+ }
70
+ /** Get all non-abstract leaf types under a given node */
71
+ function getConcreteSubtypes(typeName) {
72
+ const results = [];
73
+ for (const node of exports.ENTITY_TYPES) {
74
+ if (!node.abstract && isSubtype(node.name, typeName)) {
75
+ results.push(node);
76
+ }
77
+ }
78
+ return results;
79
+ }
80
+ /** Parse "type=zombie" from a selector string, return the entity type name or null */
81
+ function getSelectorEntityType(selector) {
82
+ const match = selector.match(/type=(?:minecraft:)?([a-z_]+)/);
83
+ if (!match)
84
+ return null;
85
+ const mcId = match[1];
86
+ const node = exports.ENTITY_TYPE_BY_MCID.get(mcId);
87
+ return node ? node.name : null;
88
+ }
89
+ /** Determine the base entity type from a selector kind:
90
+ * @a/@p/@r → "Player", @e → "Entity" or from type filter, @s → null */
91
+ function getBaseSelectorType(selector) {
92
+ const trimmed = selector.trim();
93
+ // Check for type filter first (works for any selector)
94
+ const typeFromFilter = getSelectorEntityType(trimmed);
95
+ if (trimmed.startsWith('@a') || trimmed.startsWith('@p') || trimmed.startsWith('@r')) {
96
+ return typeFromFilter ?? 'Player';
97
+ }
98
+ if (trimmed.startsWith('@e')) {
99
+ return typeFromFilter ?? 'Entity';
100
+ }
101
+ // @s — context-dependent, we don't know unless there's a type filter
102
+ if (trimmed.startsWith('@s')) {
103
+ return typeFromFilter ?? null;
104
+ }
105
+ return null;
106
+ }
107
+ //# sourceMappingURL=entity-hierarchy.js.map
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "redscript-vscode",
3
- "version": "1.0.12",
3
+ "version": "1.0.16",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "redscript-vscode",
9
- "version": "1.0.12",
9
+ "version": "1.0.16",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "redscript": "file:../../"
@@ -22,10 +22,12 @@
22
22
  }
23
23
  },
24
24
  "../..": {
25
- "version": "0.1.0",
25
+ "name": "redscript-mc",
26
+ "version": "1.2.25",
26
27
  "license": "MIT",
27
28
  "bin": {
28
- "redscript": "dist/cli.js"
29
+ "redscript": "dist/cli.js",
30
+ "rsc": "dist/cli.js"
29
31
  },
30
32
  "devDependencies": {
31
33
  "@types/jest": "^29.5.0",
@@ -2,7 +2,7 @@
2
2
  "name": "redscript-vscode",
3
3
  "displayName": "RedScript for Minecraft",
4
4
  "description": "Syntax highlighting, error diagnostics, and language support for RedScript — a compiler targeting Minecraft Java Edition",
5
- "version": "1.0.12",
5
+ "version": "1.0.16",
6
6
  "publisher": "bkmashiro",
7
7
  "icon": "icon.png",
8
8
  "license": "MIT",
@@ -129,8 +129,8 @@
129
129
  "compile": "node build.mjs",
130
130
  "watch": "node build.mjs --watch",
131
131
  "build": "npm run compile",
132
- "package": "npm run build && vsce package --no-dependencies",
133
- "publish": "npm run build && vsce publish --no-dependencies"
132
+ "package": "npm run build && npx @vscode/vsce package --no-dependencies",
133
+ "publish": "npm run build && npx @vscode/vsce publish --no-dependencies"
134
134
  },
135
135
  "devDependencies": {
136
136
  "@types/node": "^20.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "1.2.21",
3
+ "version": "1.2.25",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,236 @@
1
+ import {
2
+ isSubtype,
3
+ areCompatibleTypes,
4
+ getConcreteSubtypes,
5
+ getSelectorEntityType,
6
+ getBaseSelectorType,
7
+ } from '../types/entity-hierarchy'
8
+ import { Lexer } from '../lexer'
9
+ import { Parser } from '../parser'
10
+ import { Lowering } from '../lowering'
11
+ import type { IRModule } from '../ir/types'
12
+
13
+ function compileWithWarnings(source: string, namespace = 'test'): { ir: IRModule; warnings: Lowering['warnings'] } {
14
+ const tokens = new Lexer(source).tokenize()
15
+ const ast = new Parser(tokens).parse(namespace)
16
+ const lowering = new Lowering(namespace)
17
+ return { ir: lowering.lower(ast), warnings: lowering.warnings }
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Entity hierarchy utilities
22
+ // ---------------------------------------------------------------------------
23
+
24
+ describe('Entity type hierarchy', () => {
25
+ test('isSubtype: Zombie is a subtype of Mob', () => {
26
+ expect(isSubtype('Zombie', 'Mob')).toBe(true)
27
+ })
28
+
29
+ test('isSubtype: Player is NOT a subtype of Mob', () => {
30
+ expect(isSubtype('Player', 'Mob')).toBe(false)
31
+ })
32
+
33
+ test('isSubtype: Zombie is a subtype of Entity (transitive)', () => {
34
+ expect(isSubtype('Zombie', 'Entity')).toBe(true)
35
+ })
36
+
37
+ test('isSubtype: identity — Zombie is subtype of Zombie', () => {
38
+ expect(isSubtype('Zombie', 'Zombie')).toBe(true)
39
+ })
40
+
41
+ test('areCompatibleTypes: Player and Zombie are NOT compatible', () => {
42
+ expect(areCompatibleTypes('Player', 'Zombie')).toBe(false)
43
+ })
44
+
45
+ test('areCompatibleTypes: Zombie and Mob are compatible', () => {
46
+ expect(areCompatibleTypes('Zombie', 'Mob')).toBe(true)
47
+ })
48
+
49
+ test('areCompatibleTypes: Mob and Zombie are compatible (reverse)', () => {
50
+ expect(areCompatibleTypes('Mob', 'Zombie')).toBe(true)
51
+ })
52
+
53
+ test('getConcreteSubtypes: HostileMob includes Zombie, Skeleton, Creeper', () => {
54
+ const subtypes = getConcreteSubtypes('HostileMob')
55
+ const names = subtypes.map(n => n.name)
56
+ expect(names).toContain('Zombie')
57
+ expect(names).toContain('Skeleton')
58
+ expect(names).toContain('Creeper')
59
+ expect(names).toContain('Blaze')
60
+ expect(names).toContain('CaveSpider')
61
+ // Should NOT include passive mobs
62
+ expect(names).not.toContain('Pig')
63
+ expect(names).not.toContain('Player')
64
+ })
65
+
66
+ test('getConcreteSubtypes: PassiveMob includes Pig, Cow, Villager', () => {
67
+ const subtypes = getConcreteSubtypes('PassiveMob')
68
+ const names = subtypes.map(n => n.name)
69
+ expect(names).toContain('Pig')
70
+ expect(names).toContain('Cow')
71
+ expect(names).toContain('Villager')
72
+ expect(names).toContain('WanderingTrader')
73
+ expect(names).not.toContain('Zombie')
74
+ })
75
+
76
+ test('getSelectorEntityType: parses type=zombie', () => {
77
+ expect(getSelectorEntityType('@e[type=zombie]')).toBe('Zombie')
78
+ })
79
+
80
+ test('getSelectorEntityType: parses type=minecraft:skeleton', () => {
81
+ expect(getSelectorEntityType('@e[type=minecraft:skeleton]')).toBe('Skeleton')
82
+ })
83
+
84
+ test('getBaseSelectorType: @a → Player', () => {
85
+ expect(getBaseSelectorType('@a')).toBe('Player')
86
+ })
87
+
88
+ test('getBaseSelectorType: @e → Entity', () => {
89
+ expect(getBaseSelectorType('@e')).toBe('Entity')
90
+ })
91
+
92
+ test('getBaseSelectorType: @e[type=zombie] → Zombie', () => {
93
+ expect(getBaseSelectorType('@e[type=zombie]')).toBe('Zombie')
94
+ })
95
+
96
+ test('getBaseSelectorType: @s → null', () => {
97
+ expect(getBaseSelectorType('@s')).toBeNull()
98
+ })
99
+ })
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // W_IMPOSSIBLE_AS warning
103
+ // ---------------------------------------------------------------------------
104
+
105
+ describe('W_IMPOSSIBLE_AS warning', () => {
106
+ test('foreach @a with as @e[type=zombie] produces warning', () => {
107
+ const source = `
108
+ fn main() {
109
+ foreach (p in @a) {
110
+ as @e[type=minecraft:zombie] {
111
+ kill(@s);
112
+ }
113
+ }
114
+ }
115
+ `
116
+ const { warnings } = compileWithWarnings(source)
117
+ const impossible = warnings.filter(w => w.code === 'W_IMPOSSIBLE_AS')
118
+ expect(impossible.length).toBe(1)
119
+ expect(impossible[0].message).toContain('Player')
120
+ expect(impossible[0].message).toContain('Zombie')
121
+ })
122
+
123
+ test('foreach @e[type=zombie] with as @e[type=zombie] produces NO warning', () => {
124
+ const source = `
125
+ fn main() {
126
+ foreach (z in @e[type=minecraft:zombie]) {
127
+ as @e[type=minecraft:zombie] {
128
+ kill(@s);
129
+ }
130
+ }
131
+ }
132
+ `
133
+ const { warnings } = compileWithWarnings(source)
134
+ const impossible = warnings.filter(w => w.code === 'W_IMPOSSIBLE_AS')
135
+ expect(impossible.length).toBe(0)
136
+ })
137
+
138
+ test('foreach @e (generic) with as @e[type=zombie] produces NO warning', () => {
139
+ const source = `
140
+ fn main() {
141
+ foreach (e in @e) {
142
+ as @e[type=minecraft:zombie] {
143
+ kill(@s);
144
+ }
145
+ }
146
+ }
147
+ `
148
+ const { warnings } = compileWithWarnings(source)
149
+ const impossible = warnings.filter(w => w.code === 'W_IMPOSSIBLE_AS')
150
+ expect(impossible.length).toBe(0)
151
+ })
152
+ })
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // is_check with abstract types
156
+ // ---------------------------------------------------------------------------
157
+
158
+ describe('is_check compilation', () => {
159
+ test('concrete is_check emits single execute if entity', () => {
160
+ const source = `
161
+ fn main() {
162
+ foreach (e in @e) {
163
+ if (e is Zombie) {
164
+ kill(@s);
165
+ }
166
+ }
167
+ }
168
+ `
169
+ const { ir } = compileWithWarnings(source)
170
+ const thenFn = ir.functions.find(f => f.name.includes('then_'))
171
+ expect(thenFn).toBeDefined()
172
+
173
+ // The parent foreach function should contain the execute if entity command
174
+ const foreachFn = ir.functions.find(f => f.name.includes('foreach_'))
175
+ expect(foreachFn).toBeDefined()
176
+ const rawCmds = foreachFn!.blocks.flatMap(b => b.instrs)
177
+ .filter((i): i is any => i.op === 'raw')
178
+ .map(i => i.cmd)
179
+ expect(rawCmds.some(c => c.includes('type=minecraft:zombie'))).toBe(true)
180
+ })
181
+
182
+ test('abstract is_check (HostileMob) emits multiple type checks', () => {
183
+ const source = `
184
+ fn main() {
185
+ foreach (e in @e) {
186
+ if (e is HostileMob) {
187
+ kill(@s);
188
+ }
189
+ }
190
+ }
191
+ `
192
+ const { ir } = compileWithWarnings(source)
193
+ const foreachFn = ir.functions.find(f => f.name.includes('foreach_'))
194
+ expect(foreachFn).toBeDefined()
195
+ const rawCmds = foreachFn!.blocks.flatMap(b => b.instrs)
196
+ .filter((i): i is any => i.op === 'raw')
197
+ .map(i => i.cmd)
198
+
199
+ // Should have scoreboard setup and multiple type checks
200
+ expect(rawCmds.some(c => c.includes('scoreboard players set __is_result rs:temp 0'))).toBe(true)
201
+ expect(rawCmds.some(c => c.includes('type=minecraft:zombie'))).toBe(true)
202
+ expect(rawCmds.some(c => c.includes('type=minecraft:skeleton'))).toBe(true)
203
+ expect(rawCmds.some(c => c.includes('type=minecraft:creeper'))).toBe(true)
204
+ expect(rawCmds.some(c => c.includes('if score __is_result rs:temp matches 1'))).toBe(true)
205
+ })
206
+ })
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // selector<T> type annotation
210
+ // ---------------------------------------------------------------------------
211
+
212
+ describe('selector<T> type annotation', () => {
213
+ test('parses selector<Player> type', () => {
214
+ const source = `
215
+ fn greet(target: selector<Player>) {
216
+ say("hello");
217
+ }
218
+ `
219
+ const tokens = new Lexer(source).tokenize()
220
+ const ast = new Parser(tokens).parse('test')
221
+ const fn = ast.declarations[0]
222
+ expect(fn.params[0].type).toEqual({ kind: 'selector', entityType: 'Player' })
223
+ })
224
+
225
+ test('parses plain selector type', () => {
226
+ const source = `
227
+ fn greet(target: selector) {
228
+ say("hello");
229
+ }
230
+ `
231
+ const tokens = new Lexer(source).tokenize()
232
+ const ast = new Parser(tokens).parse('test')
233
+ const fn = ast.declarations[0]
234
+ expect(fn.params[0].type).toEqual({ kind: 'selector' })
235
+ })
236
+ })
@@ -0,0 +1,75 @@
1
+ import { VarAllocator } from '../codegen/var-allocator'
2
+
3
+ describe('VarAllocator', () => {
4
+ describe('mangle mode (default)', () => {
5
+ it('generates sequential names: a, b, ..., z, aa, ab', () => {
6
+ const alloc = new VarAllocator(true)
7
+ const names: string[] = []
8
+ for (let i = 0; i < 28; i++) {
9
+ names.push(alloc.alloc(`var${i}`))
10
+ }
11
+ expect(names[0]).toBe('$a')
12
+ expect(names[1]).toBe('$b')
13
+ expect(names[25]).toBe('$z')
14
+ expect(names[26]).toBe('$aa')
15
+ expect(names[27]).toBe('$ab')
16
+ })
17
+
18
+ it('caches: same name returns same result', () => {
19
+ const alloc = new VarAllocator(true)
20
+ const first = alloc.alloc('x')
21
+ const second = alloc.alloc('x')
22
+ expect(first).toBe(second)
23
+ })
24
+
25
+ it('constant() is content-addressed: same value returns same result', () => {
26
+ const alloc = new VarAllocator(true)
27
+ const first = alloc.constant(42)
28
+ const second = alloc.constant(42)
29
+ expect(first).toBe(second)
30
+ })
31
+
32
+ it('different variables get different names', () => {
33
+ const alloc = new VarAllocator(true)
34
+ const a = alloc.alloc('foo')
35
+ const b = alloc.alloc('bar')
36
+ expect(a).not.toBe(b)
37
+ })
38
+
39
+ it('alloc, constant, and internal share the same sequential pool', () => {
40
+ const alloc = new VarAllocator(true)
41
+ const v = alloc.alloc('x') // $a
42
+ const c = alloc.constant(1) // $b
43
+ const i = alloc.internal('ret') // $c
44
+ expect(v).toBe('$a')
45
+ expect(c).toBe('$b')
46
+ expect(i).toBe('$c')
47
+ })
48
+
49
+ it('strips $ prefix from variable names', () => {
50
+ const alloc = new VarAllocator(true)
51
+ const a = alloc.alloc('$foo')
52
+ const b = alloc.alloc('foo')
53
+ expect(a).toBe(b) // same underlying name
54
+ })
55
+ })
56
+
57
+ describe('no-mangle mode', () => {
58
+ it('uses $<name> for user vars', () => {
59
+ const alloc = new VarAllocator(false)
60
+ expect(alloc.alloc('counter')).toBe('$counter')
61
+ })
62
+
63
+ it('uses $const_<value> for constants', () => {
64
+ const alloc = new VarAllocator(false)
65
+ expect(alloc.constant(10)).toBe('$const_10')
66
+ expect(alloc.constant(-3)).toBe('$const_-3')
67
+ })
68
+
69
+ it('uses $<suffix> for internals', () => {
70
+ const alloc = new VarAllocator(false)
71
+ expect(alloc.internal('ret')).toBe('$ret')
72
+ expect(alloc.internal('p0')).toBe('$p0')
73
+ })
74
+ })
75
+ })
package/src/ast/types.ts CHANGED
@@ -25,15 +25,19 @@ export interface Span {
25
25
  export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double' | 'format_string'
26
26
 
27
27
  // Entity type hierarchy
28
- export type EntityTypeName =
28
+ export type EntityTypeName =
29
29
  | 'entity' // Base type
30
30
  | 'Player' // @a, @p, @r
31
31
  | 'Mob' // Base mob type
32
32
  | 'HostileMob' // Hostile mobs
33
33
  | 'PassiveMob' // Passive mobs
34
- // Specific mob types (common ones)
34
+ // Hostile mob types
35
35
  | 'Zombie' | 'Skeleton' | 'Creeper' | 'Spider' | 'Enderman'
36
- | 'Pig' | 'Cow' | 'Sheep' | 'Chicken' | 'Villager'
36
+ | 'Blaze' | 'Witch' | 'Slime' | 'ZombieVillager' | 'Husk'
37
+ | 'Drowned' | 'Stray' | 'WitherSkeleton' | 'CaveSpider'
38
+ // Passive mob types
39
+ | 'Pig' | 'Cow' | 'Sheep' | 'Chicken' | 'Villager' | 'WanderingTrader'
40
+ // Non-mob entities
37
41
  | 'ArmorStand' | 'Item' | 'Arrow'
38
42
 
39
43
  export type TypeNode =
@@ -43,7 +47,7 @@ export type TypeNode =
43
47
  | { kind: 'enum'; name: string }
44
48
  | { kind: 'function_type'; params: TypeNode[]; return: TypeNode }
45
49
  | { kind: 'entity'; entityType: EntityTypeName } // Entity types
46
- | { kind: 'selector' } // Selector type (multiple entities)
50
+ | { kind: 'selector'; entityType?: string } // Selector type, optionally parameterized: selector<Player>
47
51
 
48
52
  export interface LambdaParam {
49
53
  name: string