redscript-mc 2.1.0 → 2.2.1

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 (71) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +86 -21
  3. package/README.zh.md +61 -61
  4. package/dist/src/__tests__/e2e/basic.test.js +25 -0
  5. package/dist/src/__tests__/e2e/coroutine.test.js +22 -0
  6. package/dist/src/__tests__/lsp.test.js +76 -0
  7. package/dist/src/__tests__/mc-integration.test.js +25 -13
  8. package/dist/src/__tests__/mc-syntax.test.js +1 -6
  9. package/dist/src/__tests__/schedule.test.js +105 -0
  10. package/dist/src/__tests__/stdlib-include.test.d.ts +1 -0
  11. package/dist/src/__tests__/stdlib-include.test.js +86 -0
  12. package/dist/src/__tests__/typechecker.test.js +63 -0
  13. package/dist/src/cli.js +10 -3
  14. package/dist/src/compile.d.ts +1 -0
  15. package/dist/src/compile.js +33 -10
  16. package/dist/src/emit/compile.d.ts +2 -0
  17. package/dist/src/emit/compile.js +3 -2
  18. package/dist/src/emit/index.js +3 -1
  19. package/dist/src/lir/lower.js +26 -0
  20. package/dist/src/lsp/server.js +51 -0
  21. package/dist/src/mir/lower.js +341 -12
  22. package/dist/src/mir/types.d.ts +10 -0
  23. package/dist/src/optimizer/copy_prop.js +4 -0
  24. package/dist/src/optimizer/coroutine.d.ts +2 -0
  25. package/dist/src/optimizer/coroutine.js +33 -1
  26. package/dist/src/optimizer/dce.js +7 -1
  27. package/dist/src/optimizer/lir/const_imm.js +1 -1
  28. package/dist/src/optimizer/lir/dead_slot.js +1 -1
  29. package/dist/src/typechecker/index.d.ts +2 -0
  30. package/dist/src/typechecker/index.js +29 -0
  31. package/docs/ROADMAP.md +35 -0
  32. package/editors/vscode/package-lock.json +3 -3
  33. package/editors/vscode/package.json +1 -1
  34. package/editors/vscode/syntaxes/redscript.tmLanguage.json +34 -0
  35. package/examples/coroutine-demo.mcrs +51 -0
  36. package/examples/enum-demo.mcrs +95 -0
  37. package/examples/scheduler-demo.mcrs +59 -0
  38. package/jest.config.js +19 -0
  39. package/package.json +1 -1
  40. package/src/__tests__/e2e/basic.test.ts +27 -0
  41. package/src/__tests__/e2e/coroutine.test.ts +23 -0
  42. package/src/__tests__/fixtures/array-test.mcrs +21 -22
  43. package/src/__tests__/fixtures/counter.mcrs +17 -0
  44. package/src/__tests__/fixtures/foreach-at-test.mcrs +9 -10
  45. package/src/__tests__/lsp.test.ts +89 -0
  46. package/src/__tests__/mc-integration.test.ts +25 -13
  47. package/src/__tests__/mc-syntax.test.ts +1 -7
  48. package/src/__tests__/schedule.test.ts +112 -0
  49. package/src/__tests__/stdlib-include.test.ts +61 -0
  50. package/src/__tests__/typechecker.test.ts +68 -0
  51. package/src/cli.ts +9 -1
  52. package/src/compile.ts +44 -15
  53. package/src/emit/compile.ts +5 -2
  54. package/src/emit/index.ts +3 -1
  55. package/src/lir/lower.ts +27 -0
  56. package/src/lsp/server.ts +55 -0
  57. package/src/mir/lower.ts +355 -9
  58. package/src/mir/types.ts +4 -0
  59. package/src/optimizer/copy_prop.ts +4 -0
  60. package/src/optimizer/coroutine.ts +37 -1
  61. package/src/optimizer/dce.ts +6 -1
  62. package/src/optimizer/lir/const_imm.ts +1 -1
  63. package/src/optimizer/lir/dead_slot.ts +1 -1
  64. package/src/stdlib/timer.mcrs +10 -5
  65. package/src/typechecker/index.ts +39 -0
  66. package/examples/spiral.mcrs +0 -43
  67. package/src/examples/arena.mcrs +0 -44
  68. package/src/examples/counter.mcrs +0 -12
  69. package/src/examples/new_features_demo.mcrs +0 -193
  70. package/src/examples/rpg.mcrs +0 -13
  71. package/src/examples/stdlib_demo.mcrs +0 -181
