# 编译器架构设计方案

## 编译器主要模块划分

编译器可以划分为多个相对独立的模块，每个模块各司其职，按照顺序将源代码逐步翻译为目标虚拟机指令集。主要模块包括：

### 词法分析器（Lexical Analyzer）

词法分析器负责从源码文本中识别最小的语法单元**单词符号**（Token）。它逐字符读取源代码，跳过无用的空白和注释，根据定义的词法规则将字符序列切分为有意义的Token序列。词法分析器的职责包括：

- **符号识别**：识别标识符、关键字、数字字面量、字符串字面量、运算符（如`+`、`-`、`=`等）、标点符号（如逗号、括号等）以及缩进或换行等控制符号（如果语法基于缩进）。
- **过滤无关字符**：忽略空格、换行（在非显式有意义场景下）和注释内容，不生成相应Token。
- **错误检测**：当出现无法识别的字符或不合法的序列时，报告词法错误（例如不支持的字符，或字符串未正确闭合等）。

词法分析的输出是按顺序排列的Token列表，提供给下游的语法分析器使用。

### 语法分析器（Syntax Parser）

语法分析器根据语言的语法规则，将Token序列解析为抽象语法树（AST，Abstract Syntax Tree）。语法分析器的职责包括：

- **语法结构识别**：按照语言文法，将Token序列组织成语法结构。例如识别表达式、语句、函数定义、条件块、循环块、模块块等结构。对于自定义DSL类似YAML的语法，如果采用缩进表示层级，语法分析器需要结合缩进Token来确定层次结构（类似Python语法的块级缩进）。
- **AST构建**：创建对应的AST节点对象，构造出反映源代码层次结构的树形表示。每个节点表示源码中的一个构造（如二元表达式、赋值语句、函数声明等）。
- **语法错误处理**：如果Token序列不符合语法规则（如遇到意外的Token或缺少某个符号），报告语法错误。语法分析器可以尝试**错误恢复**（如跳过一定Tokens到语句末尾）以继续解析后续部分，从而发现更多错误。

语法分析的输出是编译器的中间表示——AST，它以树形结构表示整个源代码的语法结构。

### 语义分析器（Semantic Analyzer）

语义分析器在AST的基础上检查程序的语义正确性，并进行必要的语义处理。其主要职责包括：

- **符号解析**：建立**符号表**（Symbol Table），登记各种标识符的信息，包括变量、函数、模块等。分模块、分作用域检查每个标识符的**声明和定义**：例如变量在使用前是否已声明，函数调用时对应函数是否存在等。对于支持模块的语言，语义分析需要处理跨模块引用，如`CommonTasks.add_numbers`应解析到`CommonTasks`模块下名为`add_numbers`的函数定义。
- **类型检查**（如果语言是静态类型或计划支持类型检查）：检查表达式、赋值、函数参数/返回值等类型匹配是否正确。例如将一个函数返回值赋给变量时，类型是否兼容；算术运算的操作数类型是否可用等。如果当前语言未强制类型，可暂跳过严格类型检查，但设计上保留接口以便未来加入类型系统。
- **语义规则验证**：包括检查控制流语义（如`return`语句是否在函数内、循环控制语句如`break/continue`是否在循环内使用）、常量表达式求值（必要时进行常量折叠优化）等。
- **注解AST**：在AST节点上附加语义信息，例如每个表达式节点注入类型信息，每个变量/函数节点链接到其符号表条目。为后续代码生成做好准备。

语义分析阶段如发现语义错误（未声明的变量、函数不存在、类型不匹配等），将报告错误并阻止后续正确的代码生成。语义分析完成后，输出可以是**注释丰富的AST**（带有类型和符号引用），同时维护好符号表，供后续阶段使用。

### 指令生成器（Code Generator）

指令生成器将经过检查的AST转化为目标虚拟机的指令序列（即字节码或中间代码）。它的主要职责包括：

- **代码选择**：为AST中每种结构生成等价的虚拟指令序列。例如表达式运算生成对应的算术或位运算指令，条件和循环结构生成相应的分支跳转指令，函数调用生成调用和返回指令等。
- **资源分配**：为变量、常量、临时值分配虚拟机运行时所需的资源。例如给每个变量分配一个内存槽位或栈位置，函数则分配栈桢空间。指令生成阶段会利用符号表信息确定每个变量/函数地址或索引，用于生成`LOAD/STORE`等内存指令的操作数。
- **指令序列输出**：以适当的格式输出虚拟机指令列表。可以选择直接构建一个指令对象列表（在内存中运行），或者生成汇编文本/二进制文件供自定义虚拟机加载执行。**例如**，遇到函数定义时，产生相应的函数入口标签和终止的返回指令；遇到函数调用时，按照调用约定生成参数入栈指令和调用指令；遇到算术表达式则按操作次序生成`LOAD`和算术计算指令等。
- **优化（可选）**：在生成指令时做简单优化，如删除无效代码、常量折叠、简单的代数化简等（更复杂的优化可在将来引入独立优化模块，这里只考虑基础的直接翻译）。

指令生成器输出的结果是**虚拟指令列表**，它完成了从高级源语言到自定义虚拟机指令集的翻译。这些指令可以交由虚拟机解释执行，或者进一步封装成可加载的模块文件。

