redscript-mc 1.0.0
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/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
- package/.github/ISSUE_TEMPLATE/wrong_output.md +33 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +34 -0
- package/.github/workflows/ci.yml +29 -0
- package/.github/workflows/publish-extension.yml +35 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/README.zh.md +261 -0
- package/dist/__tests__/cli.test.d.ts +1 -0
- package/dist/__tests__/cli.test.js +140 -0
- package/dist/__tests__/codegen.test.d.ts +1 -0
- package/dist/__tests__/codegen.test.js +121 -0
- package/dist/__tests__/diagnostics.test.d.ts +4 -0
- package/dist/__tests__/diagnostics.test.js +149 -0
- package/dist/__tests__/e2e.test.d.ts +6 -0
- package/dist/__tests__/e2e.test.js +1528 -0
- package/dist/__tests__/lexer.test.d.ts +1 -0
- package/dist/__tests__/lexer.test.js +316 -0
- package/dist/__tests__/lowering.test.d.ts +1 -0
- package/dist/__tests__/lowering.test.js +819 -0
- package/dist/__tests__/mc-integration.test.d.ts +12 -0
- package/dist/__tests__/mc-integration.test.js +395 -0
- package/dist/__tests__/mc-syntax.test.d.ts +1 -0
- package/dist/__tests__/mc-syntax.test.js +112 -0
- package/dist/__tests__/nbt.test.d.ts +1 -0
- package/dist/__tests__/nbt.test.js +82 -0
- package/dist/__tests__/optimizer-advanced.test.d.ts +1 -0
- package/dist/__tests__/optimizer-advanced.test.js +124 -0
- package/dist/__tests__/optimizer.test.d.ts +1 -0
- package/dist/__tests__/optimizer.test.js +118 -0
- package/dist/__tests__/parser.test.d.ts +1 -0
- package/dist/__tests__/parser.test.js +717 -0
- package/dist/__tests__/repl.test.d.ts +1 -0
- package/dist/__tests__/repl.test.js +27 -0
- package/dist/__tests__/runtime.test.d.ts +1 -0
- package/dist/__tests__/runtime.test.js +276 -0
- package/dist/__tests__/structure-optimizer.test.d.ts +1 -0
- package/dist/__tests__/structure-optimizer.test.js +33 -0
- package/dist/__tests__/typechecker.test.d.ts +1 -0
- package/dist/__tests__/typechecker.test.js +364 -0
- package/dist/ast/types.d.ts +357 -0
- package/dist/ast/types.js +9 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +407 -0
- package/dist/codegen/cmdblock/index.d.ts +26 -0
- package/dist/codegen/cmdblock/index.js +45 -0
- package/dist/codegen/mcfunction/index.d.ts +34 -0
- package/dist/codegen/mcfunction/index.js +413 -0
- package/dist/codegen/structure/index.d.ts +18 -0
- package/dist/codegen/structure/index.js +249 -0
- package/dist/compile.d.ts +30 -0
- package/dist/compile.js +152 -0
- package/dist/data/arena/function/__load.mcfunction +6 -0
- package/dist/data/arena/function/__tick.mcfunction +2 -0
- package/dist/data/arena/function/announce_leaders/else_1.mcfunction +3 -0
- package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +3 -0
- package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +7 -0
- package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +4 -0
- package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +6 -0
- package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/announce_leaders/then_0.mcfunction +4 -0
- package/dist/data/arena/function/announce_leaders.mcfunction +6 -0
- package/dist/data/arena/function/arena_tick/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/arena_tick/then_0.mcfunction +4 -0
- package/dist/data/arena/function/arena_tick.mcfunction +11 -0
- package/dist/data/counter/function/__load.mcfunction +5 -0
- package/dist/data/counter/function/__tick.mcfunction +2 -0
- package/dist/data/counter/function/counter_tick/merge_2.mcfunction +1 -0
- package/dist/data/counter/function/counter_tick/then_0.mcfunction +3 -0
- package/dist/data/counter/function/counter_tick.mcfunction +11 -0
- package/dist/data/minecraft/tags/function/load.json +5 -0
- package/dist/data/minecraft/tags/function/tick.json +5 -0
- package/dist/data/quiz/function/__load.mcfunction +16 -0
- package/dist/data/quiz/function/__tick.mcfunction +6 -0
- package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/answer_a.mcfunction +4 -0
- package/dist/data/quiz/function/answer_b.mcfunction +4 -0
- package/dist/data/quiz/function/answer_c.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/else_1.mcfunction +5 -0
- package/dist/data/quiz/function/ask_question/else_4.mcfunction +5 -0
- package/dist/data/quiz/function/ask_question/else_7.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/merge_2.mcfunction +1 -0
- package/dist/data/quiz/function/ask_question/merge_5.mcfunction +2 -0
- package/dist/data/quiz/function/ask_question/merge_8.mcfunction +2 -0
- package/dist/data/quiz/function/ask_question/then_0.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/then_3.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/then_6.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question.mcfunction +7 -0
- package/dist/data/quiz/function/finish_quiz.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer/else_1.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/else_10.mcfunction +3 -0
- package/dist/data/quiz/function/handle_answer/else_16.mcfunction +3 -0
- package/dist/data/quiz/function/handle_answer/else_4.mcfunction +3 -0
- package/dist/data/quiz/function/handle_answer/else_7.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +8 -0
- package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/then_0.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/then_12.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/then_15.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer/then_3.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer/then_6.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/then_9.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer.mcfunction +11 -0
- package/dist/data/quiz/function/start_quiz.mcfunction +5 -0
- package/dist/data/shop/function/__load.mcfunction +7 -0
- package/dist/data/shop/function/__tick.mcfunction +3 -0
- package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase/else_1.mcfunction +5 -0
- package/dist/data/shop/function/complete_purchase/else_4.mcfunction +5 -0
- package/dist/data/shop/function/complete_purchase/else_7.mcfunction +3 -0
- package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +2 -0
- package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +2 -0
- package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +2 -0
- package/dist/data/shop/function/complete_purchase/then_0.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase/then_3.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase/then_6.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase.mcfunction +7 -0
- package/dist/data/shop/function/handle_shop_trigger.mcfunction +3 -0
- package/dist/data/turret/function/__load.mcfunction +5 -0
- package/dist/data/turret/function/__tick.mcfunction +4 -0
- package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +4 -0
- package/dist/data/turret/function/deploy_turret.mcfunction +8 -0
- package/dist/data/turret/function/turret_tick/at_1.mcfunction +2 -0
- package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +2 -0
- package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +2 -0
- package/dist/data/turret/function/turret_tick/tick_body.mcfunction +3 -0
- package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +1 -0
- package/dist/data/turret/function/turret_tick.mcfunction +5 -0
- package/dist/diagnostics/index.d.ts +44 -0
- package/dist/diagnostics/index.js +140 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +126 -0
- package/dist/ir/builder.d.ts +32 -0
- package/dist/ir/builder.js +99 -0
- package/dist/ir/types.d.ts +117 -0
- package/dist/ir/types.js +15 -0
- package/dist/lexer/index.d.ts +36 -0
- package/dist/lexer/index.js +458 -0
- package/dist/lowering/index.d.ts +106 -0
- package/dist/lowering/index.js +2041 -0
- package/dist/mc-test/client.d.ts +128 -0
- package/dist/mc-test/client.js +174 -0
- package/dist/mc-test/runner.d.ts +28 -0
- package/dist/mc-test/runner.js +150 -0
- package/dist/mc-test/setup.d.ts +11 -0
- package/dist/mc-test/setup.js +98 -0
- package/dist/mc-validator/index.d.ts +17 -0
- package/dist/mc-validator/index.js +322 -0
- package/dist/nbt/index.d.ts +86 -0
- package/dist/nbt/index.js +250 -0
- package/dist/optimizer/commands.d.ts +36 -0
- package/dist/optimizer/commands.js +349 -0
- package/dist/optimizer/passes.d.ts +34 -0
- package/dist/optimizer/passes.js +227 -0
- package/dist/optimizer/structure.d.ts +8 -0
- package/dist/optimizer/structure.js +344 -0
- package/dist/pack.mcmeta +6 -0
- package/dist/parser/index.d.ts +76 -0
- package/dist/parser/index.js +1193 -0
- package/dist/repl.d.ts +16 -0
- package/dist/repl.js +165 -0
- package/dist/runtime/index.d.ts +101 -0
- package/dist/runtime/index.js +1288 -0
- package/dist/typechecker/index.d.ts +42 -0
- package/dist/typechecker/index.js +629 -0
- package/docs/COMPILATION_STATS.md +142 -0
- package/docs/IMPLEMENTATION_GUIDE.md +512 -0
- package/docs/LANGUAGE_REFERENCE.md +415 -0
- package/docs/MC_MAPPING.md +280 -0
- package/docs/STRUCTURE_TARGET.md +80 -0
- package/docs/mc-reference/commands.md +259 -0
- package/editors/vscode/.vscodeignore +10 -0
- package/editors/vscode/LICENSE +21 -0
- package/editors/vscode/README.md +78 -0
- package/editors/vscode/build.mjs +28 -0
- package/editors/vscode/icon.png +0 -0
- package/editors/vscode/mcfunction-language-configuration.json +28 -0
- package/editors/vscode/out/extension.js +7236 -0
- package/editors/vscode/package-lock.json +566 -0
- package/editors/vscode/package.json +137 -0
- package/editors/vscode/redscript-language-configuration.json +28 -0
- package/editors/vscode/snippets/redscript.json +114 -0
- package/editors/vscode/src/codeactions.ts +89 -0
- package/editors/vscode/src/completion.ts +130 -0
- package/editors/vscode/src/extension.ts +239 -0
- package/editors/vscode/src/hover.ts +1120 -0
- package/editors/vscode/src/symbols.ts +207 -0
- package/editors/vscode/syntaxes/mcfunction.tmLanguage.json +740 -0
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +357 -0
- package/editors/vscode/tsconfig.json +13 -0
- package/jest.config.js +5 -0
- package/package.json +38 -0
- package/src/__tests__/cli.test.ts +130 -0
- package/src/__tests__/codegen.test.ts +128 -0
- package/src/__tests__/diagnostics.test.ts +195 -0
- package/src/__tests__/e2e.test.ts +1721 -0
- package/src/__tests__/fixtures/mc-commands-1.21.4.json +18734 -0
- package/src/__tests__/formatter.test.ts +46 -0
- package/src/__tests__/lexer.test.ts +356 -0
- package/src/__tests__/lowering.test.ts +962 -0
- package/src/__tests__/mc-integration.test.ts +409 -0
- package/src/__tests__/mc-syntax.test.ts +96 -0
- package/src/__tests__/nbt.test.ts +58 -0
- package/src/__tests__/optimizer-advanced.test.ts +144 -0
- package/src/__tests__/optimizer.test.ts +129 -0
- package/src/__tests__/parser.test.ts +800 -0
- package/src/__tests__/repl.test.ts +33 -0
- package/src/__tests__/runtime.test.ts +289 -0
- package/src/__tests__/structure-optimizer.test.ts +38 -0
- package/src/__tests__/typechecker.test.ts +395 -0
- package/src/ast/types.ts +248 -0
- package/src/cli.ts +445 -0
- package/src/codegen/cmdblock/index.ts +63 -0
- package/src/codegen/mcfunction/index.ts +471 -0
- package/src/codegen/structure/index.ts +305 -0
- package/src/compile.ts +188 -0
- package/src/diagnostics/index.ts +186 -0
- package/src/examples/README.md +77 -0
- package/src/examples/SHOWCASE_GAME.md +43 -0
- package/src/examples/arena.rs +44 -0
- package/src/examples/counter.rs +12 -0
- package/src/examples/pvp_arena.rs +131 -0
- package/src/examples/quiz.rs +90 -0
- package/src/examples/rpg.rs +13 -0
- package/src/examples/shop.rs +30 -0
- package/src/examples/showcase_game.rs +552 -0
- package/src/examples/stdlib_demo.rs +181 -0
- package/src/examples/turret.rs +27 -0
- package/src/examples/world_manager.rs +23 -0
- package/src/formatter/index.ts +22 -0
- package/src/index.ts +161 -0
- package/src/ir/builder.ts +114 -0
- package/src/ir/types.ts +119 -0
- package/src/lexer/index.ts +555 -0
- package/src/lowering/index.ts +2406 -0
- package/src/mc-test/client.ts +259 -0
- package/src/mc-test/runner.ts +140 -0
- package/src/mc-test/setup.ts +70 -0
- package/src/mc-validator/index.ts +367 -0
- package/src/nbt/index.ts +321 -0
- package/src/optimizer/commands.ts +416 -0
- package/src/optimizer/passes.ts +233 -0
- package/src/optimizer/structure.ts +441 -0
- package/src/parser/index.ts +1437 -0
- package/src/repl.ts +165 -0
- package/src/runtime/index.ts +1403 -0
- package/src/stdlib/README.md +156 -0
- package/src/stdlib/combat.rs +20 -0
- package/src/stdlib/cooldown.rs +45 -0
- package/src/stdlib/math.rs +49 -0
- package/src/stdlib/mobs.rs +99 -0
- package/src/stdlib/player.rs +29 -0
- package/src/stdlib/strings.rs +7 -0
- package/src/stdlib/timer.rs +51 -0
- package/src/templates/README.md +126 -0
- package/src/templates/combat.rs +96 -0
- package/src/templates/economy.rs +40 -0
- package/src/templates/mini-game-framework.rs +117 -0
- package/src/templates/quest.rs +78 -0
- package/src/test_programs/zombie_game.rs +25 -0
- package/src/typechecker/index.ts +737 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { Lexer } from '../../lexer'
|
|
2
|
+
import { Parser } from '../../parser'
|
|
3
|
+
import { Lowering } from '../../lowering'
|
|
4
|
+
import { nbt, TagType, writeNbt, type CompoundTag, type NbtTag } from '../../nbt'
|
|
5
|
+
import { createEmptyOptimizationStats, mergeOptimizationStats, type OptimizationStats } from '../../optimizer/commands'
|
|
6
|
+
import { optimizeWithStats } from '../../optimizer/passes'
|
|
7
|
+
import { optimizeForStructure, optimizeForStructureWithStats } from '../../optimizer/structure'
|
|
8
|
+
import { preprocessSource } from '../../compile'
|
|
9
|
+
import type { IRCommand, IRFunction, IRModule } from '../../ir/types'
|
|
10
|
+
import type { DatapackFile } from '../mcfunction'
|
|
11
|
+
|
|
12
|
+
const DATA_VERSION = 3953
|
|
13
|
+
const MAX_WIDTH = 16
|
|
14
|
+
const OBJ = 'rs'
|
|
15
|
+
|
|
16
|
+
const PALETTE_IMPULSE = 0
|
|
17
|
+
const PALETTE_CHAIN_UNCONDITIONAL = 1
|
|
18
|
+
const PALETTE_CHAIN_CONDITIONAL = 2
|
|
19
|
+
const PALETTE_REPEAT = 3
|
|
20
|
+
|
|
21
|
+
const palette = [
|
|
22
|
+
{ Name: 'minecraft:command_block', Properties: { conditional: 'false', facing: 'east' } },
|
|
23
|
+
{ Name: 'minecraft:chain_command_block', Properties: { conditional: 'false', facing: 'east' } },
|
|
24
|
+
{ Name: 'minecraft:chain_command_block', Properties: { conditional: 'true', facing: 'east' } },
|
|
25
|
+
{ Name: 'minecraft:repeating_command_block', Properties: { conditional: 'false', facing: 'east' } },
|
|
26
|
+
{ Name: 'minecraft:air', Properties: {} },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
interface CommandEntry {
|
|
30
|
+
functionName: string
|
|
31
|
+
lineNumber: number
|
|
32
|
+
command: string
|
|
33
|
+
state: number
|
|
34
|
+
conditional: boolean
|
|
35
|
+
isRepeat: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface StructureBlockInfo {
|
|
39
|
+
command: string
|
|
40
|
+
conditional: boolean
|
|
41
|
+
state: number
|
|
42
|
+
functionName: string
|
|
43
|
+
lineNumber: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface StructureCompileResult {
|
|
47
|
+
buffer: Buffer
|
|
48
|
+
blockCount: number
|
|
49
|
+
blocks: StructureBlockInfo[]
|
|
50
|
+
stats?: OptimizationStats
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function escapeJsonString(value: string): string {
|
|
54
|
+
return JSON.stringify(value).slice(1, -1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function varRef(name: string): string {
|
|
58
|
+
return name.startsWith('$') ? name : `$${name}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectConsts(fn: IRFunction): Set<number> {
|
|
62
|
+
const consts = new Set<number>()
|
|
63
|
+
for (const block of fn.blocks) {
|
|
64
|
+
for (const instr of block.instrs) {
|
|
65
|
+
if (instr.op === 'assign' && instr.src.kind === 'const') consts.add(instr.src.value)
|
|
66
|
+
if (instr.op === 'binop') {
|
|
67
|
+
if (instr.lhs.kind === 'const') consts.add(instr.lhs.value)
|
|
68
|
+
if (instr.rhs.kind === 'const') consts.add(instr.rhs.value)
|
|
69
|
+
}
|
|
70
|
+
if (instr.op === 'cmp') {
|
|
71
|
+
if (instr.lhs.kind === 'const') consts.add(instr.lhs.value)
|
|
72
|
+
if (instr.rhs.kind === 'const') consts.add(instr.rhs.value)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (block.term.op === 'return' && block.term.value?.kind === 'const') {
|
|
76
|
+
consts.add(block.term.value.value)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return consts
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function constSetup(value: number): string {
|
|
83
|
+
return `scoreboard players set $const_${value} ${OBJ} ${value}`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
|
|
87
|
+
const entries: CommandEntry[] = []
|
|
88
|
+
const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName)
|
|
89
|
+
const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
|
|
90
|
+
const loadCommands = [
|
|
91
|
+
`scoreboard objectives add ${OBJ} dummy`,
|
|
92
|
+
...module.globals.map(globalName => `scoreboard players set ${varRef(globalName)} ${OBJ} 0`),
|
|
93
|
+
...Array.from(triggerNames).flatMap(triggerName => [
|
|
94
|
+
`scoreboard objectives add ${triggerName} trigger`,
|
|
95
|
+
`scoreboard players enable @a ${triggerName}`,
|
|
96
|
+
]),
|
|
97
|
+
...Array.from(
|
|
98
|
+
new Set(module.functions.flatMap(fn => Array.from(collectConsts(fn))))
|
|
99
|
+
).map(constSetup),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
const sections: Array<{ name: string; commands: IRCommand[]; repeat?: boolean }> = []
|
|
103
|
+
|
|
104
|
+
if (loadCommands.length > 0) {
|
|
105
|
+
sections.push({
|
|
106
|
+
name: '__load',
|
|
107
|
+
commands: loadCommands.map(cmd => ({ cmd })),
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const triggerName of triggerNames) {
|
|
112
|
+
const handlers = triggerHandlers.filter(fn => fn.triggerName === triggerName)
|
|
113
|
+
sections.push({
|
|
114
|
+
name: `__trigger_${triggerName}_dispatch`,
|
|
115
|
+
commands: [
|
|
116
|
+
...handlers.map(handler => ({ cmd: `function ${module.namespace}:${handler.name}` })),
|
|
117
|
+
{ cmd: `scoreboard players set @s ${triggerName} 0` },
|
|
118
|
+
{ cmd: `scoreboard players enable @s ${triggerName}` },
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const fn of module.functions) {
|
|
124
|
+
if (!fn.commands || fn.commands.length === 0) continue
|
|
125
|
+
sections.push({
|
|
126
|
+
name: fn.name,
|
|
127
|
+
commands: fn.commands,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const tickCommands: IRCommand[] = []
|
|
132
|
+
for (const fn of module.functions.filter(candidate => candidate.isTickLoop)) {
|
|
133
|
+
tickCommands.push({ cmd: `function ${module.namespace}:${fn.name}` })
|
|
134
|
+
}
|
|
135
|
+
if (triggerNames.size > 0) {
|
|
136
|
+
for (const triggerName of triggerNames) {
|
|
137
|
+
tickCommands.push({
|
|
138
|
+
cmd: `execute as @a[scores={${triggerName}=1..}] run function ${module.namespace}:__trigger_${triggerName}_dispatch`,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (tickCommands.length > 0) {
|
|
143
|
+
sections.push({
|
|
144
|
+
name: '__tick',
|
|
145
|
+
commands: tickCommands,
|
|
146
|
+
repeat: true,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const section of sections) {
|
|
151
|
+
for (let i = 0; i < section.commands.length; i++) {
|
|
152
|
+
const command = section.commands[i]
|
|
153
|
+
const state =
|
|
154
|
+
i === 0
|
|
155
|
+
? (section.repeat ? PALETTE_REPEAT : PALETTE_IMPULSE)
|
|
156
|
+
: (command.conditional ? PALETTE_CHAIN_CONDITIONAL : PALETTE_CHAIN_UNCONDITIONAL)
|
|
157
|
+
|
|
158
|
+
entries.push({
|
|
159
|
+
functionName: section.name,
|
|
160
|
+
lineNumber: i + 1,
|
|
161
|
+
command: command.cmd,
|
|
162
|
+
conditional: Boolean(command.conditional),
|
|
163
|
+
state,
|
|
164
|
+
isRepeat: Boolean(section.repeat && i === 0),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return entries
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toFunctionName(file: DatapackFile): string | null {
|
|
173
|
+
const match = file.path.match(/^data\/[^/]+\/function\/(.+)\.mcfunction$/)
|
|
174
|
+
return match?.[1] ?? null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function collectCommandEntriesFromFiles(files: DatapackFile[]): CommandEntry[] {
|
|
178
|
+
const entries: CommandEntry[] = []
|
|
179
|
+
|
|
180
|
+
for (const file of files) {
|
|
181
|
+
const functionName = toFunctionName(file)
|
|
182
|
+
if (!functionName) continue
|
|
183
|
+
|
|
184
|
+
const lines = file.content.split('\n')
|
|
185
|
+
let isFirstCommand = true
|
|
186
|
+
const isTickFunction = functionName === '__tick'
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < lines.length; i++) {
|
|
189
|
+
const command = lines[i].trim()
|
|
190
|
+
if (command === '' || command.startsWith('#')) continue
|
|
191
|
+
|
|
192
|
+
const state = isFirstCommand
|
|
193
|
+
? (isTickFunction ? PALETTE_REPEAT : PALETTE_IMPULSE)
|
|
194
|
+
: PALETTE_CHAIN_UNCONDITIONAL
|
|
195
|
+
|
|
196
|
+
entries.push({
|
|
197
|
+
functionName,
|
|
198
|
+
lineNumber: i + 1,
|
|
199
|
+
command,
|
|
200
|
+
conditional: false,
|
|
201
|
+
state,
|
|
202
|
+
isRepeat: isTickFunction && isFirstCommand,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
isFirstCommand = false
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return entries
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function createPaletteTag(): CompoundTag[] {
|
|
213
|
+
return palette.map(entry =>
|
|
214
|
+
nbt.compound({
|
|
215
|
+
Name: nbt.string(entry.Name),
|
|
216
|
+
Properties: nbt.compound(
|
|
217
|
+
Object.fromEntries(
|
|
218
|
+
Object.entries(entry.Properties).map(([key, value]) => [key, nbt.string(value)])
|
|
219
|
+
)
|
|
220
|
+
),
|
|
221
|
+
})
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function createBlockEntityTag(entry: CommandEntry): NbtTag {
|
|
226
|
+
return nbt.compound({
|
|
227
|
+
id: nbt.string('minecraft:command_block'),
|
|
228
|
+
Command: nbt.string(entry.command),
|
|
229
|
+
auto: nbt.byte(entry.isRepeat ? 1 : 0),
|
|
230
|
+
powered: nbt.byte(0),
|
|
231
|
+
conditionMet: nbt.byte(0),
|
|
232
|
+
UpdateLastExecution: nbt.byte(1),
|
|
233
|
+
LastExecution: nbt.long(0n),
|
|
234
|
+
TrackOutput: nbt.byte(1),
|
|
235
|
+
SuccessCount: nbt.int(0),
|
|
236
|
+
LastOutput: nbt.string(''),
|
|
237
|
+
CustomName: nbt.string(`{"text":"${escapeJsonString(`${entry.functionName}:${entry.lineNumber}`)}"}`),
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function createBlockTag(entry: CommandEntry, index: number): CompoundTag {
|
|
242
|
+
const x = index % MAX_WIDTH
|
|
243
|
+
const z = Math.floor(index / MAX_WIDTH) % MAX_WIDTH
|
|
244
|
+
const y = Math.floor(index / (MAX_WIDTH * MAX_WIDTH))
|
|
245
|
+
|
|
246
|
+
return nbt.compound({
|
|
247
|
+
pos: nbt.list(TagType.Int, [nbt.int(x), nbt.int(y), nbt.int(z)]),
|
|
248
|
+
state: nbt.int(entry.state),
|
|
249
|
+
nbt: createBlockEntityTag(entry),
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function generateStructure(input: IRModule | DatapackFile[]): StructureCompileResult {
|
|
254
|
+
const entries = Array.isArray(input)
|
|
255
|
+
? collectCommandEntriesFromFiles(input)
|
|
256
|
+
: collectCommandEntriesFromModule(input)
|
|
257
|
+
|
|
258
|
+
const blockTags = entries.map(createBlockTag)
|
|
259
|
+
const sizeX = Math.max(1, Math.min(MAX_WIDTH, entries.length || 1))
|
|
260
|
+
const sizeZ = Math.max(1, Math.min(MAX_WIDTH, Math.ceil(entries.length / MAX_WIDTH) || 1))
|
|
261
|
+
const sizeY = Math.max(1, Math.ceil(entries.length / (MAX_WIDTH * MAX_WIDTH)) || 1)
|
|
262
|
+
|
|
263
|
+
const root = nbt.compound({
|
|
264
|
+
DataVersion: nbt.int(DATA_VERSION),
|
|
265
|
+
size: nbt.list(TagType.Int, [nbt.int(sizeX), nbt.int(sizeY), nbt.int(sizeZ)]),
|
|
266
|
+
palette: nbt.list(TagType.Compound, createPaletteTag()),
|
|
267
|
+
blocks: nbt.list(TagType.Compound, blockTags),
|
|
268
|
+
entities: nbt.list(TagType.Compound, []),
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
buffer: writeNbt(root, ''),
|
|
273
|
+
blockCount: entries.length,
|
|
274
|
+
blocks: entries.map(entry => ({
|
|
275
|
+
command: entry.command,
|
|
276
|
+
conditional: entry.conditional,
|
|
277
|
+
state: entry.state,
|
|
278
|
+
functionName: entry.functionName,
|
|
279
|
+
lineNumber: entry.lineNumber,
|
|
280
|
+
})),
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function compileToStructure(source: string, namespace: string, filePath?: string): StructureCompileResult {
|
|
285
|
+
const preprocessedSource = preprocessSource(source, { filePath })
|
|
286
|
+
const tokens = new Lexer(preprocessedSource, filePath).tokenize()
|
|
287
|
+
const ast = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
|
|
288
|
+
const ir = new Lowering(namespace).lower(ast)
|
|
289
|
+
const stats = createEmptyOptimizationStats()
|
|
290
|
+
const optimizedIRFunctions = ir.functions.map(fn => {
|
|
291
|
+
const optimized = optimizeWithStats(fn)
|
|
292
|
+
mergeOptimizationStats(stats, optimized.stats)
|
|
293
|
+
return optimized.fn
|
|
294
|
+
})
|
|
295
|
+
const structureOptimized = optimizeForStructureWithStats(optimizedIRFunctions, namespace)
|
|
296
|
+
mergeOptimizationStats(stats, structureOptimized.stats)
|
|
297
|
+
const optimizedModule: IRModule = {
|
|
298
|
+
...ir,
|
|
299
|
+
functions: structureOptimized.functions,
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
...generateStructure(optimizedModule),
|
|
303
|
+
stats,
|
|
304
|
+
}
|
|
305
|
+
}
|
package/src/compile.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RedScript Compile API
|
|
3
|
+
*
|
|
4
|
+
* Main compile function with proper error handling and diagnostics.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs'
|
|
8
|
+
import * as path from 'path'
|
|
9
|
+
|
|
10
|
+
import { Lexer } from './lexer'
|
|
11
|
+
import { Parser } from './parser'
|
|
12
|
+
import { Lowering } from './lowering'
|
|
13
|
+
import { optimize } from './optimizer/passes'
|
|
14
|
+
import { generateDatapackWithStats, DatapackFile } from './codegen/mcfunction'
|
|
15
|
+
import { DiagnosticError, formatError, parseErrorMessage } from './diagnostics'
|
|
16
|
+
import type { IRModule } from './ir/types'
|
|
17
|
+
import type { Program } from './ast/types'
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Compile Options
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export interface CompileOptions {
|
|
24
|
+
namespace?: string
|
|
25
|
+
filePath?: string
|
|
26
|
+
optimize?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Compile Result
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export interface CompileResult {
|
|
34
|
+
success: boolean
|
|
35
|
+
files?: DatapackFile[]
|
|
36
|
+
advancements?: DatapackFile[]
|
|
37
|
+
ast?: Program
|
|
38
|
+
ir?: IRModule
|
|
39
|
+
error?: DiagnosticError
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/
|
|
43
|
+
|
|
44
|
+
interface PreprocessOptions {
|
|
45
|
+
filePath?: string
|
|
46
|
+
seen?: Set<string>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function preprocessSource(source: string, options: PreprocessOptions = {}): string {
|
|
50
|
+
const { filePath } = options
|
|
51
|
+
const seen = options.seen ?? new Set<string>()
|
|
52
|
+
|
|
53
|
+
if (filePath) {
|
|
54
|
+
seen.add(path.resolve(filePath))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const lines = source.split('\n')
|
|
58
|
+
const imports: string[] = []
|
|
59
|
+
const bodyLines: string[] = []
|
|
60
|
+
let parsingHeader = true
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < lines.length; i++) {
|
|
63
|
+
const line = lines[i]
|
|
64
|
+
const trimmed = line.trim()
|
|
65
|
+
const match = line.match(IMPORT_RE)
|
|
66
|
+
|
|
67
|
+
if (parsingHeader && match) {
|
|
68
|
+
if (!filePath) {
|
|
69
|
+
throw new DiagnosticError(
|
|
70
|
+
'ParseError',
|
|
71
|
+
'Import statements require a file path',
|
|
72
|
+
{ line: i + 1, col: 1 },
|
|
73
|
+
lines
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const importPath = path.resolve(path.dirname(filePath), match[1])
|
|
78
|
+
if (!seen.has(importPath)) {
|
|
79
|
+
seen.add(importPath)
|
|
80
|
+
let importedSource: string
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
importedSource = fs.readFileSync(importPath, 'utf-8')
|
|
84
|
+
} catch {
|
|
85
|
+
throw new DiagnosticError(
|
|
86
|
+
'ParseError',
|
|
87
|
+
`Cannot import '${match[1]}'`,
|
|
88
|
+
{ file: filePath, line: i + 1, col: 1 },
|
|
89
|
+
lines
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
imports.push(preprocessSource(importedSource, { filePath: importPath, seen }))
|
|
94
|
+
}
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (parsingHeader && (trimmed === '' || trimmed.startsWith('//'))) {
|
|
99
|
+
bodyLines.push(line)
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
parsingHeader = false
|
|
104
|
+
bodyLines.push(line)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return [...imports, bodyLines.join('\n')].filter(Boolean).join('\n')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Main Compile Function
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
export function compile(source: string, options: CompileOptions = {}): CompileResult {
|
|
115
|
+
const { namespace = 'redscript', filePath, optimize: shouldOptimize = true } = options
|
|
116
|
+
let sourceLines = source.split('\n')
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const preprocessedSource = preprocessSource(source, { filePath })
|
|
120
|
+
sourceLines = preprocessedSource.split('\n')
|
|
121
|
+
|
|
122
|
+
// Lexing
|
|
123
|
+
const tokens = new Lexer(preprocessedSource, filePath).tokenize()
|
|
124
|
+
|
|
125
|
+
// Parsing
|
|
126
|
+
const ast = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
|
|
127
|
+
|
|
128
|
+
// Lowering
|
|
129
|
+
const ir = new Lowering(namespace).lower(ast)
|
|
130
|
+
|
|
131
|
+
// Optimization
|
|
132
|
+
const optimized: IRModule = shouldOptimize
|
|
133
|
+
? { ...ir, functions: ir.functions.map(fn => optimize(fn)) }
|
|
134
|
+
: ir
|
|
135
|
+
|
|
136
|
+
// Code generation
|
|
137
|
+
const generated = generateDatapackWithStats(optimized)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
files: [...generated.files, ...generated.advancements],
|
|
142
|
+
advancements: generated.advancements,
|
|
143
|
+
ast,
|
|
144
|
+
ir: optimized,
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// Already a DiagnosticError
|
|
148
|
+
if (err instanceof DiagnosticError) {
|
|
149
|
+
return { success: false, error: err }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Try to parse the error message for line/col info
|
|
153
|
+
if (err instanceof Error) {
|
|
154
|
+
const diagnostic = parseErrorMessage(
|
|
155
|
+
'ParseError',
|
|
156
|
+
err.message,
|
|
157
|
+
sourceLines,
|
|
158
|
+
filePath
|
|
159
|
+
)
|
|
160
|
+
return { success: false, error: diagnostic }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Unknown error
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
error: new DiagnosticError(
|
|
167
|
+
'ParseError',
|
|
168
|
+
String(err),
|
|
169
|
+
{ file: filePath, line: 1, col: 1 },
|
|
170
|
+
sourceLines
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Format Compile Error
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
export function formatCompileError(result: CompileResult): string {
|
|
181
|
+
if (result.success) {
|
|
182
|
+
return 'Compilation successful'
|
|
183
|
+
}
|
|
184
|
+
if (result.error) {
|
|
185
|
+
return formatError(result.error, result.error.sourceLines?.join('\n'))
|
|
186
|
+
}
|
|
187
|
+
return 'Unknown error'
|
|
188
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RedScript Diagnostics
|
|
3
|
+
*
|
|
4
|
+
* Error reporting with file path, line, column, and formatted error messages.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Error Types
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export type DiagnosticKind = 'LexError' | 'ParseError' | 'LoweringError' | 'TypeError'
|
|
12
|
+
|
|
13
|
+
export interface DiagnosticLocation {
|
|
14
|
+
file?: string
|
|
15
|
+
line: number
|
|
16
|
+
col: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatSourcePointer(sourceLines: string[], line: number, col: number): string[] {
|
|
20
|
+
const lineIdx = line - 1
|
|
21
|
+
if (lineIdx < 0 || lineIdx >= sourceLines.length) {
|
|
22
|
+
return []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sourceLine = sourceLines[lineIdx]
|
|
26
|
+
const safeCol = Math.max(1, Math.min(col, sourceLine.length + 1))
|
|
27
|
+
const pointer = ` ${' '.repeat(safeCol - 1)}^`
|
|
28
|
+
return [` ${sourceLine}`, pointer]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class DiagnosticError extends Error {
|
|
32
|
+
readonly kind: DiagnosticKind
|
|
33
|
+
readonly location: DiagnosticLocation
|
|
34
|
+
readonly sourceLines?: string[]
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
kind: DiagnosticKind,
|
|
38
|
+
message: string,
|
|
39
|
+
location: DiagnosticLocation,
|
|
40
|
+
sourceLines?: string[]
|
|
41
|
+
) {
|
|
42
|
+
super(message)
|
|
43
|
+
this.name = 'DiagnosticError'
|
|
44
|
+
this.kind = kind
|
|
45
|
+
this.location = location
|
|
46
|
+
this.sourceLines = sourceLines
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format the error for display:
|
|
51
|
+
* ```
|
|
52
|
+
* Error: [ParseError] line 5, col 12: Expected ';' after statement
|
|
53
|
+
* 5 | let x = 42
|
|
54
|
+
* ^ expected ';'
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
format(): string {
|
|
58
|
+
const { kind, message, location, sourceLines } = this
|
|
59
|
+
const filePart = location.file ? `${location.file}:` : ''
|
|
60
|
+
const header = `Error: [${kind}] ${filePart}line ${location.line}, col ${location.col}: ${message}`
|
|
61
|
+
|
|
62
|
+
if (!sourceLines || sourceLines.length === 0) {
|
|
63
|
+
return header
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const pointerLines = formatSourcePointer(sourceLines, location.line, location.col)
|
|
67
|
+
if (pointerLines.length === 0) {
|
|
68
|
+
return header
|
|
69
|
+
}
|
|
70
|
+
const lineNum = String(location.line).padStart(3)
|
|
71
|
+
const prefix = `${lineNum} | `
|
|
72
|
+
const sourceLine = sourceLines[location.line - 1]
|
|
73
|
+
const safeCol = Math.max(1, Math.min(location.col, sourceLine.length + 1))
|
|
74
|
+
const pointer = ' '.repeat(prefix.length + safeCol - 1) + '^'
|
|
75
|
+
const hint = message.toLowerCase().includes('expected')
|
|
76
|
+
? message.split(':').pop()?.trim() || ''
|
|
77
|
+
: ''
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
header,
|
|
81
|
+
`${prefix}${sourceLine}`,
|
|
82
|
+
`${pointer}${hint ? ` ${hint}` : ''}`,
|
|
83
|
+
].join('\n')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
toString(): string {
|
|
87
|
+
return this.format()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Diagnostic Collection
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export class DiagnosticCollector {
|
|
96
|
+
private diagnostics: DiagnosticError[] = []
|
|
97
|
+
private sourceLines: string[] = []
|
|
98
|
+
private filePath?: string
|
|
99
|
+
|
|
100
|
+
constructor(source?: string, filePath?: string) {
|
|
101
|
+
if (source) {
|
|
102
|
+
this.sourceLines = source.split('\n')
|
|
103
|
+
}
|
|
104
|
+
this.filePath = filePath
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
error(kind: DiagnosticKind, message: string, line: number, col: number): void {
|
|
108
|
+
const diagnostic = new DiagnosticError(
|
|
109
|
+
kind,
|
|
110
|
+
message,
|
|
111
|
+
{ file: this.filePath, line, col },
|
|
112
|
+
this.sourceLines
|
|
113
|
+
)
|
|
114
|
+
this.diagnostics.push(diagnostic)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
hasErrors(): boolean {
|
|
118
|
+
return this.diagnostics.length > 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getErrors(): DiagnosticError[] {
|
|
122
|
+
return this.diagnostics
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
formatAll(): string {
|
|
126
|
+
return this.diagnostics.map(d => d.format()).join('\n\n')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throwFirst(): never {
|
|
130
|
+
if (this.diagnostics.length > 0) {
|
|
131
|
+
throw this.diagnostics[0]
|
|
132
|
+
}
|
|
133
|
+
throw new Error('No diagnostics to throw')
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Helper Functions
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create a DiagnosticError from a raw error message that includes line/col info
|
|
143
|
+
* e.g., "Expected ';' at line 5, col 12"
|
|
144
|
+
*/
|
|
145
|
+
export function parseErrorMessage(
|
|
146
|
+
kind: DiagnosticKind,
|
|
147
|
+
rawMessage: string,
|
|
148
|
+
sourceLines?: string[],
|
|
149
|
+
filePath?: string
|
|
150
|
+
): DiagnosticError {
|
|
151
|
+
// Try to extract line and col from message
|
|
152
|
+
const match = rawMessage.match(/at line (\d+), col (\d+)/)
|
|
153
|
+
if (match) {
|
|
154
|
+
const line = parseInt(match[1], 10)
|
|
155
|
+
const col = parseInt(match[2], 10)
|
|
156
|
+
const message = rawMessage.replace(/ at line \d+, col \d+$/, '').trim()
|
|
157
|
+
return new DiagnosticError(kind, message, { file: filePath, line, col }, sourceLines)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fallback: line 1, col 1
|
|
161
|
+
return new DiagnosticError(kind, rawMessage, { file: filePath, line: 1, col: 1 }, sourceLines)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function formatError(error: Error | DiagnosticError, source?: string): string {
|
|
165
|
+
if (error instanceof DiagnosticError) {
|
|
166
|
+
const sourceLines = source?.split('\n') ?? error.sourceLines ?? []
|
|
167
|
+
const { file, line, col } = error.location
|
|
168
|
+
const locationPart = file
|
|
169
|
+
? ` in ${file} at line ${line}, col ${col}`
|
|
170
|
+
: ` at line ${line}, col ${col}`
|
|
171
|
+
const lines = [`Error${locationPart}:`]
|
|
172
|
+
const pointerLines = formatSourcePointer(sourceLines, line, col)
|
|
173
|
+
if (pointerLines.length > 0) {
|
|
174
|
+
lines.push(...pointerLines)
|
|
175
|
+
}
|
|
176
|
+
lines.push(error.message)
|
|
177
|
+
return lines.join('\n')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!source) {
|
|
181
|
+
return error.message
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const parsed = parseErrorMessage('ParseError', error.message, source.split('\n'))
|
|
185
|
+
return formatError(parsed, source)
|
|
186
|
+
}
|