@@ -40,6 +40,8 @@ export interface CoroutineResult {
40
40
  module: MIRModule
41
41
  /** Names of generated @tick dispatcher functions (caller must add to tick list). */
42
42
  generatedTickFunctions: string[]
43
+ /** Warning messages for skipped transforms. */
44
+ warnings: string[]
43
45
  }
44
46
 
45
47
  /**
@@ -51,11 +53,12 @@ export function coroutineTransform(
51
53
  mod: MIRModule,
52
54
  infos: CoroutineInfo[],
53
55
  ): CoroutineResult {
54
- if (infos.length === 0) return { module: mod, generatedTickFunctions: [] }
56
+ if (infos.length === 0) return { module: mod, generatedTickFunctions: [], warnings: [] }
55
57
 
56
58
  const infoMap = new Map(infos.map(i => [i.fnName, i]))
57
59
  const newFunctions: MIRFunction[] = []
58
60
  const tickFns: string[] = []
61
+ const warnings: string[] = []
59
62
 
60
63
  for (const fn of mod.functions) {
61
64
  const info = infoMap.get(fn.name)
@@ -64,6 +67,17 @@ export function coroutineTransform(
64
67
  continue
65
68
  }
66
69
 
70
+ // Skip transform if function contains macro calls — continuations are called
71
+ // directly (not via `function ... with storage`) so macro variables like
72
+ // ${px} would not be substituted, causing MC parse errors.
73
+ if (fnContainsMacroCalls(fn)) {
74
+ warnings.push(
75
+ `@coroutine cannot be applied to functions containing macro calls (skipped: ${fn.name})`,
76
+ )
77
+ newFunctions.push(fn)
78
+ continue
79
+ }
80
+
67
81
  const transformed = transformCoroutine(fn, info, mod.objective)
68
82
  newFunctions.push(transformed.initFn)
69
83
  newFunctions.push(...transformed.continuations)
@@ -74,7 +88,29 @@ export function coroutineTransform(
74
88
  return {
75
89
  module: { ...mod, functions: newFunctions },
76
90
  generatedTickFunctions: tickFns,
91
+ warnings,
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Returns true if any instruction in the function requires macro processing.
97
+ * This includes:
98
+ * - call_macro: explicit macro function invocations
99
+ * - call with fn = '__raw:\x01...': builtin calls (particle, summon, etc.) with macro params
100
+ * - call with fn = '__raw:<cmd>' where cmd contains '${': raw() commands with variable interpolation
101
+ */
102
+ function fnContainsMacroCalls(fn: MIRFunction): boolean {
103
+ for (const block of fn.blocks) {
104
+ for (const instr of [...block.instrs, block.term]) {
105
+ if (instr.kind === 'call_macro') return true
106
+ if (instr.kind === 'call' && instr.fn.startsWith('__raw:')) {
107
+ const cmd = instr.fn.slice(6)
108
+ // \x01 sentinel: builtin with macro params; '${': raw() with variable interpolation
109
+ if (cmd.startsWith('\x01') || cmd.includes('${')) return true
110
+ }
111
+ }
77
112
  }
113
+ return false
78
114
  }
79
115
 
80
116
  // ---------------------------------------------------------------------------