## 各模块的输入/输出格式

为了让各模块解耦并协同工作，需要定义清晰的接口，即各阶段输入和输出的数据格式：

- **词法分析器**：输入是源码的原始文本字符串；输出是**Token列表**（`List<Token>`）。每个Token包含类型、字面量值及位置信息。
- **语法分析器**：输入是词法分析器生成的Token列表；输出是**抽象语法树（AST）**的根节点（表示整个源码的结构）。AST由各类AST节点按树形组织。
- **语义分析器**：输入是初步的AST（以及初始的符号表）；输出是**注解过的AST**和填充完整的**符号表**。如果有语义错误，则输出错误信息而不产生有效AST。注解AST指在节点上附加类型、作用域等语义信息。
- **指令生成器**：输入是通过语义检查的AST（以及符号表）；输出是**虚拟机指令列表**，可以是一个Java内的数据结构（如`List<Instruction>`）或者文本形式的指令序列。每条指令包含操作码和操作数，构成最终可执行的字节码序列。

（在设计上，也可以在语义分析和指令生成之间增加**中间代码表示(IR)**和**优化模块**的输入输出，但当前方案下暂未引入独立的IR阶段。）

## 数据结构与类设计建议

为了实现上述各模块，需要设计合理的数据结构和类。以下是主要的Java类和结构建议：

### Token类

**Token**表示词法分析得到的最小语法单元。可以设计一个`Token`类包含以下属性：

- `type`：Token类型（例如标识符、关键字、数字、字符串、运算符等，可用枚举表示）。
- `lexeme`：Token在源代码中的字面文本。例如标识符的名字，数字的文本形式，符号本身等。
- `value`：Token的值（可选，用于存储已解析的数值、字符串字面量的实际值等）。例如数字`"20.2"`解析成一个Number类型的值20.2。
- `line`和`column`：位置信息，记录Token在源码中的行号和列号，用于错误报告和调试。

例如，可以定义枚举`TokenType`包括：IDENTIFIER、NUMBER、STRING、KEYWORD、OPERATOR、INDENT、DEDENT、NEWLINE等，根据DSL具体情况扩展。词法分析时，对每个识别出的符号创建一个Token实例，设置其类型和lexeme等。

### AST节点类层次

**抽象语法树(AST)**可以用面向对象的类层次来表示。设计时通常有一个AST节点的基类，以及按语法元素分类的子类。建议的类设计：

- 抽象基类 `ASTNode`：包含所有AST节点共有的属性和方法。例如：
    - `sourceLocation`：源代码位置（行列范围）便于错误提示。
    - `accept(ASTVisitor v)`：如果考虑使用访问者模式进行遍历，可以预留accept方法。
    - （如果需要类型系统）`type`：该节点表达式的计算类型。
- **表达式类**：派生自ASTNode，用于表示表达式的节点。例如：
    - `LiteralNode` 字面量节点：包含字面量的值和类型（如数值、字符串等）。
    - `VariableNode` 变量节点：包含变量名，可能还有引用的符号表条目用于指代其声明。
    - `BinaryOpNode` 二元运算节点：包含运算符类型以及左、右子表达式AST节点。
    - `UnaryOpNode` 一元运算节点（如负号、逻辑非），包含运算符和子表达式。
    - `CallNode` 函数调用节点：包含被调用函数的标识（函数名或引用）和实参表达式列表。若DSL支持模块命名空间，可以在节点中区分函数所属模块或在语义分析时将其解析为函数Symbol引用。
- **语句类**：派生自ASTNode，用于表示可执行的语句结构。例如：
    - `AssignNode` 赋值语句节点：包含左侧的目标（一般是VariableNode）和右侧的表达式（AST节点），表示形如`变量 = 表达式`的赋值操作。
    - `IfNode` 条件语句节点：包含条件表达式，以及then分支和else分支的语句块（可以是AST节点列表或一个BlockNode封装）。
    - `WhileNode` 或 `ForNode` 循环节点：包含循环条件表达式，以及循环体语句块节点。根据DSL具体语法选择命名，若DSL有特定循环关键字就对应命名（如`LoopNode`泛指循环）。
    - `ReturnNode` 返回语句节点：可能包含返回值表达式节点（如果函数有返回值），或无子节点表示空返回。
    - `FunctionCallStmtNode` 函数调用语句节点：用于表示独立的一条调用语句（当调用的返回值不被使用时也可视作一条语句），内部可复用`CallNode`来表示调用的结构。
    - `PrintNode` 打印语句节点：如果DSL支持`print`等输出功能，可以有专门节点，或者也可以视为特殊的函数调用节点。
    - `BlockNode`（可选）代码块节点：包含一系列顺序执行的子语句节点列表。对于缩进结构的DSL，可以用BlockNode表示缩进构成的语句序列，从而AST层次清晰地反映作用域。

- **模块与函数**：
    - `ModuleNode` 模块节点：表示一个模块/命名空间，包含模块名和该模块内定义的函数列表。方便组织多模块代码，以及支持模块间调用。
    - `FunctionNode` 函数定义节点：包含函数名、形参列表、返回类型（如果有）、函数体（通常是一个BlockNode）等属性。形参列表可为`List<ParamNode>`，其中ParamNode包含参数名及类型等信息。FunctionNode也可记录所在模块，方便区分同名函数。

