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.
- package/CHANGELOG.md +59 -0
- package/README.md +53 -10
- package/README.zh.md +53 -10
- package/dist/__tests__/cli.test.js +138 -0
- package/dist/__tests__/codegen.test.js +25 -0
- package/dist/__tests__/dce.test.d.ts +1 -0
- package/dist/__tests__/dce.test.js +137 -0
- package/dist/__tests__/e2e.test.js +190 -12
- package/dist/__tests__/lexer.test.js +31 -4
- package/dist/__tests__/lowering.test.js +172 -9
- package/dist/__tests__/mc-integration.test.js +145 -51
- package/dist/__tests__/mc-syntax.test.js +12 -0
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/parser.test.js +90 -0
- package/dist/__tests__/runtime.test.js +21 -8
- package/dist/__tests__/typechecker.test.js +188 -0
- package/dist/ast/types.d.ts +42 -3
- package/dist/cli.js +15 -10
- package/dist/codegen/mcfunction/index.js +30 -1
- package/dist/codegen/structure/index.d.ts +4 -1
- package/dist/codegen/structure/index.js +29 -2
- package/dist/compile.d.ts +11 -0
- package/dist/compile.js +40 -6
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -3
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +2 -1
- package/dist/lexer/index.js +91 -1
- package/dist/lowering/index.d.ts +32 -1
- package/dist/lowering/index.js +476 -16
- package/dist/optimizer/dce.d.ts +23 -0
- package/dist/optimizer/dce.js +591 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +160 -26
- package/dist/typechecker/index.d.ts +19 -0
- package/dist/typechecker/index.js +392 -17
- package/docs/ARCHITECTURE.zh.md +1088 -0
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/editors/vscode/.vscodeignore +3 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icon.png +0 -0
- package/editors/vscode/out/extension.js +1144 -72
- 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__/cli.test.ts +166 -0
- package/src/__tests__/codegen.test.ts +27 -0
- package/src/__tests__/dce.test.ts +129 -0
- package/src/__tests__/e2e.test.ts +201 -12
- package/src/__tests__/fixtures/event-test.mcrs +13 -0
- package/src/__tests__/fixtures/impl-test.mcrs +46 -0
- package/src/__tests__/fixtures/interval-test.mcrs +11 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
- package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
- package/src/__tests__/lexer.test.ts +35 -4
- package/src/__tests__/lowering.test.ts +187 -9
- package/src/__tests__/mc-integration.test.ts +166 -51
- package/src/__tests__/mc-syntax.test.ts +14 -0
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/parser.test.ts +102 -5
- package/src/__tests__/runtime.test.ts +24 -8
- package/src/__tests__/typechecker.test.ts +204 -0
- package/src/ast/types.ts +39 -2
- package/src/cli.ts +24 -10
- package/src/codegen/mcfunction/index.ts +31 -1
- package/src/codegen/structure/index.ts +40 -2
- package/src/compile.ts +59 -7
- package/src/events/types.ts +69 -0
- package/src/index.ts +9 -4
- package/src/ir/types.ts +4 -0
- package/src/lexer/index.ts +105 -2
- package/src/lowering/index.ts +566 -18
- package/src/optimizer/dce.ts +618 -0
- package/src/parser/index.ts +187 -29
- package/src/stdlib/README.md +34 -4
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/timer.mcrs +54 -33
- package/src/typechecker/index.ts +469 -18
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import type { IRBlock, IRFunction, IRModule, Operand, Terminator } from '../../ir/types'
|
|
20
20
|
import { optimizeCommandFunctions, type OptimizationStats, createEmptyOptimizationStats, mergeOptimizationStats } from '../../optimizer/commands'
|
|
21
|
+
import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
|
|
21
22
|
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Utilities
|
|
@@ -270,6 +271,10 @@ export function generateDatapackWithStats(
|
|
|
270
271
|
// Collect all trigger handlers
|
|
271
272
|
const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName)
|
|
272
273
|
const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
|
|
274
|
+
const eventHandlers = module.functions.filter((fn): fn is IRFunction & { eventHandler: { eventType: EventTypeName; tag: string } } =>
|
|
275
|
+
!!fn.eventHandler && isEventTypeName(fn.eventHandler.eventType)
|
|
276
|
+
)
|
|
277
|
+
const eventTypes = new Set<EventTypeName>(eventHandlers.map(fn => fn.eventHandler.eventType))
|
|
273
278
|
|
|
274
279
|
// Collect all tick functions
|
|
275
280
|
const tickFunctionNames: string[] = []
|
|
@@ -302,6 +307,19 @@ export function generateDatapackWithStats(
|
|
|
302
307
|
loadLines.push(`scoreboard players enable @a ${triggerName}`)
|
|
303
308
|
}
|
|
304
309
|
|
|
310
|
+
for (const eventType of eventTypes) {
|
|
311
|
+
const detection = EVENT_TYPES[eventType].detection
|
|
312
|
+
if (eventType === 'PlayerDeath') {
|
|
313
|
+
loadLines.push('scoreboard objectives add rs.deaths deathCount')
|
|
314
|
+
} else if (eventType === 'EntityKill') {
|
|
315
|
+
loadLines.push('scoreboard objectives add rs.kills totalKillCount')
|
|
316
|
+
} else if (eventType === 'ItemUse') {
|
|
317
|
+
loadLines.push('# ItemUse detection requires a project-specific objective/tag setup')
|
|
318
|
+
} else if (detection === 'tag' || detection === 'advancement') {
|
|
319
|
+
loadLines.push(`# ${eventType} detection expects tag ${EVENT_TYPES[eventType].tag} to be set externally`)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
305
323
|
// Generate trigger dispatch functions
|
|
306
324
|
for (const triggerName of triggerNames) {
|
|
307
325
|
const handlers = triggerHandlers.filter(fn => fn.triggerName === triggerName)
|
|
@@ -391,8 +409,20 @@ export function generateDatapackWithStats(
|
|
|
391
409
|
}
|
|
392
410
|
}
|
|
393
411
|
|
|
412
|
+
if (eventHandlers.length > 0) {
|
|
413
|
+
tickLines.push('# Event checks')
|
|
414
|
+
for (const eventType of eventTypes) {
|
|
415
|
+
const tag = EVENT_TYPES[eventType].tag
|
|
416
|
+
const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType)
|
|
417
|
+
for (const handler of handlers) {
|
|
418
|
+
tickLines.push(`execute as @a[tag=${tag}] run function ${ns}:${handler.name}`)
|
|
419
|
+
}
|
|
420
|
+
tickLines.push(`tag @a[tag=${tag}] remove ${tag}`)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
394
424
|
// Only generate __tick if there's something to run
|
|
395
|
-
if (tickFunctionNames.length > 0 || triggerNames.size > 0) {
|
|
425
|
+
if (tickFunctionNames.length > 0 || triggerNames.size > 0 || eventHandlers.length > 0) {
|
|
396
426
|
files.push({
|
|
397
427
|
path: `data/${ns}/function/__tick.mcfunction`,
|
|
398
428
|
content: tickLines.join('\n'),
|
|
@@ -5,9 +5,11 @@ import { nbt, TagType, writeNbt, type CompoundTag, type NbtTag } from '../../nbt
|
|
|
5
5
|
import { createEmptyOptimizationStats, mergeOptimizationStats, type OptimizationStats } from '../../optimizer/commands'
|
|
6
6
|
import { optimizeWithStats } from '../../optimizer/passes'
|
|
7
7
|
import { optimizeForStructure, optimizeForStructureWithStats } from '../../optimizer/structure'
|
|
8
|
+
import { eliminateDeadCode } from '../../optimizer/dce'
|
|
8
9
|
import { preprocessSource } from '../../compile'
|
|
9
10
|
import type { IRCommand, IRFunction, IRModule } from '../../ir/types'
|
|
10
11
|
import type { DatapackFile } from '../mcfunction'
|
|
12
|
+
import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
|
|
11
13
|
|
|
12
14
|
const DATA_VERSION = 3953
|
|
13
15
|
const MAX_WIDTH = 16
|
|
@@ -50,6 +52,10 @@ export interface StructureCompileResult {
|
|
|
50
52
|
stats?: OptimizationStats
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
export interface StructureCompileOptions {
|
|
56
|
+
dce?: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
function escapeJsonString(value: string): string {
|
|
54
60
|
return JSON.stringify(value).slice(1, -1)
|
|
55
61
|
}
|
|
@@ -87,6 +93,10 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
|
|
|
87
93
|
const entries: CommandEntry[] = []
|
|
88
94
|
const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName)
|
|
89
95
|
const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
|
|
96
|
+
const eventHandlers = module.functions.filter((fn): fn is IRFunction & { eventHandler: { eventType: EventTypeName; tag: string } } =>
|
|
97
|
+
!!fn.eventHandler && isEventTypeName(fn.eventHandler.eventType)
|
|
98
|
+
)
|
|
99
|
+
const eventTypes = new Set<EventTypeName>(eventHandlers.map(fn => fn.eventHandler.eventType))
|
|
90
100
|
const loadCommands = [
|
|
91
101
|
`scoreboard objectives add ${OBJ} dummy`,
|
|
92
102
|
...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
|
|
@@ -99,6 +109,14 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
|
|
|
99
109
|
).map(constSetup),
|
|
100
110
|
]
|
|
101
111
|
|
|
112
|
+
for (const eventType of eventTypes) {
|
|
113
|
+
if (eventType === 'PlayerDeath') {
|
|
114
|
+
loadCommands.push('scoreboard objectives add rs.deaths deathCount')
|
|
115
|
+
} else if (eventType === 'EntityKill') {
|
|
116
|
+
loadCommands.push('scoreboard objectives add rs.kills totalKillCount')
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
102
120
|
// Call @load functions from __load
|
|
103
121
|
for (const fn of module.functions) {
|
|
104
122
|
if (fn.isLoadInit) {
|
|
@@ -146,6 +164,20 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
|
|
|
146
164
|
})
|
|
147
165
|
}
|
|
148
166
|
}
|
|
167
|
+
if (eventHandlers.length > 0) {
|
|
168
|
+
for (const eventType of eventTypes) {
|
|
169
|
+
const tag = EVENT_TYPES[eventType].tag
|
|
170
|
+
const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType)
|
|
171
|
+
for (const handler of handlers) {
|
|
172
|
+
tickCommands.push({
|
|
173
|
+
cmd: `execute as @a[tag=${tag}] run function ${module.namespace}:${handler.name}`,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
tickCommands.push({
|
|
177
|
+
cmd: `tag @a[tag=${tag}] remove ${tag}`,
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
149
181
|
if (tickCommands.length > 0) {
|
|
150
182
|
sections.push({
|
|
151
183
|
name: '__tick',
|
|
@@ -288,10 +320,16 @@ export function generateStructure(input: IRModule | DatapackFile[]): StructureCo
|
|
|
288
320
|
}
|
|
289
321
|
}
|
|
290
322
|
|
|
291
|
-
export function compileToStructure(
|
|
323
|
+
export function compileToStructure(
|
|
324
|
+
source: string,
|
|
325
|
+
namespace: string,
|
|
326
|
+
filePath?: string,
|
|
327
|
+
options: StructureCompileOptions = {}
|
|
328
|
+
): StructureCompileResult {
|
|
292
329
|
const preprocessedSource = preprocessSource(source, { filePath })
|
|
293
330
|
const tokens = new Lexer(preprocessedSource, filePath).tokenize()
|
|
294
|
-
const
|
|
331
|
+
const parsedAst = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
|
|
332
|
+
const ast = options.dce ?? true ? eliminateDeadCode(parsedAst) : parsedAst
|
|
295
333
|
const ir = new Lowering(namespace).lower(ast)
|
|
296
334
|
const stats = createEmptyOptimizationStats()
|
|
297
335
|
const optimizedIRFunctions = ir.functions.map(fn => {
|
package/src/compile.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { Lexer } from './lexer'
|
|
|
11
11
|
import { Parser } from './parser'
|
|
12
12
|
import { Lowering } from './lowering'
|
|
13
13
|
import { optimize } from './optimizer/passes'
|
|
14
|
+
import { eliminateDeadCode } from './optimizer/dce'
|
|
14
15
|
import { generateDatapackWithStats, DatapackFile } from './codegen/mcfunction'
|
|
15
16
|
import { DiagnosticError, formatError, parseErrorMessage } from './diagnostics'
|
|
16
17
|
import type { IRModule } from './ir/types'
|
|
@@ -24,6 +25,7 @@ export interface CompileOptions {
|
|
|
24
25
|
namespace?: string
|
|
25
26
|
filePath?: string
|
|
26
27
|
optimize?: boolean
|
|
28
|
+
dce?: boolean
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
// ---------------------------------------------------------------------------
|
|
@@ -39,6 +41,17 @@ export interface CompileResult {
|
|
|
39
41
|
error?: DiagnosticError
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
export interface SourceRange {
|
|
45
|
+
startLine: number
|
|
46
|
+
endLine: number
|
|
47
|
+
filePath: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PreprocessedSource {
|
|
51
|
+
source: string
|
|
52
|
+
ranges: SourceRange[]
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/
|
|
43
56
|
|
|
44
57
|
interface PreprocessOptions {
|
|
@@ -46,7 +59,19 @@ interface PreprocessOptions {
|
|
|
46
59
|
seen?: Set<string>
|
|
47
60
|
}
|
|
48
61
|
|
|
49
|
-
|
|
62
|
+
function countLines(source: string): number {
|
|
63
|
+
return source === '' ? 0 : source.split('\n').length
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function offsetRanges(ranges: SourceRange[], lineOffset: number): SourceRange[] {
|
|
67
|
+
return ranges.map(range => ({
|
|
68
|
+
startLine: range.startLine + lineOffset,
|
|
69
|
+
endLine: range.endLine + lineOffset,
|
|
70
|
+
filePath: range.filePath,
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function preprocessSourceWithMetadata(source: string, options: PreprocessOptions = {}): PreprocessedSource {
|
|
50
75
|
const { filePath } = options
|
|
51
76
|
const seen = options.seen ?? new Set<string>()
|
|
52
77
|
|
|
@@ -55,7 +80,7 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
|
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
const lines = source.split('\n')
|
|
58
|
-
const imports:
|
|
83
|
+
const imports: PreprocessedSource[] = []
|
|
59
84
|
const bodyLines: string[] = []
|
|
60
85
|
let parsingHeader = true
|
|
61
86
|
|
|
@@ -90,7 +115,7 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
|
|
|
90
115
|
)
|
|
91
116
|
}
|
|
92
117
|
|
|
93
|
-
imports.push(
|
|
118
|
+
imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen }))
|
|
94
119
|
}
|
|
95
120
|
continue
|
|
96
121
|
}
|
|
@@ -104,7 +129,31 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
|
|
|
104
129
|
bodyLines.push(line)
|
|
105
130
|
}
|
|
106
131
|
|
|
107
|
-
|
|
132
|
+
const body = bodyLines.join('\n')
|
|
133
|
+
const parts = [...imports.map(entry => entry.source), body].filter(Boolean)
|
|
134
|
+
const combined = parts.join('\n')
|
|
135
|
+
|
|
136
|
+
const ranges: SourceRange[] = []
|
|
137
|
+
let lineOffset = 0
|
|
138
|
+
|
|
139
|
+
for (const entry of imports) {
|
|
140
|
+
ranges.push(...offsetRanges(entry.ranges, lineOffset))
|
|
141
|
+
lineOffset += countLines(entry.source)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (filePath && body) {
|
|
145
|
+
ranges.push({
|
|
146
|
+
startLine: lineOffset + 1,
|
|
147
|
+
endLine: lineOffset + countLines(body),
|
|
148
|
+
filePath: path.resolve(filePath),
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { source: combined, ranges }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function preprocessSource(source: string, options: PreprocessOptions = {}): string {
|
|
156
|
+
return preprocessSourceWithMetadata(source, options).source
|
|
108
157
|
}
|
|
109
158
|
|
|
110
159
|
// ---------------------------------------------------------------------------
|
|
@@ -113,20 +162,23 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
|
|
|
113
162
|
|
|
114
163
|
export function compile(source: string, options: CompileOptions = {}): CompileResult {
|
|
115
164
|
const { namespace = 'redscript', filePath, optimize: shouldOptimize = true } = options
|
|
165
|
+
const shouldRunDce = options.dce ?? shouldOptimize
|
|
116
166
|
let sourceLines = source.split('\n')
|
|
117
167
|
|
|
118
168
|
try {
|
|
119
|
-
const
|
|
169
|
+
const preprocessed = preprocessSourceWithMetadata(source, { filePath })
|
|
170
|
+
const preprocessedSource = preprocessed.source
|
|
120
171
|
sourceLines = preprocessedSource.split('\n')
|
|
121
172
|
|
|
122
173
|
// Lexing
|
|
123
174
|
const tokens = new Lexer(preprocessedSource, filePath).tokenize()
|
|
124
175
|
|
|
125
176
|
// Parsing
|
|
126
|
-
const
|
|
177
|
+
const parsedAst = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
|
|
178
|
+
const ast = shouldRunDce ? eliminateDeadCode(parsedAst) : parsedAst
|
|
127
179
|
|
|
128
180
|
// Lowering
|
|
129
|
-
const ir = new Lowering(namespace).lower(ast)
|
|
181
|
+
const ir = new Lowering(namespace, preprocessed.ranges).lower(ast)
|
|
130
182
|
|
|
131
183
|
// Optimization
|
|
132
184
|
const optimized: IRModule = shouldOptimize
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { TypeNode } from '../ast/types'
|
|
2
|
+
|
|
3
|
+
export const EVENT_TYPES = {
|
|
4
|
+
PlayerDeath: {
|
|
5
|
+
tag: 'rs.just_died',
|
|
6
|
+
params: ['player: Player'],
|
|
7
|
+
detection: 'scoreboard',
|
|
8
|
+
},
|
|
9
|
+
PlayerJoin: {
|
|
10
|
+
tag: 'rs.just_joined',
|
|
11
|
+
params: ['player: Player'],
|
|
12
|
+
detection: 'tag',
|
|
13
|
+
},
|
|
14
|
+
BlockBreak: {
|
|
15
|
+
tag: 'rs.just_broke_block',
|
|
16
|
+
params: ['player: Player', 'block: string'],
|
|
17
|
+
detection: 'advancement',
|
|
18
|
+
},
|
|
19
|
+
EntityKill: {
|
|
20
|
+
tag: 'rs.just_killed',
|
|
21
|
+
params: ['player: Player'],
|
|
22
|
+
detection: 'scoreboard',
|
|
23
|
+
},
|
|
24
|
+
ItemUse: {
|
|
25
|
+
tag: 'rs.just_used_item',
|
|
26
|
+
params: ['player: Player'],
|
|
27
|
+
detection: 'scoreboard',
|
|
28
|
+
},
|
|
29
|
+
} as const
|
|
30
|
+
|
|
31
|
+
export type EventTypeName = keyof typeof EVENT_TYPES
|
|
32
|
+
|
|
33
|
+
export interface EventParamSpec {
|
|
34
|
+
name: string
|
|
35
|
+
type: TypeNode
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isEventTypeName(value: string): value is EventTypeName {
|
|
39
|
+
return value in EVENT_TYPES
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getEventParamSpecs(eventType: EventTypeName): EventParamSpec[] {
|
|
43
|
+
return EVENT_TYPES[eventType].params.map(parseEventParam)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseEventParam(spec: string): EventParamSpec {
|
|
47
|
+
const match = spec.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([A-Za-z_][A-Za-z0-9_]*)$/)
|
|
48
|
+
if (!match) {
|
|
49
|
+
throw new Error(`Invalid event parameter spec: ${spec}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [, name, typeName] = match
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
type: toTypeNode(typeName),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toTypeNode(typeName: string): TypeNode {
|
|
60
|
+
if (typeName === 'Player') {
|
|
61
|
+
return { kind: 'entity', entityType: 'Player' }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeName === 'string' || typeName === 'int' || typeName === 'bool' || typeName === 'float' || typeName === 'void' || typeName === 'BlockPos' || typeName === 'byte' || typeName === 'short' || typeName === 'long' || typeName === 'double') {
|
|
65
|
+
return { kind: 'named', name: typeName }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { kind: 'struct', name: typeName }
|
|
69
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,12 +14,13 @@ 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,
|
|
20
21
|
DatapackFile,
|
|
21
22
|
} from './codegen/mcfunction'
|
|
22
|
-
import { preprocessSource } from './compile'
|
|
23
|
+
import { preprocessSource, preprocessSourceWithMetadata } from './compile'
|
|
23
24
|
import type { IRModule } from './ir/types'
|
|
24
25
|
import type { Program } from './ast/types'
|
|
25
26
|
import type { DiagnosticError } from './diagnostics'
|
|
@@ -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,14 +55,17 @@ 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
|
-
const
|
|
60
|
+
const preprocessed = preprocessSourceWithMetadata(source, { filePath })
|
|
61
|
+
const preprocessedSource = preprocessed.source
|
|
58
62
|
|
|
59
63
|
// Lexing
|
|
60
64
|
const tokens = new Lexer(preprocessedSource, filePath).tokenize()
|
|
61
65
|
|
|
62
66
|
// Parsing
|
|
63
|
-
const
|
|
67
|
+
const parsedAst = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
|
|
68
|
+
const ast = shouldRunDce ? eliminateDeadCode(parsedAst) : parsedAst
|
|
64
69
|
|
|
65
70
|
// Type checking (warn mode - collect errors but don't block)
|
|
66
71
|
let typeErrors: DiagnosticError[] | undefined
|
|
@@ -70,7 +75,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
// Lowering to IR
|
|
73
|
-
const lowering = new Lowering(namespace)
|
|
78
|
+
const lowering = new Lowering(namespace, preprocessed.ranges)
|
|
74
79
|
const ir = lowering.lower(ast)
|
|
75
80
|
|
|
76
81
|
let optimizedIR: IRModule = ir
|
package/src/ir/types.ts
CHANGED
|
@@ -107,6 +107,10 @@ export interface IRFunction {
|
|
|
107
107
|
kind: 'advancement' | 'craft' | 'death' | 'login' | 'join_team'
|
|
108
108
|
value?: string
|
|
109
109
|
}
|
|
110
|
+
eventHandler?: {
|
|
111
|
+
eventType: string
|
|
112
|
+
tag: string
|
|
113
|
+
}
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
// ---------------------------------------------------------------------------
|
package/src/lexer/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { DiagnosticError } from '../diagnostics'
|
|
|
15
15
|
export type TokenKind =
|
|
16
16
|
// Keywords
|
|
17
17
|
| 'fn' | 'let' | 'const' | 'if' | 'else' | 'while' | 'for' | 'foreach' | 'match'
|
|
18
|
-
| 'return' | 'as' | 'at' | 'in' | 'struct' | 'enum' | 'trigger' | 'namespace'
|
|
18
|
+
| 'return' | 'as' | 'at' | 'in' | 'is' | 'struct' | 'impl' | 'enum' | 'trigger' | 'namespace'
|
|
19
19
|
| 'execute' | 'run' | 'unless'
|
|
20
20
|
// Types
|
|
21
21
|
| 'int' | 'bool' | 'float' | 'string' | 'void'
|
|
@@ -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
|
| '~' | '^'
|
|
@@ -75,7 +78,9 @@ const KEYWORDS: Record<string, TokenKind> = {
|
|
|
75
78
|
as: 'as',
|
|
76
79
|
at: 'at',
|
|
77
80
|
in: 'in',
|
|
81
|
+
is: 'is',
|
|
78
82
|
struct: 'struct',
|
|
83
|
+
impl: 'impl',
|
|
79
84
|
enum: 'enum',
|
|
80
85
|
trigger: 'trigger',
|
|
81
86
|
namespace: 'namespace',
|
|
@@ -273,8 +278,52 @@ export class Lexer {
|
|
|
273
278
|
return
|
|
274
279
|
}
|
|
275
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
|
+
|
|
276
325
|
// Single-character operators and delimiters
|
|
277
|
-
const singleChar: TokenKind[] = ['+', '-', '*', '/', '%', '
|
|
326
|
+
const singleChar: TokenKind[] = ['+', '-', '*', '/', '%', '<', '>', '!', '=',
|
|
278
327
|
'{', '}', '(', ')', '[', ']', ',', ';', ':', '.']
|
|
279
328
|
if (singleChar.includes(char as TokenKind)) {
|
|
280
329
|
this.addToken(char as TokenKind, char, startLine, startCol)
|
|
@@ -287,6 +336,13 @@ export class Lexer {
|
|
|
287
336
|
return
|
|
288
337
|
}
|
|
289
338
|
|
|
339
|
+
// f-string literal
|
|
340
|
+
if (char === 'f' && this.peek() === '"') {
|
|
341
|
+
this.advance()
|
|
342
|
+
this.scanFString(startLine, startCol)
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
290
346
|
// String literal
|
|
291
347
|
if (char === '"') {
|
|
292
348
|
this.scanString(startLine, startCol)
|
|
@@ -430,6 +486,53 @@ export class Lexer {
|
|
|
430
486
|
this.addToken('string_lit', value, startLine, startCol)
|
|
431
487
|
}
|
|
432
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
|
+
|
|
433
536
|
private scanNumber(firstChar: string, startLine: number, startCol: number): void {
|
|
434
537
|
let value = firstChar
|
|
435
538
|
|