@@ -78,7 +78,8 @@ function recomputePreds(blocks: MIRBlock[]): MIRBlock[] {
78
78
 
79
79
  function hasSideEffects(instr: MIRInstr): boolean {
80
80
  if (instr.kind === 'call' || instr.kind === 'call_macro' ||
81
- instr.kind === 'call_context' || instr.kind === 'nbt_write') return true
81
+ instr.kind === 'call_context' || instr.kind === 'nbt_write' ||
82
+ instr.kind === 'score_write') return true
82
83
  // Return field temps (__rf_) write to global return slots — not dead even if unused locally
83
84
  // Option slot temps (__opt_) write observable scoreboard state — preserve even if var unused
84
85
  const dst = getDst(instr)
@@ -104,6 +105,8 @@ function getDst(instr: MIRInstr): Temp | null {
104
105
  return instr.dst
105
106
  case 'call': case 'call_macro':
106
107
  return instr.dst
108
+ case 'score_read':
109
+ return instr.dst
107
110
  default:
108
111
  return null
109
112
  }
@@ -129,6 +132,8 @@ function getUsedTemps(instr: MIRInstr): Temp[] {
129
132
  addOp(instr.cond); break
130
133
  case 'return':
131
134
  if (instr.value) addOp(instr.value); break
135
+ case 'score_write':
136
+ addOp(instr.src); break
132
137
  }
133
138
  return temps
134
139
  }