AST类需要体现出源码的结构和层次。以上只是建议，可根据DSL具体特性调整或增加节点类型。例如，若DSL采用YAML风格缩进而无显式`{}`块，则BlockNode的生成可依据缩进Token产生。

### 符号表与Symbol类

**符号表（Symbol Table）**用于在语义分析阶段记录和查询标识符的语义信息。可以设计一个符号表类（或接口）以及符号类来封装信息：

- 符号表通常可以按**作用域**分层。典型做法是在每进入一个新作用域（如进入一个函数、进入一个代码块（如if/while内部））时，创建一个新的符号表或符号表的嵌套结构。符号查找时先查当前作用域，找不到再向上层作用域查找。这可以通过符号表类中保存对父级符号表的引用实现。
- 每个符号表可以使用一个哈希表（如`Map<String, Symbol>`）存储名字到Symbol对象的映射，以便快速查找。

**Symbol类**则代表一个符号（标识符）的信息。可能有多种符号类型，例如变量符号、函数符号、模块符号等，可以用继承或字段区分：

- 公共字段：`name`（符号名称），`kind`（符号种类枚举：变量、函数、模块、参数等），`scope`（所属作用域或符号表引用）。
- **变量符号**：包含变量名、类型、在运行时的存储分配信息（比如相对于栈帧起始的偏移量、所在寄存器或全局内存地址）。当代码生成时会用到偏移量或索引来定位变量的位置。
- **函数符号**：包含函数名、返回类型、参数列表（参数也是符号，有名称和类型）、以及指向函数AST节点或其指令起始位置的引用。还可以包含该函数所属模块的信息。
- **模块符号**：包含模块名以及模块内符号表（用于存储该模块下的函数和全局符号）。模块本身也可视为一种符号，便于在跨模块调用时通过模块符号找到其内部内容。

符号表在语义分析时填充：在遇到变量声明时将Symbol加入当前符号表；函数定义时将Symbol加入模块符号表；模块定义时在全局注册模块符号等等。在语义分析阶段进行标识符解析时，通过符号表来验证标识符是否存在以及获取附加信息（比如变量类型用于类型检查）。

### 虚拟机指令表示

最终生成的**虚拟机指令**可以用适当的数据结构表示，以方便Java代码输出和操作。例如：

- 定义一个指令类 `Instruction`，包含操作码和操作数字段：
    - `opcode`：操作码，可用枚举类型定义全部指令集指令，如ADD、SUB、AND、OR、LOAD、STORE、JMP、JMP_IF、CALL、RET、PUSH、POP等。
    - `operands`：操作数列表，根据指令不同存储整数、浮点数、字符串（用于符号引用）等。例如 `LOAD_CONST 20.2` 的操作数是一个浮点常量20.2；`LOAD_VAR x` 操作数可以是变量在符号表中的索引或内存地址；`CALL f, n` 可能有操作数表示被调函数标识符或地址，以及参数个数n。
- 可以为不同指令定义子类，或者令Instruction包含所有可能字段（如 int operand1, double operand2 等）但只使用与该opcode相关的字段。
- 另一种方式是简单使用字符串或枚举加参数直接拼装指令文本，例如 `String codeLine = "LOAD_VAR " + index;` 最终组合成可供虚拟机解析执行的文本。但使用类结构可以更安全、结构化地生成和优化指令序列。

此外，可以设计一个容器类如`Program`或`ModuleCode`来存放指令列表和相关信息。例如一个Program类包含：模块名、指令列表、常量池、调试符号信息等。这样可以方便地序列化、保存到文件或加载执行。

通过上述数据结构设计，编译器各阶段可以清晰地交付和操作相应的数据：词法分析产生Token对象，语法分析组装AST节点，语义分析利用Symbol对象检查语义，代码生成产生Instruction列表。这些类在Java中应尽量设计为**不可变或只在构造时修改**（如Token、AST节点），以保证编译过程的稳定性。

## 开发顺序建议

开发应遵循编译流程顺序，分阶段实现和测试每个模块，逐步集成。建议的实施顺序：

1. **实现词法分析器**：首先编写Lexer模块。根据语言定义列出需要识别的Token类型和模式，编写解析逻辑。可采用手写状态机或正则表达式匹配的方式逐字符扫描。实现完成后，通过简单源码片段测试：输入一段源代码字符串，输出Token列表，检查是否正确识别了标识符、关键字、数字等，忽略了空白和注释。例如测试单行赋值语句、函数头等，看Token序列是否符合预期。必要时设计单元测试用例，比如给定字符串`"x = 5"`，期望输出Tokens：IDENTIFIER("x"), OPERATOR("="), NUMBER(5)。如果DSL使用缩进语法，也需测试不同行和缩进输出正确的INDENT/DEDENT/NEWLINE Token序列。

