redscript-mc 1.1.0 → 1.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 (83) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/cli.test.js +138 -0
  5. package/dist/__tests__/codegen.test.js +25 -0
  6. package/dist/__tests__/dce.test.d.ts +1 -0
  7. package/dist/__tests__/dce.test.js +137 -0
  8. package/dist/__tests__/e2e.test.js +190 -12
  9. package/dist/__tests__/lexer.test.js +31 -4
  10. package/dist/__tests__/lowering.test.js +172 -9
  11. package/dist/__tests__/mc-integration.test.js +145 -51
  12. package/dist/__tests__/mc-syntax.test.js +12 -0
  13. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  14. package/dist/__tests__/parser.test.js +90 -0
  15. package/dist/__tests__/runtime.test.js +21 -8
  16. package/dist/__tests__/typechecker.test.js +188 -0
  17. package/dist/ast/types.d.ts +42 -3
  18. package/dist/cli.js +15 -10
  19. package/dist/codegen/mcfunction/index.js +30 -1
  20. package/dist/codegen/structure/index.d.ts +4 -1
  21. package/dist/codegen/structure/index.js +29 -2
  22. package/dist/compile.d.ts +11 -0
  23. package/dist/compile.js +40 -6
  24. package/dist/events/types.d.ts +35 -0
  25. package/dist/events/types.js +59 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +7 -3
  28. package/dist/ir/types.d.ts +4 -0
  29. package/dist/lexer/index.d.ts +2 -1
  30. package/dist/lexer/index.js +91 -1
  31. package/dist/lowering/index.d.ts +32 -1
  32. package/dist/lowering/index.js +476 -16
  33. package/dist/optimizer/dce.d.ts +23 -0
  34. package/dist/optimizer/dce.js +591 -0
  35. package/dist/parser/index.d.ts +4 -0
  36. package/dist/parser/index.js +160 -26
  37. package/dist/typechecker/index.d.ts +19 -0
  38. package/dist/typechecker/index.js +392 -17
  39. package/docs/ARCHITECTURE.zh.md +1088 -0
  40. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  41. package/editors/vscode/.vscodeignore +3 -0
  42. package/editors/vscode/CHANGELOG.md +9 -0
  43. package/editors/vscode/icon.png +0 -0
  44. package/editors/vscode/out/extension.js +1144 -72
  45. package/editors/vscode/package-lock.json +2 -2
  46. package/editors/vscode/package.json +1 -1
  47. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  48. package/examples/spiral.mcrs +79 -0
  49. package/logo.png +0 -0
  50. package/package.json +1 -1
  51. package/src/__tests__/cli.test.ts +166 -0
  52. package/src/__tests__/codegen.test.ts +27 -0
  53. package/src/__tests__/dce.test.ts +129 -0
  54. package/src/__tests__/e2e.test.ts +201 -12
  55. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  56. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  57. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  58. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  59. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  60. package/src/__tests__/lexer.test.ts +35 -4
  61. package/src/__tests__/lowering.test.ts +187 -9
  62. package/src/__tests__/mc-integration.test.ts +166 -51
  63. package/src/__tests__/mc-syntax.test.ts +14 -0
  64. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  65. package/src/__tests__/parser.test.ts +102 -5
  66. package/src/__tests__/runtime.test.ts +24 -8
  67. package/src/__tests__/typechecker.test.ts +204 -0
  68. package/src/ast/types.ts +39 -2
  69. package/src/cli.ts +24 -10
  70. package/src/codegen/mcfunction/index.ts +31 -1
  71. package/src/codegen/structure/index.ts +40 -2
  72. package/src/compile.ts +59 -7
  73. package/src/events/types.ts +69 -0
  74. package/src/index.ts +9 -4
  75. package/src/ir/types.ts +4 -0
  76. package/src/lexer/index.ts +105 -2
  77. package/src/lowering/index.ts +566 -18
  78. package/src/optimizer/dce.ts +618 -0
  79. package/src/parser/index.ts +187 -29
  80. package/src/stdlib/README.md +34 -4
  81. package/src/stdlib/tags.mcrs +951 -0
  82. package/src/stdlib/timer.mcrs +54 -33
  83. package/src/typechecker/index.ts +469 -18