@@ -28,7 +28,7 @@ function countSlotUses(instrs: LIRInstr[], target: string): number {
28
28
 
29
29
  function extractSlotsFromRaw(cmd: string): Slot[] {
30
30
  const slots: Slot[] = []
31
- const re = /(\$[\w.]+)\s+(\S+)/g
31
+ const re = /(\$[\w.:]+)\s+(\S+)/g
32
32
  let m
33
33
  while ((m = re.exec(cmd)) !== null) {
34
34
  slots.push({ player: m[1], obj: m[2] })
@@ -24,7 +24,7 @@ function slotKey(s: Slot): string {
24
24
  function extractSlotsFromRaw(cmd: string): Slot[] {
25
25
  const slots: Slot[] = []
26
26
  // Match $<player> <obj> patterns (scoreboard slot references)
27
- const re = /(\$[\w.]+)\s+(\S+)/g
27
+ const re = /(\$[\w.:]+)\s+(\S+)/g
28
28
  let m
29
29
  while ((m = re.exec(cmd)) !== null) {
30
30
  slots.push({ player: m[1], obj: m[2] })
@@ -1,11 +1,16 @@
1
1
  // Timer utilities with an OOP-style API.
2
2
  //
3
- // Timer state is stored in scoreboard-backed runtime state. Because RedScript
4
- // does not yet support dynamic scoreboard player/objective names for impl
5
- // methods, this API currently uses a single shared runtime slot.
3
+ // Each Timer::new() call is statically allocated a unique compile-time ID
4
+ // (0, 1, 2, ...). The compiler intercepts Timer method calls and inlines
5
+ // them as direct scoreboard operations on per-instance slots:
6
+ // __timer_N_ticks, __timer_N_active (stored in the namespace objective)
6
7
  //
7
- // The `_id` field is reserved for a future runtime-backed instance identifier.
8
- // Today it remains `0`, while persistence is shared across Timer values.
8
+ // Restriction: Timer::new() must be called at the top level of a function,
9
+ // not inside loops or if/else bodies (compile error enforced by TypeChecker).
10
+ //
11
+ // This file provides the struct definition and method stubs. The actual
12
+ // scoreboard operations are generated by the MIR lowering pass and never
13
+ // call these method bodies directly when the _id is statically known.
9
14
 
10
15
  struct Timer {
11
16
  _id: int,
@@ -138,6 +138,9 @@ export class TypeChecker {
138
138
  private scope: Map<string, ScopeSymbol> = new Map()
139
139
  // Stack for tracking @s type in different contexts
140
140
  private selfTypeStack: EntityTypeName[] = ['entity']
141
+ // Depth of loop/conditional nesting (for static-allocation enforcement)
142
+ private loopDepth = 0
143
+ private condDepth = 0
141
144
 
142
145
  private readonly richTextBuiltins = new Map<string, { messageIndex: number }>([
143
146
  ['say', { messageIndex: 0 }],
@@ -352,17 +355,23 @@ export class TypeChecker {
352
355
  break
353
356
  case 'if':
354
357
  this.checkExpr(stmt.cond)
358
+ this.condDepth++
355
359
  this.checkIfBranches(stmt)
360
+ this.condDepth--
356
361
  break
357
362
  case 'while':
358
363
  this.checkExpr(stmt.cond)
364
+ this.loopDepth++
359
365
  this.checkBlock(stmt.body)
366
+ this.loopDepth--
360
367
  break
361
368
  case 'for':
362
369
  if (stmt.init) this.checkStmt(stmt.init)
363
370
  this.checkExpr(stmt.cond)
364
371
  this.checkExpr(stmt.step)
372
+ this.loopDepth++
365
373
  this.checkBlock(stmt.body)
374
+ this.loopDepth--
366
375
  break
367
376
  case 'foreach':
368
377
  this.checkExpr(stmt.iterable)
@@ -375,7 +384,9 @@ export class TypeChecker {
375
384
  })
376
385
  // Push self type context for @s inside the loop
377
386
  this.pushSelfType(entityType)
387
+ this.loopDepth++
378
388
  this.checkBlock(stmt.body)
389
+ this.loopDepth--
379
390
  this.popSelfType()
380
391
  } else {
381
392
  const iterableType = this.inferType(stmt.iterable)
@@ -384,7 +395,9 @@ export class TypeChecker {
384
395
  } else {
385
396
  this.scope.set(stmt.binding, { type: { kind: 'named', name: 'void' }, mutable: true })
386
397
  }
398
+ this.loopDepth++
387
399
  this.checkBlock(stmt.body)
400
+ this.loopDepth--
388
401
  }
389
402
  break
390
403
  case 'match':
@@ -712,6 +725,19 @@ export class TypeChecker {
712
725
 
713
726
  const builtin = BUILTIN_SIGNATURES[expr.fn]
714
727
  if (builtin) {
728
+ if (expr.fn === 'setTimeout' || expr.fn === 'setInterval') {
729
+ if (this.loopDepth > 0) {
730
+ this.report(
731
+ `${expr.fn}() cannot be called inside a loop. Declare timers at the top level.`,
732
+ expr
733
+ )
734
+ } else if (this.condDepth > 0) {
735
+ this.report(
736
+ `${expr.fn}() cannot be called inside an if/else body. Declare timers at the top level.`,
737
+ expr
738
+ )
739
+ }
740
+ }
715
741
  this.checkFunctionCallArgs(expr.args, builtin.params, expr.fn, expr)
716
742
  return
717
743
  }
@@ -907,6 +933,19 @@ export class TypeChecker {
907
933
  }
908
934
 
909
935
  private checkStaticCallExpr(expr: Extract<Expr, { kind: 'static_call' }>): void {
936
+ if (expr.type === 'Timer' && expr.method === 'new') {
937
+ if (this.loopDepth > 0) {
938
+ this.report(
939
+ `Timer::new() cannot be called inside a loop. Declare timers at the top level.`,
940
+ expr
941
+ )
942
+ } else if (this.condDepth > 0) {
943
+ this.report(
944
+ `Timer::new() cannot be called inside an if/else body. Declare timers at the top level.`,
945
+ expr
946
+ )
947
+ }
948
+ }
910
949
  const method = this.implMethods.get(expr.type)?.get(expr.method)
911
950
  if (!method) {
912
951
  this.report(`Type '${expr.type}' has no static method '${expr.method}'`, expr)
@@ -1,43 +0,0 @@
1
- // ===== Simple Particle Demo =====
2
- // 展示: @tick, 状态管理, f-strings, 控制命令
3
-
4
- // 状态
5
- let counter: int = 0;
6
- let running: bool = false;
7
-
8
- // ===== 主循环 =====
9
- @tick fn demo_tick() {
10
- if (!running) { return; }
11
-
12
- // 每 tick 增加计数器
13
- counter = counter + 1;
14
-
15
- // 在每个玩家位置生成粒子
16
- foreach (p in @a) at @s {
17
- particle("minecraft:end_rod", ~0, ~1, ~0, 0.5, 0.5, 0.5, 0.1, 5);
18
- }
19
-
20
- // 每 20 ticks (1秒) 报告一次
21
- if (counter % 20 == 0) {
22
- say(f"Running for {counter} ticks");
23
- }
24
- }
25
-
26
- // ===== 控制命令 =====
27
- // @keep 防止 DCE 删除
28
- @keep fn start() {
29
- running = true;
30
- counter = 0;
31
- say(f"Demo started!");
32
- }
33
-
34
- @keep fn stop() {
35
- running = false;
36
- say(f"Demo stopped at {counter} ticks.");
37
- }
38
-
39
- @keep fn reset() {
40
- running = false;
41
- counter = 0;
42
- say(f"Demo reset.");
43
- }
@@ -1,44 +0,0 @@
1
- // PvP arena scoreboard tracker.
2
- // Reads the vanilla kills objective, announces the top score every 200 ticks,
3
- // and tells the current leader(s) directly.
4
-
5
- @tick
6
- fn arena_tick() {
7
- let ticks: int = scoreboard_get("arena", #ticks);
8
- ticks = ticks + 1;
9
- scoreboard_set("arena", #ticks, ticks);
10
-
11
- if (ticks % 200 == 0) {
12
- announce_leaders();
13
- }
14
- }
15
-
16
- fn announce_leaders() {
17
- let top_kills: int = 0;
18
-
19
- foreach (player in @a) {
20
- let kills: int = scoreboard_get(player, #kills);
21
- if (kills > top_kills) {
22
- top_kills = kills;
23
- }
24
- }
25
-
26
- if (top_kills > 0) {
27
- announce("Arena update: leader check complete.");
28
- title_times(@a, 10, 40, 10);
29
- actionbar(@a, "Top kills updated");
30
-
31
- foreach (player in @a) {
32
- let kills: int = scoreboard_get(player, #kills);
33
- if (kills == top_kills) {
34
- tell(player, "You are leading the arena right now.");
35
- title(player, "Arena Leader");
36
- subtitle(player, "Hold the top score");
37
- actionbar(player, "Stay alive to keep the lead");
38
- }
39
- }
40
- } else {
41
- announce("Arena update: no PvP kills yet.");
42
- actionbar(@a, "No arena leader yet");
43
- }
44
- }
@@ -1,12 +0,0 @@
1
- // Tick counter that announces every 100 ticks.
2
-
3
- @tick
4
- fn counter_tick() {
5
- let ticks = scoreboard_get("counter", #ticks);
6
- ticks = ticks + 1;
7
- scoreboard_set("counter", #ticks, ticks);
8
-
9
- if (ticks % 100 == 0) {
10
- say("Counter reached another 100 ticks");
11
- }
12
- }
@@ -1,193 +0,0 @@
1
- /**
2
- * RedScript New Features Demo
3
- * Showcasing language features added on 2026-03-12
4
- */
5
-
6
- // ============================================
7
- // 1. Type Inference
8
- // ============================================
9
- fn type_inference_demo() {
10
- // No need to write `: int`, compiler infers automatically
11
- let health = 100;
12
- let name = "Steve";
13
- let alive = true;
14
- let speed = 1.5;
15
-
16
- // NBT suffixes can also be inferred
17
- let damage = 20b; // byte
18
- let distance = 1000s; // short
19
- let bignum = 999999L; // long
20
- let precise = 3.14d; // double
21
-
22
- say("Health: ${health}, Name: ${name}");
23
- }
24
-
25
- // ============================================
26
- // 2. For-Range Loops
27
- // ============================================
28
- fn for_range_demo() {
29
- // Loop from 0 to 9
30
- for i in 0..10 {
31
- say("Count: ${i}");
32
- }
33
-
34
- // Can be used for countdown
35
- for sec in 0..5 {
36
- title(@a, "Starting in ${sec}...");
37
- }
38
- }
39
-
40
- // ============================================
41
- // 3. NBT Structured Params
42
- // ============================================
43
- fn nbt_params_demo() {
44
- // Give item with NBT
45
- give(@s, "minecraft:diamond_sword", 1, {
46
- display: { Name: "Excalibur" },
47
- Enchantments: [
48
- { id: "minecraft:sharpness", lvl: 5 },
49
- { id: "minecraft:unbreaking", lvl: 3 }
50
- ]
51
- });
52
-
53
- // Summon entity with attributes
54
- summon("minecraft:zombie", @s, {
55
- CustomName: "Boss Zombie",
56
- Health: 100.0,
57
- Attributes: [
58
- { Name: "generic.max_health", Base: 100.0 }
59
- ]
60
- });
61
- }
62
-
63
- // ============================================
64
- // 4. Set Data Structure (Runtime Set)
65
- // ============================================
66
- fn set_demo() {
67
- // Create set
68
- let visited = set_new();
69
-
70
- // Add elements using method syntax
71
- visited.add("spawn");
72
- visited.add("castle");
73
- visited.add("dungeon");
74
-
75
- // Check if exists
76
- if (visited.contains("castle")) {
77
- say("You've been to the castle!");
78
- }
79
-
80
- // Remove element
81
- visited.remove("spawn");
82
-
83
- // Clear set
84
- visited.clear();
85
- }
86
-
87
- // ============================================
88
- // 5. Method Syntax Sugar
89
- // ============================================
90
- fn method_syntax_demo() {
91
- let items: string[] = [];
92
-
93
- // obj.method(args) → method(obj, args)
94
- items.push("sword");
95
- items.push("shield");
96
- items.push("potion");
97
-
98
- let count = items.len();
99
- say("You have ${count} items");
100
-
101
- // Sets can also use method syntax
102
- let tags = set_new();
103
- tags.add("vip");
104
- tags.add("admin");
105
-
106
- if (tags.contains("admin")) {
107
- say("Welcome, admin!");
108
- }
109
- }
110
-
111
- // ============================================
112
- // 6. #mc_name Syntax (MC Identifier Syntax)
113
- // ============================================
114
- fn mc_name_demo() {
115
- // #name compiles to bare MC name (without quotes)
116
- scoreboard_set(@s, #kills, 0);
117
- scoreboard_add(@s, #deaths, 1);
118
-
119
- let score = scoreboard_get(@s, #points);
120
-
121
- // For tag
122
- tag_add(@s, #vip);
123
-
124
- // For team
125
- team_join(@s, #red);
126
-
127
- // Comparison: strings still need quotes
128
- give(@s, "minecraft:diamond", 1);
129
- }
130
-
131
- // ============================================
132
- // 7. Block Comments
133
- // ============================================
134
-
135
- /*
136
- * This is a block comment
137
- * Can span multiple lines
138
- */
139
-
140
- /**
141
- * This is a doc comment
142
- * @param player Target player
143
- */
144
- fn documented_function() {
145
- say("Hello!");
146
- }
147
-
148
- // ============================================
149
- // Combined Example: Simple Game
150
- // ============================================
151
-
152
- struct Player {
153
- score: int,
154
- level: int,
155
- visited: string // set ID
156
- }
157
-
158
- fn init_player() {
159
- let visited_set = set_new();
160
- let p: Player = {
161
- score: 0,
162
- level: 1,
163
- visited: visited_set
164
- };
165
-
166
- // Record spawn point (using set_add since visited is a string ID)
167
- set_add(p.visited, "spawn");
168
-
169
- scoreboard_set(@s, #score, p.score);
170
- scoreboard_set(@s, #level, p.level);
171
- }
172
-
173
- @tick(rate=20)
174
- fn game_tick() {
175
- // Check once per second
176
- for i in 0..1 {
177
- let score = scoreboard_get(@s, #score);
178
- if (score >= 100) {
179
- title(@s, "Level Up!");
180
- scoreboard_set(@s, #level, 2);
181
- }
182
- }
183
- }
184
-
185
- fn reward_player(amount: int) {
186
- // Type inference + NBT params
187
- let bonus = amount * 2;
188
- scoreboard_add(@s, #score, bonus);
189
-
190
- give(@s, "minecraft:gold_ingot", amount, {
191
- display: { Name: "Reward Gold" }
192
- });
193
- }
@@ -1,13 +0,0 @@
1
- import "../stdlib/math.mcrs"
2
- import "../stdlib/combat.mcrs"
3
-
4
- fn attack(enemy: string, base: int, bonus: int) {
5
- let raw_damage = weapon_damage(base, bonus);
6
- let damage = clamp(raw_damage, 1, 20);
7
- apply_damage(enemy, damage);
8
- }
9
-
10
- @tick
11
- fn battle_tick() {
12
- attack("goblin", 4, 2);
13
- }