2. **实现语法分析器**：在Lexer正确工作的前提下，编写Parser模块。可采用**递归下降**算法手写解析器（为每个语法规则编写一个方法），或其他解析算法（如LL(1)预测分析）。首先设计或参考文法规则，明确语法层次。例如：
    - 程序 = 模块列表
    - 模块 = `module 模块名:` 缩进的函数定义列表 （假设DSL用冒号+缩进表示从属关系）
    - 函数定义 = `function 函数名(参数列表):` 缩进的语句列表
    - 语句 = 赋值语句 | 条件语句 | 循环语句 | 返回语句 | 函数调用语句 | 打印语句 ...
    - 表达式 = 算术表达式 | 函数调用 | 字面量 | 变量 等
      根据文法编写相应的解析函数。例如parseExpression处理表达式优先级，parseStatement根据下一个Token判断语句类型并调用相应子解析。逐步实现各子模块，组装AST节点。

   语法分析阶段需要经常测试：可以从简单的语句开始测试AST生成是否正确，然后逐步增加复杂结构。比如：
    - 测试表达式解析：输入如`a + b * 2`，检查生成的AST节点树形结构（BinaryOpNodes嵌套）符合期望的优先级。
    - 测试简单语句：如赋值`x = 1`是否构造出AssignNode，子节点正确。
    - 测试控制结构：如`if`和`while`语句的AST，包含条件和子块节点。
    - 测试函数：定义一个简单函数，解析后AST的FunctionNode有正确的参数列表和函数体子树。
      可以编写调试输出函数，将AST以缩进形式打印，便于人工校验结构。持续迭代，发现解析错误及时修正文法规则或解析实现。

3. **实现语义分析器**：在能够成功构造AST后，开发Semantic Analyzer。首先设计符号表数据结构，然后遍历AST进行检查：
    - **符号表建立**：比如先遍历所有模块和函数声明，将函数Symbol加入模块符号表（支持前向引用的语言需要先收集声明）。然后再遍历函数内部，将参数和局部变量声明加入对应符号表。
    - **语义检查**：第二轮或在同一遍历中，检查每个AST节点的语义：
        * 对每个变量引用节点(VariableNode)，在符号表中查找其声明Symbol，不存在则报错；存在则可以在该节点上附加符号信息（方便代码生成）。
        * 对函数调用节点(CallNode)，检查函数名解析：首先根据是否有模块前缀解析模块符号，再查找函数Symbol。校验参数个数和类型是否匹配函数签名。
        * 检查控制流：如ReturnNode是否在函数内，返回类型与函数声明的返回类型匹配；循环中的Break/Continue（如果支持）是否在循环体内。
        * 类型检查（如需要）：表达式节点，根据子节点类型推导结果类型，检查赋值两边类型兼容等等。
    - **注解/标注**：将解析出的符号和类型信息记录回AST或Symbol表。例如每个VariableNode关联它所引用的Symbol，CallNode关联目标函数的Symbol，方便后续直接使用已经解析的地址或类型信息。  
      测试语义分析需要构造各种场景：
    - 正确的代码应通过检查，符号表内容正确。
    - 刻意制作错误代码，如引用未声明变量、函数调用参数错误等，验证是否能捕获并输出适当错误信息（包含行号和错误原因）。
      对于通过的代码，可打印符号表或在AST节点附加的信息核对是否准确（如变量Symbol的偏移、类型，函数Symbol的参数列表等）。

4. **实现指令生成器**：语义检查正确后，实现将AST转换为虚拟机指令。可使用**AST遍历**(如递归或访问者模式)生成代码：
    - 考虑函数为最基本的编译单位，对每个函数的AST生成一段指令序列。可以在函数Symbol中记录起始指令的索引或地址。
    - **表达式代码生成**：采用后序遍历，对每个表达式节点生成指令：
        * 例如BinaryOpNode：先生成左子表达式的指令序列，把结果压入栈，再生成右子表达式指令，结果压栈，最后根据运算符opcode生成对应算术指令（如ADD）。如果是短路逻辑运算符，还需特殊处理跳转。
        * VariableNode（变量取值）：根据符号表偏移，生成`LOAD_VAR offset`指令，将变量值压栈。
        * LiteralNode：生成`LOAD_CONST value`指令。
        * CallNode：先计算每个实参表达式（将值按顺序压栈），然后生成调用指令`CALL func`。调用指令的操作数可以是被调用函数在全局的地址/索引，或者符号名由虚拟机在运行时链接。调用完毕后，约定返回值会压入栈顶。
    - **语句代码生成**：依照语句类型生成：
        * AssignNode：对右侧表达式生成指令（结果在栈顶），然后根据左侧变量偏移生成`STORE_VAR offset`指令将值弹栈存入变量。
        * IfNode：生成条件表达式指令，接着是一条条件跳转指令（如`JMP_IF_FALSE`）跳过then块；then块语句生成；如果有else块，则再插入无条件跳转跳过else块的指令以及相应的标签处理；else块语句生成。需要适当设置跳转目标地址，在生成过程中可能需要回填（先生成占位跳转，再知道目标位置后回填）。
        * WhileNode：需要创建循环起始标签，生成条件表达式，接条件跳转指令跳出循环（跳转到循环后位置）; 循环体指令; 最后插入跳转回循环开头的指令形成闭环; 回填跳出循环的目标位置到之前的条件跳转指令。
        * ReturnNode：如果有返回值表达式则先生成其代码（将值压栈），然后生成`RET`指令（操作数可能用于返回值或直接弹栈返回）。约定RET执行时虚拟机会将栈顶值作为返回值处理。
        * PrintNode（或调用printf等）：生成参数表达式后，调用输出函数的指令（可视作特殊的函数调用或特定的I/O指令）。
    - **汇编/输出**：将生成的指令序列输出。在测试阶段，可以先输出人类可读的指令文本（模拟汇编形式），以验证逻辑。例如：
      ```
      LOAD_VAR 0      ; 将局部变量0 (input_number) 压栈  
      LOAD_CONST 20.2 ; 将常数20.2 压栈  
      CALL CommonTasks.add_numbers, 2 ; 调用函数，加两个参数  
      STORE_VAR 1     ; 弹栈将结果存入局部变量1 (sum_result)  
      ```
      通过编译简单函数并打印指令序列，人工核对指令逻辑是否正确，尤其关注跳转指令的目标是否正确计算，函数调用前后栈平衡是否正确等。如果有自己的虚拟机执行环境，进一步可以将生成的指令送入虚拟机运行，看输出结果是否符合源代码预期。

