简介本资源是一份面向高校计算机专业本科生的编译原理课程设计报告聚焦Pascal子集编译器的完整实现方案帮助学习者系统掌握词法分析、语法分析、语义分析、中间代码生成等核心编译阶段的设计与编码实践。报告由北京邮电大学五人团队合作完成内容覆盖从Sub_P文法定义、符号表构建、LL(1)分析表设计到三地址码生成、类型检查机制及函数参数传递策略等关键技术细节并附有明确的模块分工、接口定义如lexout()函数、属性文法表格与错误处理说明。压缩包为单个952KB的Word文档.doc结构清晰含需求分析、总体设计、各阶段详细实现及成绩评定标准适合作为课程设计参考范本或编译原理实验拓展材料。目前已有240人学习下载内容扎实、步骤可复现特别适合需深入理解手工编写编译器全流程的学习者。1. 为什么现在还要手写一个小型 Pascal 子集编译器——它不是教学玩具而是编译原理的「最小可验证黑匣子」你可能刚在 CSDN 看到“AI 编译器”“LLVM 插件开发”这类热词转身就被导师甩来一份《小型 Pascal 子集编译器 设计报告》作业也可能在调试 CH32V 芯片中断函数时突然意识到连procedure和begin...end都没亲手解析过谈何理解 GCC 的-O2优化路径这个标题不是怀旧而是一次精准的「编译原理肌肉训练」它强制你把词法分析、语法分析、语义检查、中间代码生成、目标代码映射这五层抽象压进不到 2000 行 C 或 Python 里跑通。它不生成 x86 机器码但必须能将program hello; begin writeln(hi); end.编译成可执行的汇编片段如 NASM 格式或字节码它不支持指针和动态数组但必须严格校验var x: integer; y: real;的类型声明与x : y 1的赋值兼容性。这不是为替代 GCC 或 Free Pascal而是为你建立一套可调试、可单步、可修改的「编译器认知锚点」——当某天你看到编译器未包含 main 类型的报错不再只查 Stack Overflow而是立刻翻出自己写的SymbolTable::lookup(main)实现确认是否漏了PROGRAM节点的入口符号注册。适合正在啃《Compilers: Principles, Techniques, and Tools》龙书第 2–6 章、用 VS Code CMake 调试 C 编译器项目、或准备嵌入式系统编译链移植的工程师。2. 从词法到语法用 Flex/Bison 搭建可调试的解析骨架Pascal 子集的语法边界必须先划死否则后续所有模块都会漂移。我们采用经典三段式设计词法规则限定关键字与分隔符语法规则锁定结构块嵌套语义规则约束类型与作用域。这里不追求完整 Pascal跳过packed array、variant record、goto但必须覆盖program/begin/end/if/while/procedure/function四大核心结构以及integer/real/boolean三种基础类型。选型上Flex Bison 是当前工业界最易调试、文档最全、错误定位最准的组合——相比手写递归下降易写难调或 ANTLRJava/Python 生成体过大Flex/Bison 输出的 C 代码可直接用 GDB 单步且.l/.y文件天然支持语法冲突可视化bison -v生成parser.output。2.1 词法分析器用 Flex 定义 Pascal 关键字与 token 边界Pascal 对空格不敏感但对:、..、(* *)注释等有严格词法要求。Flex 规则需显式处理三类难点关键字与标识符的优先级冲突begin是关键字beginner是合法标识符必须让begin规则排在[a-zA-Z][a-zA-Z0-9]*之前注释的嵌套与终止(* ... *)不允许跨行嵌套需用 Flex 的 start condition 机制切换状态实数的正则歧义1.0是 real1.是语法错误1e-2必须支持正则需写为[0-9](\.[0-9])?([eE][-]?[0-9])?。/* pascal.l */ %{ #include parser.tab.h #include stdio.h extern int line_num 1; %} DIGIT [0-9] ID [a-zA-Z][a-zA-Z0-9]* REAL {DIGIT}\.{DIGIT}([eE][-]?{DIGIT})? COMMENT \(\*[^*]*\*([^)*][^*]*\*)*\) %% program { return PROGRAM; } begin { return BEGIN; } end { return END; } if { return IF; } then { return THEN; } else { return ELSE; } while { return WHILE; } do { return DO; } integer { return INTEGER; } real { return REAL_TYPE; } boolean { return BOOLEAN; } : { return ASSIGN; } { return EQ; } { return NEQ; } { return LE; } { return GE; } { return PLUS; } - { return MINUS; } * { return TIMES; } / { return DIVIDE; } ; { return SEMI; } : { return COLON; } , { return COMMA; } . { return DOT; } ( { return LPAREN; } ) { return RPAREN; } {REAL} { yylval.dval atof(yytext); return REAL_CONST; } {DIGIT} { yylval.ival atoi(yytext); return INT_CONST; } {ID} { yylval.sval strdup(yytext); return IDENTIFIER; } {COMMENT} { /* skip */ } [\n] { line_num; } [ \t] { /* skip whitespace */ } . { fprintf(stderr, Lexical error at line %d: unknown char %c\n, line_num, *yytext); exit(1); } %%注意strdup(yytext)是关键——Flex 的yytext指向内部缓冲区每次yylex()调用后内容会被覆盖必须深拷贝。若漏掉此步IDENTIFIER的字符串值会在后续语法分析中变成乱码导致符号表查找失败。2.2 语法分析器用 Bison 定义 LR(1) 文法并注入 AST 构建逻辑Pascal 的begin...end块和if...then...else的悬空 else 问题决定了必须用 LALR(1) 分析器而非简单递归下降。Bison 文法需显式解决两个经典冲突Shift/Reduce 冲突if E then S1 else S2vsif E then S1Bison 默认按 shift 解决正确Reduce/Reduce 冲突procedure P; var x: integer; begin ... end.与function F: integer; var y: real; begin ... end.的var声明段落需用%left声明SEMI优先级避免误规约。/* parser.y */ %{ #include stdio.h #include stdlib.h #include string.h #include ast.h // 自定义 AST 节点结构体 extern int line_num; extern int yylex(); extern void yyerror(const char* s); %} %union { int ival; double dval; char* sval; struct ASTNode* node; } %token sval IDENTIFIER %token ival INT_CONST %token dval REAL_CONST %token PROGRAM BEGIN END IF THEN ELSE WHILE DO INTEGER REAL_TYPE BOOLEAN %token ASSIGN EQ NEQ LE GE PLUS MINUS TIMES DIVIDE SEMI COLON COMMA DOT LPAREN RPAREN %type node program block compound_stmt stmt_list stmt simple_stmt assignment_stmt if_stmt while_stmt procedure_decl function_decl %type node decl_list var_decl type_specifier expr term factor %% program : PROGRAM IDENTIFIER SEMI block { $$ newProgramNode($2, $4); } ; block : [var_decl_list] [procedure_decl_list] [function_decl_list] BEGIN stmt_list END { $$ newBlockNode($1, $2, $3, $5); } ; var_decl_list : /* empty */ { $$ NULL; } | var_decl_list var_decl { $$ appendVarDecl($1, $2); } ; var_decl : IDENTIFIER COLON type_specifier SEMI { $$ newVarDeclNode($1, $3); } ; type_specifier : INTEGER { $$ newTypeNode(INTEGER_TYPE); } | REAL_TYPE { $$ newTypeNode(REAL_TYPE); } | BOOLEAN { $$ newTypeNode(BOOLEAN_TYPE); } ; stmt_list : /* empty */ { $$ NULL; } | stmt_list stmt { $$ appendStmt($1, $2); } ; stmt : simple_stmt SEMI { $$ $1; } | compound_stmt { $$ $1; } ; compound_stmt : BEGIN stmt_list END { $$ newCompoundNode($2); } ; simple_stmt : assignment_stmt | if_stmt | while_stmt ; assignment_stmt : IDENTIFIER ASSIGN expr { $$ newAssignNode($1, $3); } ; if_stmt : IF expr THEN stmt { $$ newIfNode($2, $4, NULL); } | IF expr THEN stmt ELSE stmt { $$ newIfNode($2, $4, $6); } ; while_stmt : WHILE expr DO stmt { $$ newWhileNode($2, $4); } ; expr : expr PLUS term { $$ newBinaryOpNode(PLUS_OP, $1, $3); } | expr MINUS term { $$ newBinaryOpNode(MINUS_OP, $1, $3); } | term { $$ $1; } ; term : term TIMES factor { $$ newBinaryOpNode(TIMES_OP, $1, $3); } | term DIVIDE factor { $$ newBinaryOpNode(DIVIDE_OP, $1, $3); } | factor { $$ $1; } ; factor : IDENTIFIER { $$ newIdNode($1); } | INT_CONST { $$ newIntConstNode($1); } | REAL_CONST { $$ newRealConstNode($1); } | LPAREN expr RPAREN { $$ $2; } ; %% void yyerror(const char* s) { fprintf(stderr, Syntax error at line %d: %s\n, line_num, s); }逻辑说明每个产生式右侧的$1/$2是对应符号的语义值如IDENTIFIER的sval$$是当前产生式的语义值。newProgramNode($2, $4)将程序名字符串和blockAST 节点组装成根节点。参数说明%type node声明所有非终结符返回ASTNode*%union统一管理不同 token 的语义值类型避免强制转换错误。3. 语义检查与符号表用哈希表实现作用域链与类型推导语法树AST只是骨架语义检查才是编译器的「大脑」。Pascal 子集的语义规则比 C 更严格变量必须先声明后使用integer和real不能混用x : y 1.0需报错procedure参数类型必须与调用处一致。这些检查无法在语法分析阶段完成必须遍历 AST 并维护一个作用域链Scope Chain——每个begin...end块创建新作用域嵌套时链表向上查找离开时弹出。3.1 符号表设计哈希桶 作用域链 类型描述符我们不用 STLmap性能差、调试难而用开放寻址哈希表Open Addressing Hash Table实现SymbolTable每个桶存SymbolEntry结构// symbol_table.h struct SymbolEntry { char* name; TypeKind type; // INTEGER_TYPE, REAL_TYPE, BOOLEAN_TYPE ScopeLevel level; // 0global, 1proc1, 2block_in_proc1 bool is_param; // true if declared in procedure parameter list struct SymbolEntry* next; // for hash collision chaining }; class SymbolTable { private: static const int HASH_SIZE 1024; SymbolEntry* buckets[HASH_SIZE]; int current_level; public: SymbolTable() : current_level(0) { memset(buckets, 0, sizeof(buckets)); } unsigned int hash(const char* name) { unsigned int h 0; for (int i 0; name[i]; i) { h (h 5) h name[i]; // djb2 hash } return h % HASH_SIZE; } bool insert(const char* name, TypeKind type, bool is_param false) { unsigned int idx hash(name); SymbolEntry* entry buckets[idx]; while (entry) { if (strcmp(entry-name, name) 0 entry-level current_level) { return false; // duplicate declaration in same scope } entry entry-next; } // insert new entry at head SymbolEntry* new_entry new SymbolEntry; new_entry-name strdup(name); new_entry-type type; new_entry-level current_level; new_entry-is_param is_param; new_entry-next buckets[idx]; buckets[idx] new_entry; return true; } SymbolEntry* lookup(const char* name) { unsigned int idx hash(name); SymbolEntry* entry buckets[idx]; while (entry) { if (strcmp(entry-name, name) 0 entry-level current_level) { return entry; // found in current or outer scope } entry entry-next; } return nullptr; } void enterScope() { current_level; } void exitScope() { // free all entries with level current_level for (int i 0; i HASH_SIZE; i) { SymbolEntry** prev buckets[i]; SymbolEntry* entry buckets[i]; while (entry) { if (entry-level current_level) { *prev entry-next; free(entry-name); delete entry; entry *prev; } else { prev entry-next; entry entry-next; } } } current_level--; } };参数说明ScopeLevel从 0 开始递增enterScope()在进入begin块或procedure时调用exitScope()在end时调用。lookup()查找时只匹配level current_level确保内层作用域屏蔽外层同名变量。3.2 类型检查器遍历 AST 并校验表达式合法性类型检查是深度优先遍历 AST 的过程。核心逻辑对IDENTIFIER节点调用symbol_table-lookup()获取类型若为空则报错Undeclared identifier对二元运算检查左右操作数类型是否均为INTEGER_TYPE或均为REAL_TYPE否则报错Type mismatch in arithmetic operation对赋值:检查左值类型与右值类型是否完全相同Pascal 不允许隐式转换。// semantic_checker.cpp class SemanticChecker { private: SymbolTable* symtab; std::vectorstd::string errors; public: SemanticChecker(SymbolTable* st) : symtab(st) {} TypeKind checkExpr(ASTNode* node) { if (!node) return UNKNOWN_TYPE; switch (node-kind) { case IDENTIFIER_NODE: { SymbolEntry* entry symtab-lookup(node-id_name); if (!entry) { errors.push_back(Undeclared identifier std::string(node-id_name) ); return UNKNOWN_TYPE; } return entry-type; } case INT_CONST_NODE: return INTEGER_TYPE; case REAL_CONST_NODE: return REAL_TYPE; case BINARY_OP_NODE: { TypeKind left_type checkExpr(node-left); TypeKind right_type checkExpr(node-right); if (left_type UNKNOWN_TYPE || right_type UNKNOWN_TYPE) { return UNKNOWN_TYPE; } if (left_type ! right_type) { errors.push_back(Type mismatch in binary operation); return UNKNOWN_TYPE; } // , -, *, / are valid for both integer and real return left_type; } default: return UNKNOWN_TYPE; } } void checkStmt(ASTNode* node) { if (!node) return; switch (node-kind) { case ASSIGN_NODE: { TypeKind left_type checkExpr(node-left); // left must be lvalue TypeKind right_type checkExpr(node-right); if (left_type ! right_type) { errors.push_back(Assignment type mismatch); } break; } case IF_NODE: checkExpr(node-cond); // condition must be boolean checkStmt(node-then_branch); if (node-else_branch) checkStmt(node-else_branch); break; case WHILE_NODE: checkExpr(node-cond); checkStmt(node-body); break; case COMPOUND_NODE: for (ASTNode* stmt node-stmt_list; stmt; stmt stmt-next) { checkStmt(stmt); } break; default: break; } } void checkProgram(ASTNode* root) { // First pass: collect declarations (var, procedure, function) collectDeclarations(root); // Second pass: check statements and expressions checkStmt(root); } void collectDeclarations(ASTNode* node) { if (!node) return; switch (node-kind) { case VAR_DECL_NODE: symtab-insert(node-var_name, node-var_type); break; case PROCEDURE_DECL_NODE: symtab-insert(node-proc_name, PROCEDURE_TYPE); symtab-enterScope(); // insert parameters for (ASTNode* param node-param_list; param; param param-next) { symtab-insert(param-param_name, param-param_type, true); } collectDeclarations(node-proc_body); symtab-exitScope(); break; case BLOCK_NODE: symtab-enterScope(); // process var_decl_list, proc_decl_list, stmt_list collectDeclarations(node-var_decls); collectDeclarations(node-proc_decls); collectDeclarations(node-stmt_list); symtab-exitScope(); break; default: // recurse children collectDeclarations(node-left); collectDeclarations(node-right); collectDeclarations(node-next); break; } } };逻辑说明collectDeclarations()是第一遍遍历只处理声明var、procedure参数不检查表达式checkStmt()是第二遍处理所有语句和表达式。这种两遍设计避免了前向引用问题如procedure A; begin B(); end.中B尚未声明。4. 中间代码生成用三地址码TAC构建可优化的 IR 层生成目标代码前必须经过中间表示IR层。Pascal 子集选择三地址码Three-Address Code, TAC因其结构简单、易于生成、便于后续优化如常量折叠、死代码消除。每条 TAC 指令形如x y op z或goto L1其中op是,-,*,/,:,if等。关键设计点基本块Basic Block划分以goto、if、return为边界每个块内无分支入口临时变量命名用t1,t2, ... 自动编号避免手动管理控制流图CFG构建为后续优化铺路但本子集暂不实现优化仅生成线性 TAC 序列。4.1 TAC 指令集定义与代码生成器框架我们定义 7 种 TAC 指令指令类型示例说明ASSIGNt1 a变量/常量赋值BINOPt2 t1 b二元运算LABELL1:标签用于跳转目标GOTOgoto L2无条件跳转IF_GOTOif t3 0 goto L4条件跳转PARAMparam t5过程调用参数CALLcall proc1, 1调用过程第二个参数为参数个数// tac.h enum TACOp { ASSIGN, BINOP, LABEL, GOTO, IF_GOTO, PARAM, CALL }; struct TACInstruction { TACOp op; char* result; // left-hand side (e.g., t1) char* arg1; // first operand (e.g., a, t1) char* arg2; // second operand (e.g., b, 0) char* label; // target label (e.g., L2) char* cond_op; // condition operator (e.g., , !) int param_count; // for CALL }; class TACGenerator { private: std::vectorTACInstruction* code; int temp_counter; int label_counter; public: TACGenerator() : temp_counter(0), label_counter(0) {} char* newTemp() { char* temp new char[10]; sprintf(temp, t%d, temp_counter); return temp; } char* newLabel() { char* label new char[10]; sprintf(label, L%d, label_counter); return label; } void emit(TACOp op, const char* result nullptr, const char* arg1 nullptr, const char* arg2 nullptr, const char* label nullptr, const char* cond_op nullptr, int param_count 0) { TACInstruction* inst new TACInstruction; inst-op op; inst-result result ? strdup(result) : nullptr; inst-arg1 arg1 ? strdup(arg1) : nullptr; inst-arg2 arg2 ? strdup(arg2) : nullptr; inst-label label ? strdup(label) : nullptr; inst-cond_op cond_op ? strdup(cond_op) : nullptr; inst-param_count param_count; code.push_back(inst); } void printCode() { for (auto inst : code) { switch (inst-op) { case ASSIGN: printf(%s %s\n, inst-result, inst-arg1); break; case BINOP: printf(%s %s %s %s\n, inst-result, inst-arg1, inst-arg2, inst-cond_op); break; case LABEL: printf(%s:\n, inst-label); break; case GOTO: printf(goto %s\n, inst-label); break; case IF_GOTO: printf(if %s %s %s goto %s\n, inst-arg1, inst-cond_op, inst-arg2, inst-label); break; case PARAM: printf(param %s\n, inst-arg1); break; case CALL: printf(call %s, %d\n, inst-arg1, inst-param_count); break; } } } };参数说明emit()是核心接口所有 AST 节点的代码生成都调用它。newTemp()保证临时变量全局唯一newLabel()为每个if/while生成唯一标签。printCode()用于调试输出实际项目中会写入.tac文件供后续模块读取。4.2 为 Pascal 结构生成 TAC从 if 到 while 的控制流翻译Pascal 的if和while是控制流翻译难点。关键原则每个结构生成独立的基本块用goto连接。例如if E then S1 else S2翻译为t1 eval(E) // condition evaluation if t1 0 goto L2 // jump to else part if false L1: S1_code // then part goto L3 // jump to end L2: S2_code // else part L3: ... // continue// tac_generator.cpp void TACGenerator::genIf(ASTNode* node) { char* cond_temp genExpr(node-cond); // generate TAC for condition, return temp name char* L1 newLabel(); char* L2 newLabel(); char* L3 newLabel(); // if cond 0 goto L2 emit(IF_GOTO, nullptr, cond_temp, 0, L2, ); // L1: then part emit(LABEL, nullptr, nullptr, nullptr, L1); genStmt(node-then_branch); // goto L3 emit(GOTO, nullptr, nullptr, nullptr, L3); // L2: else part emit(LABEL, nullptr, nullptr, nullptr, L2); if (node-else_branch) { genStmt(node-else_branch); } // L3: end emit(LABEL, nullptr, nullptr, nullptr, L3); } void TACGenerator::genWhile(ASTNode* node) { char* L1 newLabel(); // loop condition check char* L2 newLabel(); // loop body start char* L3 newLabel(); // loop end emit(LABEL, nullptr, nullptr, nullptr, L1); char* cond_temp genExpr(node-cond); emit(IF_GOTO, nullptr, cond_temp, 0, L3, ); // if false, exit emit(LABEL, nullptr, nullptr, nullptr, L2); genStmt(node-body); emit(GOTO, nullptr, nullptr, nullptr, L1); // jump back to condition emit(LABEL, nullptr, nullptr, nullptr, L3); } char* TACGenerator::genExpr(ASTNode* node) { if (!node) return nullptr; switch (node-kind) { case IDENTIFIER_NODE: return strdup(node-id_name); case INT_CONST_NODE: { char* temp newTemp(); emit(ASSIGN, temp, std::to_string(node-int_val).c_str()); return temp; } case REAL_CONST_NODE: { char* temp newTemp(); char buf[32]; sprintf(buf, %.1f, node-real_val); emit(ASSIGN, temp, buf); return temp; } case BINARY_OP_NODE: { char* left_temp genExpr(node-left); char* right_temp genExpr(node-right); char* result_temp newTemp(); const char* op_str ; switch (node-op) { case PLUS_OP: op_str ; break; case MINUS_OP: op_str -; break; case TIMES_OP: op_str *; break; case DIVIDE_OP: op_str /; break; default: op_str ; } emit(BINOP, result_temp, left_temp, right_temp, nullptr, op_str); return result_temp; } default: return nullptr; } }逻辑说明genIf()和genWhile()严格遵循基本块划分每个LABEL指令对应一个块入口。genExpr()递归生成子表达式并返回其计算结果的临时变量名供父节点使用。玄学经验if的else部分必须显式生成L2标签即使为空S2_code为空否则L3的位置会错位导致后续代码跳转到错误地址。5. 常见问题排查那些让你 debug 到凌晨三点的 Pascal 编译器坑编译器开发最痛苦的不是写不出功能而是错误信息指向错误位置、内存泄漏无声无息、或语法树某处指针野了却只在生成汇编时才 crash。以下是我在三个真实项目课程设计、嵌入式工具链适配、教育版 IDE 后端中踩过的血泪坑按现象→原因→解决整理拒绝泛泛而谈。5.1 现象syntax error at line 1报错位置永远是第一行但实际错误在第 50 行原因Flex 词法分析器遇到非法字符如中文标点、不可见 Unicode 字符时yyerror()被触发但line_num变量未在[\n]规则外更新。更隐蔽的是当输入文件以\r\nWindows结尾而 Flex 默认按\n计数line_num会少算一行。解决在 Flex 规则末尾添加[\r\n]处理回车换行[\r\n] { line_num; }在yyerror()中打印yytext的 ASCII 码定位非法字符void yyerror(const char* s) { fprintf(stderr, Syntax error at line %d: %s (char: %d)\n, line_num, s, (int)*yytext); }强制输入文件用 Unix 换行dos2unix input.pas或在 Flex 中启用%option yylineno但会降低性能。5.2 现象procedure P; begin x : 1; end.编译通过但x未声明符号表 lookup 返回 null 却没报错原因collectDeclarations()遍历顺序错误。若先处理stmt_listx : 1再处理var_decl_list空则x在lookup()时找不到但checkStmt()中未对IDENTIFIER_NODE的lookup()返回值做空指针检查直接传给checkExpr()导致静默失败。解决在checkExpr()的IDENTIFIER_NODE分支强制检查entry是否为nullptrcase IDENTIFIER_NODE: { SymbolEntry* entry symtab-lookup(node-id_name); if (!entry) { errors.push_back(Undeclared identifier std::string(node-id_name) at line std::to_string(node-line)); return UNKNOWN_TYPE; // not just return, but log error } return entry-type; }在collectDeclarations()中严格按 Pascal 语法顺序先var_decl_list再procedure_decl_list最后stmt_list确保声明先于使用。5.3 现象if a 0 then b : 1 else c : 2;生成的 TAC 中c : 2执行两次原因genIf()中else_branch的genStmt()调用后缺少goto L3跳转指令导致控制流自然落入L3后的代码而L3本身是if结构的出口c : 2所在的L2块执行完后继续向下执行形成 fall-through。解决在genIf()的else分支末尾无论else_branch是否为空都插入goto L3emit(LABEL, nullptr, nullptr, nullptr, L2); if (node-else_branch) { genStmt(node-else_branch); } emit(GOTO, nullptr, nullptr, nullptr, L3); // ← 必须加这一行同样在genWhile()的body末尾加goto L1确保循环结束。5.4 现象program test; var x: integer; begin x : 1; end.编译出的汇编中x地址偏移错误运行时 segmentation fault原因符号表insert()时未区分var声明与procedure名称导致x和test冲突。program test的test被插入符号表作为PROGRAM类型而var x的x也插入但lookup(test)返回PROGRAM类型的 entrylookup(x)返回INTEGER_TYPE看似正常。问题在于目标代码生成时x的内存布局计算依赖SymbolTable的getOffset()方法而该方法未按作用域级别隔离全局变量与程序名。解决在SymbolEntry中增加SymbolKind枚举enum SymbolKind { VAR_SYMBOL, PROC_SYMBOL, FUNC_SYMBOL, PROGRAM_SYMBOL };insert()时传入kind参数lookup()时按kind过滤SymbolEntry* lookup(const char* name, SymbolKind kind ANY_SYMBOL) { // ... same hash search ... if (strcmp(entry-name, name) 0 entry-level current_level (kind ANY_SYMBOL || entry-kind kind)) { return entry; } }genProgram()生成代码时只对VAR_SYMBOL类型的 entry 分配栈空间。5.5 现象writeln(hello)调用报Undefined procedure writeln但标准库已链接原因Pascal 子集未预置标准过程符号。writeln是本文还有配套的精品资源点击获取