@@ -9,8 +9,8 @@ import { Lexer, type Token, type TokenKind } from '../lexer'
9
9
  import type {
10
10
  Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, GlobalDecl, LiteralExpr, Param,
11
11
  Program, RangeExpr, SelectorFilter, SelectorKind, Span, Stmt, TypeNode, AssignOp,
12
- StructDecl, StructField, ExecuteSubcommand, EnumDecl, EnumVariant, BlockPosExpr,
13
- CoordComponent, LambdaParam
12
+ StructDecl, StructField, ExecuteSubcommand, EnumDecl, EnumVariant, BlockPosExpr, ImplBlock,
13
+ CoordComponent, LambdaParam, EntityTypeName
14
14
  } from '../ast/types'
15
15
  import type { BinOp, CmpOp } from '../ir/types'
16
16
  import { DiagnosticError } from '../diagnostics'
@@ -23,12 +23,33 @@ const PRECEDENCE: Record<string, number> = {
23
23
  '||': 1,
24
24
  '&&': 2,
25
25
  '==': 3, '!=': 3,
26
- '<': 4, '<=': 4, '>': 4, '>=': 4,
26
+ '<': 4, '<=': 4, '>': 4, '>=': 4, 'is': 4,
27
27
  '+': 5, '-': 5,
28
28
  '*': 6, '/': 6, '%': 6,
29
29
  }
30
30
 
31
- const BINARY_OPS = new Set(['||', '&&', '==', '!=', '<', '<=', '>', '>=', '+', '-', '*', '/', '%'])
31
+ const BINARY_OPS = new Set(['||', '&&', '==', '!=', '<', '<=', '>', '>=', 'is', '+', '-', '*', '/', '%'])
32
+
33
+ const ENTITY_TYPE_NAMES = new Set<EntityTypeName>([
34
+ 'entity',
35
+ 'Player',
36
+ 'Mob',
37
+ 'HostileMob',
38
+ 'PassiveMob',
39
+ 'Zombie',
40
+ 'Skeleton',
41
+ 'Creeper',
42
+ 'Spider',
43
+ 'Enderman',
44
+ 'Pig',
45
+ 'Cow',
46
+ 'Sheep',
47
+ 'Chicken',
48
+ 'Villager',
49
+ 'ArmorStand',
50
+ 'Item',
51
+ 'Arrow',
52
+ ])
32
53
 
