redscript-mc 1.2.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.
- 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 +591 -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/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 +79 -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 +618 -0
- package/src/parser/index.ts +97 -17
- package/src/typechecker/index.ts +65 -0
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
copyPropagation,
|
|
15
15
|
deadCodeEliminationWithStats,
|
|
16
16
|
} from './optimizer/passes'
|
|
17
|
+
import { eliminateDeadCode } from './optimizer/dce'
|
|
17
18
|
import {
|
|
18
19
|
countMcfunctionCommands,
|
|
19
20
|
generateDatapackWithStats,
|
|
@@ -30,6 +31,7 @@ export interface CompileOptions {
|
|
|
30
31
|
optimize?: boolean
|
|
31
32
|
typeCheck?: boolean
|
|
32
33
|
filePath?: string
|
|
34
|
+
dce?: boolean
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export interface CompileResult {
|
|
@@ -53,6 +55,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
|
|
|
53
55
|
const namespace = options.namespace ?? 'redscript'
|
|
54
56
|
const shouldOptimize = options.optimize ?? true
|
|
55
57
|
const shouldTypeCheck = options.typeCheck ?? true
|
|
58
|
+
const shouldRunDce = options.dce ?? shouldOptimize
|
|
56
59
|
const filePath = options.filePath
|
|
57
60
|
const preprocessed = preprocessSourceWithMetadata(source, { filePath })
|
|
58
61
|
const preprocessedSource = preprocessed.source
|
|
@@ -61,7 +64,8 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
|
|
|
61
64
|
const tokens = new Lexer(preprocessedSource, filePath).tokenize()
|
|
62
65
|
|
|
63
66
|
// Parsing
|
|
64
|
-
const
|
|
67
|
+
const parsedAst = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
|
|
68
|
+
const ast = shouldRunDce ? eliminateDeadCode(parsedAst) : parsedAst
|
|
65
69
|
|
|
66
70
|
// Type checking (warn mode - collect errors but don't block)
|
|
67
71
|
let typeErrors: DiagnosticError[] | undefined
|
package/src/lexer/index.ts
CHANGED
|
@@ -34,7 +34,10 @@ export type TokenKind =
|
|
|
34
34
|
| 'long_lit' // 1000L
|
|
35
35
|
| 'double_lit' // 3.14d
|
|
36
36
|
| 'string_lit' // "hello"
|
|
37
|
+
| 'f_string' // f"hello {name}"
|
|
37
38
|
| 'range_lit' // ..5 1.. 1..10
|
|
39
|
+
| 'rel_coord' // ~ ~5 ~-3 (relative coordinate)
|
|
40
|
+
| 'local_coord' // ^ ^5 ^-3 (local/facing coordinate)
|
|
38
41
|
// Operators
|
|
39
42
|
| '+' | '-' | '*' | '/' | '%'
|
|
40
43
|
| '~' | '^'
|
|
@@ -275,8 +278,52 @@ export class Lexer {
|
|
|
275
278
|
return
|
|
276
279
|
}
|
|
277
280
|
|
|
281
|
+
// Relative coordinate: ~ or ~5 or ~-3 or ~0.5
|
|
282
|
+
if (char === '~') {
|
|
283
|
+
let value = '~'
|
|
284
|
+
// Check for optional sign
|
|
285
|
+
if (this.peek() === '-' || this.peek() === '+') {
|
|
286
|
+
value += this.advance()
|
|
287
|
+
}
|
|
288
|
+
// Check for number
|
|
289
|
+
while (/[0-9]/.test(this.peek())) {
|
|
290
|
+
value += this.advance()
|
|
291
|
+
}
|
|
292
|
+
// Check for decimal part
|
|
293
|
+
if (this.peek() === '.' && /[0-9]/.test(this.peek(1))) {
|
|
294
|
+
value += this.advance() // .
|
|
295
|
+
while (/[0-9]/.test(this.peek())) {
|
|
296
|
+
value += this.advance()
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
this.addToken('rel_coord', value, startLine, startCol)
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Local coordinate: ^ or ^5 or ^-3 or ^0.5
|
|
304
|
+
if (char === '^') {
|
|
305
|
+
let value = '^'
|
|
306
|
+
// Check for optional sign
|
|
307
|
+
if (this.peek() === '-' || this.peek() === '+') {
|
|
308
|
+
value += this.advance()
|
|
309
|
+
}
|
|
310
|
+
// Check for number
|
|
311
|
+
while (/[0-9]/.test(this.peek())) {
|
|
312
|
+
value += this.advance()
|
|
313
|
+
}
|
|
314
|
+
// Check for decimal part
|
|
315
|
+
if (this.peek() === '.' && /[0-9]/.test(this.peek(1))) {
|
|
316
|
+
value += this.advance() // .
|
|
317
|
+
while (/[0-9]/.test(this.peek())) {
|
|
318
|
+
value += this.advance()
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
this.addToken('local_coord', value, startLine, startCol)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
278
325
|
// Single-character operators and delimiters
|
|
279
|
-
const singleChar: TokenKind[] = ['+', '-', '*', '/', '%', '
|
|
326
|
+
const singleChar: TokenKind[] = ['+', '-', '*', '/', '%', '<', '>', '!', '=',
|
|
280
327
|
'{', '}', '(', ')', '[', ']', ',', ';', ':', '.']
|
|
281
328
|
if (singleChar.includes(char as TokenKind)) {
|
|
282
329
|
this.addToken(char as TokenKind, char, startLine, startCol)
|
|
@@ -289,6 +336,13 @@ export class Lexer {
|
|
|
289
336
|
return
|
|
290
337
|
}
|
|
291
338
|
|
|
339
|
+
// f-string literal
|
|
340
|
+
if (char === 'f' && this.peek() === '"') {
|
|
341
|
+
this.advance()
|
|
342
|
+
this.scanFString(startLine, startCol)
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
292
346
|
// String literal
|
|
293
347
|
if (char === '"') {
|
|
294
348
|
this.scanString(startLine, startCol)
|
|
@@ -432,6 +486,53 @@ export class Lexer {
|
|
|
432
486
|
this.addToken('string_lit', value, startLine, startCol)
|
|
433
487
|
}
|
|
434
488
|
|
|
489
|
+
private scanFString(startLine: number, startCol: number): void {
|
|
490
|
+
let value = ''
|
|
491
|
+
let interpolationDepth = 0
|
|
492
|
+
let interpolationString = false
|
|
493
|
+
|
|
494
|
+
while (!this.isAtEnd()) {
|
|
495
|
+
if (interpolationDepth === 0 && this.peek() === '"') {
|
|
496
|
+
break
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (this.peek() === '\\' && this.peek(1) === '"') {
|
|
500
|
+
this.advance()
|
|
501
|
+
value += this.advance()
|
|
502
|
+
continue
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (interpolationDepth === 0 && this.peek() === '{') {
|
|
506
|
+
value += this.advance()
|
|
507
|
+
interpolationDepth = 1
|
|
508
|
+
interpolationString = false
|
|
509
|
+
continue
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const char = this.advance()
|
|
513
|
+
value += char
|
|
514
|
+
|
|
515
|
+
if (interpolationDepth === 0) continue
|
|
516
|
+
|
|
517
|
+
if (char === '"' && this.source[this.pos - 2] !== '\\') {
|
|
518
|
+
interpolationString = !interpolationString
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (interpolationString) continue
|
|
523
|
+
|
|
524
|
+
if (char === '{') interpolationDepth++
|
|
525
|
+
if (char === '}') interpolationDepth--
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (this.isAtEnd()) {
|
|
529
|
+
this.error('Unterminated f-string', startLine, startCol)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
this.advance() // closing quote
|
|
533
|
+
this.addToken('f_string', value, startLine, startCol)
|
|
534
|
+
}
|
|
535
|
+
|
|
435
536
|
private scanNumber(firstChar: string, startLine: number, startCol: number): void {
|
|
436
537
|
let value = firstChar
|
|
437
538
|
|
package/src/lowering/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { EVENT_TYPES, getEventParamSpecs, isEventTypeName } from '../events/type
|
|
|
25
25
|
const BUILTINS: Record<string, (args: string[]) => string | null> = {
|
|
26
26
|
say: ([msg]) => `say ${msg}`,
|
|
27
27
|
tell: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
|
|
28
|
+
tellraw: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
|
|
28
29
|
title: ([sel, msg]) => `title ${sel} title {"text":"${msg}"}`,
|
|
29
30
|
actionbar: ([sel, msg]) => `title ${sel} actionbar {"text":"${msg}"}`,
|
|
30
31
|
subtitle: ([sel, msg]) => `title ${sel} subtitle {"text":"${msg}"}`,
|
|
@@ -1234,6 +1235,7 @@ export class Lowering {
|
|
|
1234
1235
|
return { kind: 'const', value: 0 } // Handled inline in exprToString
|
|
1235
1236
|
|
|
1236
1237
|
case 'str_interp':
|
|
1238
|
+
case 'f_string':
|
|
1237
1239
|
// Interpolated strings are handled inline in message builtins.
|
|
1238
1240
|
return { kind: 'const', value: 0 }
|
|
1239
1241
|
|
|
@@ -2265,7 +2267,7 @@ export class Lowering {
|
|
|
2265
2267
|
}
|
|
2266
2268
|
|
|
2267
2269
|
const messageExpr = args[messageArgIndex]
|
|
2268
|
-
if (!messageExpr || messageExpr.kind !== 'str_interp') {
|
|
2270
|
+
if (!messageExpr || (messageExpr.kind !== 'str_interp' && messageExpr.kind !== 'f_string')) {
|
|
2269
2271
|
return null
|
|
2270
2272
|
}
|
|
2271
2273
|
|
|
@@ -2276,6 +2278,7 @@ export class Lowering {
|
|
|
2276
2278
|
case 'announce':
|
|
2277
2279
|
return `tellraw @a ${json}`
|
|
2278
2280
|
case 'tell':
|
|
2281
|
+
case 'tellraw':
|
|
2279
2282
|
return `tellraw ${this.exprToString(args[0])} ${json}`
|
|
2280
2283
|
case 'title':
|
|
2281
2284
|
return `title ${this.exprToString(args[0])} title ${json}`
|
|
@@ -2294,6 +2297,7 @@ export class Lowering {
|
|
|
2294
2297
|
case 'announce':
|
|
2295
2298
|
return 0
|
|
2296
2299
|
case 'tell':
|
|
2300
|
+
case 'tellraw':
|
|
2297
2301
|
case 'title':
|
|
2298
2302
|
case 'actionbar':
|
|
2299
2303
|
case 'subtitle':
|
|
@@ -2303,9 +2307,22 @@ export class Lowering {
|
|
|
2303
2307
|
}
|
|
2304
2308
|
}
|
|
2305
2309
|
|
|
2306
|
-
private buildRichTextJson(expr: Extract<Expr, { kind: 'str_interp' }>): string {
|
|
2310
|
+
private buildRichTextJson(expr: Extract<Expr, { kind: 'str_interp' | 'f_string' }>): string {
|
|
2307
2311
|
const components: Array<string | Record<string, unknown>> = ['']
|
|
2308
2312
|
|
|
2313
|
+
if (expr.kind === 'f_string') {
|
|
2314
|
+
for (const part of expr.parts) {
|
|
2315
|
+
if (part.kind === 'text') {
|
|
2316
|
+
if (part.value.length > 0) {
|
|
2317
|
+
components.push({ text: part.value })
|
|
2318
|
+
}
|
|
2319
|
+
continue
|
|
2320
|
+
}
|
|
2321
|
+
this.appendRichTextExpr(components, part.expr)
|
|
2322
|
+
}
|
|
2323
|
+
return JSON.stringify(components)
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2309
2326
|
for (const part of expr.parts) {
|
|
2310
2327
|
if (typeof part === 'string') {
|
|
2311
2328
|
if (part.length > 0) {
|
|
@@ -2354,6 +2371,19 @@ export class Lowering {
|
|
|
2354
2371
|
return
|
|
2355
2372
|
}
|
|
2356
2373
|
|
|
2374
|
+
if (expr.kind === 'f_string') {
|
|
2375
|
+
for (const part of expr.parts) {
|
|
2376
|
+
if (part.kind === 'text') {
|
|
2377
|
+
if (part.value.length > 0) {
|
|
2378
|
+
components.push({ text: part.value })
|
|
2379
|
+
}
|
|
2380
|
+
} else {
|
|
2381
|
+
this.appendRichTextExpr(components, part.expr)
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
return
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2357
2387
|
if (expr.kind === 'bool_lit') {
|
|
2358
2388
|
components.push({ text: expr.value ? 'true' : 'false' })
|
|
2359
2389
|
return
|
|
@@ -2392,6 +2422,10 @@ export class Lowering {
|
|
|
2392
2422
|
return `${expr.value}L`
|
|
2393
2423
|
case 'double_lit':
|
|
2394
2424
|
return `${expr.value}d`
|
|
2425
|
+
case 'rel_coord':
|
|
2426
|
+
return expr.value // ~ or ~5 or ~-3 - output as-is for MC commands
|
|
2427
|
+
case 'local_coord':
|
|
2428
|
+
return expr.value // ^ or ^5 or ^-3 - output as-is for MC commands
|
|
2395
2429
|
case 'bool_lit':
|
|
2396
2430
|
return expr.value ? '1' : '0'
|
|
2397
2431
|
case 'str_lit':
|
|
@@ -2399,6 +2433,7 @@ export class Lowering {
|
|
|
2399
2433
|
case 'mc_name':
|
|
2400
2434
|
return expr.value // #health → "health" (no quotes, used as bare MC name)
|
|
2401
2435
|
case 'str_interp':
|
|
2436
|
+
case 'f_string':
|
|
2402
2437
|
return this.buildRichTextJson(expr)
|
|
2403
2438
|
case 'blockpos':
|
|
2404
2439
|
return emitBlockPos(expr)
|
|
@@ -2732,6 +2767,7 @@ export class Lowering {
|
|
|
2732
2767
|
if (expr.kind === 'float_lit') return { kind: 'named', name: 'float' }
|
|
2733
2768
|
if (expr.kind === 'bool_lit') return { kind: 'named', name: 'bool' }
|
|
2734
2769
|
if (expr.kind === 'str_lit' || expr.kind === 'str_interp') return { kind: 'named', name: 'string' }
|
|
2770
|
+
if (expr.kind === 'f_string') return { kind: 'named', name: 'format_string' }
|
|
2735
2771
|
if (expr.kind === 'blockpos') return { kind: 'named', name: 'BlockPos' }
|
|
2736
2772
|
if (expr.kind === 'ident') {
|
|
2737
2773
|
const constValue = this.constValues.get(expr.name)
|