5. **集成和整体测试**：各模块分别验证后，整合完整的编译流程。从词法分析到代码生成编写一个总控程序，给定源码文件，依次调用lexer、parser、semantic、codegen，最终产出可执行指令。编写多个样例程序做集成测试：
    - 简单算术运算、条件、循环、小函数调用等，编译后在虚拟机上运行，核对运行结果。
    - 包含一些语法/语义错误的代码，看是否在编译期得到合理的错误输出。
    - 多模块调用的程序，验证跨模块函数调用的正确性。
      通过循序渐进的测试与调试，保证各阶段正确衔接。

这一开发顺序遵循由浅入深、分模块实现的原则，每完成一阶段都验证其输出，再进入下一阶段，从而快速定位和修复问题，确保最终构建出正确的编译器。

## 错误处理建议

良好的错误处理对编译器尤为重要，应提供清晰的错误信息且在可能情况下尽量恢复继续分析，以发现更多错误。以下是各阶段的错误处理建议：

- **词法分析错误**：如果Lexer遇到非法字符或不匹配任何已知Token规则的文本片段，应报告词法错误。可以抛出自定义异常`LexicalException`携带位置信息和错误原因，或者返回一个特殊的ERROR Token。由于词法错误往往会导致无法继续解析，该错误一般是不可恢复的（停止进一步编译）。对于可跳过的简单错误（如不支持的转义符），Lexer可选择跳过字符继续，但总体上应中止并报告，让用户修改源码。

- **语法分析错误**：当Parser检测到Token序列不符合语法期望（例如缺少右括号、遇到不合法的关键字顺序），应抛出`SyntaxException`或记录错误消息。错误信息应包括出现错误的源码位置和预期的语法构造。例如：“Syntax Error: 在第10行: 缺少右括号 ')', 在`func(a, b`处”。  
  为了提高用户体验，Parser可以实现**简单的错误恢复**策略：
    - **Panic模式**：跳过一定范围的Token直到可能的语句边界（例如跳到下一行换行或分号后）再继续解析。这允许找到后续语句的错误，而不被前一处错误完全阻断。
    - **插入或删除**：在容错模式下，Parser可假设缺失某符号并继续（如遇到缺少的右括号，可以假装看到一个`)`），同时记录一条错误。  
      基于实现复杂度考虑，初期实现可在报错后终止解析。但设计上应使Parser易于扩展错误恢复功能。所有语法错误都应汇总报告，错误信息应当语句清晰、指明问题和位置。

- **语义分析错误**：在AST检查过程中，如发现语义不一致，应记录**语义错误**并标记编译失败。典型语义错误及处理：
    - **未声明标识符**：如变量或函数使用前未声明。在VariableNode或CallNode分析时检测，报告错误：“Semantic Error: 未定义的变量x，位于第N行第M列” 或 “函数Foo未定义” 等。
    - **重复定义**：如同一作用域内变量重名，函数重复定义。应在插入符号表时检查，如果已存在同名符号则报告错误以及位置。
    - **类型不匹配**：如赋值语句左右类型不兼容，函数调用实参类型与形参类型不符等。报告应指出期望类型和实际类型。
    - **其他语义约束**：如`return`语句不在函数内使用，`break`不在循环中使用。这些情况在检测到时，也应报告错误和发生位置。  
      语义错误通常可以在一次遍历中收集多个而不马上终止编译。例如可以将所有错误信息存入一个列表，语义分析遍历结束后，如果列表非空，则一起输出所有错误。但是，对于某些致命错误（比如后续分析依赖的信息缺失），也可以选择中止。  
      错误消息应避免晦涩技术术语，尽量友好说明问题。例如：“类型错误：尝试将字符串赋值给数值变量 'count' (第20行)”等。

- **错误报告统一与模型**：建议定义一个通用的错误表示结构，例如`CompileError`类，包含错误类型（词法/语法/语义）、错误消息、源代码位置等。编译过程中捕获到的错误都转换为CompileError对象并收集。在编译结束时，若有错误列表，则按照出现顺序输出所有错误信息，并终止代码生成。  
  在IDE或命令行环境中，错误输出格式可以统一为“文件名:行:列: 错误类型: 描述”。这种模型便于将来拓展（如引入更多错误类型、警告类型等）。