33
54
  function computeIsSingle(raw: string): boolean {
34
55
  if (/^@[spr](\[|$)/.test(raw)) return true
@@ -135,6 +156,7 @@ export class Parser {
135
156
  const globals: GlobalDecl[] = []
136
157
  const declarations: FnDecl[] = []
137
158
  const structs: StructDecl[] = []
159
+ const implBlocks: ImplBlock[] = []
138
160
  const enums: EnumDecl[] = []
139
161
  const consts: ConstDecl[] = []
140
162
 
@@ -152,6 +174,8 @@ export class Parser {
152
174
  globals.push(this.parseGlobalDecl(true))
153
175
  } else if (this.check('struct')) {
154
176
  structs.push(this.parseStructDecl())
177
+ } else if (this.check('impl')) {
178
+ implBlocks.push(this.parseImplBlock())
155
179
  } else if (this.check('enum')) {
156
180
  enums.push(this.parseEnumDecl())
157
181
  } else if (this.check('const')) {
@@ -161,7 +185,7 @@ export class Parser {
161
185
  }
162
186
  }
163
187
 
164
- return { namespace, globals, declarations, structs, enums, consts }
188
+ return { namespace, globals, declarations, structs, implBlocks, enums, consts }
165
189
  }
166
190
 
167
191
  // -------------------------------------------------------------------------
@@ -219,6 +243,20 @@ export class Parser {
219
243
  return this.withLoc({ name, variants }, enumToken)
220
244
  }
221
245
 
246
+ private parseImplBlock(): ImplBlock {
247
+ const implToken = this.expect('impl')
248
+ const typeName = this.expect('ident').value
249
+ this.expect('{')
250
+
251
+ const methods: FnDecl[] = []
252
+ while (!this.check('}') && !this.check('eof')) {
253
+ methods.push(this.parseFnDecl(typeName))
254
+ }
255
+
256
+ this.expect('}')
257
+ return this.withLoc({ kind: 'impl_block', typeName, methods }, implToken)
258
+ }
259
+
222
260
  private parseConstDecl(): ConstDecl {
223
261
  const constToken = this.expect('const')
224
262
  const name = this.expect('ident').value
@@ -245,17 +283,17 @@ export class Parser {
245
283
  // Function Declaration
246
284
  // -------------------------------------------------------------------------
247
285
 
248
- private parseFnDecl(): FnDecl {
286
+ private parseFnDecl(implTypeName?: string): FnDecl {
249
287
  const decorators = this.parseDecorators()
250
288
 
251
289
  const fnToken = this.expect('fn')
252
290
  const name = this.expect('ident').value
253
291
  this.expect('(')
254
- const params = this.parseParams()
292
+ const params = this.parseParams(implTypeName)
255
293
  this.expect(')')
256
294
 
257
295
  let returnType: TypeNode = { kind: 'named', name: 'void' }
258
- if (this.match('->')) {
296
+ if (this.match('->') || this.match(':')) {
259
297
  returnType = this.parseType()
260
298
  }
261
299
 
@@ -277,7 +315,7 @@ export class Parser {
277
315
  }
278
316
 
279
317
  private parseDecoratorValue(value: string): Decorator {
280
- // Parse @tick or @on_trigger("name") or @on_advancement("story/mine_diamond")
318
+ // Parse @tick, @on(PlayerDeath), or @on_trigger("name")
281
319
  const match = value.match(/^@(\w+)(?:\(([^)]*)\))?$/)
282
320
  if (!match) {
283
321
  this.error(`Invalid decorator: ${value}`)
@@ -292,6 +330,14 @@ export class Parser {
292
330
 
293
331
  const args: Decorator['args'] = {}
294
332
 
333
+ if (name === 'on') {
334
+ const eventTypeMatch = argsStr.match(/^([A-Za-z_][A-Za-z0-9_]*)$/)
335
+ if (eventTypeMatch) {
336
+ args.eventType = eventTypeMatch[1]
337
+ return { name, args }
338
+ }
339
+ }
340
+
295
341
  // Handle @on_trigger("name"), @on_advancement("id"), @on_craft("item"), @on_join_team("team")
296
342
  if (name === 'on_trigger' || name === 'on_advancement' || name === 'on_craft' || name === 'on_join_team') {
297
343
  const strMatch = argsStr.match(/^"([^"]*)"$/)
@@ -328,15 +374,20 @@ export class Parser {
328
374
  return { name, args }
329
375
  }
330
376
 
331
- private parseParams(): Param[] {
377
+ private parseParams(implTypeName?: string): Param[] {
332
378
  const params: Param[] = []
333
379
 
334
380
  if (!this.check(')')) {
335
381
  do {
336
382
  const paramToken = this.expect('ident')
337
383
  const name = paramToken.value
338
- this.expect(':')
339
- const type = this.parseType()
384
+ let type: TypeNode
385
+ if (implTypeName && params.length === 0 && name === 'self' && !this.check(':')) {
386
+ type = { kind: 'struct', name: implTypeName }
387
+ } else {
388
+ this.expect(':')
389
+ type = this.parseType()
390
+ }
340
391
  let defaultValue: Expr | undefined
341
392
  if (this.match('=')) {
342
393
  defaultValue = this.parseExpr()
@@ -740,6 +791,15 @@ export class Parser {
740
791
  if (prec < minPrec) break
741
792
 
742
793
  const opToken = this.advance()
794
+ if (op === 'is') {
795
+ const entityType = this.parseEntityTypeName()
796
+ left = this.withLoc(
797
+ { kind: 'is_check', expr: left, entityType },
798
+ this.getLocToken(left) ?? opToken
799
+ )
800
+ continue
801
+ }
802
+
743
803
  const right = this.parseBinaryExpr(prec + 1) // left associative
744
804
  left = this.withLoc(
745
805
  { kind: 'binary', op: op as BinOp | CmpOp | '&&' | '||', left, right },
@@ -766,6 +826,14 @@ export class Parser {
766
826
  return this.parsePostfixExpr()
767
827
  }
768
828
 
829
+ private parseEntityTypeName(): EntityTypeName {
830
+ const token = this.expect('ident')
831
+ if (ENTITY_TYPE_NAMES.has(token.value as EntityTypeName)) {
832
+ return token.value as EntityTypeName
833
+ }
834
+ this.error(`Unknown entity type '${token.value}'`)
835
+ }
836
+
769
837
  private isSubtraction(): boolean {
770
838
  // Check if this minus is binary (subtraction) by looking at previous token
771
839
  // If previous was a value (literal, ident, ), ]) it's subtraction
@@ -871,6 +939,16 @@ export class Parser {
871
939
  private parsePrimaryExpr(): Expr {
872
940
  const token = this.peek()
873
941
 
942
+ if (token.kind === 'ident' && this.peek(1).kind === '::') {
943
+ const typeToken = this.advance()
944
+ this.expect('::')
945
+ const methodToken = this.expect('ident')
946
+ this.expect('(')
947
+ const args = this.parseArgs()
948
+ this.expect(')')
949
+ return this.withLoc({ kind: 'static_call', type: typeToken.value, method: methodToken.value, args }, typeToken)
950
+ }
951
+
874
952
  if (token.kind === 'ident' && this.peek(1).kind === '=>') {
875
953
  return this.parseSingleParamLambda()
876
954
  }
@@ -887,6 +965,18 @@ export class Parser {
887
965
  return this.withLoc({ kind: 'float_lit', value: parseFloat(token.value) }, token)
888
966
  }
889
967
 
968
+ // Relative coordinate: ~ ~5 ~-3 ~0.5
969
+ if (token.kind === 'rel_coord') {
970
+ this.advance()
971
+ return this.withLoc({ kind: 'rel_coord', value: token.value }, token)
972
+ }
973
+
974
+ // Local coordinate: ^ ^5 ^-3 ^0.5
975
+ if (token.kind === 'local_coord') {
976
+ this.advance()
977
+ return this.withLoc({ kind: 'local_coord', value: token.value }, token)
978
+ }
979
+
890
980
  // NBT suffix literals
891
981
  if (token.kind === 'byte_lit') {
892
982
  this.advance()
@@ -911,6 +1001,11 @@ export class Parser {
911
1001
  return this.parseStringExpr(token)
912
1002
  }
913
1003
 
1004
+ if (token.kind === 'f_string') {
1005
+ this.advance()
1006
+ return this.parseFStringExpr(token)
1007
+ }
1008
+
914
1009
  // MC name literal: #health → mc_name node (value = "health", without #)
915
1010
  if (token.kind === 'mc_name') {
916
1011
  this.advance()
@@ -1091,6 +1186,67 @@ export class Parser {
1091
1186
  return this.withLoc({ kind: 'str_interp', parts }, token)
1092
1187
  }
1093
1188
 
1189
+ private parseFStringExpr(token: Token): Expr {
1190
+ const parts: Array<{ kind: 'text'; value: string } | { kind: 'expr'; expr: Expr }> = []
1191
+ let current = ''
1192
+ let index = 0
1193
+
1194
+ while (index < token.value.length) {
1195
+ if (token.value[index] === '{') {
1196
+ if (current) {
1197
+ parts.push({ kind: 'text', value: current })
1198
+ current = ''
1199
+ }
1200
+
1201
+ index++
1202
+ let depth = 1
1203
+ let exprSource = ''
1204
+ let inString = false
1205
+
1206
+ while (index < token.value.length && depth > 0) {
1207
+ const char = token.value[index]
1208
+
1209
+ if (char === '"' && token.value[index - 1] !== '\\') {
1210
+ inString = !inString
1211
+ }
1212
+
1213
+ if (!inString) {
1214
+ if (char === '{') {
1215
+ depth++
1216
+ } else if (char === '}') {
1217
+ depth--
1218
+ if (depth === 0) {
1219
+ index++
1220
+ break
1221
+ }
1222
+ }
1223
+ }
1224
+
1225
+ if (depth > 0) {
1226
+ exprSource += char
1227
+ }
1228
+ index++
1229
+ }
1230
+
1231
+ if (depth !== 0) {
1232
+ this.error('Unterminated f-string interpolation')
1233
+ }
1234
+
1235
+ parts.push({ kind: 'expr', expr: this.parseEmbeddedExpr(exprSource) })
1236
+ continue
1237
+ }
1238
+
1239
+ current += token.value[index]
1240
+ index++
1241
+ }
1242
+
1243
+ if (current) {
1244
+ parts.push({ kind: 'text', value: current })
1245
+ }
1246
+
1247
+ return this.withLoc({ kind: 'f_string', parts }, token)
1248
+ }
1249
+
1094
1250
  private parseEmbeddedExpr(source: string): Expr {
1095
1251
  const tokens = new Lexer(source, this.filePath).tokenize()
1096
1252
  const parser = new Parser(tokens, source, this.filePath)
@@ -1265,20 +1421,11 @@ export class Parser {
1265
1421
  return this.peek(offset + 1).kind === 'int_lit' ? 2 : 0
1266
1422
  }
1267
1423
 
1268
- if (token.kind !== '~' && token.kind !== '^') {
1269
- return 0
1270
- }
1271
-
1272
- const next = this.peek(offset + 1)
1273
- if (next.kind === ',' || next.kind === ')') {
1424
+ // rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) are single tokens now
1425
+ if (token.kind === 'rel_coord' || token.kind === 'local_coord') {
1274
1426
  return 1
1275
1427
  }
1276
- if (next.kind === 'int_lit') {
1277
- return 2
1278
- }
1279
- if (next.kind === '-' && this.peek(offset + 2).kind === 'int_lit') {
1280
- return 3
1281
- }
1428
+
1282
1429
  return 0
1283
1430
  }
1284
1431
 
@@ -1296,17 +1443,28 @@ export class Parser {
1296
1443
  private parseCoordComponent(): CoordComponent {
1297
1444
  const token = this.peek()
1298
1445
 
1299
- if (token.kind === '~' || token.kind === '^') {
1446
+ // Handle rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) tokens
1447
+ if (token.kind === 'rel_coord') {
1448
+ this.advance()
1449
+ // Parse the offset from the token value (e.g., "~5" -> 5, "~" -> 0, "~-3" -> -3)
1450
+ const offset = this.parseCoordOffsetFromValue(token.value.slice(1))
1451
+ return { kind: 'relative', offset }
1452
+ }
1453
+
1454
+ if (token.kind === 'local_coord') {
1300
1455
  this.advance()
1301
- const offset = this.parseSignedCoordOffset()
1302
- return token.kind === '~'
1303
- ? { kind: 'relative', offset }
1304
- : { kind: 'local', offset }
1456
+ const offset = this.parseCoordOffsetFromValue(token.value.slice(1))
1457
+ return { kind: 'local', offset }
1305
1458
  }
1306
1459
 
1307
1460
  return { kind: 'absolute', value: this.parseSignedCoordOffset(true) }
1308
1461
  }
1309
1462
 
1463
+ private parseCoordOffsetFromValue(value: string): number {
1464
+ if (value === '' || value === undefined) return 0
1465
+ return parseFloat(value)
1466
+ }
1467
+
1310
1468
  private parseSignedCoordOffset(requireValue = false): number {
1311
1469
  let sign = 1
1312
1470
  if (this.match('-')) {
@@ -70,10 +70,13 @@ Cooldown system using scoreboards.
70
70
  - `is_on_cooldown(target)` → int
71
71
 
72
72
  ### timer.mcrs
73
- Timer utilities.
74
- - `timer_start(name, ticks)`
75
- - `timer_tick()`
76
- - `timer_done(name)` → int
73
+ Timer utilities with an OOP API.
74
+ - `Timer::new(ticks)` → `Timer`
75
+ - `timer.start()`, `timer.pause()`, `timer.reset()`
76
+ - `timer.done()` → bool
77
+ - `timer.elapsed()` → int
78
+ - `timer.remaining()` → int
79
+ - `timer.tick()` — manual tick update; current runtime uses one shared timer slot
77
80
 
78
81
  ### combat.mcrs
79
82
  Combat helpers.
@@ -162,3 +165,30 @@ Player input detection (right click, sneak, look direction).
162
165
  // Position ranges
163
166
  @a[x=-5..5, y=62..68, z=-5..5] // In specific area
164
167
  ```
168
+
169
+ ### tags.mcrs
170
+ Minecraft Java Edition tag constants generated from the Minecraft Fandom tag list.
171
+
172
+ #### Coverage
173
+ - 171 `BLOCK_*` constants for Java block tags
174
+ - 14 `ENTITY_*` constants for Java entity type tags
175
+ - 99 `ITEM_*` constants for Java item tags
176
+ - 2 `FLUID_*` constants for Java fluid tags
177
+ - 27 `DAMAGE_*` constants for Java damage type tags
178
+
179
+ #### Naming
180
+ - Constants use `SCREAMING_SNAKE_CASE`
181
+ - Each constant is prefixed by category: `BLOCK_`, `ENTITY_`, `ITEM_`, `FLUID_`, `DAMAGE_`
182
+ - Each value is the full tag selector string, for example `#minecraft:mineable/axe`
183
+
184
+ #### Usage
185
+ ```mcrs
186
+ import "stdlib/tags.mcrs"
187
+
188
+ // Select skeleton variants
189
+ kill(@e[type=ENTITY_SKELETONS]);
190
+
191
+ // Use block and item tags in your own helpers
192
+ const LOGS: string = BLOCK_LOGS;
193
+ const SWORDS: string = ITEM_SWORDS;
194
+ ```