redscript-mc 1.2.0 → 1.2.2
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/CHANGELOG.md +5 -0
- package/README.md +53 -10
- package/README.zh.md +53 -10
- package/dist/__tests__/dce.test.d.ts +1 -0
- package/dist/__tests__/dce.test.js +137 -0
- package/dist/__tests__/lexer.test.js +19 -2
- package/dist/__tests__/lowering.test.js +8 -0
- package/dist/__tests__/mc-syntax.test.js +12 -0
- package/dist/__tests__/parser.test.js +10 -0
- package/dist/__tests__/runtime.test.js +13 -0
- package/dist/__tests__/typechecker.test.js +30 -0
- package/dist/ast/types.d.ts +22 -2
- package/dist/cli.js +15 -10
- package/dist/codegen/structure/index.d.ts +4 -1
- package/dist/codegen/structure/index.js +4 -2
- package/dist/compile.d.ts +1 -0
- package/dist/compile.js +4 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/lexer/index.d.ts +2 -1
- package/dist/lexer/index.js +89 -1
- package/dist/lowering/index.js +37 -1
- package/dist/optimizer/dce.d.ts +23 -0
- package/dist/optimizer/dce.js +592 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +81 -16
- package/dist/typechecker/index.d.ts +2 -0
- package/dist/typechecker/index.js +49 -0
- package/docs/ARCHITECTURE.zh.md +1088 -0
- package/editors/vscode/.vscodeignore +3 -0
- package/editors/vscode/icon.png +0 -0
- package/editors/vscode/out/extension.js +834 -19
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
- package/examples/spiral.mcrs +41 -0
- package/logo.png +0 -0
- package/package.json +1 -1
- package/src/__tests__/dce.test.ts +129 -0
- package/src/__tests__/lexer.test.ts +21 -2
- package/src/__tests__/lowering.test.ts +9 -0
- package/src/__tests__/mc-syntax.test.ts +14 -0
- package/src/__tests__/parser.test.ts +11 -0
- package/src/__tests__/runtime.test.ts +16 -0
- package/src/__tests__/typechecker.test.ts +33 -0
- package/src/ast/types.ts +14 -1
- package/src/cli.ts +24 -10
- package/src/codegen/structure/index.ts +13 -2
- package/src/compile.ts +5 -1
- package/src/index.ts +5 -1
- package/src/lexer/index.ts +102 -1
- package/src/lowering/index.ts +38 -2
- package/src/optimizer/dce.ts +619 -0
- package/src/parser/index.ts +97 -17
- package/src/typechecker/index.ts +65 -0
package/src/parser/index.ts
CHANGED
|
@@ -965,6 +965,18 @@ export class Parser {
|
|
|
965
965
|
return this.withLoc({ kind: 'float_lit', value: parseFloat(token.value) }, token)
|
|
966
966
|
}
|
|
967
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
|
+
|
|
968
980
|
// NBT suffix literals
|
|
969
981
|
if (token.kind === 'byte_lit') {
|
|
970
982
|
this.advance()
|
|
@@ -989,6 +1001,11 @@ export class Parser {
|
|
|
989
1001
|
return this.parseStringExpr(token)
|
|
990
1002
|
}
|
|
991
1003
|
|
|
1004
|
+
if (token.kind === 'f_string') {
|
|
1005
|
+
this.advance()
|
|
1006
|
+
return this.parseFStringExpr(token)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
992
1009
|
// MC name literal: #health → mc_name node (value = "health", without #)
|
|
993
1010
|
if (token.kind === 'mc_name') {
|
|
994
1011
|
this.advance()
|
|
@@ -1169,6 +1186,67 @@ export class Parser {
|
|
|
1169
1186
|
return this.withLoc({ kind: 'str_interp', parts }, token)
|
|
1170
1187
|
}
|
|
1171
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
|
+
|
|
1172
1250
|
private parseEmbeddedExpr(source: string): Expr {
|
|
1173
1251
|
const tokens = new Lexer(source, this.filePath).tokenize()
|
|
1174
1252
|
const parser = new Parser(tokens, source, this.filePath)
|
|
@@ -1343,20 +1421,11 @@ export class Parser {
|
|
|
1343
1421
|
return this.peek(offset + 1).kind === 'int_lit' ? 2 : 0
|
|
1344
1422
|
}
|
|
1345
1423
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
const next = this.peek(offset + 1)
|
|
1351
|
-
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') {
|
|
1352
1426
|
return 1
|
|
1353
1427
|
}
|
|
1354
|
-
|
|
1355
|
-
return 2
|
|
1356
|
-
}
|
|
1357
|
-
if (next.kind === '-' && this.peek(offset + 2).kind === 'int_lit') {
|
|
1358
|
-
return 3
|
|
1359
|
-
}
|
|
1428
|
+
|
|
1360
1429
|
return 0
|
|
1361
1430
|
}
|
|
1362
1431
|
|
|
@@ -1374,17 +1443,28 @@ export class Parser {
|
|
|
1374
1443
|
private parseCoordComponent(): CoordComponent {
|
|
1375
1444
|
const token = this.peek()
|
|
1376
1445
|
|
|
1377
|
-
|
|
1446
|
+
// Handle rel_coord (~, ~5, ~-3) and local_coord (^, ^5, ^-3) tokens
|
|
1447
|
+
if (token.kind === 'rel_coord') {
|
|
1378
1448
|
this.advance()
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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') {
|
|
1455
|
+
this.advance()
|
|
1456
|
+
const offset = this.parseCoordOffsetFromValue(token.value.slice(1))
|
|
1457
|
+
return { kind: 'local', offset }
|
|
1383
1458
|
}
|
|
1384
1459
|
|
|
1385
1460
|
return { kind: 'absolute', value: this.parseSignedCoordOffset(true) }
|
|
1386
1461
|
}
|
|
1387
1462
|
|
|
1463
|
+
private parseCoordOffsetFromValue(value: string): number {
|
|
1464
|
+
if (value === '' || value === undefined) return 0
|
|
1465
|
+
return parseFloat(value)
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1388
1468
|
private parseSignedCoordOffset(requireValue = false): number {
|
|
1389
1469
|
let sign = 1
|
|
1390
1470
|
if (this.match('-')) {
|
package/src/typechecker/index.ts
CHANGED
|
@@ -73,6 +73,8 @@ const MC_TYPE_TO_ENTITY: Record<string, EntityTypeName> = {
|
|
|
73
73
|
|
|
74
74
|
const VOID_TYPE: TypeNode = { kind: 'named', name: 'void' }
|
|
75
75
|
const INT_TYPE: TypeNode = { kind: 'named', name: 'int' }
|
|
76
|
+
const STRING_TYPE: TypeNode = { kind: 'named', name: 'string' }
|
|
77
|
+
const FORMAT_STRING_TYPE: TypeNode = { kind: 'named', name: 'format_string' }
|
|
76
78
|
|
|
77
79
|
const BUILTIN_SIGNATURES: Record<string, BuiltinSignature> = {
|
|
78
80
|
setTimeout: {
|
|
@@ -106,6 +108,16 @@ export class TypeChecker {
|
|
|
106
108
|
// Stack for tracking @s type in different contexts
|
|
107
109
|
private selfTypeStack: EntityTypeName[] = ['entity']
|
|
108
110
|
|
|
111
|
+
private readonly richTextBuiltins = new Map<string, { messageIndex: number }>([
|
|
112
|
+
['say', { messageIndex: 0 }],
|
|
113
|
+
['announce', { messageIndex: 0 }],
|
|
114
|
+
['tell', { messageIndex: 1 }],
|
|
115
|
+
['tellraw', { messageIndex: 1 }],
|
|
116
|
+
['title', { messageIndex: 1 }],
|
|
117
|
+
['actionbar', { messageIndex: 1 }],
|
|
118
|
+
['subtitle', { messageIndex: 1 }],
|
|
119
|
+
])
|
|
120
|
+
|
|
109
121
|
constructor(source?: string, filePath?: string) {
|
|
110
122
|
this.collector = new DiagnosticCollector(source, filePath)
|
|
111
123
|
}
|
|
@@ -510,6 +522,24 @@ export class TypeChecker {
|
|
|
510
522
|
}
|
|
511
523
|
break
|
|
512
524
|
|
|
525
|
+
case 'f_string':
|
|
526
|
+
for (const part of expr.parts) {
|
|
527
|
+
if (part.kind !== 'expr') {
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
this.checkExpr(part.expr)
|
|
531
|
+
const partType = this.inferType(part.expr)
|
|
532
|
+
if (
|
|
533
|
+
!(partType.kind === 'named' && (partType.name === 'int' || partType.name === 'string' || partType.name === 'format_string'))
|
|
534
|
+
) {
|
|
535
|
+
this.report(
|
|
536
|
+
`f-string placeholder must be int or string, got ${this.typeToString(partType)}`,
|
|
537
|
+
part.expr
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
break
|
|
542
|
+
|
|
513
543
|
case 'array_lit':
|
|
514
544
|
for (const elem of expr.elements) {
|
|
515
545
|
this.checkExpr(elem)
|
|
@@ -544,6 +574,12 @@ export class TypeChecker {
|
|
|
544
574
|
this.checkTpCall(expr)
|
|
545
575
|
}
|
|
546
576
|
|
|
577
|
+
const richTextBuiltin = this.richTextBuiltins.get(expr.fn)
|
|
578
|
+
if (richTextBuiltin) {
|
|
579
|
+
this.checkRichTextBuiltinCall(expr, richTextBuiltin.messageIndex)
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
547
583
|
const builtin = BUILTIN_SIGNATURES[expr.fn]
|
|
548
584
|
if (builtin) {
|
|
549
585
|
this.checkFunctionCallArgs(expr.args, builtin.params, expr.fn, expr)
|
|
@@ -602,6 +638,28 @@ export class TypeChecker {
|
|
|
602
638
|
// Built-in functions are not checked for arg count
|
|
603
639
|
}
|
|
604
640
|
|
|
641
|
+
private checkRichTextBuiltinCall(expr: Extract<Expr, { kind: 'call' }>, messageIndex: number): void {
|
|
642
|
+
for (let i = 0; i < expr.args.length; i++) {
|
|
643
|
+
this.checkExpr(expr.args[i], i === messageIndex ? undefined : STRING_TYPE)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const message = expr.args[messageIndex]
|
|
647
|
+
if (!message) {
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const messageType = this.inferType(message)
|
|
652
|
+
if (
|
|
653
|
+
messageType.kind !== 'named' ||
|
|
654
|
+
(messageType.name !== 'string' && messageType.name !== 'format_string')
|
|
655
|
+
) {
|
|
656
|
+
this.report(
|
|
657
|
+
`Argument ${messageIndex + 1} of '${expr.fn}' expects string or format_string, got ${this.typeToString(messageType)}`,
|
|
658
|
+
message
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
605
663
|
private checkInvokeExpr(expr: Extract<Expr, { kind: 'invoke' }>): void {
|
|
606
664
|
this.checkExpr(expr.callee)
|
|
607
665
|
const calleeType = this.inferType(expr.callee)
|
|
@@ -838,6 +896,13 @@ export class TypeChecker {
|
|
|
838
896
|
}
|
|
839
897
|
}
|
|
840
898
|
return { kind: 'named', name: 'string' }
|
|
899
|
+
case 'f_string':
|
|
900
|
+
for (const part of expr.parts) {
|
|
901
|
+
if (part.kind === 'expr') {
|
|
902
|
+
this.checkExpr(part.expr)
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return FORMAT_STRING_TYPE
|
|
841
906
|
case 'blockpos':
|
|
842
907
|
return { kind: 'named', name: 'BlockPos' }
|
|
843
908
|
case 'ident':
|