总之，错误处理应贯彻“**早发现、早报告**”和“**清晰定位**”的原则。前端检测（词法/语法/语义）的错误比运行期错误更容易理解，因此编译器应尽量完善前端检查，让用户在编译阶段就修复问题。设计良好的错误处理模块能显著提升编译器的可用性和健壮性。

## 可扩展性建议

在编译器架构设计中，应考虑未来可能的功能扩展，例如优化器、调试支持、类型检查的加强等。通过模块化和面向对象设计，可以使编译器易于扩充而不需推倒重来。以下是几个方面的扩展建议：

### 优化扩展

目前的编译流程直接从AST生成虚拟机指令。为了将来支持代码优化，可以在架构中预留**中间表示(IR)**或优化阶段：
- **中间表示**：可在语义分析之后，引入一个中间代码生成阶段，将AST转换为一种更低级但易于优化的表示形式（例如三地址码、控制流图、四元组等）。然后在此IR上进行优化处理，如常量折叠、死代码消除、公共子表达式消除等，再将优化后的IR翻译为最终指令。由于当前直接生成虚拟机指令也能工作，中间表示阶段可以以后按需添加。确保AST结构设计和代码生成模块松耦合，有助于将来插入IR阶段而影响最小。
- **模块化优化**：如果暂不采用中间表示，也可以在现有AST基础上进行优化。设计Visitor模式方便新增遍历。例如可以新增`OptimizationVisitor`遍历AST，对某些模式进行变换（如乘2替换为左移1位指令等）。通过访问者，不需修改原有Parser或AST结构即可插入优化流程。
- **可配置开关**：架构上可以让优化阶段可选启用，如提供编译选项控制是否运行优化。当扩展优化功能后，不影响原有正常编译，只在需要时插入额外步骤。

总的来说，通过引入清晰的中间层和访问者模式，优化器可以作为一个插件式模块加入，提高代码运行效率。

### 调试支持扩展

为了支持将来的调试和分析，需要在编译时生成必要的调试符号信息，并设计编译器输出使之便于调试：
- **调试符号信息**：在代码生成阶段，可以收集源代码与指令的对应关系。例如维护一个映射，记录每条虚拟机指令对应的源代码行号、所在函数。当运行在调试模式的虚拟机遇到断点或异常时，可以通过该映射还原源码位置。这要求编译器在输出指令时附带这些元数据（可以存在一个专门的调试信息区段或者一张表）。
- **变量和符号信息**：为了在调试时查看变量值，编译器应保留变量的符号表或名称到运行时存储位置的映射。可以在输出中包含局部变量表结构，使调试器在检查内存或栈时知道哪个槽位对应哪个变量名。同样地，函数的符号也可保留用于更友好的堆栈调用信息。
- **调试选项**：编译器设计中可以加入调试选项，比如`-g`开启调试符号生成。当此选项启用时，上述额外的符号/行号信息会打包进输出；未启用时则省略这些信息以优化体积和速度。
- **友好的错误和日志**：在非运行时的调试，例如编译错误场景，编译器本身也可提供**日志**或**可视化**AST的功能，帮助调试编译器或DSL程序。例如在调试模式下输出详细的解析过程、符号表状态等。这些都可通过配置开关实现，不影响正常编译路径。

通过这些设计，将来可以实现单步执行、断点、查看变量等调试功能，使自定义语言的开发体验更接近成熟语言环境。

### 类型检查扩展

如果当前DSL类型系统简单（比如可能只有数值类型或弱类型），未来可能引入更丰富的**类型检查**，包括自定义数据类型、类型推导、函数重载等。为此，在现有设计中可以做好准备：

- **类型信息在AST和Symbol中的存储**：即使暂时只有基本类型，也可以在AST节点和Symbol类中加入类型字段。例如LiteralNode记载其类型(整型、浮点型等)，Variable Symbol记录声明类型或推导类型。这样将来增加类型时，不需大改数据结构。
- **类型检查模块化**：目前语义分析或代码生成可能对类型未严格要求，但将来可以把类型检查作为语义分析中的独立步骤。例如引入一个`TypeChecker`访问AST，根据上下文推导类型、检测不一致。通过将类型检查逻辑集中，未来扩展如增加类型转换规则、泛型等会更容易管理。
- **错误和警告机制**：随着类型系统复杂化，可能出现类型相关的警告（例如隐式类型转换的精度丢失）等。因此错误报告机制应能区分严重错误和一般警告，允许编译器在遇到非致命问题时提示但继续编译。这可以通过扩展CompileError类加入级别属性实现。
- **标准库类型支持**：未来或许增加更多内建类型（字符串、布尔、集合等）和相应操作。编译器符号表可设计为易于注册内建类型和函数，比如启动时预先载入一些核心库Symbol。如果有类型检查，需要让这些内建符号携带正确的类型签名。

通过以上扩展措施，编译器能够从当前的简单类型逐步演进到强类型系统，而不用推翻既有架构。

### 其它设计扩展考虑

- **模块和多文件编译**：如果将来需要支持多文件编译或模块单独编译，编译器架构应考虑**分步编译和链接**。例如可以实现每个模块单独编译成中间表示或字节码文件，然后通过链接器组合。这要求符号表设计支持全局符号解析、未定义符号留待链接解决等。当前实现可以简单假设所有源码一次性编译，未来可逐步转为更灵活的编译流程。
- **后端移植**：当前后端目标是自定义虚拟机指令集。若未来考虑输出其他形式（例如直接生成Java字节码或机器码），可以在后端模块中采用接口/抽象工厂模式，使得不同的**CodeEmitter**实现对应不同目标。当增加新后端时，复用前端，只需实现新的指令选择和输出模块。这也是扩展性的一环。
- **性能与并行**：对于大型源码，将来可考虑词法和语法分析的性能（比如引入多线程编译不同模块）。设计时保持模块边界清晰有助于并行化，例如每个模块AST可并行语义分析，再汇总符号表。虽然目前可能不需要，但良好的模块划分为将来性能优化留出空间。

综上，编译器架构在初始设计时就应秉承**高内聚、低耦合**原则，各阶段通过清晰的数据接口衔接。这样在需要添加新功能时，可以在原有框架基础上拓展新模块或替换某模块的实现，而不影响其他部分。例如，可以方便地插入一个优化器、启用调试信息生成、增强类型系统。这种可扩展性设计确保编译器能够随着用户需求的演进而演化，而无需推倒重写。

## 编译流程示例：代码片段完整编译过程

下面通过一个简单示例展示从源代码到虚拟机指令的完整编译过程。示例源代码片段为：

```plaintext
sum_result = CommonTasks.add_numbers(input_number, 20.2)
```

假定这行代码位于某个函数体内，作用是调用模块`CommonTasks`中的函数`add_numbers`，传入参数`input_number`和字面量`20.2`，将返回值赋给本地变量`sum_result`。编译过程各阶段处理如下：

1. **源代码**：编译器读取到这一行源代码字符串：
   ```plaintext
   sum_result = CommonTasks.add_numbers(input_number, 20.2)
   ```
   （在完整源码中，这可能缩进于一个函数定义下。）

2. **词法分析（Token序列）**：Lexer按规则将该行拆解成Token序列：
    - 标识符`sum_result` → Token(TYPE=IDENTIFIER, lexeme="sum_result")
    - 运算符`=` → Token(TYPE=OPERATOR, lexeme="=")
    - 标识符`CommonTasks` → Token(TYPE=IDENTIFIER, lexeme="CommonTasks")
    - 分隔符`.` → Token(TYPE=DOT, lexeme=".")
    - 标识符`add_numbers` → Token(TYPE=IDENTIFIER, lexeme="add_numbers")
    - 分隔符`(` → Token(TYPE=LPAREN, lexeme="(")
    - 标识符`input_number` → Token(TYPE=IDENTIFIER, lexeme="input_number")
    - 分隔符`,` → Token(TYPE=COMMA, lexeme=",")
    - 数字字面量`20.2` → Token(TYPE=NUMBER, lexeme="20.2", value=20.2)
    - 分隔符`)` → Token(TYPE=RPAREN, lexeme=")")
    - （行结束）换行或缩进减少 → Token(TYPE=NEWLINE, lexeme="\n") 等（具体取决于语法对换行的处理）

   词法分析输出的Token列表按顺序传递给Parser。对于本行，无非法字符，词法分析顺利完成。

3. **语法分析（AST）**：Parser根据语法规则识别出这是一个**赋值语句**，带有一个函数调用表达式。构造出的AST（抽象语法树）结构可以表示为：

   ```plaintext
   AssignNode
   ├── target: VariableNode("sum_result")
   └── value: CallNode
         ├── func: "CommonTasks.add_numbers"  (可能拆分为 Module="CommonTasks", FuncName="add_numbers")
         └── args:
             ├── VariableNode("input_number")
             └── LiteralNode(20.2)
   ```

   解释：
    - 根节点是一个AssignNode，表示赋值操作。
    - AssignNode的`target`子节点是VariableNode("sum_result")，表示将值存入的目标变量。
    - AssignNode的`value`子树是一棵函数调用表达式。CallNode表示调用`CommonTasks.add_numbers`，它包含对被调用函数的引用以及参数列表。
    - 在CallNode下，`args`列表有两个元素：VariableNode("input_number")表示第一个实参引用了变量`input_number`，LiteralNode(20.2)表示第二个实参是20.2这个字面量常数。

   AST很好地反映了语句的层次结构：赋值语句的右侧本身是一个复合的函数调用表达式。此阶段Parser已确保语法正确，例如括号配对、逗号分隔等都符合规则。如果有语法错误，此时Parser会报错而无法生成上述AST。

4. **语义分析**：Semantic Analyzer对AST进行符号和类型检查：
    - 符号表在进入当前函数时已包含其参数列表以及局部变量声明。我们假设符号表中已存在符号：`input_number`（函数参数），`sum_result`（事先声明的本地变量）。如果`sum_result`尚未声明，那么在此赋值前应报语义错误“未声明变量sum_result”。
    - 解析函数调用符号：`CommonTasks.add_numbers`应拆解为模块名和函数名。首先在全局/模块符号表中查找模块`CommonTasks`，如果找到，再在该模块的符号表中查找函数`add_numbers`。假设在其他源码中有对CommonTasks模块及其函数的声明，那么符号表能解析成功，得到函数符号（包含其参数类型列表、返回类型等）。
    - 检查函数参数匹配：假设`add_numbers`函数期望两个参数，第一个类型与`input_number`类型匹配（例如都是数值型），第二个参数应接受一个数值常量20.2（浮点型）。如果类型不符（比如`input_number`是整型但函数需要浮点型），可以在此报类型警告或错误。如果类型系统不严格，也可以允许自动转换。
    - 将符号信息注入AST：VariableNode("input_number")绑定到符号表中`input_number`的Symbol实例（包含其类型、偏移等）；VariableNode("sum_result")绑定到`sum_result`的Symbol。CallNode可能存储解析到的函数Symbol引用（包括其在虚拟机中的入口地址或一个符号ID，参数类型签名等）。
    - 其他检查：确认`CommonTasks.add_numbers`函数是否有返回值。如果函数声明有返回，赋值接收返回是合理的；如果函数为void却被用在赋值，将报告语义错误。这里假设add_numbers有返回值。

   经过语义分析，这行代码没有发现错误，AST节点被注解上了相关符号和类型信息，符号表也准备好供代码生成使用。例如，符号表可能提供：`input_number`在当前函数栈帧偏移0的位置，`sum_result`偏移1的位置，函数`CommonTasks.add_numbers`对应全局函数索引或入口地址等信息。

5. **指令生成**：Code Generator遍历AST生成目标虚拟机指令序列。按照前述AST：
    - 对AssignNode进行代码生成时，先处理右侧的`CallNode`（因为需要计算出值再赋给左侧）。
    - 生成函数调用代码：
        1. **参数1** (`VariableNode("input_number")): 生成加载变量指令，将`input_number`值压入栈。例如：  
        `LOAD_VAR 0    ; 将偏移0处的变量 input_number 压入栈`  
        （注：操作数0是`input_number`在当前作用域的偏移，由符号表提供。LOAD_VAR指令属于内存操作，将局部变量读取到计算栈）
        2. **参数2** (`LiteralNode(20.2)`): 生成加载常量指令，将20.2压入栈，例如：  
           `LOAD_CONST 20.2    ; 将常数20.2 压入栈`  
           （编译器可能在常量池维护20.2并引用之，这里为简化直接写值）
        3. **调用** (`CallNode`本身): 现在实参已按顺序在栈上，生成函数调用指令。比如：  
           `CALL CommonTasks.add_numbers, 2    ; 调用CommonTasks模块的add_numbers函数，传入2个参数`  
           该指令会跳转到指定函数执行，完成后将返回值压入栈顶。这里操作数标识了被调用函数，可以是一个引用索引、地址，或模块+函数名称组合，由虚拟机解析。参数个数2可能用于调用约定（虚拟机根据此清理参数等）。
    - 函数调用完成后，虚拟机栈顶留下了`add_numbers`的返回值。接下来生成AssignNode左侧赋值的代码：
        4. **赋值目标** (`VariableNode("sum_result")`): 赋值将把栈顶值存入目标变量。根据符号表，`sum_result`是当前函数偏移1的局部变量，于是生成：  
           `STORE_VAR 1   ; 将栈顶值弹出并存入偏移1处的局部变量 sum_result`  
           STORE_VAR指令弹出一个值存入指定位置，实现赋值操作。完成后栈顶返回值被清除且写入变量。

   汇总以上步骤，最终的指令序列（伪代码形式）如下：
   ```plaintext
   LOAD_VAR 0            ; 载入 input_number (参数1)
   LOAD_CONST 20.2       ; 载入常数20.2    (参数2)
   CALL CommonTasks.add_numbers, 2   ; 调用函数，计算 add_numbers(input_number, 20.2)
   STORE_VAR 1           ; 将返回值存入 sum_result
   ```
   这段指令完成了源代码要求的功能：调用函数并获取结果到目标变量。若在函数末尾，还可能需要相应的`RET`指令，但这里仅示范单条语句的翻译。

6. **虚拟机执行**（编译输出的利用）：编译器将上述指令输出到目标格式（内存或二进制文件）。之后用户的自定义虚拟机可以载入并解释执行这段指令。例如，在运行时当执行到`CALL CommonTasks.add_numbers`时，虚拟机跳转到事先编译好的`CommonTasks.add_numbers`函数指令序列执行，完成后将返回值压栈，再继续后续`STORE_VAR 1`。执行完毕后，虚拟机的栈帧中偏移1处（sum_result的位置）就存放了计算结果。由此可见，源代码中的意图已经通过编译后的指令得到实现。

这个示例展示了编译器如何分阶段处理源代码：词法分析获得Token流，语法分析形成AST，语义分析确保符号合法并注解信息，最后代码生成转化为低级指令。整个过程中，各模块各尽其职，将高级DSL逐步翻译为自定义虚拟机可以理解和执行的形式。

